Generazione procedurale del mondo in Unity

La generazione del mondo in Unity si riferisce al processo di creazione o generazione procedurale di mondi virtuali, terreni, paesaggi o ambienti all'interno del motore di gioco Unity. Questa tecnica è comunemente utilizzata in vari tipi di giochi, come giochi open-world, giochi di ruolo, simulazioni e altro, per creare dinamicamente mondi di gioco vasti e diversificati.

Unity fornisce un quadro flessibile e un'ampia gamma di strumenti e API per l'implementazione di queste tecniche di generazione del mondo. È possibile scrivere script personalizzati utilizzando C# per generare e manipolare il mondo di gioco o utilizzare le funzionalità integrate Unity come il sistema Terrain, le funzioni di rumore e le interfacce di scripting per ottenere i risultati desiderati. Inoltre, ci sono anche risorse di terze parti e plugin disponibili su Unity Asset Store che possono assistere nelle attività di generazione del mondo.

Esistono diversi approcci alla generazione del mondo in Unity e la scelta dipende dai requisiti specifici del gioco. Ecco alcuni metodi comunemente utilizzati:

  • Generazione procedurale del terreno con rumore Perlin
  • Automi cellulari
  • Diagrammi di Voronoi
  • Posizionamento procedurale degli oggetti

Generazione procedurale del terreno con rumore Perlin

La generazione procedurale del terreno in Unity può essere ottenuta utilizzando vari algoritmi e tecniche. Un approccio diffuso consiste nell'utilizzare il rumore Perlin per generare la mappa dell'altezza e quindi applicare varie tecniche di texture e fogliame per creare un terreno realistico o stilizzato.

Il rumore Perlin è un tipo di rumore gradiente sviluppato da Ken Perlin. Genera uno schema regolare e continuo di valori che appaiono casuali ma hanno una struttura coerente. Il rumore Perlin è ampiamente utilizzato per creare terreni, nuvole, trame e altre forme organiche dall'aspetto naturale.

In Unity, è possibile utilizzare la funzione 'Mathf.PerlinNoise()' per generare rumore Perlin. Richiede due coordinate come input e restituisce un valore compreso tra 0 e 1. Campionando il rumore Perlin a frequenze e ampiezze diverse, è possibile creare diversi livelli di dettaglio e complessità nel contenuto procedurale.

Ecco un esempio di come implementarlo in Unity:

  • Nell'editor Unity, vai a "GameObject -> 3D Object -> Terrain". Questo creerà un terreno predefinito nella scena.
  • Crea un nuovo script C# chiamato "TerrainGenerator" e attaccalo all'oggetto terreno. Ecco uno script di esempio che genera un terreno procedurale utilizzando il rumore 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;
    }
}
  • Allega lo script "TerrainGenerator" all'oggetto Terreno nell'editor Unity.
  • Nella finestra Ispettore dell'oggetto terreno, regola la larghezza, l'altezza, la scala, gli offset e l'intensità del rumore per modificare l'aspetto del terreno generato.
  • Premi il pulsante Riproduci nell'editor Unity e il terreno procedurale dovrebbe quindi essere generato in base all'algoritmo del rumore Perlin.

Generazione Unity Terrain con rumore Perlin.

Nota: questo script genera una mappa base dell'altezza del terreno utilizzando il rumore Perlin. Per creare terreni più complessi, modifica lo script per incorporare algoritmi di rumore aggiuntivi, applica tecniche di erosione o levigatura, aggiungi texture o posiziona fogliame e oggetti in base alle caratteristiche del terreno.

Automi cellulari

Gli automi cellulari sono un modello computazionale costituito da una griglia di celle, in cui ciascuna cella si evolve in base a un insieme di regole predefinite e agli stati delle celle vicine. È un concetto potente utilizzato in vari campi, tra cui informatica, matematica e fisica. Gli automi cellulari possono esibire modelli di comportamento complessi che emergono da regole semplici, rendendoli utili per simulare fenomeni naturali e generare contenuti procedurali.

La teoria di base dietro gli automi cellulari coinvolge i seguenti elementi:

  1. Griglia: una griglia è un insieme di celle disposte secondo uno schema regolare, ad esempio un reticolo quadrato o esagonale. Ogni cella può avere un numero finito di stati.
  2. Vicini: ogni cella ha celle vicine, che in genere sono le celle immediatamente adiacenti. Il quartiere può essere definito in base a diversi modelli di connettività, come i quartieri di von Neumann (su, giù, sinistra, destra) o Moore (inclusa la diagonale).
  3. Regole: Il comportamento di ciascuna cella è determinato da un insieme di regole che specificano come si evolve in base al suo stato attuale e agli stati delle celle vicine. Queste regole vengono generalmente definite utilizzando istruzioni condizionali o tabelle di ricerca.
  4. Aggiornamento: L'automa cellulare si evolve aggiornando simultaneamente lo stato di ciascuna cella secondo le regole. Questo processo viene ripetuto iterativamente, creando una sequenza di generazioni.

