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. Python python对象 deque

    # deque对象 ''' class collections.deque([ iterable [,maxlen ] ] ) 返回一个从左到右(使用append())初始化的新deque对象,其中包 ...

  2. iOS isEqual

    如何重写 hash 方法 一个合理的 hash 方法要尽量让 hash 表中的元素均匀分布,来保证较高的查询性能. 如果两个对象可以被视为同一个对象,那么他们的 hash 值要一样. mattt 在文 ...

  3. Innodb的三大关健特性

    今天看<MySql技术内幕InnoDB存储引擎>一书,学习了Mysql的三大关健特性,并记录如下: 插入缓冲 双写(double write) 自适应Hash索引 在记录这些特性之前,先对 ...

  4. ajax2.0之文件上传加跨域

    express_server.js const express=require('express'); //主体 const body=require('body-parser'); //接收普通PO ...

  5. man手册、zip备份

                                                                                                        ...

  6. python 函数--递归函数

    一.递归函数的定义:在一个函数里面调用函数本身 python限制最大层数:998层 def foo(n): print(n) n+=1 foo(n) foo(1)

  7. 3.K均值算法

    一.概念 K-means中心思想:事先确定常数K,常数K意味着最终的聚类类别数,首先随机选定初始点为质心,并通过计算每一个样本与质心之间的相似度(这里为欧式距离),将样本点归到最相似的类中,接着,重新 ...

  8. 使用 RestTemplate 进行第三方Rest服务调用

    1. 前言 RestTemplate 是 Spring 提供的一个调用 Restful 服务的抽象层,它简化的同 Restful 服务的通信方式,隐藏了不必要的一些细节,让我们更加优雅地在应用中调用 ...

  9. 运行jmeter.bat时 提示 not able to find java executable or version

    安装过好几次,这是第一次遇到运行jmeter.bat时 提示 not able to find java executable or version Please check your Java in ...

  10. leetcode 30 day challenge Counting Elements

    Counting Elements Given an integer array arr, count element x such that x + 1 is also in arr. If the ...