Procedurele wereldgeneratie in eenheid

De wereldgeneratie in Unity verwijst naar het proces van het creëren of procedureel genereren van virtuele werelden, terreinen, landschappen of omgevingen binnen de Unity game-engine. Deze techniek wordt vaak gebruikt in verschillende soorten games, zoals open-wereldgames, RPG's, simulaties en meer, om op dynamische wijze enorme en diverse gamewerelden te creëren.

Unity biedt een flexibel raamwerk en een breed scala aan tools en API's voor het implementeren van deze wereldgeneratietechnieken. Je kunt aangepaste scripts schrijven met C# om de spelwereld te genereren en te manipuleren, of Unity ingebouwde functies zoals het Terrain-systeem, ruisfuncties en scriptinterfaces gebruiken om de gewenste resultaten te bereiken. Daarnaast zijn er ook middelen van derden en plug-ins beschikbaar op de Unity Asset Store die kunnen helpen bij taken voor het genereren van werelden.

Er zijn verschillende benaderingen voor het genereren van werelden in Unity, en de keuze hangt af van de specifieke vereisten van het spel. Hier zijn enkele veelgebruikte methoden:

  • Procedurele terreingeneratie met Perlin-ruis
  • Cellulaire automaten
  • Voronoi-diagrammen
  • Procedurele objectplaatsing

Procedurele terreingeneratie met Perlin-ruis

Procedurele terreingeneratie in Unity kan worden bereikt met behulp van verschillende algoritmen en technieken. Een populaire benadering is om Perlin-ruis te gebruiken om de hoogtekaart te genereren en vervolgens verschillende textuur- en bladertechnieken toe te passen om een ​​realistisch of gestileerd terrein te creëren.

Perlin-ruis is een soort gradiëntruis ontwikkeld door Ken Perlin. Het genereert een vloeiend, continu patroon van waarden die willekeurig lijken, maar een samenhangende structuur hebben. Perlin-ruis wordt veel gebruikt voor het creëren van natuurlijk ogende terreinen, wolken, texturen en andere organische vormen.

In Unity kan men de functie 'Mathf.PerlinNoise()' gebruiken om Perlin-ruis te genereren. Het heeft twee coördinaten nodig als invoer en retourneert een waarde tussen 0 en 1. Door Perlin-ruis op verschillende frequenties en amplitudes te bemonsteren, is het mogelijk om verschillende niveaus van detail en complexiteit in de procedurele inhoud te creëren.

Hier is een voorbeeld van hoe je dit kunt implementeren in Unity:

  • Ga in de Unity-editor naar "GameObject -> 3D Object -> Terrain". Hierdoor wordt een standaardterrein in de scène gecreëerd.
  • Maak een nieuw C#-script met de naam "TerrainGenerator" en koppel dit aan het terreinobject. Hier is een voorbeeldscript dat een procedureel terrein genereert met behulp van Perlin-ruis:
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;
    }
}
  • Voeg het "TerrainGenerator"-script toe aan het Terrain-object in de Unity Editor.
  • Pas in het infovenster voor het terreinobject de breedte, hoogte, schaal, verschuivingen en ruisintensiteit aan om het uiterlijk van het gegenereerde terrein aan te passen.
  • Druk op de Afspelen-knop in de Unity-editor en het procedurele terrein moet dan worden gegenereerd op basis van het Perlin-ruisalgoritme.

Unity Terrain-generatie met Perlin-ruis.

Opmerking: dit script genereert een basishoogtekaart van het terrein met behulp van Perlin-ruis. Om complexere terreinen te maken, past u het script aan om extra ruisalgoritmen op te nemen, past u erosie- of verzachtingstechnieken toe, voegt u textuur toe of plaatst u bladeren en objecten op basis van de kenmerken van het terrein.

Cellulaire automaten

Cellulaire automaten zijn een rekenmodel dat bestaat uit een raster van cellen, waarbij elke cel evolueert op basis van een reeks vooraf gedefinieerde regels en de toestanden van de aangrenzende cellen. Het is een krachtig concept dat op verschillende gebieden wordt gebruikt, waaronder informatica, wiskunde en natuurkunde. Cellulaire automaten kunnen complexe gedragspatronen vertonen die voortkomen uit eenvoudige regels, waardoor ze bruikbaar zijn voor het simuleren van natuurverschijnselen en het genereren van procedurele inhoud.