Gli automi cellulari hanno varie applicazioni nel mondo reale, tra cui:

  1. Simulazione di fenomeni naturali: gli automi cellulari possono simulare il comportamento di sistemi fisici, come la dinamica dei fluidi, gli incendi boschivi, il flusso del traffico e la dinamica della popolazione. Definendo regole appropriate, gli automi cellulari possono catturare i modelli e le dinamiche emergenti osservati nei sistemi del mondo reale.
  2. Generazione di contenuti procedurali: gli automi cellulari possono essere utilizzati per generare contenuti procedurali in giochi e simulazioni. Ad esempio, possono essere impiegati per creare terreni, sistemi di grotte, distribuzione della vegetazione e altre strutture organiche. Ambienti complessi e realistici possono essere generati specificando regole che governano la crescita e l'interazione delle cellule.

Ecco un semplice esempio di implementazione di un automa cellulare di base in Unity per simulare il gioco della vita:

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;
    }
}
  • Collega lo script "CellularAutomaton" a un GameObject nella scena Unity e assegna una cella prefabbricata al campo 'cellPrefab' nell'ispettore.

Automa cellulare in Unity.

In questo esempio, una griglia di celle è rappresentata da un array booleano, dove 'true' indica una cella viva e 'false' rappresenta una cella morta. Per aggiornare la griglia vengono applicate le regole del gioco della vita e la rappresentazione visiva delle celle viene aggiornata di conseguenza. Il metodo 'CreateCells()' crea un GameObject per ogni cella e il metodo 'UpdateCells()' aggiorna il colore di ciascun GameObject in base allo stato della griglia.

Nota: questo è solo un esempio di base e ci sono molte varianti ed estensioni degli automi cellulari che possono essere esplorate. Le regole, i comportamenti delle celle e le configurazioni della griglia possono essere modificati per creare diverse simulazioni e generare vari modelli e comportamenti.

Diagrammi di Voronoi

I diagrammi di Voronoi, noti anche come tassellazioni di Voronoi o partizioni di Voronoi, sono strutture geometriche che dividono uno spazio in regioni in base alla vicinanza a un insieme di punti chiamati semi o siti. Ogni regione in un diagramma di Voronoi è costituita da tutti i punti nello spazio che sono più vicini a un particolare seme che a qualsiasi altro seme.

La teoria di base dietro i diagrammi di Voronoi coinvolge i seguenti elementi:

  1. Semi/Siti: I semi o i siti sono un insieme di punti nello spazio. Questi punti possono essere generati casualmente o posizionati manualmente. Ogni seme rappresenta un punto centrale per una regione di Voronoi.
  2. Celle/Regioni di Voronoi: Ogni cella o regione di Voronoi corrisponde a un'area dello spazio che è più vicina a un particolare seme che a qualsiasi altro seme. I confini delle regioni sono formati dalle bisettrici perpendicolari dei segmenti di linea che collegano i semi vicini.
  3. Triangolazione di Delaunay: i diagrammi di Voronoi sono strettamente correlati alla triangolazione di Delaunay. La triangolazione di Delaunay è una triangolazione dei punti seme in modo tale che nessun seme si trovi all'interno del cerchio circoscritto di alcun triangolo. La triangolazione di Delaunay può essere utilizzata per costruire diagrammi di Voronoi e viceversa.

I diagrammi di Voronoi hanno varie applicazioni nel mondo reale, tra cui:

  1. Generazione di contenuti procedurali: i diagrammi di Voronoi possono essere utilizzati per generare terreni procedurali, paesaggi naturali e forme organiche. Utilizzando i semi come punti di controllo e assegnando attributi (come elevazione o tipo di bioma) alle celle di Voronoi, è possibile creare ambienti realistici e vari.
  2. Game Design: i diagrammi di Voronoi possono essere utilizzati nella progettazione del gioco per suddividere lo spazio a fini di gioco. Ad esempio, nei giochi di strategia, i diagrammi di Voronoi possono essere utilizzati per dividere la mappa di gioco in territori o zone controllate da diverse fazioni.
  3. Pathfinding e AI: i diagrammi di Voronoi possono aiutare nella pathfinding e nella navigazione AI fornendo una rappresentazione dello spazio che consente un calcolo efficiente del seme o della regione più vicina. Possono essere utilizzati per definire mesh di navigazione o influenzare le mappe per gli agenti AI.

In Unity ci sono diversi modi per generare e utilizzare i diagrammi di Voronoi:

  1. Generazione procedurale: gli sviluppatori possono implementare algoritmi per generare diagrammi di Voronoi da una serie di punti seed in Unity. Vari algoritmi, come l'algoritmo di Fortune o l'algoritmo di rilassamento di Lloyd, possono essere utilizzati per costruire diagrammi di Voronoi.
  2. Generazione del terreno: i diagrammi di Voronoi possono essere utilizzati nella generazione del terreno per creare paesaggi diversi e realistici. Ogni cella Voronoi può rappresentare un diverso elemento del terreno, come montagne, valli o pianure. Attributi come elevazione, umidità o vegetazione possono essere assegnati a ciascuna cella, risultando in un terreno vario e visivamente accattivante.
  3. Partizionamento della mappa: i diagrammi di Voronoi possono essere utilizzati per dividere le mappe di gioco in regioni a scopo di gioco. È possibile assegnare attributi o proprietà diversi a ciascuna regione per creare zone di gioco distinte. Questo può essere utile per giochi di strategia, meccaniche di controllo territoriale o progettazione di livelli.

