Unity无限地形生成(基于柏林噪声的简单生成)
要求:构建一个户外开放世界游戏,为该游戏添加天空,地形,植物,并支持场景里自由漫游。这里实现一个无限地形的产生;
实现漫游
漫游的功能由玩家移动和摄像机跟随组成:
- 玩家移动脚本:挂载在玩家物体上
(1)核心思想:获得键盘按下,使用Translate()来移动,并由共有变量speed来控制移动的速度。
(2)代码实现:
if (Input.GetKey(KeyCode.W))
{
this.gameObject.transform.Translate(Vector3.forward *speed * Time.deltaTime);
}
if (Input.GetKey(KeyCode.S))
{
this.gameObject.transform.Translate(Vector3.back * speed*Time.deltaTime);
}
if (Input.GetKey(KeyCode.A))
{
this.gameObject.transform.Translate(Vector3.left *speed* Time.deltaTime);
}
if (Input.GetKey(KeyCode.D))
{
this.gameObject.transform.Translate(Vector3.right *speed* Time.deltaTime);
}
- 摄像机跟随脚本,挂载在主摄像机上,并需要添加主摄像机的跟随目标的位置
(1)核心思路:记录相机和目标物体的初始间距,随着目标物体的运动,相机保持这个间距跟踪目标物体。
(2)代码实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraFellow : MonoBehaviour
{
private Vector3 offset;
public Transform player;
void Start()
{
offset = player.position - transform.position;
}
void Update()
{
transform.position = Vector3.Lerp(transform.position, player.position - offset, Time.deltaTime * 5);
}
}
产生地形块
既然要产生无限地形,那么设定这个地形的单位地形是很关键;这里的地形块使用unity的plane来设计, 操作mesh来设计地形
(1) 核心思路:采用噪声来随机mesh的顶点的高度,然后再覆盖原来的mesh的顶点,这样可以使得plane有地形的起伏;使用柏林噪声可以保证每个地形块在拼接的时候可以比较光滑起伏,这是无限地形的关键就是怎么样使得地形随机的情况下保证拼接的光滑。然后就是使用random来产生随机的顶点索引,再该顶点上设置植物。
(2) 核心代码:
a.使用柏林噪声更新高度
相关参数:
public int heightScale = 5;
public float detailScale = 5.0f;
b.使用mathf.PerlinNoise函数对顶点高度进行设置,然后再覆盖:
Mesh mesh = this.GetComponent<MeshFilter>().mesh;
Vector3[] vertices = mesh.vertices;
for (int v = 0; v < vertices.Length; v++)
{
vertices[v].y = Mathf.PerlinNoise((vertices[v].x + this.transform.position.x) / detailScale,
(vertices[v].z + this.transform.position.z) / detailScale) * heightScale;
}
mesh.vertices = vertices;
c.随机植被的位置:
public GameObject gass;
public GameObject tree1;
public GameObject tree2;
int a = Random.Range(0, vertices.Length - 1);//随机位置产生草
gass.transform.position = transform.TransformPoint(vertices[a]);
int b = Random.Range(0, vertices.Length - 1);//随机位置产生树
tree1.transform.position = transform.TransformPoint(vertices[b]);
int c = Random.Range(0, vertices.Length - 1);//随机位置产生树
tree2.transform.position = transform.TransformPoint(vertices[c]);
效果:
使用地形块拼接更大的场景,并根据玩家的位置更新地形,达到无限地形的效果。使用hash来记录产生的地形块,通过玩家的位置变化来判断是否移动;
(1) 核心思路
初始化生成一个大的网格比如代码中是2020的,这里以33的来说明;中间的位置是玩家的初始位置;每一个地形方格的位置以及他们创建的时间(这是为了判断这个方格是不是新的方格)保留在一个hash表中;
假设玩家向上移动,到上面那个方格;则玩家当前的位置为新的玩家初始位置,在周围生成一圈33的网格(实际上只往前产生一排网格,原来的保留下来),这些33的网格更新创建时间为当前时间,现在我们有了一个3*4的网格了;然后是删除后面一排网格(这些网格不在player的周围),判断标准是这些网格的创建时间不是等于当前时间;这样使得玩家始终在地形的中央;
(2) 关键代码
初始化地图,在player的周围生成地形,注意要使用hash表记录这些网格,这里可以建设player为(0,0):
this.gameObject.transform.position = Vector3.zero;
starPos = Vector3.zero;
float updateTime = Time.realtimeSinceStartup;
//拼接为一个全新的20*20的大地形
for(int x=-halfTilesX;x<halfTilesX;x++)
for(int z=-halfTilesZ;z<halfTilesZ;z++)
{
Vector3 pos = new Vector3(x * planeSize + starPos.x, 0, z * planeSize + starPos.z);
GameObject t = (GameObject)Instantiate(plane, pos, Quaternion.identity);
//使用Tile类来记录这些产生的gameobjects存储于hash表中
//以位置信息记录标签
string tilename = "Tile_" + ((int)(pos.x)).ToString() + "_" + ((int)(pos.z)).ToString();
//修改原来的地形的名字
t.name = tilename;
//实例化Tile对象
Tile tile = new Tile(t, updateTime);
//添加到hash表中
tiles.Add(tilename,tile);
}
随着玩家移动更新地形:
int xMove = (int)(player.transform.position.x - starPos.x);
int yMove = (int)(player.transform.position.z - starPos.z);
if(Mathf.Abs(xMove)>=planeSize||Mathf.Abs(yMove)>=planeSize)
{
float updateTime = Time.realtimeSinceStartup;
//这个比较关键即计算玩家位置是否靠近下一个网格(这里是向下取整)
int playerX = (int)(Mathf.Floor(player.transform.position.x / planeSize) * planeSize);
int playerZ = (int)(Mathf.Floor(player.transform.position.z / planeSize) * planeSize);
for (int x = -halfTilesX; x < halfTilesX; x++)
for (int z = -halfTilesZ; z < halfTilesZ; z++)
{
Vector3 pos = new Vector3(x * planeSize + playerX, 0, z * planeSize +playerZ);
//以位置信息记录标签
string tilename = "Tile_" + ((int)(pos.x)).ToString() + "_" + ((int)(pos.z)).ToString();
//判断这是一个新的格子吗,即不存在于
if(!tiles.ContainsKey(tilename))
{
GameObject t = (GameObject)Instantiate(plane, pos, Quaternion.identity);
//地形的名字:坐标
t.name = tilename;
//实例化Tile对象
Tile tile = new Tile(t, updateTime);
//添加到hash表中
tiles.Add(tilename, tile);
}
else
{
//更新时间
(tiles[tilename] as Tile).creatoinTime = updateTime;
}
}
根据地形格子的创建时间来更新hash表:
Hashtable newTerrain = new Hashtable();
foreach(Tile tls in tiles.Values)
{
//当前这个格子不在player周围,删除
if(tls.creatoinTime!=updateTime)
{
//销毁
Destroy(tls.theTile);
}
else
{
newTerrain.Add(tls.theTile.name, tls);
}
}
tiles = newTerrain;
//更新初始位置
starPos = player.transform.position
(3)效果(使得player始终在地图的中间):
最终的效果:
不足之处:
使用柏林噪声产生的地形虽然连接比较平滑,但是地形形成上比较重复,不够随机。然后每次更新都需要删除大量的object的做法好像也不是太好。学生实验作业,可能有些问题,望指正。
参考资料:https://www.youtube.com/watch?v=dycHQFEz8VI