ebSkola

5.4 Procedural generation

Stundas uzdevums: Ģenerēt nejaušus, bet sakārtotus līmeņus ar algoritmu.

SR 2.4.19. Algoritmu efektivitāte SR 2.4.16. Datu struktūras

80 min plāns: Teorija un paraugs (~10 min) · 1. uzdevums (~20 min) - ģenerē labirintu ar Random Walk · 2. uzdevums (~25 min) - pārvērs režģi par spēles laukumu · 3. uzdevums (~20 min) - pārbaudi, vai labirints ir izejams · Iesniegšana (~5 min).

Pirms sāc: atver savu projektu. Šodien līmeņus vairs nezīmēsi ar roku - tos uzģenerēs kods. Sāc ar mazu režģi 15x15, nevis milzīgu.

Teorija: Procedural generation pamati

Procedural generation ir tehnika, kas algoritmiski rada saturu (līmeņus, labirintus, pasaules) bez manuālas dizainas. Klasiskais pieteikums - procedurāls klases labirints.

Pamata pieejas:

  • Random Walk - sāc no centra, ej nejaušā virzienā.
  • BSP (Binary Space Partition) - dali telpu pa pusēm, savieno.
  • Cellular Automata - sākumā nejauši, tad smooth ar reglām.
using System;

// Random walk labirints
public class MazeGenerator
{
    private const int Width = 50;
    private const int Height = 30;
    private const int Wall = 1;
    private const int Floor = 0;
    private readonly int[,] grid = new int[Width, Height];

    public void Generate(int seed)
    {
        Random rng = new Random(seed);

        // Sāc visu kā sienas
        for (int x = 0; x < Width; x++)
            for (int y = 0; y < Height; y++)
                grid[x, y] = Wall;

        // Random walk
        int px = Width / 2;
        int py = Height / 2;
        for (int step = 0; step < 1000; step++)
        {
            grid[px, py] = Floor;
            int dir = rng.Next(0, 4);
            if (dir == 0 && px > 1) px--;
            else if (dir == 1 && px < Width - 2) px++;
            else if (dir == 2 && py > 1) py--;
            else if (dir == 3 && py < Height - 2) py++;
        }
    }
}

Kā šo izmantot projektā

Atceries: ar redzamu efektu editorā nepietiek. Paskaidro, kura C# klase glabā stāvokli, kura metode to maina un kā Godot node struktūra izmanto šo kodu.

Pārbaudi: C# kods pārbauda failu kļūdas, validē datus, izmanto versijas lauku un pamato algoritmu sarežģītību.

public class PersistenceCheckpoint
{
    public string Lesson = "5.4 Procedural generation";
    public bool UsesCSharp = true;
    public bool HandlesMissingFile = true;
    public bool ValidatesData = true;
    public bool DocumentsComplexity = true;
}

Praktiskie uzdevumi

1. uzdevums -

Ģenerē labirintu ar Random Walk

Beigās kods pats izraks ejas tukšā režģī.

  1. Izveido int[,] grid = new int[15, 15];.
  2. Aizpildi to visu ar vērtību 1 (siena).
  3. Sāc no centra un iestati tur vērtību 0 (eja).
  4. Uzraksti ciklu, kas 200 reizes paiet nejaušā virzienā.
  5. Iestati katrā solī jauno šūnu uz 0.
  6. Pārbaudi, ka gājējs nepamet režģa robežas.
  7. Izdrukā režģi Output panelī ar simboliem # un atstarpi.

Gatavs, kad: Output panelī redzams režģis ar savienotām ejām, un tās nekad neiziet ārpus malām.

2. uzdevums -

Pārvērs režģi par spēles laukumu

Beigās uzģenerēto labirintu varēs redzēt un tajā staigāt.

  1. Pievieno scēnai TileMap vai izmanto ColorRect mezglus.
  2. Uzraksti divkāršu ciklu pār visu režģi.
  3. Novieto sienas mezglu katrā vietā, kur vērtība ir 1.
  4. Reizini režģa koordinātes ar šūnas izmēru, piemēram 64.
  5. Novieto spēlētāju vietā, kur vērtība ir 0.
  6. Palaid un pastaigā pa labirintu.
  7. Palaid vēlreiz un pārliecinies, ka labirints ir citāds.

Gatavs, kad: katrā palaišanas reizē labirints ir atšķirīgs, un spēlētājs var pa to staigāt.

3. uzdevums -

Pārbaudi, vai labirints ir izejams

