Unity 使用柏林噪声(Perlin Noise)生成网格地图

前言

最近在尝试制作一个基于网格地图的RPG游戏,所以想着自己生成一个网格地图。

但是网格地图的生成有很多的方式,大多数的方式都达不到我的要求,我需要一个地图可以随机生成各种地形,也可以储存一些地形数据。

柏林噪声是一个非常好的地形生成选择,但是我查阅了很多的资料,发现国内关于柏林噪声生成地图的文章寥寥无几,而且很少有特别详细的使用方法,所以我在这里把我自己制作地图的过程整理了一下,供大家参考。

预制体制作

为了方便演示,我直接使用了Cube,更改了一下颜色,其中白色的模拟(Mountian),绿色的模拟(Ground)

image-20211207132040141

基础网格地图

 

简易方案(废弃)

我最开始制作网格地图想着直接用脚本生成一个 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的属性进行了设置,当然这是为之后角色的加入服务,所以可以忽略。

那么这样生成的地图呈现出来的效果是这样的。

image-20211207132923063

可以看到,高山和平原都已经有了雏形,但是我们发现依靠简单随机数生成的地图杂乱无章,毫无规律可言,与现实中的地形并没有什么关联。

引入柏林噪声(Perlin Noise)

首先摘抄一段柏林噪声的解释

Perlin Noise 是 Ken Perlin 在 1983 年开发的一种梯度噪音,这是一种用于在计算机生成的表面上产生自然出现纹理的技术,使用 Perlin 噪声合成的纹理通常用于 CGI,通过模仿自然界中纹理的受控随机外观,使计算机生成的视觉元素(如物体表面,火焰,烟雾或云)看起来更自然。

作者: Wilen Wu 链接: 柏林噪声(Python) | Wilen's Blog 来源: Wilen's Blog

image-20211207133334478

柏林噪声在二维平面生成的结果就像上面的图片。

熟悉地形制作的朋友肯定对这样的图片比较熟悉,这样的灰度图可以当作地形的高度使用,所以我们就将柏林噪声和我们的高度绑定。

 
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;
                 }
             }
         }
     }
 }
 ​

生成的地图如下

image-20211207135342256

具体代码大家可以自己查看,我这里就简述一下思路。

在创建完成基础的高山、平原、湖泊之后,我又新建了一层柏林噪声,用它的高度再次模拟出了森林和沙漠,并且在原来的基础上将森林和沙漠赋值给单元格(其中高山和湖泊不会被替换)。

其中的原理有些类似于PS的蒙版,最基础的地形需要放在最下层,上层可以放森林、沙漠等,再往上还可以生成村庄、城市等,也可以在湖泊中生成码头,岛屿等地形。

写在最后

柏林噪声真的是一个非常有用的工具,相较于直白的Random,可以更加生动地完成更多东西的制作。

如果喜欢这篇文章可以点个赞,也可以关注BoomGisland - By a game designer

如果文章中有什么阐述不清的,也可以评论区问我。

  • 19
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值