Procedural Level Generator是在Unity应用商店中发布的一款免费的轻量级关卡生成器:

可以直接搜索关键字在应用商店中查找并下载。

和我之前生成关卡的想法不同,这个插件生成地图的方式类似于拼积木,它将每一个地图分为一个一个的部分,无论是房间还是通道,都叫做Section,只是用不同的标签来规定和约束这些部分,并逐一的将这些部分在空间中连接起来,每一个部分需要自己手动定义它的预制体,形状,碰撞盒子以及出口列表,通过出口列表来判断下一个部分的连接位置和方向,用碰撞盒子的Bounds.Intersects(Bounds bounds);方法来判断一个部分的生成是否会是一个无效的连接:

         public bool IsSectionValid(Bounds newSection, Bounds sectionToIgnore) =>
!RegisteredColliders.Except(sectionToIgnore.Colliders).Any(c => c.bounds.Intersects(newSection.Colliders.First().bounds)); //
// 摘要:
// Does another bounding box intersect with this bounding box?
//
// 参数:
// bounds:
public bool Intersects(Bounds bounds);

利用提前制作Section预制体的方式来连接生成整个关卡的方式,确实避免了很多让人头疼的算法设计,但可能插件本身也只是为了提供一个基本思路,因此有些地方值得优化。

1.缺少门的概念

很多时候,进入一个地图的房间,我们需要门的解锁和开关来对探索进行限制,也有可能进入一个满是怪物的房间,这个房间的所有门会自动关闭,给玩家一种身陷敌营是时候浴血奋战的错觉。故而考虑在Section中给每个类增加一个自带Door的列表,该列表可以没有任何元素,例如很多通道之间是不需要门来进行连接的,但房间与通道之间,房间与房间之间,可以同时创建门来执行必要的约束限制。

定义门的类,注意保持在插件的命名空间之下:

 using UnityEngine;
using System.Collections.Generic; namespace LevelGenerator.Scripts
{
public class Door : MonoBehaviour
{
public List<string> Tag1s = new List<string>();
public List<string> Tag2s = new List<string>(); public Transform ExitTransdorm { get; set; }
public void Initialize(LevelGenerator levelGenerator)
{
transform.SetParent(levelGenerator.Container);
}
}
}

这里只定义了最基础的一些属性和方法,主要是门连接的两个Section的标签列表,用于更为准确的判定该门的所属。

在Section类中添加放置门的方法:

         /// <summary>
/// initialize door datas
/// </summary>
/// <param name="exit">place transform</param>
/// <param name="next">next section</param>
public void PlaceDoor(Transform exit, Section next)
{
var t = Instantiate(LevelGenerator.Doors.PickOne(), exit);
t.Initialize(LevelGenerator);
Doors.Add(t.gameObject); var d = t.GetComponent<Door>();
d.Tag1s.AddRange(Tags);
d.Tag2s.AddRange(next.Tags);
d.ExitTransdorm = exit; //send door initialize event
if (Idx > || next.Idx > )
EventManager.QueueEvent(new DoorInitEvent(t.transform, Idx, next.Idx));
}

并且在每一个门创建后及时记录在Section的Doors列表中,发送创建完成的事件,这里使用的事件系统可以详见:

https://www.cnblogs.com/koshio0219/p/11209191.html

调用就是在成功生成每一个Section之后:

         protected void GenerateSection(Transform exit)
{
var candidate = IsAdvancedExit(exit)
? BuildSectionFromExit(exit.GetComponent<AdvancedExit>())
: BuildSectionFromExit(exit); if (LevelGenerator.IsSectionValid(candidate.Bounds, Bounds))
{
candidate.LastSections.Add(this);
NextSections.Add(candidate); if (LevelGenerator.SpaceTags.Contains(candidate.Tags.First()) && LevelGenerator.CheckSpaceTags(candidate))
{
Destroy(candidate.gameObject);
NextSection.Remove(candidate);
GenerateSection(exit);
return;
} candidate.Initialize(LevelGenerator, order);
candidate.LastExits.Add(exit); PlaceDoor(exit, candidate);
}
else
{
Destroy(candidate.gameObject);
PlaceDeadEnd(exit);
}
}