Beigās tu būsi pārliecinājies, ka izeja tiešām ir sasniedzama.

  1. Novieto izeju tālākajā režģa stūrī, kur vērtība ir 0.
  2. Uzraksti metodi, kas no starta izplatās pa visām savienotajām ejām.
  3. Atzīmē apmeklētās šūnas otrā režģī.
  4. Pārbaudi, vai izejas šūna tika atzīmēta.
  5. Ģenerē labirintu atkārtoti, kamēr izeja ir sasniedzama.
  6. Saskaiti, cik reižu vidēji vajadzēja ģenerēt.
  7. Pieraksti, kāpēc šī pārbaude ir obligāta.

Gatavs, kad: spēle nekad nesāk ar labirintu, kurā izeja nav sasniedzama.

Papildu uzdevums - Pievieno sēklu atkārtojamībai

Ja pamatdarbs ir gatavs, ļauj atkārtot to pašu labirintu.

  1. Pievieno lauku int seed;.
  2. Iestati to ar Random rng = new Random(seed); pirms ģenerēšanas.
  3. Ģenerē divas reizes ar to pašu sēklu un salīdzini rezultātu.
  4. Ģenerē ar citu sēklu un salīdzini vēlreiz.
  5. Izdrukā sēklu ekrānā, lai to varētu pierakstīt.

Gatavs, kad: ar vienu un to pašu sēklu labirints vienmēr sanāk identisks.

Ko sagatavo

Stundas mērķis: Tava spēle pati uzģenerē labirintu ar Random Walk algoritmu, pārvērš to par spēlējamu laukumu un pirms sākuma pārbauda, ka izeja tiešām ir sasniedzama.

GitHub krātuvē jābūt:

  1. Fails MazeGenerator.cs.
  2. Trīs ekrānuzņēmumi ar trim dažādiem uzģenerētiem labirintiem.
  3. Ekrānuzņēmums ar Output paneli, kurā redzams izdrukātais režģis.
  4. README.md: kāpēc obligāti jāpārbauda, vai izeja ir sasniedzama.

Kā iesniedz: Veic commit un push. Skolotāja norādītajā vietā ievieto tikai GitHub krātuves saiti; visiem prasītajiem failiem, README.md pierakstiem un pierādījumiem jābūt pašā krātuvē.

Biežākās kļūdas

  • Always same maze: Aizmirsts mainīt seed vai tiek katru reizi izmantota viena un tā pati vērtība.
  • Walls disconnected: Pārbaudi flood fill - vai visas istabas savienotas.
  • Slow generation: Lielam grid lieto efficient algorithms - A* vai Voronoi.

Godot ekrānuzņēmumi

Procedurāli ģenerēta labirints karte: pelēks fons (sienas), gaišāka taka (random walk ceļš) ar zaļu spawn point un sarkanu exit.
Procedurāli ģenerēta labirints karte: pelēks fons (sienas), gaišāka taka (random walk ceļš) ar zaļu spawn point un sarkanu exit.
Sarežģītāks labirints: 6 taisnstūrveida klases zonas savienotas ar koridoriem, dažas ar galdiem un šķēršļiem.
Sarežģītāks labirints: 6 taisnstūrveida klases zonas savienotas ar koridoriem, dažas ar galdiem un šķēršļiem.

Koda piemērs (paplašināts)

using Godot;
using System;

public class MazeGenerator
{
    private const int Width = 50;
    private const int Height = 30;
    private readonly int[,] grid = new int[Width, Height];

    public void Generate(int seed)
    {
        Random rng = new Random(seed);

        // Visi sienas
        for (int x = 0; x < Width; x++)
            for (int y = 0; y < Height; y++)
                grid[x, y] = 1;  // siena

        // Random walk
        int px = Width / 2;
        int py = Height / 2;
        for (int step = 0; step < 800; step++)
        {
            grid[px, py] = 0;  // grīda
            int dir = rng.Next(0, 4);
            if (dir == 0 && px > 1) px--;
            else if (dir == 1 && px < Width - 2) px++;
            else if (dir == 2 && py > 1) py--;
            else if (dir == 3 && py < Height - 2) py++;
        }
    }

    public void RenderToTileMap(TileMap tileMap)
    {
        for (int x = 0; x < Width; x++)
        {
            for (int y = 0; y < Height; y++)
            {
                int tileId = grid[x, y];
                tileMap.SetCell(0, new Vector2I(x, y), tileId, Vector2I.Zero);
            }
        }
    }
}
Katra spēles palaišanas reize ģenerē atšķirīgu labirintu: dažādas telpas, koridori un bonusa materiālu izvietojums.