# 一、生成世界随机地图模块

# 1. Mathf.PerlinNoise 源码推测

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
public static float PerlinNoise(float x, float y)
{
int xi = Mathf.FloorToInt(x) & 255;
int yi = Mathf.FloorToInt(y) & 255;

float xf = x - Mathf.Floor(x);
float yf = y - Mathf.Floor(y);

float u = Fade(xf);
float v = Fade(yf);

int aa = Permutation[Permutation[xi] + yi];
int ab = Permutation[Permutation[xi] + yi + 1];
int ba = Permutation[Permutation[xi + 1] + yi];
int bb = Permutation[Permutation[xi + 1] + yi + 1];

float x1, x2, y1;
x1 = Mathf.Lerp(Grad(aa, xf, yf), Grad(ba, xf - 1, yf), u);
x2 = Mathf.Lerp(Grad(ab, xf, yf - 1), Grad(bb, xf - 1, yf - 1), u);
y1 = Mathf.Lerp(x1, x2, v);

return (y1 + 1) * 0.5f; // 将结果映射到 [0, 1]
}

private static float Fade(float t)
{
return t * t * t * (t * (t * 6 - 15) + 10);
}

private static float Grad(int hash, float x, float y)
{
int h = hash & 3;
float u = h < 2 ? x : y;
float v = h < 2 ? y : x;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}

// 一个常见的伪随机排列表
private static readonly int[] Permutation = { ... }; // 省略完整表

# 2. 各个方法分析

# 2.1 世界片段的生成与管理

  1. 创建世界片段(圆柱体
    row
    1
    2
    3
    4
    5
    6
    7
    8
    void GenerateWorldPiece(int i){ 
    // 创建新的圆柱体,并将其存储在 pieces 数组中
    pieces[i] = CreateCylinder();
    // 根据索引设置片段的位置
    pieces[i].transform.Translate(Vector3.forward * (dimensions.y * scale * Mathf.PI) * i);
    // 更新片段,使其包含终点并能够移动
    UpdateSinglePiece(pieces[i]);
    }
  2. 创建圆柱形片段
    模块: CreateCylinder
  • 功能
    • 创建一个圆柱形的世界片段,添加必要的组件( MeshFilterMeshRendererMeshCollider )。
    • 为圆柱体生成网格形状并赋予材质。
  • 主要方法
    row
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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;
    }
  1. 为圆柱体生成实际网格并返回
  • 功能
    • 为圆柱体生成实际的网格。
    • 定义顶点、UV 坐标、三角形索引,构建一个 3D 几何体。
    • 调用 CreateShape 完成网格顶点和形状的定义。
  • 主要方法
    row
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Mesh 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;
    }
  1. 计算顶点位置并基于柏林噪声修改高度
    模块: 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
      100
      void 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 过渡 片段之间更新

  1. 使连接更加丝滑
    模块: UpdateWorldPieces
  • 功能

    • 移除不再显示的旧片段。
    • 创建新的片段,衔接在当前场景的末端。
    • 保证两个片段的边界平滑连接。
    • 动态调整场景片段,形成 “无限滚动” 的效果。
      row
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      IEnumerator 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 动态调整模块:场景中的物品管理

  1. 模块: UpdateAllItems
  • 功能

    • 找到所有带有标签 Item 的物品(如障碍物和门)。
    • 判断物品是否接近玩家,若接近则显示物品。
    • 对底部的物品开启阴影投射,上部物品则关闭阴影,避免不必要的阴影绘制。
      row
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      void 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;
      }
      }
      }
  1. 模块: CreateItem
  • 功能

    • 根据随机概率在片段上生成物品(障碍物或门)。
    • 将物品位置设置在网格的某些顶点处。
    • 确保物品的朝向正确,使其面向圆柱体中心。
      row
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      void 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 场景控制模块:片段的移动与过渡

  1. 模块: 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
      27
      void 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 波动圆柱体表面的数组值以达成总体圆柱体但是细节有坡度的效果。