程序化地形生成
在许多沙盒或者肉鸽游戏中都会用到程序化生成的技术,程序化生成是指使用算法和工具自动创建游戏内容的过程。想要将其用好,需要同时掌控好确定性的部分和随机性的部分,并让二者有机结合。
最近在玩未知伤亡,里面的程序化生成我感觉做的很棒,虽然有性能优化的余地,但依旧是我非常值得学习的。我打算参考未知伤亡,来制作一个程序化生成的案例。
噪声算法
在我的理解中,噪声算法的优点就在于其满足了连续性和随机性。随机性保证了在不同的种子下可以产生完全不同的地形,噪声的连续性也保证了地形的连续性,同时,不同的噪声可以相互调制,从而实现不同粒度和不同维度下的随机性。
未知伤亡使用的是FastNoiseLite这个库,单文件,很好用。
《未知伤亡》的World Generation
其主要流程参考如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| InstantiateWorld ↓ 分配方块/液体数组,创建所有区块 WorldPreprocess ↓ 重置玩家位置、环境效果和层修饰器 WorldCreateBackground ↓ 为区块创建对应生物群系背景 WorldGenerateTerrain ↓ 噪声地形 → 矿物/人工线条 → 结构 → 液体 WorldGenerateWorldBorders ↓ 左右写入不可破坏的无限岩 UpdateWorld ↓ worldBlocks 投影到 Tilemap,生成碰撞 WorldPlacePlayer ↓ 寻找出生点,首层创建逃生舱区域 WorldPlaceEntities ↓ 植物、战利品、陷阱、敌人等 FinishWorldGeneration ↓ 层修饰器、区块可见性、音乐与 UI
|
在InstantiateWorld中触发GenerateWorld这一协程,后续所有过程都是协程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private IEnumerator GenerateWorld() { loadingObject.SetActive(value: true); generatingWorld = true; yield return WorldPreprocess(); yield return WorldCreateBackground(); yield return WorldGenerateTerrain(); yield return WorldGenerateWorldBorders(); SetLoadingText("gencreatingcolliders"); yield return null; UpdateWorld(); yield return WorldPlacePlayer(); yield return WorldPlaceEntities(); yield return FinishWorldGeneration(); }
|
最简单的方块生成
参考未知伤亡第一层(洞穴生态)的生成,这一层的思路是,先用一个Cellular噪声(变量名:caveNoise)来确定全局地图的方块和空白区域,为了让方块的分布更有节奏感,同时还有一个Perlin噪声,这个噪声的值作为影响Cellular噪声的频率的参数,参与方块的生成中。
所有的方块信息被存在一个二维数组中,0表示为空气,其他数字表示为不同类型的方块,当前的情况下,数组内只有0和1两个数字,也就是说当前地图只有一种默认方块。为了有机地加入其他方块,引入了另外一个Perlin噪声,变量名:dirtPerlinNoise
这个噪声只对当前标记为1的方块其作用,他的目的是随机地替换掉默认方块,然后根据方块所在位置的噪声值和给定阈值的比较结果转换成符合当前地形的其他类型方块。
同时,还有无条件的1%概率替换成随机类型的基础方块。
同时越底部的方块越有可能被替换成沙子和岩沙块。
这里贴上部分CU的反编译代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| else if (biomeDepth <= 1) { FastNoiseLite caveNoise = new FastNoiseLite(UnityEngine.Random.Range(0, int.MaxValue)); caveNoise.SetNoiseType(FastNoiseLite.NoiseType.Cellular); caveNoise.SetFrequency(0.06f); caveNoise.SetFractalOctaves(3); caveNoise.SetFractalType(FastNoiseLite.FractalType.FBm); caveNoise.SetFractalLacunarity(1.5f); FastNoiseLite dirtPerlinNoise = new FastNoiseLite(UnityEngine.Random.Range(0, int.MaxValue)); dirtPerlinNoise.SetNoiseType(FastNoiseLite.NoiseType.Perlin); dirtPerlinNoise.SetFractalType(FastNoiseLite.FractalType.FBm); dirtPerlinNoise.SetFractalOctaves(7); dirtPerlinNoise.SetFrequency(0.035f); FastNoiseLite frequencyMap = new FastNoiseLite(UnityEngine.Random.Range(0, int.MaxValue)); frequencyMap.SetNoiseType(FastNoiseLite.NoiseType.Perlin); frequencyMap.SetFrequency(0.00037f); FastNoiseLite biomeMap = new FastNoiseLite(UnityEngine.Random.Range(0, int.MaxValue)); biomeMap.SetNoiseType(FastNoiseLite.NoiseType.Cellular); biomeMap.SetFrequency(0.04f); biomeMap.SetCellularDistanceFunction(FastNoiseLite.CellularDistanceFunction.EuclideanSq); biomeMap.SetCellularReturnType(FastNoiseLite.CellularReturnType.Distance); biomeMap.SetCellularJitter(1f); biomeMap.SetFractalType(FastNoiseLite.FractalType.Ridged); biomeMap.SetFractalLacunarity(1.5f); for (int i = 0; i < width; i++) { for (int k = 0; k < height; k++) { caveNoise.SetFrequency(0.06f + frequencyMap.GetNoise(i, k) * 0.01f); worldBlocks[i, k] = ((caveNoise.GetNoise(i, k) > -0.715f) ? ((ushort)1) : ((ushort)0)); float noise = dirtPerlinNoise.GetNoise(i, k); if (worldBlocks[i, k] > 0 && noise < -0.1f) { worldBlocks[i, k] = (ushort)(((double)noise < -0.33) ? 16 : 2); } if (worldBlocks[i, k] > 0 && UnityEngine.Random.Range(0f, 1f) > 0.99f) { worldBlocks[i, k] = (ushort)UnityEngine.Random.Range(1, 5); } if (biomeMap.GetNoise(i, k) > 0.1f) { worldBlocks[i, k] = (ushort)UnityEngine.Random.Range(3, 5); } if (worldBlocks[i, k] > 0 && biomeMap.GetNoise(i, k) < -0.8f) { worldBlocks[i, k] = 15; } if (biomeDepth == 1 && (float)k < (float)height * 0.5f) { float num = (float)k / (float)height * 2f; if (UnityEngine.Random.Range(0f, 1f) > num && worldBlocks[i, k] == 2) { worldBlocks[i, k] = 12; } if ((float)k < (float)height * 0.33f && UnityEngine.Random.Range(0f, 1f) > num * 3f && worldBlocks[i, k] == 1) { worldBlocks[i, k] = 13; } } } tileCounter++; if (tileCounter > 100) { tileCounter = 0; yield return null; } }
|
参考CU的过程,我用三个类型的方块实现了类似的生成,但做的很粗糙,只有生成方块和替换方块这两步。

