Procedurel verdensgeneration i enhed

Verdensgenerationen i Unity refererer til processen med at skabe eller proceduremæssigt generere virtuelle verdener, terræner, landskaber eller miljøer i Unity spilmotoren. Denne teknik bruges almindeligvis i forskellige typer spil, såsom open-world-spil, RPG'er, simuleringer og mere, for dynamisk at skabe store og forskelligartede spilverdener.

Unity giver en fleksibel ramme og en bred vifte af værktøjer og API'er til implementering af disse verdensgenerationsteknikker. Man kan skrive brugerdefinerede scripts ved hjælp af C# til at generere og manipulere spilverdenen eller bruge Unity indbyggede funktioner som Terrain-systemet, støjfunktioner og scripting-grænseflader for at opnå de ønskede resultater. Derudover er der også tredjepartsaktiver og plugins tilgængelige på Unity Asset Store, som kan hjælpe med verdensgenereringsopgaver.

Der er flere tilgange til verdensgenerering i Unity, og valget afhænger af spillets specifikke krav. Her er et par almindeligt anvendte metoder:

  • Procedurel terrængenerering med Perlin-støj
  • Mobilautomater
  • Voronoi-diagrammer
  • Procedurel objektplacering

Procedurel terrængenerering med Perlin-støj

Procedurel terrængenerering i Unity kan opnås ved hjælp af forskellige algoritmer og teknikker. En populær tilgang er at bruge Perlin-støj til at generere højdekortet og derefter anvende forskellige teksturerings- og løvteknikker for at skabe et realistisk eller stiliseret terræn.

Perlin-støj er en type gradientstøj udviklet af Ken Perlin. Det genererer et jævnt, kontinuerligt mønster af værdier, der forekommer tilfældige, men som har en sammenhængende struktur. Perlin-støj er meget brugt til at skabe naturligt udseende terræner, skyer, teksturer og andre organiske former.

I Unity kan man bruge funktionen 'Mathf.PerlinNoise()' til at generere Perlin-støj. Det tager to koordinater som input og returnerer en værdi mellem 0 og 1. Ved at sample Perlin-støj ved forskellige frekvenser og amplituder er det muligt at skabe forskellige niveauer af detaljer og kompleksitet i det proceduremæssige indhold.

Her er et eksempel på, hvordan du implementerer dette i Unity:

  • Gå til "GameObject -> 3D Object -> Terrain" i Unity Editor. Dette vil skabe et standardterræn i scenen.
  • Opret et nyt C#-script kaldet "TerrainGenerator" og vedhæft det til terrænobjektet. Her er et eksempel på et script, der genererer et proceduremæssigt terræn ved hjælp af Perlin-støj:
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;
    }
}
  • Vedhæft "TerrainGenerator"-scriptet til Terrain-objektet i Unity-editoren.
  • I Inspector-vinduet for terrænobjektet skal du justere bredden, højden, skalaen, forskydninger og støjintensiteten for at justere det genererede terræns udseende.
  • Tryk på Play-knappen i Unity Editor, og det proceduremæssige terræn skal derefter genereres baseret på Perlin-støjalgoritmen.

Unity Terrain generation med Perlin-støj.

Bemærk: Dette script genererer et grundlæggende terrænhøjdekort ved hjælp af Perlin-støj. For at skabe mere komplekse terræn skal du ændre scriptet for at inkorporere yderligere støjalgoritmer, anvende erosion eller udjævningsteknikker, tilføje teksturering eller placere løv og objekter baseret på terrænets funktioner.

Mobilautomater

Cellulære automater er en beregningsmodel, der består af et gitter af celler, hvor hver celle udvikler sig baseret på et sæt foruddefinerede regler og tilstandene for dens naboceller. Det er et kraftfuldt koncept, der bruges på forskellige områder, herunder datalogi, matematik og fysik. Cellulære automater kan udvise komplekse adfærdsmønstre, der opstår fra simple regler, hvilket gør dem nyttige til at simulere naturlige fænomener og generere proceduremæssigt indhold.

Den grundlæggende teori bag cellulære automater involverer følgende elementer:

  1. Gitter: Et gitter er en samling af celler arrangeret i et regulært mønster, såsom et kvadratisk eller sekskantet gitter. Hver celle kan have et begrænset antal tilstande.
  2. Naboer: Hver celle har naboceller, som typisk er dens umiddelbart tilstødende celler. Nabolaget kan defineres ud fra forskellige forbindelsesmønstre, såsom von Neumann (op, ned, venstre, højre) eller Moore (inklusive diagonale) kvarterer.
  3. Regler: Hver celles adfærd bestemmes af et sæt regler, der specificerer, hvordan den udvikler sig baseret på dens aktuelle tilstand og tilstandene for dens naboceller. Disse regler er typisk defineret ved hjælp af betingede sætninger eller opslagstabeller.
  4. Opdatering: Den cellulære automat udvikler sig ved at opdatere tilstanden for hver celle samtidigt i henhold til reglerne. Denne proces gentages iterativt, hvilket skaber en sekvens af generationer.