De basistheorie achter cellulaire automaten omvat de volgende elementen:

  1. Raster: Een raster is een verzameling cellen die in een regelmatig patroon zijn gerangschikt, zoals een vierkant of zeshoekig rooster. Elke cel kan een eindig aantal toestanden hebben.
  2. Buren: Elke cel heeft aangrenzende cellen, meestal de direct aangrenzende cellen. De buurt kan worden gedefinieerd op basis van verschillende connectiviteitspatronen, zoals von Neumann (omhoog, omlaag, links, rechts) of Moore (inclusief diagonale) buurten.
  3. Regels: Het gedrag van elke cel wordt bepaald door een reeks regels die specificeren hoe deze evolueert op basis van de huidige status en de status van de aangrenzende cellen. Deze regels worden doorgaans gedefinieerd met behulp van voorwaardelijke instructies of opzoektabellen.
  4. Update: De cellulaire automaat evolueert door de status van elke cel tegelijkertijd bij te werken volgens de regels. Dit proces wordt iteratief herhaald, waardoor een reeks generaties ontstaat.

Cellulaire automaten hebben verschillende toepassingen in de echte wereld, waaronder:

  1. Simulatie van natuurverschijnselen: Cellulaire automaten kunnen het gedrag van fysieke systemen simuleren, zoals vloeistofdynamica, bosbranden, verkeersstromen en bevolkingsdynamiek. Door de juiste regels te definiëren kunnen cellulaire automaten de opkomende patronen en dynamiek vastleggen die worden waargenomen in systemen in de echte wereld.
  2. Procedurele inhoud genereren: Mobiele automaten kunnen worden gebruikt om procedurele inhoud te genereren in games en simulaties. Ze kunnen bijvoorbeeld worden gebruikt om terrein, grottensystemen, vegetatieverdeling en andere organische structuren te creëren. Complexe en realistische omgevingen kunnen worden gegenereerd door regels te specificeren die de groei en interactie van cellen regelen.

Hier is een eenvoudig voorbeeld van het implementeren van een eenvoudige cellulaire automaat in Unity om het levensspel te simuleren:

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;
    }
}
  • Voeg het "CellularAutomaton"-script toe aan een GameObject in de Unity-scène en wijs een celprefab toe aan het veld 'cellPrefab' in de inspecteur.

Cellulaire automaat in Unity.

In dit voorbeeld wordt een raster van cellen weergegeven door een Booleaanse array, waarbij 'true' een levende cel aangeeft en 'false' een dode cel vertegenwoordigt. De regels van het levensspel worden toegepast om het raster bij te werken, en de visuele weergave van cellen wordt dienovereenkomstig bijgewerkt. De methode 'CreateCells()' maakt voor elke cel een GameObject, en de methode 'UpdateCells()' werkt de kleur van elk GameObject bij op basis van de rasterstatus.

Opmerking: dit is slechts een basisvoorbeeld en er zijn veel variaties en uitbreidingen op cellulaire automaten die kunnen worden onderzocht. De regels, celgedragingen en rasterconfiguraties kunnen worden aangepast om verschillende simulaties te creëren en verschillende patronen en gedragingen te genereren.

Voronoi-diagrammen

Voronoi-diagrammen, ook wel Voronoi-mozaïekpatronen of Voronoi-partities genoemd, zijn geometrische structuren die een ruimte in regio's verdelen op basis van de nabijheid van een reeks punten die zaden of locaties worden genoemd. Elke regio in een Voronoi-diagram bestaat uit alle punten in de ruimte die dichter bij een bepaald zaadje liggen dan bij enig ander zaadje.

