Unity 随机房间地图生成

时间:2023-03-09 06:19:58
Unity 随机房间地图生成

无论是在迷宫还是类似于地牢的游戏地图中,利用程序来生成每次都不一样的地图是一件叫人兴奋不已的事。

这时我们需要解决两个非常重要的随机事件:

1.在一定范围内随机出各不相同但又不能互相重叠的房间

2.优美生成连接这些房间的通道

基本的UML思路图:

Unity 随机房间地图生成

这次我们先讨论如何快速生成符合各种随机要求的房间。

一般来说,一个房间的高度是一个相对固定的值,可以根据面板上的参数进行必要的调整,而真正参与随机的应该是房间的长,宽和位置。

建立房间的数据结构,根据需求可以随时补充和添加:

 using System.Collections.Generic;
using UnityEngine; public class RoomData
{
public int Id;
//房间的Transform等属性
public RoomTran RoomTran;
//该房间的战斗类型
public RoomBattleType BattleType;
//该房间与哪些其余房间互相连接
public List<RoomData> CrossRooms;
//房间内的怪物列表
public List<GameObject> Monsters;
//是否是端点房间
public bool bEndRoom;
//是否是主路径房间
public bool bMainCrossRoom;
} public class RoomTran
{
public int Length;
public int Width;
//长宽中心点
public Vector2Int CenterPos;
//高度位置
public float PosY;
} public enum RoomBattleType
{
Rest,
NormalBattle,
BossBattle
}

RoonBuilder属性和控制参数:

     //建筑单位方块
public GameObject BuildUnit; //房间高度值
public int FixedUnitHeight;
//生成的房间层数
public int LayerCount;
//长宽随机范围
public Vector2Int GenRange; //随机类型
public RoomRandType RandType;
//随机的房间形状类型
public RoomShapeType Shape; //房间大小的随机列表,用于枚举随机
public List<Vector2Int> RoomRandSizes = new List<Vector2Int>(); //随机的房间最大面积
public int MaxRoomArea;
//最大随机数量(随机试验次数)
public int MaxRoomCount; //最小边长度
private int MinRoomEdge;
//最大长宽比
public int MaxLengthWidthScale = ; //标准方向
Vector3Int Dx = new Vector3Int(, , );
Vector3Int Dy = new Vector3Int(, , );
Vector3Int Dz = new Vector3Int(, , ); //建筑单位标签
const string S_TAG = "Unit"; private MapSystem MapManager;

单房间轮廓生成:

     /// <summary>
/// 生成单一房间的轮廓
/// </summary>
/// <param name="centerPos">房间中点位置</param>
/// <param name="length">长</param>
/// <param name="width">宽</param>
/// <param name="parent">父物体</param>
void GenOneRoom(Vector3 centerPos, int length, int width, Transform parent = null)
{
var to = new Vector3(length - , FixedUnitHeight - , width - ) * .5f; //顶点
var ned = centerPos - to;
var fod = centerPos + to; var v3 = new Vector3(ned.x, fod.y, ned.z);
var v4 = new Vector3(ned.x, fod.y, fod.z);
var v5 = new Vector3(ned.x, ned.y, fod.z); var v6 = new Vector3(fod.x, ned.y, ned.z);
var v7 = new Vector3(fod.x, ned.y, fod.z);
var v8 = new Vector3(fod.x, fod.y, ned.z); //顶点位置(8个)
InsSetPos(ned, parent);
InsSetPos(fod, parent);
InsSetPos(v3, parent);
InsSetPos(v4, parent);
InsSetPos(v5, parent);
InsSetPos(v6, parent);
InsSetPos(v7, parent);
InsSetPos(v8, parent); //12条棱(4*3)
//长
InsOneEdge(length, ned, Dx, parent);
InsOneEdge(length, v3, Dx, parent);
InsOneEdge(length, v4, Dx, parent);
InsOneEdge(length, v5, Dx, parent);
//高
InsOneEdge(FixedUnitHeight, ned, Dy, parent);
InsOneEdge(FixedUnitHeight, v5, Dy, parent);
InsOneEdge(FixedUnitHeight, v6, Dy, parent);
InsOneEdge(FixedUnitHeight, v7, Dy, parent);
//宽
InsOneEdge(width, ned, Dz, parent);
InsOneEdge(width, v3, Dz, parent);
InsOneEdge(width, v6, Dz, parent);
InsOneEdge(width, v8, Dz, parent);
} //生成一条边上的建筑单位但不包含顶点位置
void InsOneEdge(int edge, Vector3 v, Vector3 dir, Transform parent = null)
{
//忽略首尾单位
for (int i = ; i < edge - ; i++)
{
InsSetPos(v + i * dir, parent);
}
} void InsSetPos(Vector3 pos, Transform parent = null)
{
var ins = Instantiate(BuildUnit);
ins.transform.position = pos;
ins.transform.parent = parent;
}

