5.4 Procedural generation
Stundas uzdevums: Ģenerēt nejaušus, bet sakārtotus līmeņus ar algoritmu.
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žģī.
- Izveido
int[,] grid = new int[15, 15];. - Aizpildi to visu ar vērtību 1 (siena).
- Sāc no centra un iestati tur vērtību 0 (eja).
- Uzraksti ciklu, kas 200 reizes paiet nejaušā virzienā.
- Iestati katrā solī jauno šūnu uz 0.
- Pārbaudi, ka gājējs nepamet režģa robežas.
- 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.
- Pievieno scēnai
TileMapvai izmantoColorRectmezglus. - Uzraksti divkāršu ciklu pār visu režģi.
- Novieto sienas mezglu katrā vietā, kur vērtība ir 1.
- Reizini režģa koordinātes ar šūnas izmēru, piemēram 64.
- Novieto spēlētāju vietā, kur vērtība ir 0.
- Palaid un pastaigā pa labirintu.
- 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.
- Novieto izeju tālākajā režģa stūrī, kur vērtība ir 0.
- Uzraksti metodi, kas no starta izplatās pa visām savienotajām ejām.
- Atzīmē apmeklētās šūnas otrā režģī.
- Pārbaudi, vai izejas šūna tika atzīmēta.
- Ģenerē labirintu atkārtoti, kamēr izeja ir sasniedzama.
- Saskaiti, cik reižu vidēji vajadzēja ģenerēt.
- 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.
- Pievieno lauku
int seed;. - Iestati to ar
Random rng = new Random(seed);pirms ģenerēšanas. - Ģenerē divas reizes ar to pašu sēklu un salīdzini rezultātu.
- Ģenerē ar citu sēklu un salīdzini vēlreiz.
- 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:
- Fails
MazeGenerator.cs. - Trīs ekrānuzņēmumi ar trim dažādiem uzģenerētiem labirintiem.
- Ekrānuzņēmums ar Output paneli, kurā redzams izdrukātais režģis.
- 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
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);
}
}
}
}