De basistheorie achter Voronoi-diagrammen omvat de volgende elementen:

  1. Seeds/Sites: Seeds of sites zijn een reeks punten in de ruimte. Deze punten kunnen willekeurig worden gegenereerd of handmatig worden geplaatst. Elk zaadje vertegenwoordigt een middelpunt voor een Voronoi-regio.
  2. Voronoi-cellen/-regio's: Elke Voronoi-cel of -regio komt overeen met een gebied in de ruimte dat dichter bij een bepaald zaadje ligt dan bij enig ander zaadje. De grenzen van de gebieden worden gevormd door de middelloodlijnen van de lijnsegmenten die aangrenzende zaden verbinden.
  3. Delaunay-triangulatie: Voronoi-diagrammen zijn nauw verwant aan de Delaunay-triangulatie. Delaunay-triangulatie is een triangulatie van de zaadpunten, zodat geen enkel zaadje zich binnen de omgeschreven cirkel van een driehoek bevindt. De Delaunay-triangulatie kan worden gebruikt om Voronoi-diagrammen te construeren, en vice versa.

Voronoi-diagrammen hebben verschillende toepassingen in de echte wereld, waaronder:

  1. Procedurele inhoud genereren: Voronoi-diagrammen kunnen worden gebruikt om procedureel terrein, natuurlijke landschappen en organische vormen te genereren. Door de zaden als controlepunten te gebruiken en attributen (zoals hoogte of bioomtype) aan de Voronoi-cellen toe te wijzen, kunnen realistische en gevarieerde omgevingen worden gecreëerd.
  2. Game-ontwerp: Voronoi-diagrammen kunnen worden gebruikt bij het ontwerpen van games om ruimte te verdelen voor gameplay-doeleinden. In strategiespellen kunnen Voronoi-diagrammen bijvoorbeeld worden gebruikt om de spelkaart te verdelen in gebieden of zones die worden gecontroleerd door verschillende facties.
  3. Pathfinding en AI: Voronoi-diagrammen kunnen helpen bij het vinden van paden en AI-navigatie door een weergave van de ruimte te bieden die een efficiënte berekening van het dichtstbijzijnde zaad of gebied mogelijk maakt. Ze kunnen worden gebruikt om navigatienetwerken te definiëren of kaarten voor AI-agenten te beïnvloeden.

In Unity zijn er verschillende manieren om Voronoi-diagrammen te genereren en te gebruiken:

  1. Procedurele generatie: Ontwikkelaars kunnen algoritmen implementeren om Voronoi-diagrammen te genereren op basis van een reeks startpunten in Unity. Verschillende algoritmen, zoals het algoritme van Fortune of het Lloyd-relaxatiealgoritme, kunnen worden gebruikt om Voronoi-diagrammen te construeren.
  2. Terreingeneratie: Voronoi-diagrammen kunnen worden gebruikt bij het genereren van terreinen om diverse en realistische landschappen te creëren. Elke Voronoi-cel kan een ander terreinkenmerk vertegenwoordigen, zoals bergen, valleien of vlaktes. Aan elke cel kunnen attributen zoals hoogte, vocht of vegetatie worden toegewezen, wat resulteert in een gevarieerd en visueel aantrekkelijk terrein.
  3. Kaartpartitionering: Voronoi-diagrammen kunnen worden gebruikt om gamekaarten in regio's te verdelen voor gameplay-doeleinden. Het is mogelijk om aan elke regio verschillende attributen of eigenschappen toe te wijzen om verschillende gameplayzones te creëren. Dit kan handig zijn voor strategiespellen, territoriale controlemechanismen of levelontwerp.

Er zijn Unity-pakketten en middelen beschikbaar die Voronoi-diagramfunctionaliteit bieden, waardoor het gemakkelijker wordt om op Voronoi gebaseerde functies in Unity-projecten op te nemen. Deze pakketten bevatten vaak algoritmen voor het genereren van Voronoi-diagrammen, visualisatietools en integratie met het Unity-weergavesysteem.

Hier is een voorbeeld van het genereren van een 2D Voronoi-diagram in Unity met behulp van het algoritme van Fortune:

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;
            }
        }
    }
}
  • Om deze code te gebruiken, maakt u een prefab bol en wijst u deze toe aan het veld SeedPrefab in de Unity-inspecteur. Pas de variabelen numSeeds en diagramSize aan om het aantal seeds en de grootte van het diagram te bepalen.

Voronoi-diagram in Unity.

