前言
最近在尝试制作一个基于网格地图的RPG游戏,所以想着自己生成一个网格地图。
但是网格地图的生成有很多的方式,大多数的方式都达不到我的要求,我需要一个地图可以随机生成各种地形,也可以储存一些地形数据。
柏林噪声是一个非常好的地形生成选择,但是我查阅了很多的资料,发现国内关于柏林噪声生成地图的文章寥寥无几,而且很少有特别详细的使用方法,所以我在这里把我自己制作地图的过程整理了一下,供大家参考。
预制体制作
为了方便演示,我直接使用了Cube,更改了一下颜色,其中白色的模拟(Mountian),绿色的模拟(Ground)
基础网格地图
简易方案(废弃)
我最开始制作网格地图想着直接用脚本生成一个 Vector3数列,将网格的位置信息存储进去就可以,但是实践之后发现并不是很实用,因为地图的每个单元格(Grid)都需要存储它自身的信息。
这里我先把我之前的网格地图代码放出来,如果需要比较简易的版本,可以直接使用这个方案。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapManager : MonoBehaviour
{
public GameObject mapPointPrefab;
public GameObject[,] mapPoint;
public GameObject mapPoints;
// Start is called before the first frame update
void Start()
{
CreateMap();
}
public void CreateMap()
{
mapPoint = new GameObject[10, 10];
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
Vector2 point = new Vector2(-4.5f + i, -4.5f + j);
mapPoint[i,j] = Instantiate(mapPointPrefab, point, this.transform.rotation,mapPoints.transform);
}
}
}
}
此方案详细信息见:HOneiii/MapSystem
单例模式MapManager创建
简易方案最大的问题就是不容易扩展,作为一个地图管理器,它需要被其他的物体方便地访问,所以我MapManger脚本以单例模式重写了。
泛型单例模式的脚本这里就不呈现了。
设计一个地图管理器,首先我们需要设计它的几个基本信息。
public enum GridState //单元格状态
{
ground,
mountain,
}
public struct Map//地图信息结构体
{
public GameObject gridCube;//Map[x,z]代表的GameObject
public Vector3 gridLocation;//位置信息
public GridState gridState;//单元格的状态
public bool canWalk;//是否可以行走
}
public class MapManager : Singleton<MapManager>
{
public Map[,] map; //地图数据
}
我在上面设计了一个Map结构体,里面有地图的几个基本信息。GridState中我列出了两种基本的形态,后期还可以视情况添加。
然后我在MapManager中声明了一个Map[,]二维数组,用来存储地图上的单元格信息。
随机高度地图生成
既然是一个地图,那么高度相同是没有任何意义的,我们需要改变单元格高度来创造一个真实的地形。
所以我用随机函数随机了gridHeight的数值,并且生成了地图。
public void Init(int xSize//x轴格数, int zSize//y轴格数, float cellSize//单元格大小)
{
map = new Map[xSize, zSize];
[Header("地图块")]
public GameObject Ground;
public GameObject Mountain;
//随机高度
map[x,z].gridHeight = Random.Range(0f, 5f);
for (int x = 0; x < xSize; x++)
{
for (int z = 0; z < zSize; z++)
{
//生成地图二维坐标
map[x, z].gridLocation = new Vector3(cellSize * x, 0, cellSize * z);
//判断高度 给单元格赋上状态
if (gridHeight < 3.6f)
{
map[x, z].gridState = GridState.ground;
}
else if (gridHeight >= 3.6f)
{
map[x, z].gridState = GridState.mountain;
}
//根据单元格的状态生成单元格
switch (map[x, z].gridState)
{
case GridState.ground:
map[x, z].gridCube = Instantiate(Ground, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = true;
break;
case GridState.mountain:
map[x, z].gridCube = Instantiate(Mountain, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = false;
break;
}
}
在生成单元格时我还将canWalk的属性进行了设置,当然这是为之后角色的加入服务,所以可以忽略。
那么这样生成的地图呈现出来的效果是这样的。
可以看到,高山和平原都已经有了雏形,但是我们发现依靠简单随机数生成的地图杂乱无章,毫无规律可言,与现实中的地形并没有什么关联。
引入柏林噪声(Perlin Noise)
首先摘抄一段柏林噪声的解释
Perlin Noise 是 Ken Perlin 在 1983 年开发的一种梯度噪音,这是一种用于在计算机生成的表面上产生自然出现纹理的技术,使用 Perlin 噪声合成的纹理通常用于 CGI,通过模仿自然界中纹理的受控随机外观,使计算机生成的视觉元素(如物体表面,火焰,烟雾或云)看起来更自然。
作者: Wilen Wu 链接: 柏林噪声(Python) | Wilen's Blog 来源: Wilen's Blog
柏林噪声在二维平面生成的结果就像上面的图片。
熟悉地形制作的朋友肯定对这样的图片比较熟悉,这样的灰度图可以当作地形的高度使用,所以我们就将柏林噪声和我们的高度绑定。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GridState
{
ground,
mountain,
lake
}
public struct Map
{
public GameObject gridCube;
public Vector3 gridLocation;
public GridState gridState;
public bool canWalk;
}
public class MapManager : Singleton<MapManager>
{
public Map[,] map; //地图数据
public float sightDistance;
[Header("地图块")]
public GameObject Ground;
public GameObject Mountain;
public GameObject Lake;
[Header("柏林噪声参数")]
public float frequency;//柏林噪声频率
public float scale;//柏林噪声振幅
public void Init(int xSize, int zSize, float cellSize)
{
map = new Map[xSize, zSize];
//柏林噪声伪随机误差
float xRandom = Random.Range(0f, 100f);
float zRandom = Random.Range(0f, 100f);
for (int x = 0; x < xSize; x++)
{
for (int z = 0; z < zSize; z++)
{
//生成地图二维坐标
map[x, z].gridLocation = new Vector3(cellSize * x, 0, cellSize * z);
//柏林噪声生成地图高度信息
float xFloat = x;
float zFloat = z;
float xSizeFloat = xSize;
float zSizeFloat = zSize;
float gridHeight = Mathf.PerlinNoise(xFloat / xSizeFloat * frequency + xRandom, zFloat / zSizeFloat * frequency + zRandom) * scale;
//地图单位格高度判断并更改类型
//生成 高山 平原 湖泊
if (gridHeight > 1.5f && gridHeight < 3.6f)
{
map[x, z].gridState = GridState.ground;
}
else if (gridHeight >= 3.6f)
{
map[x, z].gridState = GridState.mountain;
}
else if (gridHeight <= 1.5f)
{
map[x, z].gridState = GridState.lake;
}
//地图单位格高度设置以及生成
switch (map[x, z].gridState)
{
case GridState.ground:
map[x, z].gridCube = Instantiate(Ground, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = true;
break;
case GridState.mountain:
map[x, z].gridCube = Instantiate(Mountain, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight + 1f, 0);
map[x,z].canWalk = false;
break;
case GridState.lake:
map[x, z].gridCube = Instantiate(Lake, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, 1.4f, 0);
map[x,z].canWalk = false;
break;
}
}
}
}
}
在代码中,我新建了一个Lake的状态,在高度为1.5以下时表现为湖泊。
最重要的代码是这段
float xRandom = Random.Range(0f, 100f);
float zRandom = Random.Range(0f, 100f);
------------------------------------------
float xFloat = x;
float zFloat = z;
float xSizeFloat = xSize;
float zSizeFloat = zSize;
float gridHeight = Mathf.PerlinNoise(xFloat / xSizeFloat * frequency + xRandom, zFloat / zSizeFloat * frequency + zRandom) * scale;
前面的几个变量声明是为了将int型的x,z转换为float型,这是一个非常关键的点,由于Mathf.PerlinNoise最后输出结果与输入数据的小数相关,所以如果为int型,最后出现的单元格高度都是一样的。
Matf.PerlinNoise中的frequency为频率,scale为振幅。我们可以将其看作一块褶皱的布,频率就是褶皱的密度,振幅就相当于褶皱的高度。
上面的xRandom与zRandom所起到的作用为每次创建的地图都不相同,所以需要将其放在循环外,否则创建的地图还是没有任何的关联性,依旧杂乱无章。
现在创建完成的地图如下,已经很像Minecraft中的地形感觉了。
加入更多地形
那么这样一个地形如何能够添加更多的单元格类型呢,我就在这里贴上增加了森林、沙漠的代码,供大家参考。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GridState
{
ground,
mountain,
lake,
forest,
desert
}
public struct Map
{
public GameObject gridCube;
public Vector3 gridLocation;
public GridState gridState;
public bool canWalk;
}
public class MapManager : Singleton<MapManager>
{
public Map[,] map; //地图数据
public float sightDistance;
[Header("地图块")]
public GameObject Ground;
public GameObject Mountain;
public GameObject Lake;
public GameObject Forest;
public GameObject Desert;
[Header("柏林噪声参数")]
public float frequency;//柏林噪声频率
public float scale;//柏林噪声振幅
public void Init(int xSize, int zSize, float cellSize)
{
map = new Map[xSize, zSize];
//柏林噪声伪随机误差
float xRandom = Random.Range(0f, 100f);
float zRandom = Random.Range(0f, 100f);
for (int x = 0; x < xSize; x++)
{
for (int z = 0; z < zSize; z++)
{
//生成地图二维坐标
map[x, z].gridLocation = new Vector3(cellSize * x, 0, cellSize * z);
//柏林噪声生成地图高度信息
float xFloat = x;
float zFloat = z;
float xSizeFloat = xSize;
float zSizeFloat = zSize;
float gridHeight = Mathf.PerlinNoise(xFloat / xSizeFloat * frequency + xRandom, zFloat / zSizeFloat * frequency + zRandom) * scale;
float gridType = Mathf.PerlinNoise(xFloat / xSizeFloat * frequency + xRandom/3, zFloat / zSizeFloat * frequency + zRandom/3) * scale;
//地图单位格高度判断并更改类型
//生成 高山 平原 湖泊
if (gridHeight > 1.5f && gridHeight < 3.6f)
{
map[x, z].gridState = GridState.ground;
}
else if (gridHeight >= 3.6f)
{
map[x, z].gridState = GridState.mountain;
}
else if (gridHeight <= 1.5f)
{
map[x, z].gridState = GridState.lake;
}
//生成 森林 沙漠
if (map[x, z].gridState == GridState.ground)
{
if (gridType >= 3.2f)
{
map[x, z].gridState = GridState.forest;
}
else if (gridType <= 1.5f)
{
map[x, z].gridState = GridState.desert;
}
}
//地图单位格高度设置以及生成
switch (map[x, z].gridState)
{
case GridState.ground:
map[x, z].gridCube = Instantiate(Ground, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = true;
break;
case GridState.mountain:
map[x, z].gridCube = Instantiate(Mountain, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight + 1f, 0);
map[x,z].canWalk = false;
break;
case GridState.lake:
map[x, z].gridCube = Instantiate(Lake, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, 1.4f, 0);
map[x,z].canWalk = false;
break;
case GridState.forest:
map[x, z].gridCube = Instantiate(Forest, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = true;
break;
case GridState.desert:
map[x, z].gridCube = Instantiate(Desert, map[x, z].gridLocation, Quaternion.identity, this.transform);
map[x, z].gridCube.transform.position = map[x, z].gridLocation + new Vector3(0, gridHeight, 0);
map[x,z].canWalk = true;
break;
}
}
}
}
}
生成的地图如下
具体代码大家可以自己查看,我这里就简述一下思路。
在创建完成基础的高山、平原、湖泊之后,我又新建了一层柏林噪声,用它的高度再次模拟出了森林和沙漠,并且在原来的基础上将森林和沙漠赋值给单元格(其中高山和湖泊不会被替换)。
其中的原理有些类似于PS的蒙版,最基础的地形需要放在最下层,上层可以放森林、沙漠等,再往上还可以生成村庄、城市等,也可以在湖泊中生成码头,岛屿等地形。
写在最后
柏林噪声真的是一个非常有用的工具,相较于直白的Random,可以更加生动地完成更多东西的制作。
如果喜欢这篇文章可以点个赞,也可以关注BoomGisland - By a game designer
如果文章中有什么阐述不清的,也可以评论区问我。