Roguelike电子游戏这一概念起源于上世纪八十年代的电子游戏——《Rogue》,该游戏具有随机地图、敌人、战利品,玩家永久死亡,游戏非线性等特点。
游戏的随机性保证玩家的每一次冒险体验都独一无二,在操作角色与敌人射爆厮杀时,永久死亡的机制让人血脉喷张,加上非线性游戏流程的特点能让玩家在每次战斗交替中间,根据自己的战斗、生存能力选择合适的探索路线,适当地将主导权交给玩家。
《Rogue》推出后大受好评,随之而来的是优秀的传承者们,如当时的《矮人要塞》、《马基埃亚尔的传说》,再到现在的《以撒的结合》、《死亡细胞》、《空洞骑士》等游戏(由于具有类似机制的游戏太多,因此业界将它们定义为“Roguelike”游戏)。它们继承了《Rogue》的优秀设计理念,并在其基础上再次创新,将Roguelike游戏发扬光大。
在Roguelike游戏中,随机地图的生成是最基础的功能。现在我们自己设计一套算法来实现它,具体需求如下:
- 随机生成N个房间,这每个房间至少要与一个房间连接(上下左右四个连接方向)
- 设置房间的门:如果有一个方向连接着另一个房间,则房间的该方向要有一扇门
- 生成的房间中要有一个开始房间(玩家出生的房间),和一个结束房间(BOSS房或通关房)
- 开始房间和结束房间要尽可能远
该功能的Demo类似于:

准备工作
由于房间是游戏开始前自动生成,因此我们需要做一个房间预制体:

