Unity 中的程序世界生成

Unity 中的世界生成是指在 Unity 游戏引擎中创建或按程序生成虚拟世界、地形、景观或环境的过程。这种技术通常用于各种类型的游戏,例如开放世界游戏、角色扮演游戏、模拟游戏等,以动态创建广阔且多样化的游戏世界。

Unity 提供了灵活的框架以及广泛的工具和 API 来实现这些世界生成技术。人们可以使用 C# 编写自定义脚本来生成和操纵游戏世界,或利用 Unity 内置功能(如地形系统、噪声函数和脚本接口)来实现所需的结果。此外,Unity Asset Store 上还提供第三方资源和 插件 ,可以帮助完成世界生成任务。

Unity 中有多种世界生成方法,选择取决于游戏的具体要求。下面介绍几种常用的方法:

  • 使用 Perlin 噪声生成程序地形
  • 元胞自动机
  • 沃罗努图
  • 程序对象放置

使用 Perlin 噪声生成程序地形

Unity 中的程序地形生成可以使用各种算法和技术来实现。一种流行的方法是使用 Perlin 噪声生成高度图,然后应用各种纹理和树叶技术来创建逼真或风格化的地形。

Perlin 噪声是 Ken Perlin 开发的一种梯度噪声。它生成平滑、连续的值模式,这些值看似随机,但具有连贯的结构。Perlin 噪声广泛用于创建自然的地形、云、纹理和其他有机形状。

在Unity中,可以使用函数'Mathf.PerlinNoise()'来生成柏林噪声。它采用两个坐标作为输入,并返回 0 到 1 之间的值。通过以不同频率和幅度对 Perlin 噪声进行采样,可以在程序内容中创建不同级别的细节和复杂性。

以下是如何在 Unity 中实现此功能的示例:

  • 在 Unity 编辑器中,转到 "GameObject -> 3D Object -> Terrain"。这将在场景中创建默认地形。
  • 创建 一个名为 "TerrainGenerator" 的新 C# 脚本,并将 附加到地形对象。下面是一个使用 Perlin 噪声生成程序地形的示例脚本:
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    public int width = 512;       // Width of the terrain
    public int height = 512;      // Height of the terrain
    public float scale = 10f;     // Scale of the terrain
    public float offsetX = 100f;  // X offset for noise
    public float offsetY = 100f;  // Y offset for noise
    public float noiseIntensity = 0.1f; //Intensity of the noise

    private void Start()
    {
        Terrain terrain = GetComponent<Terrain>();

        // Create a new instance of TerrainData
        TerrainData terrainData = new TerrainData();

        // Set the heightmap resolution and size of the TerrainData
        terrainData.heightmapResolution = width;
        terrainData.size = new Vector3(width, 600, height);

        // Generate the terrain heights
        float[,] heights = GenerateHeights();
        terrainData.SetHeights(0, 0, heights);

        // Assign the TerrainData to the Terrain component
        terrain.terrainData = terrainData;
    }

    private float[,] GenerateHeights()
    {
        float[,] heights = new float[width, height];

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                // Generate Perlin noise value for current position
                float xCoord = (float)x / width * scale + offsetX;
                float yCoord = (float)y / height * scale + offsetY;
                float noiseValue = Mathf.PerlinNoise(xCoord, yCoord);

                // Set terrain height based on noise value
                heights[x, y] = noiseValue * noiseIntensity;
            }
        }

        return heights;
    }
}
  • "TerrainGenerator" 脚本附加到 Unity 编辑器中的 Terrain 对象。
  • 在地形对象的检查器窗口中,调整宽度、高度、比例、偏移和噪声强度以调整生成的地形的外观。
  • 按 Unity 编辑器中的 Play 按钮,然后应根据 Perlin 噪声算法生成程序地形。

使用 Perlin 噪声生成 Unity 地形。

注意:此脚本使用 Perlin 噪声生成基本地形高度图。要创建更复杂的地形,请修改脚本以合并其他噪声算法、应用侵蚀或平滑技术、添加纹理或根据地形特征放置树叶和对象。

元胞自动机

元胞自动机是一种由单元网格组成的计算模型,其中每个单元根据一组预定义的规则及其相邻单元的状态而演化。它是一个强大的概念,应用于各个领域,包括计算机科学、数学和物理学。元胞自动机可以表现出从简单规则中产生的复杂行为模式,这使得它们可用于模拟自然现象和生成程序内容。