Cellulære automater har forskellige applikationer fra den virkelige verden, herunder:

  1. Simulering af naturlige fænomener: Cellulære automater kan simulere opførsel af fysiske systemer, såsom væskedynamik, skovbrande, trafikstrøm og befolkningsdynamik. Ved at definere passende regler kan cellulære automater fange de nye mønstre og dynamikker, der observeres i systemer i den virkelige verden.
  2. Generering af proceduremæssigt indhold: Cellulær automat kan bruges til at generere proceduremæssigt indhold i spil og simuleringer. For eksempel kan de bruges til at skabe terræn, hulesystemer, vegetationsfordeling og andre organiske strukturer. Komplekse og realistiske miljøer kan genereres ved at specificere regler, der styrer cellernes vækst og interaktion.

Her er et simpelt eksempel på implementering af en grundlæggende cellulær automat i Unity for at simulere livets spil:

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;
    }
}
  • Vedhæft "CellularAutomaton"-scriptet til et GameObject i Unity-scenen og tildel en cellepræfabrikat til feltet 'cellPrefab' i inspektøren.

Mobilautomat i Unity.

I dette eksempel er et gitter af celler repræsenteret af en boolsk matrix, hvor 'true' angiver en levende celle og 'false' repræsenterer en død celle. Reglerne for livets spil anvendes til at opdatere gitteret, og den visuelle repræsentation af celler opdateres i overensstemmelse hermed. 'CreateCells()'-metoden opretter et GameObject for hver celle, og metoden 'UpdateCells()' opdaterer farven på hvert GameObject baseret på gittertilstanden.

Bemærk: Dette er blot et grundlæggende eksempel, og der er mange variationer og udvidelser til cellulære automater, der kan udforskes. Reglerne, celleadfærden og gitterkonfigurationerne kan ændres for at skabe forskellige simuleringer og generere forskellige mønstre og adfærd.

Voronoi-diagrammer

Voronoi-diagrammer, også kendt som Voronoi-tesselationer eller Voronoi-partitioner, er geometriske strukturer, der opdeler et rum i regioner baseret på nærhed til et sæt punkter kaldet frø eller steder. Hver region i et Voronoi-diagram består af alle punkter i rummet, der er tættere på et bestemt frø end på noget andet frø.

Den grundlæggende teori bag Voronoi-diagrammer involverer følgende elementer:

  1. Frø/steder: Frø eller steder er et sæt punkter i rummet. Disse punkter kan genereres tilfældigt eller placeres manuelt. Hvert frø repræsenterer et midtpunkt for en Voronoi-region.
  2. Voronoi-celler/regioner: Hver Voronoi-celle eller region svarer til et område af rummet, der er tættere på et bestemt frø end på noget andet frø. Grænserne for regionerne er dannet af de vinkelrette halveringslinjer af linjesegmenterne, der forbinder nabofrø.
  3. Delaunay-triangulering: Voronoi-diagrammer er tæt beslægtede med Delaunay-triangulering. Delaunay-triangulering er en triangulering af frøpunkterne, således at intet frø er inde i en trekants omkreds. Delaunay-trianguleringen kan bruges til at konstruere Voronoi-diagrammer og omvendt.

Voronoi-diagrammer har forskellige applikationer fra den virkelige verden, herunder:

  1. Procedural Content Generation: Voronoi-diagrammer kan bruges til at generere proceduremæssigt terræn, naturlige landskaber og organiske former. Ved at bruge frøene som kontrolpunkter og tildele attributter (såsom højde eller biomtype) til Voronoi-cellerne, kan der skabes realistiske og varierede miljøer.
  2. Spildesign: Voronoi-diagrammer kan bruges i spildesign til at opdele plads til gameplay-formål. For eksempel kan Voronoi-diagrammer i strategispil bruges til at opdele spilkortet i territorier eller zoner, der kontrolleres af forskellige fraktioner.
  3. Pathfinding og AI: Voronoi-diagrammer kan hjælpe med pathfinding og AI-navigation ved at give en repræsentation af rummet, der muliggør effektiv beregning af det nærmeste frø eller område. De kan bruges til at definere navigationsmasker eller påvirke kort for AI-agenter.

I Unity er der flere måder at generere og bruge Voronoi-diagrammer på:

  1. Procedurel generation: Udviklere kan implementere algoritmer til at generere Voronoi-diagrammer fra et sæt frøpunkter i Unity. Forskellige algoritmer, såsom Fortunes algoritme eller Lloyd afslapningsalgoritmen, kan bruges til at konstruere Voronoi-diagrammer.
  2. Terrængenerering: Voronoi-diagrammer kan bruges i terrængenerering til at skabe mangfoldige og realistiske landskaber. Hver Voronoi-celle kan repræsentere et andet terræntræk, såsom bjerge, dale eller sletter. Attributter som højde, fugt eller vegetation kan tildeles hver celle, hvilket resulterer i et varieret og visuelt tiltalende terræn.
  3. Kortpartitionering: Voronoi-diagrammer kan bruges til at opdele spilkort i områder til spilformål. Det er muligt at tildele forskellige attributter eller egenskaber til hver region for at skabe forskellige gameplay-zoner. Dette kan være nyttigt til strategispil, territorial kontrolmekanik eller niveaudesign.