In dit voorbeeld genereert het VoronoiDiagram-script een Voronoi-diagram door willekeurig startpunten binnen de opgegeven diagramgrootte te plaatsen. De methode 'GenerateVoronoiDiagram()' berekent de Voronoi-cellen op basis van de startpunten, en de methode 'VisualizeVoronoiDiagram()' instantiëert een bol GameObject op elk punt van de Voronoi-cellen, waardoor het diagram wordt gevisualiseerd.

Opmerking: dit voorbeeld biedt een basisvisualisatie van het Voronoi-diagram, maar het is mogelijk om het verder uit te breiden door extra functies toe te voegen, zoals het verbinden van de celpunten met lijnen of het toewijzen van verschillende attributen aan elke cel voor het genereren van terrein of voor gameplay-doeleinden.

Over het geheel genomen bieden Voronoi-diagrammen een veelzijdig en krachtig hulpmiddel voor het genereren van procedurele inhoud, het verdelen van ruimte en het creëren van interessante en gevarieerde omgevingen in Unity.

Procedurele objectplaatsing

Bij procedurele objectplaatsing in Unity worden objecten algoritmisch gegenereerd en in een scène geplaatst, in plaats van ze handmatig te positioneren. Het is een krachtige techniek die voor verschillende doeleinden wordt gebruikt, zoals het op een natuurlijke en dynamische manier bevolken van omgevingen met bomen, rotsen, gebouwen of andere objecten.

Hier is een voorbeeld van procedurele objectplaatsing in 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;
    }
}
  • Om dit script te gebruiken, maak je een leeg GameObject in de Unity-scène en koppel het "ObjectPlacement"-script eraan. Wijs het object prefab toe en pas de parameters 'numObjects' en 'spawnArea' in de inspecteur aan zodat ze aan de vereisten voldoen. Wanneer de scène wordt uitgevoerd, worden de objecten procedureel binnen het gedefinieerde spawn-gebied geplaatst.

Procedurele objectplaatsing in Unity.

In dit voorbeeld is het script 'ObjectPlacement' verantwoordelijk voor het procedureel plaatsen van objecten in de scène. Het veld 'objectPrefab' moet worden toegewezen aan de prefab van het te plaatsen object. De variabele 'numObjects' bepaalt het aantal te plaatsen objecten, en de variabele 'spawnArea' definieert het gebied waarin de objecten willekeurig worden geplaatst.

De 'PlaceObjects()'-methode doorloopt het gewenste aantal objecten en genereert willekeurige spawn-posities binnen het gedefinieerde spawn-gebied. Vervolgens wordt het object prefab op elke willekeurige positie geïnstantieerd met een willekeurige rotatie.

Opmerking: het is mogelijk om deze code verder te verbeteren door verschillende plaatsingsalgoritmen op te nemen, zoals op rasters gebaseerde plaatsing, op dichtheid gebaseerde plaatsing of op regels gebaseerde plaatsing, afhankelijk van de specifieke vereisten van het project.

Conclusie

Procedurele generatietechnieken in Unity bieden krachtige hulpmiddelen voor het creëren van dynamische en meeslepende ervaringen. Of het nu gaat om het genereren van terreinen met behulp van Perlin-ruis of fractal-algoritmen, het creëren van diverse omgevingen met Voronoi-diagrammen, het simuleren van complex gedrag met cellulaire automaten, of het bevolken van scènes met procedureel geplaatste objecten: deze technieken bieden flexibiliteit, efficiëntie en eindeloze mogelijkheden voor het genereren van inhoud. Door gebruik te maken van deze algoritmen en ze te integreren in Unity-projecten kunnen ontwikkelaars realistische terreingeneratie, levensechte simulaties, visueel aantrekkelijke omgevingen en boeiende gameplay-mechanica realiseren. Procedureel genereren bespaart niet alleen tijd en moeite, maar maakt ook het creëren van unieke en steeds veranderende ervaringen mogelijk die spelers boeien en virtuele werelden tot leven brengen.

Voorgestelde artikelen
Het belang van storytelling bij de ontwikkeling van Unity-games
Kies in eenheid de juiste Skybox voor uw omgeving
Hoe bomen op terrein in eenheid te schilderen
Onmisbare middelen voor algemene doeleinden voor eenheid
Het implementeren van Object-Oriented Programming (OOP)-concepten in eenheid
Een interactief menusysteem in eenheid bouwen
Gids voor audio in eenheid