元胞自动机背后的基本理论涉及以下要素:

  1. Grid:网格是以规则图案排列的单元格的集合,例如正方形或六边形格子。每个单元可以有有限数量的状态。
  2. 邻居:每个小区都有相邻小区,通常是其直接相邻的小区。邻域可以根据不同的连接模式来定义,例如冯·诺依曼(上、下、左、右)或摩尔(包括对角线)邻域。
  3. 规则:每个单元的行为由一组规则确定,这些规则指定它如何根据其当前状态及其相邻单元的状态演化。这些规则通常使用条件语句或查找表来定义。
  4. 更新:元胞自动机通过根据规则同时更新每个元胞的状态来进化。迭代地重复这个过程,创建一个世代序列。

元胞自动机有各种实际应用,包括:

  1. 自然现象的模拟:元胞自动机可以模拟物理系统的行为,例如流体动力学、森林火灾、交通流和人口动态。通过定义适当的规则,元胞自动机可以捕获现实世界系统中观察到的涌现模式和动态。
  2. 程序内容生成:元胞自动机可用于在游戏和模拟中生成程序内容。例如,它们可用于创建地形、洞穴系统、植被分布和其他有机结构。通过指定控制细胞生长和相互作用的规则可以生成复杂而真实的环境。

下面是一个在 Unity 中实现基本元胞自动机来模拟生命游戏的简单示例:

using UnityEngine;

public class CellularAutomaton : MonoBehaviour
{
    public int width = 50;
    public int height = 50;
    public float cellSize = 1f;
    public float updateInterval = 0.1f;
    public Renderer cellPrefab;

    private bool[,] grid;
    private Renderer[,] cells;
    private float timer = 0f;
    private bool[,] newGrid;

    private void Start()
    {
        InitializeGrid();
        CreateCells();
    }

    private void Update()
    {
        timer += Time.deltaTime;

        if (timer >= updateInterval)
        {
            UpdateGrid();
            UpdateCells();
            timer = 0f;
        }
    }

    private void InitializeGrid()
    {
        grid = new bool[width, height];
        newGrid = new bool[width, height];

        // Initialize the grid randomly
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                grid[x, y] = Random.value < 0.5f;
            }
        }
    }

    private void CreateCells()
    {
        cells = new Renderer[width, height];

        // Create a GameObject for each cell in the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Vector3 position = new Vector3(x * cellSize, 0f, y * cellSize);
                Renderer cell = Instantiate(cellPrefab, position, Quaternion.identity);
                cell.material.color = Color.white;
                cells[x, y] = cell;
            }
        }
    }

    private void UpdateGrid()
    {
        // Apply the rules to update the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                int aliveNeighbors = CountAliveNeighbors(x, y);

                if (grid[x, y])
                {
                    // Cell is alive
                    if (aliveNeighbors < 2 || aliveNeighbors > 3)
                        newGrid[x, y] = false; // Die due to underpopulation or overpopulation
                    else
                        newGrid[x, y] = true; // Survive
                }
                else
                {
                    // Cell is dead
                    if (aliveNeighbors == 3)
                        newGrid[x, y] = true; // Revive due to reproduction
                    else
                        newGrid[x, y] = false; // Remain dead
                }
            }
        }

        grid = newGrid;
    }

    private void UpdateCells()
    {
        // Update the visual representation of cells based on the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Renderer renderer = cells[x, y];
                renderer.sharedMaterial.color = grid[x, y] ? Color.black : Color.white;
            }
        }
    }

    private int CountAliveNeighbors(int x, int y)
    {
        int count = 0;

        for (int i = -1; i <= 1; i++)
        {
            for (int j = -1; j <= 1; j++)
            {
                if (i == 0 && j == 0)
                    continue;

                int neighborX = x + i;
                int neighborY = y + j;

                if (neighborX >= 0 && neighborX < width && neighborY >= 0 && neighborY < height)
                {
                    if (grid[neighborX, neighborY])
                        count++;
                }
            }
        }

        return count;
    }
}
  • "CellularAutomaton" 脚本附加到Unity 场景中的游戏对象,并将单元预制件分配给检查器中的'cellPrefab' 字段。

Unity 中的元胞自动机。

在此示例中,单元格网格由布尔数组表示,其中 'true' 表示活动单元格,'false' 表示死单元格。应用生命游戏的规则来更新网格,并且细胞的视觉表示也相应地更新。'CreateCells()' 方法为每个单元格创建一个 GameObject,并且 'UpdateCells()' 方法根据网格状态更新每个 GameObject 的颜色。

注意:这只是一个基本示例,可以探索元胞自动机的许多变体和扩展。可以修改规则、单元行为和网格配置以创建不同的模拟并生成各种模式和行为。

沃罗努图

Voronoi 图,也称为 Voronoi 镶嵌或 Voronoi 分区,是一种几何结构,它根据一组称为种子或站点的点的接近程度将空间划分为多个区域。沃罗努图中的每个区域都由空间中距离特定种子比距离任何其他种子更近的所有点组成。