这里唯一值得注意的地方是房间顶点位置的单位不要重复生成。(因为想偷懒的话真的很容易重复Orz)。

随机RoomTran结构:

     RoomTran RanRoomTran(Vector3 centerPos)
{
var rt = new RoomTran(); switch (RandType)
{
case RoomRandType.AllRand:
int temp;
var oe = MaxRoomArea / MinRoomEdge;
switch (Shape)
{
case RoomShapeType.LengthMain:
rt.Length = Random.Range(MinRoomEdge + , oe + );
temp = MaxRoomArea / rt.Length;
if (temp >= rt.Length)
rt.Width = Random.Range(MinRoomEdge, rt.Length);
else
rt.Width = Random.Range(MinRoomEdge, temp + );
break;
case RoomShapeType.WidthMain:
rt.Width = Random.Range(MinRoomEdge + , oe + );
temp = MaxRoomArea / rt.Width;
if (temp >= rt.Width)
rt.Length = Random.Range(MinRoomEdge, rt.Width);
else
rt.Length = Random.Range(MinRoomEdge, temp + );
break;
case RoomShapeType.Coustom:
rt.Length = Random.Range(MinRoomEdge, oe + );
temp = MaxRoomArea / rt.Length;
rt.Width = Random.Range(MinRoomEdge, temp + );
break;
}
break;
case RoomRandType.EnumRand:
var rc = RoomRandSizes.Count;
if (rc == )
{
//未填写时设定随机默认值
rt.Length = ;
rt.Width = ;
}
else
{
var ridx = Random.Range(,rc);
var t = RoomRandSizes[ridx];
if (t.x < || t.y < )
{
//填写错误时设定随机默认值
rt.Length = ;
rt.Width = ;
}
else
{
switch (Shape)
{
case RoomShapeType.LengthMain:
rt.Length = t.x > t.y ? t.x : t.y;
rt.Width = t.x < t.y ? t.x : t.y;
break;
case RoomShapeType.WidthMain:
rt.Width = t.x > t.y ? t.x : t.y;
rt.Length = t.x < t.y ? t.x : t.y;
break;
case RoomShapeType.Coustom:
rt.Length = Random.value < .5f ? t.x : t.y;
rt.Width = t.y == rt.Length ? t.x : t.y;
break;
}
}
}
break;
} rt.CenterPos = new Vector2Int(Random.Range((int)(centerPos.x - GenRange.x * .5f), (int)(centerPos.x + GenRange.x * .5f)),
Random.Range((int)(centerPos.z - GenRange.y * .5f), (int)(centerPos.z + GenRange.y * .5f))); rt.PosY = centerPos.y; var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); //射线检测重叠
if (RayRoomCheck(roomCenter, rt.Length, rt.Width))
{
return null;
}
return rt;
}

用的是射线检测重叠,生成了重叠的房间就会被视作是一次失败的随机试验,之前尝试过直接用物理系统推开失败了,可能是使用有误,如果有知道原因的欢迎与笔者分享,共同进步;

当然了,你也可以用Unity自带的Bounds.Intersects(Bounds other)方法来判断两个生成的房间盒子是否重叠,但缺点是没有办法控制两个房间边缘的间隔的最小距离,用纯粹的数学方法判断三个轴向的边缘值大小同样也是可行的:

     //生成房间前射线检测下范围内有无其他房间
