无论是在迷宫还是类似于地牢的游戏地图中,利用程序来生成每次都不一样的地图是一件叫人兴奋不已的事。
这时我们需要解决两个非常重要的随机事件:
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)(自定义-全随机):