Proceduralne pokolenie świata w jedności

Generowanie świata w Unity odnosi się do procesu tworzenia lub proceduralnego generowania wirtualnych światów, terenów, krajobrazów lub środowisk w silniku gry Unity. Technika ta jest powszechnie stosowana w różnego rodzaju grach, takich jak gry z otwartym światem, gry RPG, symulacje i inne, w celu dynamicznego tworzenia rozległych i różnorodnych światów gier.

Unity zapewnia elastyczne ramy oraz szeroką gamę narzędzi i interfejsów API do wdrażania tych technik generacji światowej. Można pisać własne skrypty, używając C# do generowania i manipulowania światem gry lub wykorzystywać wbudowane funkcje Unity, takie jak system Terrain, funkcje szumu i interfejsy skryptowe, aby osiągnąć pożądane rezultaty. Dodatkowo na Unity Asset Store dostępne są także zasoby i wtyczki stron trzecich, które mogą pomóc w zadaniach związanych z generowaniem świata.

Istnieje kilka podejść do generowania świata w Unity, a wybór zależy od konkretnych wymagań gry. Oto kilka powszechnie stosowanych metod:

  • Proceduralne generowanie terenu z szumem Perlina
  • Automaty komórkowe
  • Diagramy Woronoja
  • Proceduralne rozmieszczenie obiektów

Proceduralne generowanie terenu z szumem Perlina

Proceduralne generowanie terenu w Unity można osiągnąć przy użyciu różnych algorytmów i technik. Jednym z popularnych podejść jest użycie szumu Perlina do wygenerowania mapy wysokości, a następnie zastosowanie różnych technik teksturowania i listowia w celu stworzenia realistycznego lub stylizowanego terenu.

Szum Perlina to rodzaj szumu gradientowego opracowanego przez Kena Perlina. Generuje gładki, ciągły wzór wartości, które wydają się losowe, ale mają spójną strukturę. Szum perlinowy jest szeroko stosowany do tworzenia naturalnie wyglądających terenów, chmur, tekstur i innych organicznych kształtów.

W Unity można użyć funkcji 'Mathf.PerlinNoise()' do wygenerowania szumu Perlina. Jako dane wejściowe przyjmuje dwie współrzędne i zwraca wartość z zakresu od 0 do 1. Próbkując szum Perlina przy różnych częstotliwościach i amplitudach, można uzyskać różne poziomy szczegółowości i złożoności treści proceduralnej.

Oto przykład, jak zaimplementować to w Unity:

  • W edytorze Unity przejdź do "GameObject -> 3D Object -> Terrain". Spowoduje to utworzenie domyślnego terenu na scenie.
  • Utwórz nowy skrypt C# o nazwie "TerrainGenerator" i dołącz go do obiektu terenu. Oto przykładowy skrypt, który generuje teren proceduralny przy użyciu szumu Perlina:
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;
    }
}
  • Dołącz skrypt "TerrainGenerator" do obiektu Terrain w edytorze Unity.
  • W oknie Inspektora obiektu terenu dostosuj szerokość, wysokość, skalę, przesunięcia i intensywność szumu, aby dostosować wygląd wygenerowanego terenu.
  • Naciśnij przycisk Play w edytorze Unity, a następnie teren proceduralny powinien zostać wygenerowany w oparciu o algorytm szumu Perlina.

Generowanie terenu w Unity z szumem Perlina.

Uwaga: ten skrypt generuje podstawową mapę wysokości terenu przy użyciu szumu Perlina. Aby utworzyć bardziej złożone tereny, zmodyfikuj skrypt, aby uwzględnić dodatkowe algorytmy szumu, zastosuj techniki erozji lub wygładzania, dodaj teksturowanie lub umieść listowie i obiekty w oparciu o cechy terenu.

Automaty komórkowe

Automaty komórkowe to model obliczeniowy składający się z siatki komórek, gdzie każda komórka ewoluuje w oparciu o zestaw predefiniowanych reguł i stany sąsiadujących komórek. Jest to potężna koncepcja stosowana w różnych dziedzinach, w tym w informatyce, matematyce i fizyce. Automaty komórkowe mogą wykazywać złożone wzorce zachowań wyłaniające się z prostych reguł, co czyni je przydatnymi do symulowania zjawisk naturalnych i generowania treści proceduralnych.