bool RayRoomCheck(Vector3 cp, int length, int width)
{
bool result = false;
//长宽至少留一格间隙,高度与地板格对齐
var to = new Vector3(length + , FixedUnitHeight - , width + ) * .5f;
var ned = cp - to; var vx2 = ned + new Vector3(, , width + ) * .5f;
var vx3 = ned + new Vector3(, , width + ); var vx4 = ned + new Vector3(length + , , width * .5f + .5f);
var vx5 = ned + new Vector3(length + , , width + ); var vz2 = ned + new Vector3(length + , , ) * .5f;
var vz3 = ned + new Vector3(length + , , ); var vz4 = ned + new Vector3(length * .5f + .5f, , width + );
var vz5 = ned + new Vector3(length + , , width + ); result =
//4组射线,每组3条
RayCast(ned, Dx, length + , S_TAG) ||
RayCast(vx2, Dx, length + , S_TAG) ||
RayCast(vx3, Dx, length + , S_TAG) || RayCast(vx4, Dx * -, length + , S_TAG) ||
RayCast(vx5, Dx * -, length + , S_TAG) ||
RayCast(vz3, Dx * -, length + , S_TAG) || RayCast(ned, Dz, width + , S_TAG) ||
RayCast(vz2, Dz, width + , S_TAG) ||
RayCast(vz3, Dz, width + , S_TAG) || RayCast(vz4, Dz * -, width + , S_TAG) ||
RayCast(vz5, Dz * -, width + , S_TAG) ||
RayCast(vx3, Dz * -, width + , S_TAG); return result;
}

这里将射线的起点和终点都延长了一格,是为了避免两个生成的房间贴得太紧,这样至少每个房间与其它房间间隔一个单位格或以上。