由于通道与通道之间不需要放门,因此在所有Section生成完毕之后将一部分门删除:(此方法位于关卡生成器这个控制类中)

         /// <summary>
/// Clear the corridor doors
/// </summary>
protected void CheckDeleteDoors()
{
foreach (var s in registeredSections)
{
if (s != null)
{
var temp = new List<GameObject>();
foreach (var d in s.Doors)
{
var ds = d.GetComponent<Door>();
if (ds.Tag1s.Contains("corridor") && ds.Tag2s.Contains("corridor"))
{
temp.Add(d);
Destroy(d);
}
} foreach(var t in temp)
{
s.Doors.Remove(t);
}
}
}
}

这里注意一点,遍历列表的时候不能直接对列表的元素进行移除,所以先建立了一个临时需要移除的列表作为替代,遍历临时列表以移除元素,当然了,你用通用方式for循环倒着遍历也是可行的,个人不太喜欢用for循环而已。

说句题外话,可能有人会有疑惑,为什么不直接在创建门的时候做条件限制,非要等到最后统一再来遍历删除呢,其实最主要的原因是为了尽量少的变动原始的代码逻辑和结构,而更倾向于添加新的方法来对插件进行附加功能的完善,这样可以很大的程度上减少bug触发的概率,毕竟别人写的插件你很可能总有漏想的地方,随意的改动和删除对方已经写过的内容并非良策,最好是只添加代码而不对原始代码进行任何的改动或删除,仅以这样的方式来达到完善功能的目的。调试的时候也只用关注自己添加的部分即可。

2.路径的末尾很可能是通道

关于这一点,可能会根据游戏的不同而异,因为这个插件在生成地图的过程中,无论是房间还是通道,都是同一个类Section,这样没办法保证路径末尾是一个房间,还是通道。可以添加一个功能用于检查和删除端点是通道的部分。

在Section中添加以下属性方便遍历删除:

         [HideInInspector]
public List<GameObject> DeadEnds = new List<GameObject>();
[HideInInspector]
public List<Transform> LastExits = new List<Transform>();
[HideInInspector]
public List<Section> LastSections = new List<Section>();
[HideInInspector]
public List<Section> NextSections = new List<Section>();
[HideInInspector]
public List<GameObject> Doors = new List<GameObject>();

分别代表每一个Section的死亡端点列表,上一个Section的列表,下一个Section的列表(类似于双向链表),与上一个Section连接的位置列表,门的列表,有了这些数据结构,无论怎么遍历,修改和获取数据都是会变得非常容易。添加的地方自然是生成Section的方法中,放置端点的方法中,及放置门的方法中。

开始检查并删除末尾的通道:(根据实际需求是否调用)

         /// <summary>
/// clear end sections and update datas
/// </summary>
protected void DeleteEndSections()
{
var temp = new List<Section>();
foreach (var s in registeredSections)
{
temp.Add(s);
DeleteEndSection(s);
} foreach(var t in temp)
{
foreach (var c in t.Bounds.Colliders)
{
DeadEndColliders.Remove(c);
}
registeredSections.Remove(t);
}
} /// <summary>
/// clear the end corridors and doors , place deadend prafabs' instances
/// </summary>
/// <param name="s">the check section</param>
protected void DeleteEndSection(Section s)
{
if (s.Tags.Contains("corridor"))
{
if (s.DeadEnds.Count == s.ExitsCount)
{
//删除通道以及通道的端点方块
Destroy(s.gameObject);
foreach (var e in s.DeadEnds)
{
Destroy(e);
} foreach (var ls in s.LastSections)
{
//删除末端通道后需要在上一个节点的退出点放置端点方块(不然墙壁上就会有洞)
foreach (var le in s.LastExits)
{
ls.PlaceDeadEnd(le);
} //同样的,悬空的门应该删除
var temp = new List<GameObject>();
foreach (var d in ls.Doors)
{
var ds = d.GetComponent<Door>();
if (s.LastExits.Contains(ds.ExitTransdorm))
{
temp.Add(d);
Destroy(d);
}
} foreach (var t in temp)
{
ls.Doors.Remove(t);
} //递归遍历,因为端点的通道可能很长,要直到遍历到非通道为止
DeleteEndSection(ls);
}
}
}
}

3.没有间隔随机的规则系统

