# 一、生成世界随机地图模块
# 1. Mathf.PerlinNoise 源码推测
1 | public static float PerlinNoise(float x, float y) |
# 2. 各个方法分析
# 2.1 世界片段的生成与管理
- 创建世界片段(圆柱体
row 1
2
3
4
5
6
7
8void GenerateWorldPiece(int i){
// 创建新的圆柱体,并将其存储在 pieces 数组中
pieces[i] = CreateCylinder();
// 根据索引设置片段的位置
pieces[i].transform.Translate(Vector3.forward * (dimensions.y * scale * Mathf.PI) * i);
// 更新片段,使其包含终点并能够移动
UpdateSinglePiece(pieces[i]);
} - 创建圆柱形片段
模块:CreateCylinder
- 功能:
- 创建一个圆柱形的世界片段,添加必要的组件(
MeshFilter
、MeshRenderer
、MeshCollider
)。 - 为圆柱体生成网格形状并赋予材质。
- 创建一个圆柱形的世界片段,添加必要的组件(
- 主要方法:
row 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public GameObject CreateCylinder(){
// 创建新的圆柱体并命名
GameObject newCylinder = new GameObject();
newCylinder.name = "World piece";
// 设置当前圆柱体为新创建的对象
currentCylinder = newCylinder;
// 添加 MeshFilter 和 MeshRenderer 组件
MeshFilter meshFilter = newCylinder.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = newCylinder.AddComponent<MeshRenderer>();
// 为圆柱体设置材质
meshRenderer.material = meshMaterial;
// 生成网格并赋值
meshFilter.mesh = Generate();
// 为圆柱体添加匹配网格的碰撞器
newCylinder.AddComponent<MeshCollider>();
return newCylinder;
}
- 为圆柱体生成实际网格并返回
- 功能:
- 为圆柱体生成实际的网格。
- 定义顶点、UV 坐标、三角形索引,构建一个 3D 几何体。
- 调用
CreateShape
完成网格顶点和形状的定义。
- 主要方法:
row 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Mesh Generate(){
// 创建并命名新网格
Mesh mesh = new Mesh();
mesh.name = "MESH";
// 初始化顶点、UV和三角形数组
Vector3[] vertices = null;
Vector2[] uvs = null;
int[] triangles = null;
// 填充网格数据
CreateShape(ref vertices, ref uvs, ref triangles);
// 为网格设置顶点、UV和三角形
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.triangles = triangles;
// 重新计算法线
mesh.RecalculateNormals();
return mesh;
}
- 计算顶点位置并基于柏林噪声修改高度
模块:CreateShape
功能:
- 计算圆柱体的每个顶点位置,并基于 Perlin 噪声修改高度。
- 处理世界片段之间的平滑过渡,确保多个片段连接时没有明显的断裂。
- 根据顶点生成网格的 UV 坐标和三角形。
row 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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100void CreateShape(ref Vector3[] vertices, ref Vector2[] uvs, ref int[] triangles){
// 获取当前片段在x轴和z轴的大小
int xCount = (int)dimensions.x;
int zCount = (int)dimensions.y;
// 初始化顶点数组和UV数组,根据片段的维度大小
vertices = new Vector3[(xCount + 1) * (zCount + 1)];
uvs = new Vector2[(xCount + 1) * (zCount + 1)];
int index = 0; // 当前顶点索引
// 获取圆柱体的半径
float radius = xCount * scale * 0.5f;
// 嵌套循环遍历x轴和z轴上的所有顶点
for(int x = 0; x <= xCount; x++){
for(int z = 0; z <= zCount; z++){
// 计算顶点在圆柱上的角度
float angle = x * Mathf.PI * 2f / xCount;
// 使用角度的余弦和正弦值确定顶点位置
vertices[index] = new Vector3(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius, z * scale * Mathf.PI);
// 更新UV坐标,用于纹理映射
uvs[index] = new Vector2(x * scale, z * scale);
// 根据Perlin噪声缩放和偏移值,计算用于噪声的x和z值
float pX = (vertices[index].x * perlinScale) + offset;
float pZ = (vertices[index].z * perlinScale) + offset;
// 获取圆柱的中心位置,但保持z值不变,用于指向中心
Vector3 center = new Vector3(0, 0, vertices[index].z);
// 根据Perlin噪声值和波浪高度,调整顶点向圆柱中心的移动
vertices[index] += (center - vertices[index]).normalized * Mathf.PerlinNoise(pX, pZ) * waveHeight;
// 以下部分处理片段之间的平滑过渡:
// 检查是否有起始点(beginPoints)并且当前顶点位于网格的起始部分(z越小越靠近片段开头)
if(z < startTransitionLength && beginPoints[0] != Vector3.zero){
// 计算当前顶点的Perlin噪声占比,越靠近新片段,噪声占比越高
float perlinPercentage = z * (1f / startTransitionLength);
// 从起始点获取对应的顶点,但保持z值为当前顶点的z
Vector3 beginPoint = new Vector3(beginPoints[x].x, beginPoints[x].y, vertices[index].z);
// 将起始点和当前顶点通过线性插值结合,实现片段之间的平滑过渡
vertices[index] = (perlinPercentage * vertices[index]) + ((1f - perlinPercentage) * beginPoint);
}
else if(z == zCount){
// 如果当前顶点是最后一行顶点,更新起始点(beginPoints),以便下一个片段平滑连接
beginPoints[x] = vertices[index];
}
// 随机在网格顶点位置生成物品(如障碍物或门)
if(Random.Range(0, startObstacleChance) == 0 && !(gate == null && obstacles.Length == 0))
CreateItem(vertices[index], x);
// 增加当前顶点索引
index++;
}
}
// 初始化三角形数组(每个小方块由2个三角形组成,每个三角形由3个顶点组成,所以每个方块有6个顶点)
triangles = new int[xCount * zCount * 6];
// 用于构建方块的顶点索引基础
int[] boxBase = new int[6];
int current = 0; // 当前三角形数组的索引
// 遍历x轴上的所有位置
for(int x = 0; x < xCount; x++){
// 创建一个基础,用于填充当前x位置的一行方块
boxBase = new int[]{
x * (zCount + 1),
x * (zCount + 1) + 1,
(x + 1) * (zCount + 1),
x * (zCount + 1) + 1,
(x + 1) * (zCount + 1) + 1,
(x + 1) * (zCount + 1),
};
// 遍历z轴上的所有位置
for(int z = 0; z < zCount; z++){
// 增加基础顶点索引,移动到下一个方块的位置
for(int i = 0; i < 6; i++){
boxBase[i] = boxBase[i] + 1;
}
// 为当前方块添加两个三角形的顶点索引
for(int j = 0; j < 6; j++){
triangles[current + j] = boxBase[j] - 1;
}
// 增加当前三角形数组的索引
current += 6;
}
}
}
# 2.2 过渡 片段之间更新
- 使连接更加丝滑
模块:UpdateWorldPieces
功能:
- 移除不再显示的旧片段。
- 创建新的片段,衔接在当前场景的末端。
- 保证两个片段的边界平滑连接。
- 动态调整场景片段,形成 “无限滚动” 的效果。
row 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15IEnumerator UpdateWorldPieces(){
// 移除第一个片段(不再可见)
Destroy(pieces[0]);
// 将第二个片段赋值为第一个片段
pieces[0] = pieces[1];
// 创建新的第二个片段
pieces[1] = CreateCylinder();
// 设置新片段的位置和旋转,保证与前一个片段衔接
pieces[1].transform.position = pieces[0].transform.position + Vector3.forward * (dimensions.y * scale * Mathf.PI);
pieces[1].transform.rotation = pieces[0].transform.rotation;
// 更新新生成的片段
UpdateSinglePiece(pieces[1]);
// 等待一帧
yield return 0;
}
# 2.3 动态调整模块:场景中的物品管理
- 模块:
UpdateAllItems
功能:
- 找到所有带有标签
Item
的物品(如障碍物和门)。 - 判断物品是否接近玩家,若接近则显示物品。
- 对底部的物品开启阴影投射,上部物品则关闭阴影,避免不必要的阴影绘制。
row 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void UpdateAllItems(){
// 查找所有带有 "Item" 标签的物体
GameObject[] items = GameObject.FindGameObjectsWithTag("Item");
// 遍历所有物品
for(int i = 0; i < items.Length; i++){
// 获取物品的所有 MeshRenderer 组件
foreach(MeshRenderer renderer in items[i].GetComponentsInChildren<MeshRenderer>()){
// 判断物品是否足够接近玩家
bool show = items[i].transform.position.z < showItemDistance;
// 如果需要显示物品,更新阴影投射模式
// 圆柱体世界中,仅底部的物体需要投射阴影
if(show) renderer.shadowCastingMode = (items[i].transform.position.y < shadowHeight) ? ShadowCastingMode.On : ShadowCastingMode.Off;
// 根据是否展示物品启用或禁用
Renderer renderer.enabled = show;
}
}
}
- 找到所有带有标签
- 模块:
CreateItem
功能:
- 根据随机概率在片段上生成物品(障碍物或门)。
- 将物品位置设置在网格的某些顶点处。
- 确保物品的朝向正确,使其面向圆柱体中心。
row 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void CreateItem(Vector3 vert, int x){
// 获取圆柱体的中心位置,但保持z值与顶点的z值一致
Vector3 zCenter = new Vector3(0, 0, vert.z);
// 检查中心点与顶点之间是否有正确的角度
// 如果向量为零或顶点位于圆柱体的四分之一或四分之三处(特定位置),则不生成物品
if(zCenter - vert == Vector3.zero || x == (int)dimensions.x / 4 || x == (int)dimensions.x / 4 * 3)
return;
// 根据随机概率生成新物品,有小概率生成门(gate),大概率生成障碍物
GameObject newItem = Instantiate((Random.Range(0, gateChance) == 0) ? gate : obstacles[Random.Range(0, obstacles.Length)]);
// 将物品朝向圆柱体中心方向旋转
newItem.transform.rotation = Quaternion.LookRotation(zCenter - vert, Vector3.up);
// 设置物品的位置为当前顶点位置
newItem.transform.position = vert;
// 将新生成的物品设置为当前圆柱体的子物体,以便随圆柱体一起移动和旋转
newItem.transform.SetParent(currentCylinder.transform, false);
}
# 2.4 场景控制模块:片段的移动与过渡
- 模块:
UpdateSinglePiece
功能:
- 为片段添加移动功能,使其沿
z
轴向玩家方向移动。 - 根据灯光的旋转速度设置片段的旋转速度。
- 更新 Perlin 噪声的偏移量,确保每个片段的地形独特。
- 动态调整障碍物的出现概率,增加游戏难度。
row 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
27void UpdateSinglePiece(GameObject piece){
// 为新生成的片段添加基础移动脚本,使其向玩家方向移动
BasicMovement movement = piece.AddComponent<BasicMovement>();
// 设置移动速度为全局速度的负值(向玩家方向移动)
movement.movespeed = -globalSpeed;
// 如果灯光(Directional Light)有移动脚本,则设置片段的旋转速度与灯光的旋转速度一致
if(lampMovement != null)
movement.rotateSpeed = lampMovement.rotateSpeed;
// 为当前片段创建一个终点对象,用于标记片段的末尾
GameObject endPoint = new GameObject();
// 设置终点的位置为片段当前位置加上z轴方向的偏移量
endPoint.transform.position = piece.transform.position + Vector3.forward * (dimensions.y * scale * Mathf.PI);
// 将终点设置为当前片段的子物体
endPoint.transform.parent = piece.transform;
// 为终点命名
endPoint.name = "End Point";
// 增加Perlin噪声的偏移量,确保每个片段的地形形状不同
offset += randomness;
// 减小障碍物生成的概率,随着时间推移增加障碍物的密度
if(startObstacleChance > 5)
startObstacleChance -= obstacleChanceAcceleration;
}
- 为片段添加移动功能,使其沿
# 3. 一些理解
实现方案:地图为两个圆柱体进行嵌套,使整个场景达成了一种伪无限的效果,并且由于柏林噪声的平滑随机性,使圆柱体中的地形会有一些波动。所以在一开始创建了顶点、uv 三角形的数组后通过计算角度将数值注入数组,并通过柏林噪声以及传入值 waveHeight 波动圆柱体表面的数组值以达成总体圆柱体但是细节有坡度的效果。