完整的房间结构生成脚本:

 using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events; public enum RoomRandType
{
//全随机
AllRand,
//枚举大小随机
EnumRand
} public enum RoomShapeType
{
//宽>=长
WidthMain,
//长>=宽
LengthMain,
//自定义,无形状要求
Coustom
}
//x-length z-width y-height public class RoomBuilder : MonoBehaviour
{
//建筑单位方块
public GameObject BuildUnit; //房间高度值
public int FixedUnitHeight;
//生成的房间层数
public int LayerCount;
//长宽随机范围
public Vector2Int GenRange; //随机类型
public RoomRandType RandType;
//随机的房间形状类型
public RoomShapeType Shape; //房间大小的随机列表,用于枚举随机
public List<Vector2Int> RoomRandSizes = new List<Vector2Int>(); //随机的房间最大面积
public int MaxRoomArea;
//最大随机数量(随机试验次数)
public int MaxRoomCount; //最小边长度
private int MinRoomEdge;
//最大长宽比
public int MaxLengthWidthScale = ; //标准方向
Vector3Int Dx = new Vector3Int(, , );
Vector3Int Dy = new Vector3Int(, , );
Vector3Int Dz = new Vector3Int(, , ); //建筑单位标签
const string S_TAG = "Unit"; private MapSystem MapManager; void Awake()
{
MapManager = GetComponent<MapSystem>();
} public IEnumerator GenRooms(Vector3Int centerPos,UnityAction complete)
{
var temp = (int)Mathf.Sqrt(MaxRoomArea * 1.0f / MaxLengthWidthScale);
MinRoomEdge = temp > ? temp : ; //每层至少1
for (int i = ; i <= LayerCount; i++)
{
SetGenOneRoom(centerPos, i);
yield return new WaitForSeconds(.1f);
} //超过的随机布置
var oc = MaxRoomCount - LayerCount;
if (oc > )
{
for (int i = ; i <= oc; i++)
{
var r = Random.Range(, LayerCount + );
SetGenOneRoom(centerPos, r);
yield return new WaitForSeconds(.1f);
}
} //所有房间生成完成后发送一个委托信号,以便后续创建房间数据和计算必要连接
complete();
} void SetGenOneRoom(Vector3Int cp, int r)
{
var layerCenter = cp - new Vector3(, (LayerCount - * r + ) * .5f * FixedUnitHeight, ); var rt = RanRoomTran(layerCenter);
if (rt != null)
{
var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); GameObject temp = new GameObject(r.ToString());
temp.transform.position = roomCenter;
temp.tag = S_TAG; //给生成的房间添加碰撞盒子并设置大小
GenOneRoom(roomCenter, rt.Length, rt.Width, temp.transform);
var bc = temp.AddComponent<BoxCollider>();
bc.size = new Vector3(rt.Length, FixedUnitHeight, rt.Width); //目前用物理方式似乎难以推开重叠的房间,可能是哪里使用方法有误,改为用用射线检测解决...
//var rb = temp.AddComponent<Rigidbody>();
//rb.useGravity = false;
//rb.drag = Mathf.Infinity;
//rb.constraints = RigidbodyConstraints.FreezePositionY;
//rb.freezeRotation = true; //将房间数据存入临时列表
MapManager.GenRooms.Add(rt);
MapManager.UnCrossRooms.Add(rt);
}
} RoomTran RanRoomTran(Vector3 centerPos)
{
var rt = new RoomTran(); switch (RandType)
{
case RoomRandType.AllRand:
int temp;
var oe = MaxRoomArea / MinRoomEdge;
switch (Shape)
{
case RoomShapeType.LengthMain:
rt.Length = Random.Range(MinRoomEdge + , oe + );
temp = MaxRoomArea / rt.Length;
if (temp >= rt.Length)
rt.Width = Random.Range(MinRoomEdge, rt.Length);
else
rt.Width = Random.Range(MinRoomEdge, temp + );
break;
case RoomShapeType.WidthMain:
rt.Width = Random.Range(MinRoomEdge + , oe + );
temp = MaxRoomArea / rt.Width;
if (temp >= rt.Width)
rt.Length = Random.Range(MinRoomEdge, rt.Width);
else
rt.Length = Random.Range(MinRoomEdge, temp + );
break;
case RoomShapeType.Coustom:
rt.Length = Random.Range(MinRoomEdge, oe + );
temp = MaxRoomArea / rt.Length;
rt.Width = Random.Range(MinRoomEdge, temp + );
break;
}
break;
case RoomRandType.EnumRand:
var rc = RoomRandSizes.Count;
if (rc == )
{
//未填写时设定随机默认值
rt.Length = ;
rt.Width = ;
}
else
{
var ridx = Random.Range(,rc);
var t = RoomRandSizes[ridx];
if (t.x < || t.y < )
{
//填写错误时设定随机默认值
rt.Length = ;
rt.Width = ;
}
else
{
switch (Shape)
{
case RoomShapeType.LengthMain:
rt.Length = t.x > t.y ? t.x : t.y;
rt.Width = t.x < t.y ? t.x : t.y;
break;
case RoomShapeType.WidthMain:
rt.Width = t.x > t.y ? t.x : t.y;
rt.Length = t.x < t.y ? t.x : t.y;
break;
case RoomShapeType.Coustom:
rt.Length = Random.value < .5f ? t.x : t.y;
rt.Width = t.y == rt.Length ? t.x : t.y;
break;
}
}
}
break;
} rt.CenterPos = new Vector2Int(Random.Range((int)(centerPos.x - GenRange.x * .5f), (int)(centerPos.x + GenRange.x * .5f)),
Random.Range((int)(centerPos.z - GenRange.y * .5f), (int)(centerPos.z + GenRange.y * .5f))); rt.PosY = centerPos.y; var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); //射线检测重叠
if (RayRoomCheck(roomCenter, rt.Length, rt.Width))
{
return null;
}
return rt;
} //生成房间前射线检测下范围内有无其他房间
bool RayRoomCheck(Vector3 cp, int length, int width)
{
bool result = false;
//长宽至少留一格间隙,高度与地板格对齐
var to = new Vector3(length + , FixedUnitHeight - , width + ) * .5f;
var ned = cp - to; var vx2 = ned + new Vector3(, , width + ) * .5f;
var vx3 = ned + new Vector3(, , width + ); var vx4 = ned + new Vector3(length + , , width * .5f + .5f);
var vx5 = ned + new Vector3(length + , , width + ); var vz2 = ned + new Vector3(length + , , ) * .5f;
var vz3 = ned + new Vector3(length + , , ); var vz4 = ned + new Vector3(length * .5f + .5f, , width + );
var vz5 = ned + new Vector3(length + , , width + ); result =
//4组射线,每组3条
RayCast(ned, Dx, length + , S_TAG) ||
RayCast(vx2, Dx, length + , S_TAG) ||
RayCast(vx3, Dx, length + , S_TAG) || RayCast(vx4, Dx * -, length + , S_TAG) ||
RayCast(vx5, Dx * -, length + , S_TAG) ||
RayCast(vz3, Dx * -, length + , S_TAG) || RayCast(ned, Dz, width + , S_TAG) ||
RayCast(vz2, Dz, width + , S_TAG) ||
RayCast(vz3, Dz, width + , S_TAG) || RayCast(vz4, Dz * -, width + , S_TAG) ||
RayCast(vz5, Dz * -, width + , S_TAG) ||
RayCast(vx3, Dz * -, width + , S_TAG); return result;
} bool RayCast(Vector3 ori, Vector3 dir, float mD, string tag)
{
Ray ray = new Ray(ori, dir);
RaycastHit info;
if (Physics.Raycast(ray, out info, mD))
{
if (info.transform.tag == tag)
return true;
}
return false;
} /// <summary>
/// 生成单一房间的轮廓
/// </summary>
/// <param name="centerPos">房间中点位置</param>
/// <param name="length">长</param>
/// <param name="width">宽</param>
/// <param name="parent">父物体</param>
void GenOneRoom(Vector3 centerPos, int length, int width, Transform parent = null)
{
var to = new Vector3(length - , FixedUnitHeight - , width - ) * .5f; //顶点
var ned = centerPos - to;
var fod = centerPos + to; var v3 = new Vector3(ned.x, fod.y, ned.z);
var v4 = new Vector3(ned.x, fod.y, fod.z);
var v5 = new Vector3(ned.x, ned.y, fod.z); var v6 = new Vector3(fod.x, ned.y, ned.z);
var v7 = new Vector3(fod.x, ned.y, fod.z);
var v8 = new Vector3(fod.x, fod.y, ned.z); //顶点位置(8个)
InsSetPos(ned, parent);
InsSetPos(fod, parent);
InsSetPos(v3, parent);
InsSetPos(v4, parent);
InsSetPos(v5, parent);
InsSetPos(v6, parent);
InsSetPos(v7, parent);
InsSetPos(v8, parent); //12条棱(4*3)
//长
InsOneEdge(length, ned, Dx, parent);
InsOneEdge(length, v3, Dx, parent);
InsOneEdge(length, v4, Dx, parent);
InsOneEdge(length, v5, Dx, parent);
//高
InsOneEdge(FixedUnitHeight, ned, Dy, parent);
InsOneEdge(FixedUnitHeight, v5, Dy, parent);
InsOneEdge(FixedUnitHeight, v6, Dy, parent);
InsOneEdge(FixedUnitHeight, v7, Dy, parent);
//宽
InsOneEdge(width, ned, Dz, parent);
InsOneEdge(width, v3, Dz, parent);
InsOneEdge(width, v6, Dz, parent);
InsOneEdge(width, v8, Dz, parent);
} //生成一条边上的建筑单位但不包含顶点位置
void InsOneEdge(int edge, Vector3 v, Vector3 dir, Transform parent = null)
{
//忽略首尾单位
for (int i = ; i < edge - ; i++)
{
InsSetPos(v + i * dir, parent);
}
} void InsSetPos(Vector3 pos, Transform parent = null)
{
var ins = Instantiate(BuildUnit);
ins.transform.position = pos;
ins.transform.parent = parent;
}
}

