Unity大型场景程序化生成及优化技术—FPS迷宫生成和优化
1、知名游戏中的大型场景生成
场景程序化生成技术是一个广泛应用在游戏开发中的技术,较早的使用这类技术有名游戏《暗黑破坏神》系列,无论是1代、2代、3代,都使用了这个技术,如下图1-1:每次进入野外场景的时候能看到随机的场景(随机地形、随机怪物、随机藏宝点)
图1-1 《暗黑破坏神3》随机地下城
另外一个知名游戏是手游《列王的纷争(COK)》,在这个游戏里的世界地图,也是程序化生成的,如下图1-2中所示,手机窗口中只能看到这个庞大地图中的一小点,地图上面会有程序随机生成的山脉、湖泊等景观,加上资源点、怪物等等。
图1-2 《COK》中的世界地图
还有一个可能是用程序化生成场景技术的游戏中技术点最多的一个游戏,它的名字叫《我的世界》,如图1-3,这个游戏世界是由海量的方块组成的,初步看起来逻辑比较简单,实际上,为了能生成拟真度很高的场景以及能让游戏流畅的跑在各个手机上,需要不少功夫。
图1-3《我的世界》中的无限场景
我们正在制作一个FPS游戏项目,其中也用到了程序化生成场景的技术,如图1-4,本文的主要内容就是给大家介绍一下这个FPS游戏项目中我们具体是怎么用程序生成迷宫场景以及如何对迷宫场景的显示进行优化。
图1-4 FPS项目中迷宫场景
2、迷宫数据生成算法
首先,项目的策划目标提出了明确的需求,要生成一个比较复杂的迷宫,纵横通道和房间交错的地下场景,每次玩家进入到场景中进行互动的时候,场景都要有不同,但我们能控制场景的规模大小及其他一些自定义的参数。我们实验了几种常用的迷宫生成算法,分别是如下几种:
-
Recursive division (递归分割算法)
-
Recursive backtracker ( 递归回溯,也是深度优先算法)
-
Randomized Prim’s algorithm(随机Prim算法)
这几种算法实验的过程比较简单,分别用算法生成随机迷宫,然后在Unity用简单的方式根据数据绘制出整个迷宫,最后让策划去比较选择哪种算法下的迷宫是项目最想要的,本文将逐一介绍一下。
2.1、迷宫算法的基本参数
下面的图2-1,展示的是一个规模为30*20的迷宫初始化后的视图,其中白色的格子代表房间,黑色的网格线代表墙,因为还未进行任何后续迷宫算法处理,所以每个房间都是用墙隔开的,都无法通行,本文提到的三种算法,都是在这样一个初始状态下,通过不同的方式把房间打通得到最终的迷宫。
图2-1迷宫数据的基本组成和参数
2.2、递归分割算法
递归分割算法的效果如图2-2所示:
图2-2 递归分割算法的效果
递归分割算法跟其他两个算法一样,首先是继承了公共父类MazeGenerator,它定义了基本的迷宫数据结构,迷宫规模,还有根据迷宫数据绘制场景的成员函数GenMazeScene其代码如下,其中int [,]mMazeData这个三维数组是要生成的迷宫数据,前两个维度下标分别表示行和列,第三个维度
1.using UnityEngine;
2.using UnityEngine.UI;
3.
4./// <summary>
5./// 4面墙的类型
6./// </summary>
7.enum RectWallType
8.{
9. Left = 0,
10. Up = 1,
11. Right = 2,
12. Down = 3,
13. WallNum = 4,
14.}
15.
16./// <summary>
17./// 6面墙类型
18./// </summary>
19.enum HexWallType
20.{
21. Up = 0, //正上方:不管奇偶c,r+1
22. Down = 1, //正下方:不管奇偶c,r-1
23. LeftUp = 2, //左上方:本列是奇数列c-1,r+1,本列是偶数列c-1,r
24. RightUp = 3, //右上方:本列是奇数列c+1,r+1,本列是偶数列c+1,r
25. LeftDown = 4, //左下方:本列是奇数列c-1,r,本列是偶数列c-1,r-1
26. RightDown = 5, //右下方:本列是奇数列c+1,r,本列是偶数列c+1,r-1
27. WallNum = 6,
28.}
29.
30./// <summary>
31./// 房间坐标类型
32./// </summary>
33.public class RoomCoordinate
34.{
35. public int row; //行
36. public int col; //列
37.
38. public RoomCoordinate(int r, int c)
39. {
40. row = r;
41. col = c;
42. }
43.}
44.
45./// <summary>
46./// 迷宫生成器父类
47./// </summary>
48.public abstract class MazeGenerator : MonoBehaviour
49.{
50. protected int[,,] mMazeData; //迷宫数据
51.
52. protected GameObject mMazePrefab;
53.
54. public int mRowCount = 10; //迷宫行个数
55.
56. public int mColCount = 10; //迷宫列个数
57.
58. public abstract void GenMazeData();
59.
60. /// <summary>
61. /// 生成场景
62. /// </summary>
63. public virtual void GenMazeScene()
64. {
65. for (int r = 0; r < mRowCount; r++)
66. {
67. for (int c = 0; c < mColCount; c++)
68. {
69. GameObject o = Instantiate<GameObject>(mMazePrefab);
70. o.transform.position = new Vector3(c * 4, 0, r * 4);
71.
72. if (mMazeData[r, c, (int)RectWallType.Left] == 1)
73. {
74. o.transform.Find("Left").gameObject.SetActive(false);
75. }
76.
77. if (mMazeData[r, c, (int)RectWallType.Right] == 1)
78. {
79. o.transform.Find("Right").gameObject.SetActive(false);
80. }
81.
82. if (mMazeData[r, c, (int)RectWallType.Up] == 1)
83. {
84. o.transform.Find("Up").gameObject.SetActive(false);
85. }
86.
87. if (mMazeData[r, c, (int)RectWallType.Down] == 1)
88. {
89. o.transform.Find("Down").gameObject.SetActive(false);
90. }
91. }
92. }
93. }
94.}
然后是递归分割算法的代码,它的主要代码是RecursiveDiv,这是一个递归函数,可以这样理解:把整个迷宫作为一个大房间,每次递归时把当前的房间递归分成4个小房间,并随机打通其中的3个房间,直到这个待分割的房间为一个基本房间单元为止。代码如下
1.public class RecursiveDivisionGen : MazeGenerator
2.{
3. void Start()
4. {
5. if (mRowCount <= 0 || mColCount <= 0)
6. return;
7.
8. //初始化房间数据,默认值为0表示封闭的墙
9. mMazeData = new int[mRowCount, mColCount, (int)RectWallType.WallNum];