Voronoi 图背后的基本理论涉及以下元素:

  1. 种子/站点:种子或站点是空间中的一组点。这些点可以随机生成或手动放置。每个种子代表 Voronoi 区域的一个中心点。
  2. Voronoi 单元/区域:每个 Voronoi 单元或区域对应于比任何其他种子更接近特定种子的空间区域。区域的边界由连接相邻种子的线段的垂直平分线形成。
  3. Delaunay 三角剖分:Voronoi 图与 Delaunay 三角剖分密切相关。Delaunay 三角剖分是种子点的三角剖分,使得没有种子位于任何三角形的外接圆内。Delaunay 三角剖分可用于构造 Voronoi 图,反之亦然。

Voronoi 图有各种实际应用,包括:

  1. 程序内容生成:Voronoi 图可用于生成程序地形、自然景观和有机形状。通过使用种子作为控制点并向 Voronoi 单元分配属性(例如海拔或生物群落类型),可以创建真实且多样化的环境。
  2. 游戏设计:Voronoi 图可用于游戏设计中,以出于游戏目的划分空间。例如,在策略游戏中,Voronoi 图可用于将游戏地图划分为不同派系控制的领土或区域。
  3. 寻路和 AI:Voronoi 图可以通过提供空间表示来帮助寻路和 AI 导航,从而可以有效计算最近的种子或区域。它们可用于定义人工智能代理的导航网格或影响图。

在 Unity 中,有多种方法可以生成和利用 Voronoi 图:

  1. 程序生成:开发人员可以实现算法,从 Unity 中的一组种子点生成 Voronoi 图。可以使用各种算法(例如 Fortune 算法或 Lloyd 松弛算法)来构造 Voronoi 图。
  2. 地形生成:Voronoi 图可用于地形生成,以创建多样化且逼真的景观。每个 Voronoi 单元可以代表不同的地形特征,例如山脉、山谷或平原。海拔、湿度或植被等属性可以分配给每个单元格,从而产生多样化且具有视觉吸引力的地形。
  3. 地图分区:Voronoi 图可用于将游戏地图划分为区域以用于游戏目的。可以为每个区域分配不同的属性或特性来创建不同的游戏区域。这对于策略游戏、领土控制机制或关卡设计非常有用。

有 Unity 包和资源可提供 Voronoi 图功能,从而更轻松地将基于 Voronoi 的功能合并到 Unity 项目中。这些软件包通常包括 Voronoi 图生成算法、可视化工具以及与 Unity 渲染系统的集成。

以下是使用 Fortune 算法在 Unity 中生成 2D Voronoi 图的示例:

using UnityEngine;
using System.Collections.Generic;

public class VoronoiDiagram : MonoBehaviour
{
    public int numSeeds = 50;
    public int diagramSize = 50;
    public GameObject seedPrefab;

    private List<Vector2> seeds = new List<Vector2>();
    private List<List<Vector2>> voronoiCells = new List<List<Vector2>>();

    private void Start()
    {
        GenerateSeeds();
        GenerateVoronoiDiagram();
        VisualizeVoronoiDiagram();
    }

    private void GenerateSeeds()
    {
        // Generate random seeds within the diagram size
        for (int i = 0; i < numSeeds; i++)
        {
            float x = Random.Range(0, diagramSize);
            float y = Random.Range(0, diagramSize);
            seeds.Add(new Vector2(x, y));
        }
    }

    private void GenerateVoronoiDiagram()
    {
        // Compute the Voronoi cells based on the seeds
        for (int i = 0; i < seeds.Count; i++)
        {
            List<Vector2> cell = new List<Vector2>();
            voronoiCells.Add(cell);
        }

        for (int x = 0; x < diagramSize; x++)
        {
            for (int y = 0; y < diagramSize; y++)
            {
                Vector2 point = new Vector2(x, y);
                int closestSeedIndex = FindClosestSeedIndex(point);
                voronoiCells[closestSeedIndex].Add(point);
            }
        }
    }

    private int FindClosestSeedIndex(Vector2 point)
    {
        int closestIndex = 0;
        float closestDistance = Vector2.Distance(point, seeds[0]);

        for (int i = 1; i < seeds.Count; i++)
        {
            float distance = Vector2.Distance(point, seeds[i]);
            if (distance < closestDistance)
            {
                closestDistance = distance;
                closestIndex = i;
            }
        }

        return closestIndex;
    }