Podstawowa teoria automatów komórkowych obejmuje następujące elementy:

  1. Siatka: Siatka to zbiór komórek ułożonych w regularny wzór, na przykład kratkę kwadratową lub sześciokątną. Każda komórka może mieć skończoną liczbę stanów.
  2. Sąsiedzi: Każda komórka ma sąsiednie komórki, którymi zazwyczaj są bezpośrednio sąsiadujące komórki. Sąsiedztwo można zdefiniować w oparciu o różne wzorce łączności, takie jak sąsiedztwa von Neumanna (góra, dół, lewo, prawo) lub sąsiedztwa Moore'a (w tym ukośne).
  3. Reguły: Zachowanie każdej komórki jest określone przez zestaw reguł określających jej ewolucję w oparciu o jej bieżący stan i stany sąsiadujących komórek. Reguły te są zazwyczaj definiowane przy użyciu instrukcji warunkowych lub tabel przeglądowych.
  4. Aktualizacja: Automat komórkowy ewoluuje, aktualizując stan każdej komórki jednocześnie, zgodnie z regułami. Proces ten powtarza się iteracyjnie, tworząc sekwencję pokoleń.

Automaty komórkowe mają różne zastosowania w świecie rzeczywistym, w tym:

  1. Symulacja zjawisk naturalnych: Automaty komórkowe mogą symulować zachowanie układów fizycznych, takie jak dynamika płynów, pożary lasów, przepływ ruchu i dynamika populacji. Definiując odpowiednie reguły, automaty komórkowe mogą uchwycić wyłaniające się wzorce i dynamikę obserwowane w systemach świata rzeczywistego.
  2. Generowanie treści proceduralnych: Automatów komórkowych można używać do generowania treści proceduralnych w grach i symulacjach. Można je na przykład wykorzystać do tworzenia terenu, systemów jaskiń, rozmieszczenia roślinności i innych struktur organicznych. Złożone i realistyczne środowiska można wygenerować poprzez określenie reguł rządzących wzrostem i interakcją komórek.

Oto prosty przykład implementacji podstawowego automatu komórkowego w Unity w celu symulacji gry w życie:

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;
    }
}
  • Dołącz skrypt "CellularAutomaton" do obiektu GameObject w scenie Unity i przypisz prefabrykat komórki do pola 'cellPrefab' w inspektorze.

Automat komórkowy w Unity.

W tym przykładzie siatka komórek jest reprezentowana przez tablicę boolowską, gdzie 'true' oznacza komórkę żywą, a 'false' oznacza komórkę martwą. Do aktualizacji siatki stosowane są zasady gry w życie, a wizualna reprezentacja komórek jest odpowiednio aktualizowana. Metoda 'CreateCells()' tworzy obiekt GameObject dla każdej komórki, a metoda 'UpdateCells()' aktualizuje kolor każdego obiektu GameObject w oparciu o stan siatki.

Uwaga: to tylko podstawowy przykład i istnieje wiele odmian i rozszerzeń automatów komórkowych, które można zbadać. Reguły, zachowania komórek i konfiguracje siatki można modyfikować, aby tworzyć różne symulacje i generować różne wzorce i zachowania.

Diagramy Woronoja

Diagramy Woronoja, znane również jako teselacje Woronoja lub podziały Woronoja, to struktury geometryczne dzielące przestrzeń na regiony na podstawie bliskości zestawu punktów zwanych nasionami lub miejscami. Każdy region na diagramie Woronoja składa się ze wszystkich punktów w przestrzeni, które są bliżej określonego nasienia niż jakiegokolwiek innego nasienia.