Der er Unity-pakker og aktiver tilgængelige, som giver Voronoi-diagramfunktionalitet, hvilket gør det nemmere at inkorporere Voronoi-baserede funktioner i Unity-projekter. Disse pakker inkluderer ofte Voronoi-diagramgenereringsalgoritmer, visualiseringsværktøjer og integration med Unity-gengivelsessystemet.

Her er et eksempel på generering af et 2D Voronoi-diagram i Unity ved hjælp af Fortunes algoritme:

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;
            }
        }
    }
}
  • For at bruge denne kode skal du oprette en sfære-præfabrikat og tildele den til seedPrefab-feltet i Unity-inspektøren. Juster variablerne numSeeds og diagramSize for at kontrollere antallet af frø og størrelsen af ​​diagrammet.

Voronoi-diagram i Unity.

I dette eksempel genererer VoronoiDiagram-scriptet et Voronoi-diagram ved tilfældigt at placere frøpunkter inden for den angivne diagramstørrelse. Metoden 'GenerateVoronoiDiagram()' beregner Voronoi-cellerne baseret på frøpunkterne, og metoden 'VisualizeVoronoiDiagram()' instansierer et sfære GameObject ved hvert punkt i Voronoi-cellerne og visualiserer diagrammet.

Bemærk: Dette eksempel giver en grundlæggende visualisering af Voronoi-diagrammet, men det er muligt at udvide det yderligere ved at tilføje yderligere funktioner, såsom at forbinde cellepunkterne med linjer eller tildele forskellige attributter til hver celle til terrængenerering eller gameplay-formål.

Samlet set tilbyder Voronoi-diagrammer et alsidigt og kraftfuldt værktøj til at generere proceduremæssigt indhold, opdele plads og skabe interessante og varierede miljøer i Unity.

Procedurel objektplacering

Procedurel objektplacering i Unity involverer generering og placering af objekter i en scene algoritmisk i stedet for manuelt at placere dem. Det er en kraftfuld teknik, der bruges til forskellige formål, såsom at befolke miljøer med træer, klipper, bygninger eller andre objekter på en naturlig og dynamisk måde.

Her er et eksempel på proceduremæssig objektplacering i 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;
    }
}
  • For at bruge dette script skal du oprette et tomt GameObject i Unity-scenen og vedhæfte "ObjectPlacement"-scriptet til det. Tildel objektets præfabrikat og juster parametrene 'numObjects' og 'spawnArea' i inspektøren, så de passer til kravene. Når du kører scenen, vil objekterne blive placeret proceduremæssigt inden for det definerede spawn-område.

Procedurel objektplacering i Unity.

I dette eksempel er 'ObjectPlacement' scriptet ansvarligt for proceduremæssigt at placere objekter i scenen. Feltet 'objectPrefab' skal tildeles præfabrikatet for det objekt, der skal placeres. Variablen 'numObjects' bestemmer antallet af objekter, der skal placeres, og variablen 'spawnArea' definerer det område, hvor objekterne vil blive placeret tilfældigt.

'PlaceObjects()'-metoden går gennem det ønskede antal objekter og genererer tilfældige spawn-positioner inden for det definerede spawn-område. Det instansierer derefter objektet præfabrikeret ved hver tilfældig position med en tilfældig rotation.

Bemærk: Det er muligt at forbedre denne kode yderligere ved at inkorporere forskellige placeringsalgoritmer, såsom gitterbaseret placering, tæthedsbaseret placering eller regelbaseret placering, afhængigt af de specifikke krav til projektet.

Konklusion

Proceduregenereringsteknikker i Unity giver kraftfulde værktøjer til at skabe dynamiske og fordybende oplevelser. Uanset om det er at generere terræn ved hjælp af Perlin-støj eller fraktale algoritmer, skabe forskellige miljøer med Voronoi-diagrammer, simulere kompleks adfærd med cellulære automater eller udfylde scener med proceduremæssigt placerede objekter, tilbyder disse teknikker fleksibilitet, effektivitet og uendelige muligheder for indholdsgenerering. Ved at udnytte disse algoritmer og integrere dem i Unity-projekter kan udviklere opnå realistisk terrængenerering, naturtro simuleringer, visuelt tiltalende miljøer og engagerende gameplay-mekanik. Proceduregenerering sparer ikke kun tid og kræfter, men muliggør også skabelsen af ​​unikke og evigt skiftende oplevelser, der fængsler spillere og bringer virtuelle verdener til live.