    private void VisualizeVoronoiDiagram()
    {
        // Visualize the Voronoi cells by instantiating a sphere for each cell point
        for (int i = 0; i < voronoiCells.Count; i++)
        {
            List<Vector2> cell = voronoiCells[i];
            Color color = Random.ColorHSV();

            foreach (Vector2 point in cell)
            {
                Vector3 position = new Vector3(point.x, 0, point.y);
                GameObject sphere = Instantiate(seedPrefab, position, Quaternion.identity);
                sphere.GetComponent<Renderer>().material.color = color;
            }
        }
    }
}
  • 要使用此代码,请创建一个球体预制件并将其分配给 Unity 检查器中的 SeedPrefab 字段。调整 numSeeds 和diagramSize 变量来控制种子的数量和图表的大小。

Unity 中的 Voronoi 图。

在此示例中,VoronoiDiagram 脚本通过在指定的图大小内随机放置种子点来生成 Voronoi 图。'GenerateVoronoiDiagram()' 方法根据种子点计算 Voronoi 单元,而 'VisualizeVoronoiDiagram()' 方法在 Voronoi 单元的每个点实例化一个球体 GameObject,从而可视化图表。

注意:此示例提供了 Voronoi 图的基本可视化,但可以通过添加其他功能来进一步扩展它,例如用线连接单元格点或为每个单元格分配不同的属性以用于地形生成或游戏目的。

总体而言,Voronoi 图提供了一个多功能且强大的工具,用于生成程序内容、分区空间以及在 Unity 中创建有趣且多样化的环境。

程序对象放置

Unity 中的程序对象放置涉及通过算法在场景中生成和放置对象,而不是手动定位它们。它是一种强大的技术,可用于多种目的,例如以自然和动态的方式用树木、岩石、建筑物或其他物体填充环境。

以下是 Unity 中程序对象放置的示例:

using UnityEngine;

public class ObjectPlacement : MonoBehaviour
{
    public GameObject objectPrefab;
    public int numObjects = 50;
    public Vector3 spawnArea = new Vector3(10f, 0f, 10f);

    private void Start()
    {
        PlaceObjects();
    }

    private void PlaceObjects()
    {
        for (int i = 0; i < numObjects; i++)
        {
            Vector3 spawnPosition = GetRandomSpawnPosition();
            Quaternion spawnRotation = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
            Instantiate(objectPrefab, spawnPosition, spawnRotation);
        }
    }

    private Vector3 GetRandomSpawnPosition()
    {
        Vector3 center = transform.position;
        Vector3 randomPoint = center + new Vector3(
            Random.Range(-spawnArea.x / 2, spawnArea.x / 2),
            0f,
            Random.Range(-spawnArea.z / 2, spawnArea.z / 2)
        );
        return randomPoint;
    }
}
  • 要使用此脚本,请在 Unity 场景中创建一个空游戏对象,并为其添加 attach "ObjectPlacement" 脚本。分配对象预制件并在检查器中调整 'numObjects''spawnArea' 参数以满足要求。运行场景时,对象将按程序放置在定义的生成区域内。

Unity 中的程序对象放置。

在此示例中,'ObjectPlacement' 脚本负责按程序将对象放置在场景中。'objectPrefab' 字段应分配有要放置的对象的预制件。'numObjects' 变量确定要放置的对象的数量,变量 'spawnArea' 定义对象随机放置的区域。

'PlaceObjects()' 方法循环遍历所需数量的对象,并在定义的生成区域内生成随机生成位置。然后,它通过随机旋转在每个随机位置实例化对象预制件。

注意:可以通过合并各种放置算法(例如基于网格的放置、基于密度的放置或基于规则的放置)来进一步增强此代码,具体取决于项目的具体要求。

结论

Unity 中的程序生成技术为创建动态和沉浸式体验提供了强大的工具。 无论是使用 Perlin 噪声或分形算法生成地形、使用 Voronoi 图创建多样化的环境、使用元胞自动机模拟复杂的行为,还是使用程序放置的对象填充场景,这些技术都为内容生成提供了灵活性、效率和无限的可能性。 通过利用这些算法并将其集成到 Unity 项目中,开发人员可以实现逼真的地形生成、逼真的模拟、视觉上吸引人的环境以及引人入胜的游戏机制。 程序生成不仅可以节省时间和精力,还可以创造独特且不断变化的体验,吸引玩家并使虚拟世界栩栩如生。

推荐文章
讲故事在 Unity 游戏开发中的重要性
如何在 Unity 中在地形上绘制树木
如何将动画导入到 Unity
在 Unity 中为您的环境选择正确的天空盒
保护 Unity 游戏免遭盗版的策略
如何在 Unity 中制作受 FNAF 启发的游戏
如何在 Unity 中为您的游戏选择合适的背景音乐