Podstawowa teoria diagramów Woronoja obejmuje następujące elementy:

  1. Nasiona/Miejsca: Nasiona lub Miejsca to zbiór punktów w przestrzeni. Punkty te mogą być generowane losowo lub umieszczane ręcznie. Każde ziarno reprezentuje punkt centralny regionu Woronoja.
  2. Komórki/Regiony Woronoja: Każda komórka lub region Woronoja odpowiada obszarowi przestrzeni, który jest bliżej określonego nasienia niż jakiegokolwiek innego nasienia. Granice regionów tworzą prostopadłe dwusieczne odcinków linii łączących sąsiednie nasiona.
  3. Triangulacja Delaunaya: Diagramy Woronoja są ściśle powiązane z triangulacją Delaunaya. Triangulacja Delaunaya to triangulacja punktów początkowych w taki sposób, że żaden materiał siewny nie znajduje się wewnątrz okręgu opisanego na dowolnym trójkącie. Triangulację Delaunaya można wykorzystać do konstruowania diagramów Woronoja i odwrotnie.

Diagramy Woronoja mają różne zastosowania w świecie rzeczywistym, w tym:

  1. Generowanie treści proceduralnych: diagramów Woronoja można używać do generowania terenu proceduralnego, naturalnych krajobrazów i kształtów organicznych. Wykorzystując nasiona jako punkty kontrolne i przypisując atrybuty (takie jak wysokość lub typ biomu) komórkom Voronoi, można stworzyć realistyczne i zróżnicowane środowiska.
  2. Projekt gry: Diagramy Woronoja można wykorzystać w projektowaniu gier do podziału przestrzeni na potrzeby rozgrywki. Na przykład w grach strategicznych diagramy Woronoja można wykorzystać do podzielenia mapy gry na terytoria lub strefy kontrolowane przez różne frakcje.
  3. Znajdowanie ścieżki i AI: diagramy Woronoja mogą pomóc w znajdowaniu ścieżki i nawigacji AI, zapewniając reprezentację przestrzeni, która pozwala na efektywne obliczenie najbliższego ziarna lub regionu. Można je wykorzystać do zdefiniowania siatek nawigacyjnych lub map wpływu dla agentów AI.

W Unity istnieje kilka sposobów generowania i wykorzystywania diagramów Woronoja:

  1. Generowanie proceduralne: Programiści mogą implementować algorytmy do generowania diagramów Woronoja na podstawie zestawu punktów początkowych w Unity. Do konstruowania diagramów Woronoja można zastosować różne algorytmy, takie jak algorytm Fortune'a lub algorytm relaksacji Lloyda.
  2. Generowanie terenu: Diagramy Woronoja można wykorzystać do generowania terenu w celu stworzenia różnorodnych i realistycznych krajobrazów. Każda komórka Woronoja może reprezentować inny element terenu, taki jak góry, doliny lub równiny. Do każdej komórki można przypisać takie atrybuty, jak wysokość, wilgotność lub roślinność, co pozwala uzyskać zróżnicowany i atrakcyjny wizualnie teren.
  3. Podział mapy: Diagramy Woronoja można wykorzystać do podzielenia map gry na regiony na potrzeby rozgrywki. Do każdego regionu można przypisać różne atrybuty lub właściwości, aby stworzyć odrębne strefy rozgrywki. Może to być przydatne w grach strategicznych, mechanice kontroli terytorialnej lub projektowaniu poziomów.

Dostępne są pakiety i zasoby Unity, które zapewniają funkcjonalność diagramu Voronoi, ułatwiając włączanie funkcji opartych na Voronoi do projektów Unity. Pakiety te często zawierają algorytmy generowania diagramów Woronoja, narzędzia do wizualizacji i integrację z systemem renderowania Unity.

Oto przykład wygenerowania dwuwymiarowego diagramu Woronoja w Unity przy użyciu algorytmu 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;
            }
        }
    }
}
  • Aby użyć tego kodu, utwórz prefabrykat kuli i przypisz go do pola nasionPrefab w inspektorze Unity. Dostosuj zmienne numSeeds i diagramSize, aby kontrolować liczbę nasion i rozmiar diagramu.

Diagram Woronoja w Jedności.

W tym przykładzie skrypt VoronoiDiagram generuje diagram Voronoi poprzez losowe umieszczanie punktów początkowych w obrębie określonego rozmiaru diagramu. Metoda 'GenerateVoronoiDiagram()' oblicza komórki Woronoja na podstawie punktów początkowych, a metoda 'VisualizeVoronoiDiagram()' tworzy instancję kuli GameObject w każdym punkcie komórek Woronoja, wizualizując diagram.