在MapSystem中可以在房间结构生成完后创建一个默认的数据结构:

     public void RandRoomDatas()
{
if (RoomBuilder == null||MapData ==null)
return; RoomBuilder.StartCoroutine(RoomBuilder.GenRooms(MapData.MapCenter,()=>
{
CreatRoomData();
RandRoomCrosses();
}));
} void CreatRoomData()
{
for (int i = ; i < GenRooms.Count + ; i++)
{
var rd = new RoomData();
rd.Id = i;
rd.RoomTran = GenRooms[i - ];
rd.BattleType = RoomBattleType.NormalBattle;
if (rd.Id == )
rd.BattleType = RoomBattleType.Rest;
rd.CrossRooms = new List<RoomData>();
rd.Monsters = new List<GameObject>();
rd.bEndRoom = false;
rd.bMainCrossRoom = false; MapData.RoomDataDic.Add(rd.Id, rd);
}
}

效果图:(单层-枚举列表随机)

Unity 随机房间地图生成

单层(全随机-长条形房间随机):

Unity 随机房间地图生成

多层(层数5)(自定义-全随机):

Unity 随机房间地图生成

参考资料:

https://indienova.com/indie-game-development/rooms-and-mazes-a-procedural-dungeon-generator/?tdsourcetag=s_pctim_aiomsg

https://mp.weixin.qq.com/s/3yM-mAAXq_fX5tcy82s0uQ