Unity 随机房间地图生成
汐夜 人气:0无论是在迷宫还是类似于地牢的游戏地图中,利用程序来生成每次都不一样的地图是一件叫人兴奋不已的事。
这时我们需要解决两个非常重要的随机事件:
1.在一定范围内随机出各不相同但又不能互相重叠的房间
2.优美生成连接这些房间的通道
基本的UML思路图:
这次我们先讨论如何快速生成符合各种随机要求的房间。
一般来说,一个房间的高度是一个相对固定的值,可以根据面板上的参数进行必要的调整,而真正参与随机的应该是房间的长,宽和位置。
建立房间的数据结构,根据需求可以随时补充和添加:
1 using System.Collections.Generic; 2 using UnityEngine; 3 4 public class RoomData 5 { 6 public int Id; 7 //房间的Transform等属性 8 public RoomTran RoomTran; 9 //该房间的战斗类型 10 public RoomBattleType BattleType; 11 //该房间与哪些其余房间互相连接 12 public List<RoomData> CrossRooms; 13 //房间内的怪物列表 14 public List<GameObject> Monsters; 15 //是否是端点房间 16 public bool bEndRoom; 17 //是否是主路径房间 18 public bool bMainCrossRoom; 19 } 20 21 public class RoomTran 22 { 23 public int Length; 24 public int Width; 25 //长宽中心点 26 public Vector2Int CenterPos; 27 //高度位置 28 public float PosY; 29 } 30 31 public enum RoomBattleType 32 { 33 Rest, 34 NormalBattle, 35 BossBattle 36 }
RoonBuilder属性和控制参数:
1 //建筑单位方块 2 public GameObject BuildUnit; 3 4 //房间高度值 5 public int FixedUnitHeight; 6 //生成的房间层数 7 public int LayerCount; 8 //长宽随机范围 9 public Vector2Int GenRange; 10 11 //随机类型 12 public RoomRandType RandType; 13 //随机的房间形状类型 14 public RoomShapeType Shape; 15 16 //房间大小的随机列表,用于枚举随机 17 public List<Vector2Int> RoomRandSizes = new List<Vector2Int>(); 18 19 //随机的房间最大面积 20 public int MaxRoomArea; 21 //最大随机数量(随机试验次数) 22 public int MaxRoomCount; 23 24 //最小边长度 25 private int MinRoomEdge; 26 //最大长宽比 27 public int MaxLengthWidthScale = 2; 28 29 //标准方向 30 Vector3Int Dx = new Vector3Int(1, 0, 0); 31 Vector3Int Dy = new Vector3Int(0, 1, 0); 32 Vector3Int Dz = new Vector3Int(0, 0, 1); 33 34 //建筑单位标签 35 const string S_TAG = "Unit"; 36 37 private MapSystem MapManager;
单房间轮廓生成:
1 /// <summary> 2 /// 生成单一房间的轮廓 3 /// </summary> 4 /// <param name="centerPos">房间中点位置</param> 5 /// <param name="length">长</param> 6 /// <param name="width">宽</param> 7 /// <param name="parent">父物体</param> 8 void GenOneRoom(Vector3 centerPos, int length, int width, Transform parent = null) 9 { 10 var to = new Vector3(length - 1, FixedUnitHeight - 1, width - 1) * .5f; 11 12 //顶点 13 var ned = centerPos - to; 14 var fod = centerPos + to; 15 16 var v3 = new Vector3(ned.x, fod.y, ned.z); 17 var v4 = new Vector3(ned.x, fod.y, fod.z); 18 var v5 = new Vector3(ned.x, ned.y, fod.z); 19 20 var v6 = new Vector3(fod.x, ned.y, ned.z); 21 var v7 = new Vector3(fod.x, ned.y, fod.z); 22 var v8 = new Vector3(fod.x, fod.y, ned.z); 23 24 //顶点位置(8个) 25 InsSetPos(ned, parent); 26 InsSetPos(fod, parent); 27 InsSetPos(v3, parent); 28 InsSetPos(v4, parent); 29 InsSetPos(v5, parent); 30 InsSetPos(v6, parent); 31 InsSetPos(v7, parent); 32 InsSetPos(v8, parent); 33 34 //12条棱(4*3) 35 //长 36 InsOneEdge(length, ned, Dx, parent); 37 InsOneEdge(length, v3, Dx, parent); 38 InsOneEdge(length, v4, Dx, parent); 39 InsOneEdge(length, v5, Dx, parent); 40 //高 41 InsOneEdge(FixedUnitHeight, ned, Dy, parent); 42 InsOneEdge(FixedUnitHeight, v5, Dy, parent); 43 InsOneEdge(FixedUnitHeight, v6, Dy, parent); 44 InsOneEdge(FixedUnitHeight, v7, Dy, parent); 45 //宽 46 InsOneEdge(width, ned, Dz, parent); 47 InsOneEdge(width, v3, Dz, parent); 48 InsOneEdge(width, v6, Dz, parent); 49 InsOneEdge(width, v8, Dz, parent); 50 } 51 52 //生成一条边上的建筑单位但不包含顶点位置 53 void InsOneEdge(int edge, Vector3 v, Vector3 dir, Transform parent = null) 54 { 55 //忽略首尾单位 56 for (int i = 1; i < edge - 1; i++) 57 { 58 InsSetPos(v + i * dir, parent); 59 } 60 } 61 62 void InsSetPos(Vector3 pos, Transform parent = null) 63 { 64 var ins = Instantiate(BuildUnit); 65 ins.transform.position = pos; 66 ins.transform.parent = parent; 67 }
这里唯一值得注意的地方是房间顶点位置的单位不要重复生成。(因为想偷懒的话真的很容易重复Orz)。
随机RoonTran结构:
1 RoomTran RanRoomTran(Vector3 centerPos) 2 { 3 var rt = new RoomTran(); 4 5 switch (RandType) 6 { 7 case RoomRandType.AllRand: 8 int temp; 9 var oe = MaxRoomArea / MinRoomEdge; 10 switch (Shape) 11 { 12 case RoomShapeType.LengthMain: 13 rt.Length = Random.Range(MinRoomEdge + 1, oe + 1); 14 temp = MaxRoomArea / rt.Length; 15 if (temp >= rt.Length) 16 rt.Width = Random.Range(MinRoomEdge, rt.Length); 17 else 18 rt.Width = Random.Range(MinRoomEdge, temp + 1); 19 break; 20 case RoomShapeType.WidthMain: 21 rt.Width = Random.Range(MinRoomEdge + 1, oe + 1); 22 temp = MaxRoomArea / rt.Width; 23 if (temp >= rt.Width) 24 rt.Length = Random.Range(MinRoomEdge, rt.Width); 25 else 26 rt.Length = Random.Range(MinRoomEdge, temp + 1); 27 break; 28 case RoomShapeType.Coustom: 29 rt.Length = Random.Range(MinRoomEdge, oe + 1); 30 temp = MaxRoomArea / rt.Length; 31 rt.Width = Random.Range(MinRoomEdge, temp + 1); 32 break; 33 } 34 break; 35 case RoomRandType.EnumRand: 36 var rc = RoomRandSizes.Count; 37 if (rc == 0) 38 { 39 //未填写时设定随机默认值 40 rt.Length = 3; 41 rt.Width = 3; 42 } 43 else 44 { 45 var ridx = Random.Range(0,rc); 46 var t = RoomRandSizes[ridx]; 47 if (t.x < 3 || t.y < 3) 48 { 49 //填写错误时设定随机默认值 50 rt.Length = 3; 51 rt.Width = 3; 52 } 53 else 54 { 55 switch (Shape) 56 { 57 case RoomShapeType.LengthMain: 58 rt.Length = t.x > t.y ? t.x : t.y; 59 rt.Width = t.x < t.y ? t.x : t.y; 60 break; 61 case RoomShapeType.WidthMain: 62 rt.Width = t.x > t.y ? t.x : t.y; 63 rt.Length = t.x < t.y ? t.x : t.y; 64 break; 65 case RoomShapeType.Coustom: 66 rt.Length = Random.value < .5f ? t.x : t.y; 67 rt.Width = t.y == rt.Length ? t.x : t.y; 68 break; 69 } 70 } 71 } 72 break; 73 } 74 75 rt.CenterPos = new Vector2Int(Random.Range((int)(centerPos.x - GenRange.x * .5f), (int)(centerPos.x + GenRange.x * .5f)), 76 Random.Range((int)(centerPos.z - GenRange.y * .5f), (int)(centerPos.z + GenRange.y * .5f))); 77 78 rt.PosY = centerPos.y; 79 80 var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); 81 82 //射线检测重叠 83 if (RayRoomCheck(roomCenter, rt.Length, rt.Width)) 84 { 85 return null; 86 } 87 return rt; 88 }
用的是射线检测重叠,生成了重叠的房间就会被视作是一次失败的随机试验,之前尝试过直接用物理系统推开失败了,可能是使用有误,如果有知道原因的欢迎与笔者分享,共同进步,有更好的避免矩形重叠的算法当然更好
(无奈笔者没能想出来):
1 //生成房间前射线检测下范围内有无其他房间 2 bool RayRoomCheck(Vector3 cp, int length, int width) 3 { 4 bool result = false; 5 //长宽至少留一格间隙,高度与地板格对齐 6 var to = new Vector3(length + 1, FixedUnitHeight - 1, width + 1) * .5f; 7 var ned = cp - to; 8 9 var vx2 = ned + new Vector3(0, 0, width + 1) * .5f; 10 var vx3 = ned + new Vector3(0, 0, width + 1); 11 12 var vx4 = ned + new Vector3(length + 1, 0, width * .5f + .5f); 13 var vx5 = ned + new Vector3(length + 1, 0, width + 1); 14 15 var vz2 = ned + new Vector3(length + 1, 0, 0) * .5f; 16 var vz3 = ned + new Vector3(length + 1, 0, 0); 17 18 var vz4 = ned + new Vector3(length * .5f + .5f, 0, width + 1); 19 var vz5 = ned + new Vector3(length + 1, 0, width + 1); 20 21 result = 22 //4组射线,每组3条 23 RayCast(ned, Dx, length + 1, S_TAG) || 24 RayCast(vx2, Dx, length + 1, S_TAG) || 25 RayCast(vx3, Dx, length + 1, S_TAG) || 26 27 RayCast(vx4, Dx * -1, length + 1, S_TAG) || 28 RayCast(vx5, Dx * -1, length + 1, S_TAG) || 29 RayCast(vz3, Dx * -1, length + 1, S_TAG) || 30 31 RayCast(ned, Dz, width + 1, S_TAG) || 32 RayCast(vz2, Dz, width + 1, S_TAG) || 33 RayCast(vz3, Dz, width + 1, S_TAG) || 34 35 RayCast(vz4, Dz * -1, width + 1, S_TAG) || 36 RayCast(vz5, Dz * -1, width + 1, S_TAG) || 37 RayCast(vx3, Dz * -1, width + 1, S_TAG); 38 39 return result; 40 }
这里将射线的起点和终点都延长了一格,是为了避免两个生成的房间贴得太紧,这样至少每个房间与其它房间间隔一个单位格或以上。
完整的房间结构生成脚本:
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 using UnityEngine.Events; 5 6 public enum RoomRandType 7 { 8 //全随机 9 AllRand, 10 //枚举大小随机 11 EnumRand 12 } 13 14 public enum RoomShapeType 15 { 16 //宽>=长 17 WidthMain, 18 //长>=宽 19 LengthMain, 20 //自定义,无形状要求 21 Coustom 22 } 23 //x-length z-width y-height 24 25 public class RoomBuilder : MonoBehaviour 26 { 27 //建筑单位方块 28 public GameObject BuildUnit; 29 30 //房间高度值 31 public int FixedUnitHeight; 32 //生成的房间层数 33 public int LayerCount; 34 //长宽随机范围 35 public Vector2Int GenRange; 36 37 //随机类型 38 public RoomRandType RandType; 39 //随机的房间形状类型 40 public RoomShapeType Shape; 41 42 //房间大小的随机列表,用于枚举随机 43 public List<Vector2Int> RoomRandSizes = new List<Vector2Int>(); 44 45 //随机的房间最大面积 46 public int MaxRoomArea; 47 //最大随机数量(随机试验次数) 48 public int MaxRoomCount; 49 50 //最小边长度 51 private int MinRoomEdge; 52 //最大长宽比 53 public int MaxLengthWidthScale = 2; 54 55 //标准方向 56 Vector3Int Dx = new Vector3Int(1, 0, 0); 57 Vector3Int Dy = new Vector3Int(0, 1, 0); 58 Vector3Int Dz = new Vector3Int(0, 0, 1); 59 60 //建筑单位标签 61 const string S_TAG = "Unit"; 62 63 private MapSystem MapManager; 64 65 void Awake() 66 { 67 MapManager = GetComponent<MapSystem>(); 68 } 69 70 public IEnumerator GenRooms(Vector3Int centerPos,UnityAction complete) 71 { 72 var temp = (int)Mathf.Sqrt(MaxRoomArea * 1.0f / MaxLengthWidthScale); 73 MinRoomEdge = temp > 3 ? temp : 3; 74 75 //每层至少1 76 for (int i = 1; i <= LayerCount; i++) 77 { 78 SetGenOneRoom(centerPos, i); 79 yield return new WaitForSeconds(.1f); 80 } 81 82 //超过的随机布置 83 var oc = MaxRoomCount - LayerCount; 84 if (oc > 0) 85 { 86 for (int i = 1; i <= oc; i++) 87 { 88 var r = Random.Range(1, LayerCount + 1); 89 SetGenOneRoom(centerPos, r); 90 yield return new WaitForSeconds(.1f); 91 } 92 } 93 94 //所有房间生成完成后发送一个委托信号,以便后续创建房间数据和计算必要连接 95 complete(); 96 } 97 98 void SetGenOneRoom(Vector3Int cp, int r) 99 { 100 var layerCenter = cp - new Vector3(0, (LayerCount - 2 * r + 1) * .5f * FixedUnitHeight, 0); 101 102 var rt = RanRoomTran(layerCenter); 103 if (rt != null) 104 { 105 var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); 106 107 GameObject temp = new GameObject(r.ToString()); 108 temp.transform.position = roomCenter; 109 temp.tag = S_TAG; 110 111 //给生成的房间添加碰撞盒子并设置大小 112 GenOneRoom(roomCenter, rt.Length, rt.Width, temp.transform); 113 var bc = temp.AddComponent<BoxCollider>(); 114 bc.size = new Vector3(rt.Length, FixedUnitHeight, rt.Width); 115 116 //目前用物理方式似乎难以推开重叠的房间,可能是哪里使用方法有误,改为用用射线检测解决... 117 //var rb = temp.AddComponent<Rigidbody>(); 118 //rb.useGravity = false; 119 //rb.drag = Mathf.Infinity; 120 //rb.constraints = RigidbodyConstraints.FreezePositionY; 121 //rb.freezeRotation = true; 122 123 //将房间数据存入临时列表 124 MapManager.GenRooms.Add(rt); 125 MapManager.UnCrossRooms.Add(rt); 126 } 127 } 128 129 RoomTran RanRoomTran(Vector3 centerPos) 130 { 131 var rt = new RoomTran(); 132 133 switch (RandType) 134 { 135 case RoomRandType.AllRand: 136 int temp; 137 var oe = MaxRoomArea / MinRoomEdge; 138 switch (Shape) 139 { 140 case RoomShapeType.LengthMain: 141 rt.Length = Random.Range(MinRoomEdge + 1, oe + 1); 142 temp = MaxRoomArea / rt.Length; 143 if (temp >= rt.Length) 144 rt.Width = Random.Range(MinRoomEdge, rt.Length); 145 else 146 rt.Width = Random.Range(MinRoomEdge, temp + 1); 147 break; 148 case RoomShapeType.WidthMain: 149 rt.Width = Random.Range(MinRoomEdge + 1, oe + 1); 150 temp = MaxRoomArea / rt.Width; 151 if (temp >= rt.Width) 152 rt.Length = Random.Range(MinRoomEdge, rt.Width); 153 else 154 rt.Length = Random.Range(MinRoomEdge, temp + 1); 155 break; 156 case RoomShapeType.Coustom: 157 rt.Length = Random.Range(MinRoomEdge, oe + 1); 158 temp = MaxRoomArea / rt.Length; 159 rt.Width = Random.Range(MinRoomEdge, temp + 1); 160 break; 161 } 162 break; 163 case RoomRandType.EnumRand: 164 var rc = RoomRandSizes.Count; 165 if (rc == 0) 166 { 167 //未填写时设定随机默认值 168 rt.Length = 3; 169 rt.Width = 3; 170 } 171 else 172 { 173 var ridx = Random.Range(0,rc); 174 var t = RoomRandSizes[ridx]; 175 if (t.x < 3 || t.y < 3) 176 { 177 //填写错误时设定随机默认值 178 rt.Length = 3; 179 rt.Width = 3; 180 } 181 else 182 { 183 switch (Shape) 184 { 185 case RoomShapeType.LengthMain: 186 rt.Length = t.x > t.y ? t.x : t.y; 187 rt.Width = t.x < t.y ? t.x : t.y; 188 break; 189 case RoomShapeType.WidthMain: 190 rt.Width = t.x > t.y ? t.x : t.y; 191 rt.Length = t.x < t.y ? t.x : t.y; 192 break; 193 case RoomShapeType.Coustom: 194 rt.Length = Random.value < .5f ? t.x : t.y; 195 rt.Width = t.y == rt.Length ? t.x : t.y; 196 break; 197 } 198 } 199 } 200 break; 201 } 202 203 rt.CenterPos = new Vector2Int(Random.Range((int)(centerPos.x - GenRange.x * .5f), (int)(centerPos.x + GenRange.x * .5f)), 204 Random.Range((int)(centerPos.z - GenRange.y * .5f), (int)(centerPos.z + GenRange.y * .5f))); 205 206 rt.PosY = centerPos.y; 207 208 var roomCenter = new Vector3(rt.CenterPos.x, rt.PosY, rt.CenterPos.y); 209 210 //射线检测重叠 211 if (RayRoomCheck(roomCenter, rt.Length, rt.Width)) 212 { 213 return null; 214 } 215 return rt; 216 } 217 218 //生成房间前射线检测下范围内有无其他房间 219 bool RayRoomCheck(Vector3 cp, int length, int width) 220 { 221 bool result = false; 222 //长宽至少留一格间隙,高度与地板格对齐 223 var to = new Vector3(length + 1, FixedUnitHeight - 1, width + 1) * .5f; 224 var ned = cp - to; 225 226 var vx2 = ned + new Vector3(0, 0, width + 1) * .5f; 227 var vx3 = ned + new Vector3(0, 0, width + 1); 228 229 var vx4 = ned + new Vector3(length + 1, 0, width * .5f + .5f); 230 var vx5 = ned + new Vector3(length + 1, 0, width + 1); 231 232 var vz2 = ned + new Vector3(length + 1, 0, 0) * .5f; 233 var vz3 = ned + new Vector3(length + 1, 0, 0); 234 235 var vz4 = ned + new Vector3(length * .5f + .5f, 0, width + 1); 236 var vz5 = ned + new Vector3(length + 1, 0, width + 1); 237 238 result = 239 //4组射线,每组3条 240 RayCast(ned, Dx, length + 1, S_TAG) || 241 RayCast(vx2, Dx, length + 1, S_TAG) || 242 RayCast(vx3, Dx, length + 1, S_TAG) || 243 244 RayCast(vx4, Dx * -1, length + 1, S_TAG) || 245 RayCast(vx5, Dx * -1, length + 1, S_TAG) || 246 RayCast(vz3, Dx * -1, length + 1, S_TAG) || 247 248 RayCast(ned, Dz, width + 1, S_TAG) || 249 RayCast(vz2, Dz, width + 1, S_TAG) || 250 RayCast(vz3, Dz, width + 1, S_TAG) || 251 252 RayCast(vz4, Dz * -1, width + 1, S_TAG) || 253 RayCast(vz5, Dz * -1, width + 1, S_TAG) || 254 RayCast(vx3, Dz * -1, width + 1, S_TAG); 255 256 return result; 257 } 258 259 bool RayCast(Vector3 ori, Vector3 dir, float mD, string tag) 260 { 261 Ray ray = new Ray(ori, dir); 262 RaycastHit info; 263 if (Physics.Raycast(ray, out info, mD)) 264 { 265 if (info.transform.tag == tag) 266 return true; 267 } 268 return false; 269 } 270 271 /// <summary> 272 /// 生成单一房间的轮廓 273 /// </summary> 274 /// <param name="centerPos">房间中点位置</param> 275 /// <param name="length">长</param> 276 /// <param name="width">宽</param> 277 /// <param name="parent">父物体</param> 278 void GenOneRoom(Vector3 centerPos, int length, int width, Transform parent = null) 279 { 280 var to = new Vector3(length - 1, FixedUnitHeight - 1, width - 1) * .5f; 281 282 //顶点 283 var ned = centerPos - to; 284 var fod = centerPos + to; 285 286 var v3 = new Vector3(ned.x, fod.y, ned.z); 287 var v4 = new Vector3(ned.x, fod.y, fod.z); 288 var v5 = new Vector3(ned.x, ned.y, fod.z); 289 290 var v6 = new Vector3(fod.x, ned.y, ned.z); 291 var v7 = new Vector3(fod.x, ned.y, fod.z); 292 var v8 = new Vector3(fod.x, fod.y, ned.z); 293 294 //顶点位置(8个) 295 InsSetPos(ned, parent); 296 InsSetPos(fod, parent); 297 InsSetPos(v3, parent); 298 InsSetPos(v4, parent); 299 InsSetPos(v5, parent); 300 InsSetPos(v6, parent); 301 InsSetPos(v7, parent); 302 InsSetPos(v8, parent); 303 304 //12条棱(4*3) 305 //长 306 InsOneEdge(length, ned, Dx, parent); 307 InsOneEdge(length, v3, Dx, parent); 308 InsOneEdge(length, v4, Dx, parent); 309 InsOneEdge(length, v5, Dx, parent); 310 //高 311 InsOneEdge(FixedUnitHeight, ned, Dy, parent); 312 InsOneEdge(FixedUnitHeight, v5, Dy, parent); 313 InsOneEdge(FixedUnitHeight, v6, Dy, parent); 314 InsOneEdge(FixedUnitHeight, v7, Dy, parent); 315 //宽 316 InsOneEdge(width, ned, Dz, parent); 317 InsOneEdge(width, v3, Dz, parent); 318 InsOneEdge(width, v6, Dz, parent); 319 InsOneEdge(width, v8, Dz, parent); 320 } 321 322 //生成一条边上的建筑单位但不包含顶点位置 323 void InsOneEdge(int edge, Vector3 v, Vector3 dir, Transform parent = null) 324 { 325 //忽略首尾单位 326 for (int i = 1; i < edge - 1; i++) 327 { 328 InsSetPos(v + i * dir, parent); 329 } 330 } 331 332 void InsSetPos(Vector3 pos, Transform parent = null) 333 { 334 var ins = Instantiate(BuildUnit); 335 ins.transform.position = pos; 336 ins.transform.parent = parent; 337 } 338 }
在MapSystem中可以在房间结构生成完后创建一个默认的数据结构:
1 public void RandRoomDatas() 2 { 3 if (RoomBuilder == null||MapData ==null) 4 return; 5 6 RoomBuilder.StartCoroutine(RoomBuilder.GenRooms(MapData.MapCenter,()=> 7 { 8 CreatRoomData(); 9 RandRoomCrosses(); 10 })); 11 } 12 13 void CreatRoomData() 14 { 15 for (int i = 1; i < GenRooms.Count + 1; i++) 16 { 17 var rd = new RoomData(); 18 rd.Id = i; 19 rd.RoomTran = GenRooms[i - 1]; 20 rd.BattleType = RoomBattleType.NormalBattle; 21 if (rd.Id == 1) 22 rd.BattleType = RoomBattleType.Rest; 23 rd.CrossRooms = new List<RoomData>(); 24 rd.Monsters = new List<GameObject>(); 25 rd.bEndRoom = false; 26 rd.bMainCrossRoom = false; 27 28 MapData.RoomDataDic.Add(rd.Id, rd); 29 } 30 }
效果图:(单层-枚举列表随机)
单层(全随机-长条形房间随机):
多层(层数5)(自定义-全随机):
参考资料:
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
加载全部内容