Uwaga: ten przykład zapewnia podstawową wizualizację diagramu Woronoja, ale można ją rozszerzyć, dodając dodatkowe funkcje, takie jak łączenie punktów komórek liniami lub przypisywanie różnych atrybutów do każdej komórki na potrzeby generowania terenu lub rozgrywki.

Ogólnie rzecz biorąc, diagramy Woronoja oferują wszechstronne i potężne narzędzie do generowania treści proceduralnych, dzielenia przestrzeni oraz tworzenia interesujących i różnorodnych środowisk w Unity.

Proceduralne rozmieszczenie obiektów

Proceduralne rozmieszczanie obiektów w Unity polega na generowaniu i umieszczaniu obiektów w scenie algorytmicznie, a nie na ręcznym ich pozycjonowaniu. Jest to potężna technika wykorzystywana do różnych celów, takich jak zapełnianie środowiska drzewami, skałami, budynkami lub innymi obiektami w naturalny i dynamiczny sposób.

Oto przykład umieszczenia obiektu proceduralnego w 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;
    }
}
  • Aby użyć tego skryptu, utwórz pusty GameObject w scenie Unity i dołącz skrypt "ObjectPlacement" do niego. Przypisz prefabrykat obiektu i dostosuj parametry 'numObjects' i 'spawnArea' w inspektorze, aby dopasować je do wymagań. Podczas uruchamiania sceny obiekty będą umieszczane proceduralnie w określonym obszarze odradzania.

Proceduralne rozmieszczanie obiektów w Unity.

W tym przykładzie skrypt 'ObjectPlacement' jest odpowiedzialny za proceduralne umieszczanie obiektów w scenie. Do pola 'objectPrefab' należy przypisać prefabrykat obiektu do umieszczenia. Zmienna 'numObjects' określa liczbę obiektów do umieszczenia, a zmienna 'spawnArea' określa obszar, w którym obiekty zostaną losowo rozmieszczone.

Metoda 'PlaceObjects()' przechodzi przez żądaną liczbę obiektów i generuje losowe pozycje odradzania w zdefiniowanym obszarze odradzania. Następnie tworzy instancję prefabrykatu obiektu w każdej losowej pozycji z losową rotacją.

Uwaga: możliwe jest dalsze ulepszenie tego kodu poprzez włączenie różnych algorytmów rozmieszczania, takich jak rozmieszczanie w oparciu o siatkę, rozmieszczanie w oparciu o gęstość lub rozmieszczanie w oparciu o reguły, w zależności od konkretnych wymagań projektu.

Wniosek

Techniki generowania proceduralnego w Unity zapewniają potężne narzędzia do tworzenia dynamicznych i wciągających doświadczeń. Niezależnie od tego, czy chodzi o generowanie terenów przy użyciu szumu Perlina lub algorytmów fraktalnych, tworzenie różnorodnych środowisk za pomocą diagramów Woronoja, symulowanie złożonych zachowań za pomocą automatów komórkowych, czy też zapełnianie scen obiektami umieszczanymi proceduralnie, techniki te oferują elastyczność, wydajność i nieograniczone możliwości generowania treści. Wykorzystując te algorytmy i integrując je z projektami Unity, programiści mogą uzyskać realistyczne generowanie terenu, realistyczne symulacje, atrakcyjne wizualnie środowiska i wciągającą mechanikę rozgrywki. Generowanie proceduralne nie tylko oszczędza czas i wysiłek, ale także umożliwia tworzenie unikalnych i ciągle zmieniających się doświadczeń, które urzekają graczy i ożywiają wirtualne światy.

Sugerowane artykuły
Znaczenie opowiadania historii w tworzeniu gier w Unity
Niezbędne zasoby ogólnego przeznaczenia dla Unity
Opanowanie komponentu transformacji Unity
Interfejs API skryptów Unity i Unity Pro
Wskazówki na Twitterze dotyczące jedności
Jak malować drzewa na terenie w Unity
Jak importować animacje do Unity