白色部分为房间主体,黄色部分为房间的门,房间上的UI文本用于显示初始房间到这里的步数(方便调试)。
Room.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 每个房间身上挂在该脚本,
/// 用于设置房间的门,在房间模型上显示开始房间走到该房间的步数
/// </summary>
/// <param name="upDoor">房间上方的门</param>
public class Room : MonoBehaviour
{
//房间四个方向的门,在开始时设置它们
public GameObject upDoor, downDoor, leftDoor, rightDoor;
//房间四个方向是否有其他房间,若有,则添加门
public bool upRoom, downRoom, leftRoom, rightRoom;
//开始房间到该房间的步数文本显示
public Text stepsText;
//房间门的个数
public int numDoors;
void Start()
{
upDoor.SetActive(upRoom);
downDoor.SetActive(downRoom);
leftDoor.SetActive(leftRoom);
rightDoor.SetActive(rightRoom);
}
}
随机生成房间
我们在场景里新建一个空物体,命名为 RoomGenerator ,将脚本 RoomGenerator.cs 挂在它身上,并在该物体下再新建一个空物体 Point
算法流程大致如下,从 Point 位置开始,生成一个房间,然后将 Point 随机向四个方向移动一个单位,若移动后的位置没有房间,则生成房间,若有则继续移动,直到找到可以生成房间的位置。
代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class RoomGenerator : MonoBehaviour
{
//生成房间的四个方向
public enum Direction { up,down,left,right};
public Direction direction;
[Header("房间信息")]
public GameObject roomPrefab;
public int roomNumber;
[Header("位置控制")]
public Transform generatorPoint;
public float xOffset, yOffset;
public List<Room> rooms = new List<Room>();
public LayerMask roomLayer;
void Start()
{
GenerateRoom();
}
void Update()
{
if(Input.anyKeyDown)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
/// <summary>
/// 随机生成房间:
/// 在generatorPoint的位置生成room,并加入List →→
/// 随机移动generatorPoint →→ #1 or #2
/// 如果移动后的位置没有room,则执行开头步骤 #1
/// 如果移动后的位置有room,则继续随机移动 #2
/// </summary>
private void GenerateRoom()
{
for (int i = 0; i < roomNumber; i++)
{
//生成房间并将其脚本加入进rooms数组
rooms.Add(Instantiate(roomPrefab, generatorPoint.position, Quaternion.identity).GetComponent<Room>());
//移动generatorPoint
//为了解决room重叠问题而添加循环检测
//当generatorPoint走到有room的位置时,继续走,直到走到空位置
do
{
direction = (Direction)UnityEngine.Random.Range(0, 4);//取值为0到3整数
switch (direction)
{
case Direction.up:
generatorPoint.position += new Vector3(0, yOffset, 0);
break;
case Direction.down:
generatorPoint.position += new Vector3(0, -yOffset, 0);
break;
case Direction.left:
generatorPoint.position += new Vector3(-xOffset, 0, 0);
break;
case Direction.right:
generatorPoint.position += new Vector3(xOffset, 0, 0);
break;
}
} while (Physics2D.OverlapCircle(generatorPoint.position,0.2f,roomLayer));
}
}
}
设置房间
设置房间,包括房间的门、添加房间的墙壁和初始房间到次房间的距离。最后一项我们留到后面讨论,这里只设置前两项。
设置门的大致思路:遍历每个房间,查看它们上下左右是否有其他房间,若有,则将对应方向的布尔值设置为真,房间会根据四个方向的布尔值来设置让哪些门显示出来。
房间的墙壁我也做成了预制体,包括了所有开口情况一共十五个预制体。为了方便管理和调用,我们将它们声明在一个类里。
代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class RoomGenerator : MonoBehaviour
{
//生成房间的四个方向
public enum Direction { up,down,left,right};
public Direction direction;
[Header("房间信息")]
public GameObject roomPrefab;
public int roomNumber;
[Header("位置控制")]
public Transform generatorPoint;
public float xOffset, yOffset;
public List<Room> rooms = new List<Room>();
public LayerMask roomLayer;
public Wall wall;
void Start()
{
GenerateRoom();
SetupRooms();
}
void Update()
{
if(Input.anyKeyDown)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
/// <summary>
/// 随机生成房间:
/// 在generatorPoint的位置生成room,并加入List →→
/// 随机移动generatorPoint →→ #1 or #2
/// 如果移动后的位置没有room,则执行开头步骤 #1
/// 如果移动后的位置有room,则继续随机移动 #2
/// </summary>
private void GenerateRoom()
{
for (int i = 0; i < roomNumber; i++)
{
//生成房间并将其脚本加入进rooms数组
rooms.Add(Instantiate(roomPrefab, generatorPoint.position, Quaternion.identity).GetComponent<Room>());
//移动generatorPoint
//为了解决room重叠问题而添加循环检测
//当generatorPoint走到有room的位置时,继续走,直到走到空位置
do
{
direction = (Direction)UnityEngine.Random.Range(0, 4);//取值为0到3整数
switch (direction)
{
case Direction.up:
generatorPoint.position += new Vector3(0, yOffset, 0);
break;
case Direction.down:
generatorPoint.position += new Vector3(0, -yOffset, 0);
break;
case Direction.left:
generatorPoint.position += new Vector3(-xOffset, 0, 0);
break;
case Direction.right:
generatorPoint.position += new Vector3(xOffset, 0, 0);
break;
}
} while (Physics2D.OverlapCircle(generatorPoint.position,0.2f,roomLayer));
}
}
/// <summary>
/// 设置房间,包括房间的门、到初始房间的距离、添加房间的墙壁
/// </summary>
/// <remarks>
/// 1、设置房间的门,若房间四个方向上有其他房间,则添加门
/// 2、计算初始房间到每个房间的距离(走几步)
/// 3、给房间添加墙壁:先看该room有几扇门,再判断是哪几扇门,最后生成对应墙壁的预制体
/// </remarks>
private void SetupRooms()
{
foreach(var room in rooms)
{
//设置门
room.upRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(0, yOffset, 0), 0.2f, roomLayer);
room.downRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(0, -yOffset, 0), 0.2f, roomLayer);
room.leftRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(-xOffset, 0, 0), 0.2f, roomLayer);
room.rightRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(xOffset, 0, 0), 0.2f, roomLayer);
//计算初始房间到该房间的步数
//在这里调用方法来设置房间到初始房间的步数
//生成房间的墙壁
if (room.upRoom)
room.numDoors++;
if (room.downRoom)
room.numDoors++;
if (room.leftRoom)
room.numDoors++;
if (room.rightRoom)
room.numDoors++;
switch (room.numDoors)
{
//只有一扇门的墙壁,下同
case 1:
{
if (room.upRoom)
Instantiate(wall.Wall_Up, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom)
Instantiate(wall.Wall_Down, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom)
Instantiate(wall.Wall_Left, room.gameObject.transform.position, Quaternion.identity);
else if (room.rightRoom)
Instantiate(wall.Wall_Right, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 2:
{
if (room.upRoom && room.leftRoom)
Instantiate(wall.Wall_UpLeft, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.rightRoom)
Instantiate(wall.Wall_UpRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom)
Instantiate(wall.Wall_UpDown, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom && room.rightRoom)
Instantiate(wall.Wall_LeftRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom && room.rightRoom)
Instantiate(wall.Wall_DownRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom && room.leftRoom)
Instantiate(wall.Wall_DownLeft, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 3:
{
if (room.leftRoom && room.rightRoom && room.upRoom)
Instantiate(wall.Wall_LeftRightUp, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom && room.rightRoom && room.downRoom)
Instantiate(wall.Wall_LeftRightDown, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom && room.leftRoom)
Instantiate(wall.Wall_UpDownLeft, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom && room.rightRoom)
Instantiate(wall.Wall_UpDownRight, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 4:
{
Instantiate(wall.Wall_All, room.gameObject.transform.position, Quaternion.identity);
break;
}
}
}
}
}
//新建一个类来保存所有墙壁的预制体,方便管理
[Serializable]//让它显示在Inspector面板上
public class Wall
{
public GameObject Wall_Up, Wall_Down, Wall_Left, Wall_Right,
Wall_UpLeft, Wall_UpRight, Wall_UpDown, Wall_LeftRight, Wall_DownRight, Wall_DownLeft,
Wall_LeftRightUp, Wall_LeftRightDown, Wall_UpDownLeft, Wall_UpDownRight,
Wall_All;
}
设置开始与结束房间
设置开始与结束房间,关键在于计算每个房间与初始房间的距离。接下来将介绍三种方法:
-
坐标计算方法
-
DFS寻找方法
-
坐标计算方法
我们的 RoomGenerator 游戏物体和它下面的 Point (代码里为generatorPoint)物体的坐标都为 (0,0,0) ,并且我们直接将生成在这里的房间(第一个房间)看作是开始房间。剩下的就是找结束房间,要求是尽可能离开始房间远,也就是离 (0,0,0) 远,因此我们可以直接通过计算得到。
遍历每个房间,将它们横纵坐标的绝对值加起来,得到步数,将步数写道房间的UI文本上。
得到了每个房间的步数,最后就是标记开始房间和结束房间了,这里我们通过改变房间的颜色来简单实现——开始房间为绿色,结束房间为红色。
方法代码如下:
private int CalculateToFindSteps(Vector3 position)
{
int res = (int)(Mathf.Abs(position.x / xOffset) + Mathf.Abs(position.y / yOffset));
return res;
}
//选择开始房间和结束房间并上色,方便区分
private void PaintRooms()
{
//开始房间
rooms[0].GetComponent<SpriteRenderer>().color = startColor;
Room tempRoom = null;
int count = 0;
foreach(var room in rooms)
{
if(int.Parse(room.stepsText.text)>count)
{
tempRoom = room;
count = int.Parse(room.stepsText.text);
}
}
tempRoom.GetComponent<SpriteRenderer>().color = endColor;
}
计算坐标方法的缺陷,如下图:

该方法将最右下角的房间标记为结束房间,虽然它是距离上最远的,但不是“路程”上最远的房间,虽然不是什么致命的缺陷,但看起来就是很不爽啊!
在下一节 “DFS寻找方法” 结尾来讨论 这两个方法的优缺点。
DFS寻找方法
DFS寻找结束房间的方法是为了解决坐标计算方法而提出的,能够避免结束房间后面还连接着普通房间的情况。
主要思路是:遍历所有房间,每个房间调用DFS方法,将自己的坐标当作参数传进去;DFS方法通过递归,从开始房间即 (0,0,0) 位置开始寻找参数坐标位置,每递归一次步数加一,当走到参数坐标位置时,返回步数。
方法代码如下:
/// <summary>
/// 用DFS的方法计算每个房间到初始房间的步数
/// </summary>
/// <param name="roomTransform">该房间的坐标</param>
/// <param name="pointPosition">当前点的坐标,初始值为初始房间的坐标即(0,0,0)</param>
private void DfsToFindSteps(Transform roomTransform,Vector3 pointPosition)
{
//当已经走到了目标房间,则不继续递归,返回
if (isFindRoom)
return;
//当当前位置没有房间或走到了重复房间,则返回剪枝
if (!Physics2D.OverlapCircle(pointPosition, 0.2f, roomLayer) || record.Contains(pointPosition))
{
return;
}
//走到了目标房间
if (pointPosition == roomTransform.position)
{
isFindRoom = true;
return;
}
//已走过房间也要排除在外,剪枝,用list存储已走过房间
stepsToStart++;
record.Add(pointPosition);
DfsToFindSteps(roomTransform, pointPosition + new Vector3(0, yOffset, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(0, -yOffset, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(-xOffset, 0, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(xOffset, 0, 0));
if (!isFindRoom)
{
//进入这里,说明当前坐标延申出的四个房间走不通,因此执行下面两句
stepsToStart--;
record.Remove(pointPosition);
}
}
DFS有寻找顺序

可以看到,由于我们算法写的搜索顺序为上、下、左、右,因此最后的结束房间为——从开始房间上方开始,绕了一圈回到了开始房间下方。(好蠢啊,虽然这种情况是小概率事件,具体看生成的房间分布情况)
坐标计算方法与DFS寻找方法优缺点:
坐标计算方法的优点在于:算法简单,快;不会出现路程较远的房间在开始房间旁边的情况。 但也有一个缺陷:会出现步数最大房间的后哦面还连通着另一个房间——这就导致,若步数最大的房间作为BOSS通关房间,那么后面一个房间就进不去了。 解决方案是在击败BOSS后,仍然可以探索地图(设计上);或者在该房间周围再生成一个新房间作为BOSS房(技术上)。
DFS方法优点在于:不会出现必须经过BOSS房才能进入下一个房间的情况。 但缺点也很明显:算法较难,当房间较多时时间成本高;可能会出现步数最大房间在初始房间旁边的情况; 第二个缺点,是因为DFS其实也是有搜索方向优先级的,这个优先级由我们自己定义。 解决方案也有两种:设置一个成就,比如探索第一个房间就为BOSS房,并击败了BOSS,这样会让玩家以为这是游戏的隐藏机制(设计上);或者计算房间与初始房间的相对坐标通过相对坐标来决定DFS的查找方向优先级——比如房间在初始房间的左上角,则优先执行向上和向左的搜索,其他情况同理(技术上)。
BFS寻找方法
BFS寻找方法能完美解决上面两种方法的缺陷,为什么不先介绍BFS呢?因为如果先介绍了那前面两种算法该放在文章的哪里呢嘎嘎嘎嘎嘎(?
BFS是一轮一轮地检查队列里的元素的,我们让每一轮只检查上一轮所有房间的所有邻居房间,如果检查到的位置与目标房间位置相等,则返回;若这一轮没有检查到,则将这一轮房间的所有邻居房间入队,并且记录步数加一,直到找到目标房间位置。
具体方法代码如下:
/// <summary>
/// 用BFS的方式来计算开始房间到该房间的步数
/// </summary>
/// <param name="roomTransform">该房间的位置</param>
/// <remarks>
/// 从开始房间开始BFS,每次对内元素出队就与 roomTransform 的位置进行比较
/// 若相等,表示找到了,将它的 stepsToStart 值设置好,并返回
/// stepsToStart 的值如何确定?——
/// 每执行完“一轮”出队操作,将 count + 1,count 就是步数
/// </remarks>
private void BfsToFindSteps(Transform roomTransform)
{
stepsToStart = 0;
record.Clear();
int count = 0;
bfsQueue.Enqueue(rooms[0].transform.position);
Vector3 pointPosition;//用来找邻居坐标
while(bfsQueue.Count!=0)
{
int size = bfsQueue.Count;
for(int i=0;i<size;++i)
{
Vector3 tempTransform = bfsQueue.Dequeue();
if(roomTransform.position==tempTransform)//走到了目标房间,给出步数并返回
{
stepsToStart = count;
bfsQueue.Clear();
return;
}
record.Add(tempTransform);
pointPosition = tempTransform;//要开始找邻居坐标了!站好位置
//邻居入队
if(!record.Contains(pointPosition + new Vector3(0, yOffset, 0)) && Physics2D.OverlapCircle(pointPosition+new Vector3(0,yOffset,0),0.2f,roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(0, yOffset, 0));
}
if (!record.Contains(pointPosition + new Vector3(0, -yOffset, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(0, -yOffset, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(0, -yOffset, 0));
}
if (!record.Contains(pointPosition + new Vector3(-xOffset, 0, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(-xOffset, 0, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(-xOffset, 0, 0));
}
if (!record.Contains(pointPosition + new Vector3(xOffset, 0, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(xOffset, 0, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(xOffset, 0, 0));
}
}
count++;
}
}
总结
RoomGenerator.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class RoomGenerator : MonoBehaviour
{
//生成房间的四个方向
public enum Direction { up,down,left,right};
public Direction direction;
[Header("房间信息")]
public GameObject roomPrefab;
public int roomNumber;
public Color startColor, endColor;
private int stepsToStart;
[Header("位置控制")]
public Transform generatorPoint;
public float xOffset, yOffset;
public List<Room> rooms = new List<Room>();
public LayerMask roomLayer;
public List<Vector3> record = new List<Vector3>();//DFS和BFS的记录数组
public bool isFindRoom;//DFS查找方式会用到
public Queue<Vector3> bfsQueue = new Queue<Vector3>();//BFS的队列
public Wall wall;
void Start()
{
GenerateRoom();
SetupRooms();
PaintRooms();
}
void Update()
{
if(Input.anyKeyDown)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
/// <summary>
/// 随机生成房间:
/// 在generatorPoint的位置生成room,并加入List →→
/// 随机移动generatorPoint →→ #1 or #2
/// 如果移动后的位置没有room,则执行开头步骤 #1
/// 如果移动后的位置有room,则继续随机移动 #2
/// </summary>
private void GenerateRoom()
{
for (int i = 0; i < roomNumber; i++)
{
//生成房间并将其脚本加入进rooms数组
rooms.Add(Instantiate(roomPrefab, generatorPoint.position, Quaternion.identity).GetComponent<Room>());
//移动generatorPoint
//为了解决room重叠问题而添加循环检测
//当generatorPoint走到有room的位置时,继续走,直到走到空位置
do
{
direction = (Direction)UnityEngine.Random.Range(0, 4);//取值为0到3整数
switch (direction)
{
case Direction.up:
generatorPoint.position += new Vector3(0, yOffset, 0);
break;
case Direction.down:
generatorPoint.position += new Vector3(0, -yOffset, 0);
break;
case Direction.left:
generatorPoint.position += new Vector3(-xOffset, 0, 0);
break;
case Direction.right:
generatorPoint.position += new Vector3(xOffset, 0, 0);
break;
}
} while (Physics2D.OverlapCircle(generatorPoint.position,0.2f,roomLayer));
}
}
//选择开始房间和结束房间并上色,方便区分
private void PaintRooms()
{
//开始房间
rooms[0].GetComponent<SpriteRenderer>().color = startColor;
//用Dfs方法后,上色
//DfsPaint();
//上色
Paint();
}
/// <summary>
/// 用DFS方法后的上色方法
/// </summary>
/// <remarks>
/// 为什么要给用DFS方法计算步数后再单独写一个它专属的上色方法?
/// 因为DFS的bug(feature),可能会出现两个或以上的最大步数房间
/// 但这两个房间与初始房间的直线距离可能不同
/// 我们就将直线距离长的作为最终房间
/// </remarks>
private void DfsPaint()
{
int index = 0;
//存放步数相同房间的索引
List<int> tempRooms = new List<int>();
for (int i = 1; i < roomNumber; ++i)
{
if (int.Parse(rooms[i].stepsText.text) > int.Parse(rooms[index].stepsText.text))
index = i;
//如果有步数相同的,则存储起来,后面选择距离远的那一个房间
if (int.Parse(rooms[i].stepsText.text) == int.Parse(rooms[index].stepsText.text))
tempRooms.Add(i);
}
if (tempRooms.Count > 0)
{
for (int i = 0; i < tempRooms.Count; ++i)
{
//如果该房间步数等于最大步数
if (int.Parse(rooms[tempRooms[i]].stepsText.text) == int.Parse(rooms[index].stepsText.text))
{
//并且该房间离开始房间的距离比index房间距离长
if (rooms[tempRooms[i]].gameObject.transform.position.sqrMagnitude > rooms[index].gameObject.transform.position.sqrMagnitude)
{
//那么index更改为该房间的索引
index = tempRooms[i];
}
}
}
}
rooms[index].GetComponent<SpriteRenderer>().color = endColor;
}
/// <summary>
/// 给最终房间上色
/// </summary>
private void Paint()
{
Room tempRoom = null;
int count = 0;
foreach(var room in rooms)
{
if(int.Parse(room.stepsText.text)>count)
{
tempRoom = room;
count = int.Parse(room.stepsText.text);
}
}
tempRoom.GetComponent<SpriteRenderer>().color = endColor;
}
/// <summary>
/// 设置房间,包括房间的门、到初始房间的距离、添加房间的墙壁
/// </summary>
/// <remarks>
/// 1、设置房间的门,若房间四个方向上有其他房间,则添加门
/// 2、计算初始房间到每个房间的距离(走几步)
/// 3、给房间添加墙壁:先看该room有几扇门,再判断是哪几扇门,最后生成对应墙壁的预制体
/// </remarks>
private void SetupRooms()
{
foreach(var room in rooms)
{
//设置门
room.upRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(0, yOffset, 0), 0.2f, roomLayer);
room.downRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(0, -yOffset, 0), 0.2f, roomLayer);
room.leftRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(-xOffset, 0, 0), 0.2f, roomLayer);
room.rightRoom = Physics2D.OverlapCircle(room.transform.position + new Vector3(xOffset, 0, 0), 0.2f, roomLayer);
//计算初始房间到该房间的步数
/******************************************************/
//用DFS的方法计算初始房间到该房间的步数
//stepsToStart = 0;
//record.Clear();
//isFindRoom = false;
//DfsToFindSteps(room.gameObject.transform, new Vector3(0, 0, 0));
//room.stepsText.text = stepsToStart.ToString();
/******************************************************/
/******************************************************/
//用计算坐标的方法计算初始房间到该房间的步数
//room.stepsText.text = CalculateToFindSteps(room.gameObject.transform.position).ToString();
/******************************************************/
/******************************************************/
//用BFS的方法计算初始房间到该房间的步数
BfsToFindSteps(room.gameObject.transform);
room.stepsText.text = stepsToStart.ToString();
/******************************************************/
//生成房间的墙壁
if (room.upRoom)
room.numDoors++;
if (room.downRoom)
room.numDoors++;
if (room.leftRoom)
room.numDoors++;
if (room.rightRoom)
room.numDoors++;
switch (room.numDoors)
{
//只有一扇门的墙壁,下同
case 1:
{
if (room.upRoom)
Instantiate(wall.Wall_Up, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom)
Instantiate(wall.Wall_Down, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom)
Instantiate(wall.Wall_Left, room.gameObject.transform.position, Quaternion.identity);
else if (room.rightRoom)
Instantiate(wall.Wall_Right, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 2:
{
if (room.upRoom && room.leftRoom)
Instantiate(wall.Wall_UpLeft, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.rightRoom)
Instantiate(wall.Wall_UpRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom)
Instantiate(wall.Wall_UpDown, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom && room.rightRoom)
Instantiate(wall.Wall_LeftRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom && room.rightRoom)
Instantiate(wall.Wall_DownRight, room.gameObject.transform.position, Quaternion.identity);
else if (room.downRoom && room.leftRoom)
Instantiate(wall.Wall_DownLeft, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 3:
{
if (room.leftRoom && room.rightRoom && room.upRoom)
Instantiate(wall.Wall_LeftRightUp, room.gameObject.transform.position, Quaternion.identity);
else if (room.leftRoom && room.rightRoom && room.downRoom)
Instantiate(wall.Wall_LeftRightDown, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom && room.leftRoom)
Instantiate(wall.Wall_UpDownLeft, room.gameObject.transform.position, Quaternion.identity);
else if (room.upRoom && room.downRoom && room.rightRoom)
Instantiate(wall.Wall_UpDownRight, room.gameObject.transform.position, Quaternion.identity);
break;
}
case 4:
{
Instantiate(wall.Wall_All, room.gameObject.transform.position, Quaternion.identity);
break;
}
}
}
}
/// <summary>
/// 用DFS的方法计算每个房间到初始房间的步数
/// </summary>
/// <param name="roomTransform">该房间的坐标</param>
/// <param name="pointPosition">当前点的坐标,初始值为初始房间的坐标即(0,0,0)</param>
private void DfsToFindSteps(Transform roomTransform,Vector3 pointPosition)
{
//当已经走到了目标房间,则不继续递归,返回
if (isFindRoom)
return;
//当当前位置没有房间或走到了重复房间,则返回剪枝
if (!Physics2D.OverlapCircle(pointPosition, 0.2f, roomLayer) || record.Contains(pointPosition))
{
return;
}
//走到了目标房间
if (pointPosition == roomTransform.position)
{
isFindRoom = true;
return;
}
//已走过房间也要排除在外,剪枝,用list存储已走过房间
stepsToStart++;
record.Add(pointPosition);
DfsToFindSteps(roomTransform, pointPosition + new Vector3(0, yOffset, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(0, -yOffset, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(-xOffset, 0, 0));
DfsToFindSteps(roomTransform, pointPosition + new Vector3(xOffset, 0, 0));
if (!isFindRoom)
{
//进入这里,说明当前坐标延申出的四个房间走不通,因此执行下面两句
stepsToStart--;
record.Remove(pointPosition);
}
}
private int CalculateToFindSteps(Vector3 position)
{
int res = (int)(Mathf.Abs(position.x / xOffset) + Mathf.Abs(position.y / yOffset));
return res;
}
/*DFS和计算房间坐标两种方法都可以实现查找房间与起点房间的步数
* 计算房间坐标的方法优点在于:
* 算法简单,快;不会出现步数较大的房间在起点房间旁边的情况
* 但也有一个缺陷:会出现步数最大房间的后面还连通着另一个房间——
* 这就导致,若步数最大房间作为boss通关房间,那么后面一个房间就进不去了
* 解决方案是在击败boss后,仍然可以探索地图(设计上);或者在该房间周围再生成一个新房间作为boss房(技术上)
*
* DFS方法优点在于:不会出现必须经过boss房才能进入下一个房间的情况
* 但缺点也很明显:算法较难,当房间较多时时间成本高;可能会出现步数最大房间在初始房间旁边的情况
* 第二个缺点,是因为DFS其实也是有搜索方向优先级的,这个优先级由我们自己定义
* 如上面的代码就是以“上下左右”的顺序来查找到达房间的路径
* 解决方案也有两种:
* 设计上:设置一个成就,如探索第一个房间就为boss房,并击败了boss,这样会让玩家以为这是游戏的隐藏机制
* 技术上:计算房间与初始房间的相对坐标,通过相对坐标来决定DFS的查找方向优先级——
* 比如若房间在初始房间的左上角,则优先执行向上和向左的搜索,其他情况同理
*/
/// <summary>
/// 用BFS的方式来计算开始房间到该房间的步数
/// </summary>
/// <param name="roomTransform">该房间的位置</param>
/// <remarks>
/// 从开始房间开始BFS,每次对内元素出队就与 roomTransform 的位置进行比较
/// 若相等,表示找到了,将它的 stepsToStart 值设置好,并返回
/// stepsToStart 的值如何确定?——
/// 每执行完“一轮”出队操作,将 count + 1,count 就是步数
/// </remarks>
private void BfsToFindSteps(Transform roomTransform)
{
stepsToStart = 0;
record.Clear();
int count = 0;
bfsQueue.Enqueue(rooms[0].transform.position);
Vector3 pointPosition;//用来找邻居坐标
while(bfsQueue.Count!=0)
{
int size = bfsQueue.Count;
for(int i=0;i<size;++i)
{
Vector3 tempTransform = bfsQueue.Dequeue();
if(roomTransform.position==tempTransform)//走到了目标房间,给出步数并返回
{
stepsToStart = count;
bfsQueue.Clear();
return;
}
record.Add(tempTransform);
pointPosition = tempTransform;//要开始找邻居坐标了!站好位置
//邻居入队
if(!record.Contains(pointPosition + new Vector3(0, yOffset, 0)) && Physics2D.OverlapCircle(pointPosition+new Vector3(0,yOffset,0),0.2f,roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(0, yOffset, 0));
}
if (!record.Contains(pointPosition + new Vector3(0, -yOffset, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(0, -yOffset, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(0, -yOffset, 0));
}
if (!record.Contains(pointPosition + new Vector3(-xOffset, 0, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(-xOffset, 0, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(-xOffset, 0, 0));
}
if (!record.Contains(pointPosition + new Vector3(xOffset, 0, 0)) && Physics2D.OverlapCircle(pointPosition + new Vector3(xOffset, 0, 0), 0.2f, roomLayer))
{
bfsQueue.Enqueue(pointPosition + new Vector3(xOffset, 0, 0));
}
}
count++;
}
}
}
//新建一个类来保存所有墙壁的预制体,方便管理
[Serializable]//让它显示在Inspector面板上
public class Wall
{
public GameObject Wall_Up, Wall_Down, Wall_Left, Wall_Right,
Wall_UpLeft, Wall_UpRight, Wall_UpDown, Wall_LeftRight, Wall_DownRight, Wall_DownLeft,
Wall_LeftRightUp, Wall_LeftRightDown, Wall_UpDownLeft, Wall_UpDownRight,
Wall_All;
}