在实际生成随机地图的过程中,很容易发现一个严重的问题,在随机的过程中,同类型的房间接连出现,例如,玩家刚刚进入了一个商店类型的房间,后面又马上可能再进入一个商店类型的房间,这样显然很不好,而为了避免这种情况发生,就要考虑给随机系统添加额外的随机规则。

在生成器的控制类中添加需要间隔随机的标签列表:

         /// <summary>
/// The tags that need space
/// </summary>
public string[] SpaceTags;

在生成具体Section的过程中要对下一个生成的Section进行标签检查:

                 candidate.LastSections.Add(this);
NextSections.Add(candidate); //对间隔标签进行检查
if (LevelGenerator.SpaceTags.Contains(candidate.Tags.First()) && LevelGenerator.CheckSpaceTags(candidate))
{
Destroy(candidate.gameObject);
NextSections.Remove(candidate);
GenerateSection(exit);
return;
} candidate.Initialize(LevelGenerator, order);
candidate.LastExits.Add(exit); PlaceDoor(exit, candidate);

只有通过检查才能继续初始化和生成其他数据,不然就重新随机。具体的检查算法如下:

         private bool bSpace;

         /// <summary>
/// check the space tags
/// </summary>
/// <param name="section">next creat scetion</param>
/// <returns>is successive tag</returns>
public bool CheckSpaceTags(Section section)
{
foreach (var ls in section.LastSections)
{
if (ls.Tags.Contains("corridor"))
{
//包含通道时别忘了遍历该通道的其他分支
if (OtherNextCheck(ls, section))
return bSpace = true; bSpace = false;
CheckSpaceTags(ls);
}
else
{
if (SpaceTags.Contains(ls.Tags.First()))
{
return bSpace = true;
}
else
{
//即使上一个房间未包含间隔标签,但该房间的其他分支也需要考虑
if (OtherNextCheck(ls, section))
return bSpace = true;
}
}
} return bSpace;
} bool result;
bool OtherNextCheck(Section section,Section check)
{
foreach(var ns in section.NextSections)
{
//如果是之前的Section分支则跳过此次遍历
if (ns == check)
continue; if (ns.Tags.Contains("corridor"))
{
result = false;
OtherNextCheck(ns, check);
}
else
{
if (SpaceTags.Contains(ns.Tags.First()))
{
return result = true;
}
}
} return result;
}

总共有三种情况不符合要求:

1.包含间隔标签房间的上一个房间也包含间隔标签。(最直接的一种情况,直接Pass)

2.虽然包含间隔标签的房间的上一个房间不包含间隔标签,但连接它们通道的某一其他分支中的第一个房间包含间隔标签。

3.虽然包含间隔标签的房间的上一个房间不包含间隔标签,且连接它们通道的任何一个其他分支中的第一个房间也不包含间隔标签,但上一个房间的其他分支中的第一个房间包含间隔标签。

上面三种情况都会造成一次战斗结束后可能同时又多个商店房间的情况。

随机生成关卡的效果展示:(图中选中的部分为门,间隔标签房间即是其中有内容物的小房间)

我将改动之后的插件重新进行了打包,以供下载参考:

https://files.cnblogs.com/files/koshio0219/LevelGenerator.zip

更多有关随机地图关卡的随笔可见:

https://www.cnblogs.com/koshio0219/p/12739913.html