Sono disponibili pacchetti e risorse Unity che forniscono la funzionalità del diagramma Voronoi, semplificando l'incorporazione di funzionalità basate su Voronoi nei progetti Unity. Questi pacchetti spesso includono algoritmi di generazione di diagrammi Voronoi, strumenti di visualizzazione e integrazione con il sistema di rendering Unity.

Ecco un esempio di generazione di un diagramma Voronoi 2D in Unity utilizzando l'algoritmo di 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;
            }
        }
    }
}
  • Per utilizzare questo codice, crea una sfera prefabbricata e assegnala al campo seedPrefab nell'ispettore Unity. Regola le variabili numSeeds e diagramSize per controllare il numero di semi e la dimensione del diagramma.

Diagramma di Voronoi in Unity.

In questo esempio, lo script VoronoiDiagram genera un diagramma Voronoi posizionando casualmente i punti seed all'interno della dimensione del diagramma specificata. Il metodo 'GenerateVoronoiDiagram()' calcola le celle di Voronoi in base ai punti seed e il metodo 'VisualizeVoronoiDiagram()' istanzia una sfera GameObject in ogni punto delle celle di Voronoi, visualizzando il diagramma.

Nota: questo esempio fornisce una visualizzazione di base del diagramma di Voronoi, ma è possibile estenderlo ulteriormente aggiungendo funzionalità aggiuntive, come collegare i punti delle celle con linee o assegnare attributi diversi a ciascuna cella per la generazione del terreno o per scopi di gioco.

Nel complesso, i diagrammi di Voronoi offrono uno strumento versatile e potente per generare contenuti procedurali, partizionare lo spazio e creare ambienti interessanti e vari in Unity.

Posizionamento procedurale degli oggetti

Il posizionamento procedurale degli oggetti in Unity implica la generazione e il posizionamento degli oggetti in una scena in modo algoritmico, anziché posizionarli manualmente. È una tecnica potente utilizzata per vari scopi, come popolare ambienti con alberi, rocce, edifici o altri oggetti in modo naturale e dinamico.

Ecco un esempio di posizionamento di oggetti procedurali 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;
    }
}
  • Per utilizzare questo script, crea un GameObject vuoto nella scena Unity e attacca lo script "ObjectPlacement" ad esso. Assegna l'oggetto prefabbricato e regola i parametri 'numObjects' e 'spawnArea' nell'ispettore per soddisfare i requisiti. Durante l'esecuzione della scena, gli oggetti verranno posizionati proceduralmente all'interno dell'area di spawn definita.

Posizionamento procedurale degli oggetti in Unity.

In questo esempio, lo script 'ObjectPlacement' è responsabile del posizionamento procedurale degli oggetti nella scena. Al campo 'objectPrefab' deve essere assegnato il prefabbricato dell'oggetto da posizionare. La variabile 'numObjects' determina il numero di oggetti da posizionare e la variabile 'spawnArea' definisce l'area in cui gli oggetti verranno posizionati in modo casuale.

Il metodo 'PlaceObjects()' scorre il numero desiderato di oggetti e genera posizioni di spawn casuali all'interno dell'area di spawn definita. Quindi crea un'istanza dell'oggetto prefabbricato in ciascuna posizione casuale con una rotazione casuale.

Nota: è possibile migliorare ulteriormente questo codice incorporando vari algoritmi di posizionamento, come il posizionamento basato su griglia, il posizionamento basato sulla densità o il posizionamento basato su regole, a seconda dei requisiti specifici del progetto.

Conclusione

Le tecniche di generazione procedurale in Unity forniscono strumenti potenti per creare esperienze dinamiche e coinvolgenti. Che si tratti di generare terreni utilizzando il rumore Perlin o algoritmi frattali, creando ambienti diversi con diagrammi di Voronoi, simulando comportamenti complessi con automi cellulari o popolando scene con oggetti posizionati proceduralmente, queste tecniche offrono flessibilità, efficienza e infinite possibilità per la generazione di contenuti. Sfruttando questi algoritmi e integrandoli nei progetti Unity, gli sviluppatori possono ottenere una generazione di terreni realistici, simulazioni realistiche, ambienti visivamente accattivanti e meccaniche di gioco coinvolgenti. La generazione procedurale non solo fa risparmiare tempo e fatica, ma consente anche la creazione di esperienze uniche e in continua evoluzione che affascinano i giocatori e danno vita ai mondi virtuali.