Unity Procedural Level Generator 基础总结与功能优化的更多相关文章

  1. 【转载】利用Unity自带的合图切割功能将合图切割成子图

    虽然目前网上具有切割合图功能的工具不少,但大部分都是自动切割或者根据plist之类的合图文件切割的, 这种切割往往不可自己微调或者很难维调,导致效果不理想. 今天逛贴吧发现了一位网友写的切割合图插件很 ...

  2. 《Unity 3D游戏客户端基础框架》概述

    框架概述: 做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框 ...

  3. C# Unity依赖注入利用Attribute实现AOP功能

    使用场景? 很多时候, 我们定义一个功能, 当我们要对这个功能进行扩展的时候, 按照常规的思路, 我们一般都是利用OOP的思想, 在原有的功能上进行扩展. 那么有没有一种东西, 可以实现当我们需要扩展 ...

  4. [译]Vulkan教程(14)图形管道基础之固定功能

    [译]Vulkan教程(14)图形管道基础之固定功能 Fixed functions 固定功能 The older graphics APIs provided default state for m ...

  5. HTML&CSS基础-html注释功能

    HTML&CSS基础-html注释功能 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.  一.什么是HTML(Hypertext Markup Language) 超文本标记 ...

  6. MySQL慢日志线上问题分析及功能优化

    本文来源于数据库内核专栏. MySQL慢日志(slow log)是MySQL DBA及其他开发.运维人员需经常关注的一类信息.使用慢日志可找出执行时间较长或未走索引等SQL语句,为进行系统调优提供依据 ...

  7. WeTest功能优化第3期:业内首创,有声音的云真机

    第3期功能优化目录 [云真机远程调试]音频同步传输实现测试有声 [兼容性测试报告]新增视频助力动态定位问题 [云真机远程调试]菜单栏优化助力机型选择 本期介绍的新功能,秉承创造用户需求的理念,在云真机 ...

  8. WeTest功能优化第2期:云真机智能投屏,调试告别鼠标

    第2期功能优化目录 [云真机视频映射]云真机画面本地映射[兼容性测试报告]新增问题机型聚类功能[新增Android9.0]同步上线最新安卓系统 本期介绍的云测产品功能优化,既有重磅级技术突破,也有报告 ...

  9. WeTest功能优化第1期:截图960px,云真机映射功能了解

    第1期功能优化目录 [全线产品测试截图优化]安卓机型测试截图分辨率上升至960px [云真机新增Android 9]最新安卓系统,等你pick [云真机新增键盘映射功能]电脑键盘码字,云真机同步显示  ...

随机推荐

  1. MySQL的MVCC机制

    1.MVCC简介 1.1 MVCC是什么? MVCC,Multi-Version Concurrency Control,多版本并发控制.MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对 ...

  2. Ruby学习计划-(1)搭建开发环境

    环境搭建        工欲善其事,必先利其器.要学习一门新的语言当然也需要搭建好开发环境,这样才能更加高效的完成工作提高自身的工作效率.PS:由于自己使用的是MacBookPro,因此之后的所有问题 ...

  3. Pyhton基本图形绘制

    目前学习Python中,记录一些内容~ 以下为部分练习内容 1.Python蟒蛇绘制 1 #PythonDraw.py 2 import turtle as t #t作为turtle的别名:另一种方法 ...

  4. 经常登录Linux,用户密码背后的知识了解一下

    一,用户密码存放在哪里? 说到这个问题,绝大部分的同学肯定都知道/etc/passwd这个文件,不错,这个文件里存储的就是用户名,密码等信息. 每一行都是一个account,每一行有7个信息,分别用 ...

  5. C++头文件应该干的事情

    C++头文件应该干的事情 最近在写自己项目的时候,头文件老是编译错误,后来发现还是对头文件掌握不牢. 头文件应该干什么? 所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的 ...

  6. Mac通过homebrew 安装mysql

    来源:http://timtang.me/blog/2011/12/07/mac-homebrew-mysql/ 使用MBP有一年了,开始想在工作中使用mac由于各种不习惯最终失败,导致自己使用了一年 ...

  7. Spring (五):AOP

    本文是按照狂神说的教学视频学习的笔记,强力推荐,教学深入浅出一遍就懂!b站搜索狂神说或点击下面链接 https://space.bilibili.com/95256449?spm_id_from=33 ...

  8. 测试老司机都在用的表白神器你会吗?-Fiddler之AutoResponse线上调试

    一.Fiddler在线调试功能和表白神器介绍 ​ 在以往的工作中,线上有bug,就需要把文件弄到本地来改,但经常会碰见本地环境又和线上不一样,导致调试困难,闭着眼睛改好之后传到线上去看对不对,不对的话 ...

  9. MTK Android SwitchPreference(设置-智能辅助-导航栏-导航栏可隐藏)

    1.界面布局文件 packages/apps/PrizeSettings/res/xml/navigation_bar_prize.xml ------------------------------ ...

  10. 01-启动jmeter目录功能

    1.bin :存储了jmeter的可执行程序,如启动脚本.配置程序 docs:    api扩展文档存放 lib:   lib\ext   存储了jmeter的整合的功能(如.jar文件程序,和第三方 ...