În această parte finală a seriei tutorial, vom construi primul tutorial și vom învăța despre implementarea pickups, declanșatoare, schimbarea nivelului, urmărirea traseului, urmărirea traseului, derularea nivelului, înălțimea izometrică și proiectile izometrice.
Pickups sunt elemente care pot fi colectate în cadrul nivelului, de obicei pur și simplu de mers pe jos peste ele - de exemplu, monede, pietre prețioase, bani, muniții, etc.
Datele de preluare pot fi plasate direct în datele noastre de nivel, după cum urmează:
[[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0, 8,0,1], [1,0,0,0,0,1], [1,1,1,1,1]]
În acest nivel de date, vom folosi 8
pentru a indica un pickup pe o placă de iarbă (1
și 0
reprezintă pereți și plăci de walkabble, respectiv, ca mai înainte). Aceasta ar putea fi o singură imagine de țiglă cu o plăcuță de iarbă suprapusă cu imaginea de preluare. Trecând prin această logică, vom avea nevoie de două stări diferite de țiglă pentru fiecare țiglă care are un pickup, adică una cu pickup și una fără a fi afișată după colectarea colectată.
Arta tipică izometrică va avea mai multe plăci walkable - presupunem că avem 30. Abordarea de mai sus înseamnă că dacă avem pickup-uri N, vom avea nevoie de N x 30 de plăci în plus față de cele 30 de plăci originale, deoarece fiecare țiglă va trebui să aibă o versiune cu pickups și unul fără. Acest lucru nu este foarte eficient; în schimb, ar trebui să încercăm să creăm dinamic aceste combinații.
Pentru a rezolva acest lucru, am putea folosi aceeași metodă pe care am folosit-o pentru a plasa eroul în primul tutorial. Ori de câte ori vom întâlni o piesă de preluare, vom plasa mai întâi o țiglă și apoi vom așeza pickup-ul deasupra plăcii de iarbă. În acest fel, avem nevoie de N plăci de pickup în plus față de 30 de plăci walkable, dar am avea nevoie de valori numerice pentru a reprezenta fiecare combinație în datele de nivel. Pentru a rezolva necesitatea unor valori de reprezentare N x 30, putem păstra o separație pickupArray
să stocheze exclusiv datele de preluare în afară de levelData
. Nivelul completat cu pickup-ul este prezentat mai jos:
Pentru exemplul nostru, păstrez lucrurile simple și nu folosesc o matrice suplimentară pentru pickup-uri.
Detectarea detectoarelor se face în același mod ca detectarea dalelor de coliziune, dar după mișcarea personajului.
dacă (onPickupTile ()) pickupItem (); funcția onPickupTile () // verificați dacă există un pickup pe întoarcerea țiglă-eroi (levelData [heroMapTile.y] [heroMapTile.x] == 8);
În funcție onPickupTile ()
, verificăm dacă levelData
valoarea array la heroMapTile
coordonata este o placă de pickup sau nu. Numărul din levelData
array la acea coordonată de dale indică tipul de preluare. Verificăm coliziuni înainte de a muta caracterul, dar trebuie să verificăm după aceea pickup-urile, deoarece în cazul coliziunilor, personajul nu ar trebui să ocupe locul în cazul în care acesta este deja ocupat de țiglă de coliziune, dar în cazul pickups personajul este liber să se miște peste ea.
Un alt lucru care trebuie luat în considerare este faptul că datele de coliziune nu se schimbă de obicei, dar datele de preluare se modifică ori de câte ori ridicăm un element. (De obicei aceasta implică doar modificarea valorii în levelData
array de la, să zicem, 8
la 0
.)
Acest lucru conduce la o problemă: ce se întâmplă atunci când trebuie să reporniți nivelul și, astfel, readuceți toate pickup-urile înapoi la pozițiile lor inițiale? Nu avem informațiile necesare pentru a face acest lucru, cum ar fi levelData
matricea a fost modificată pe măsură ce jucătorul a luat obiecte. Soluția este să folosiți o matrice duplicată pentru nivelul în timp ce jucați și să păstrați originalul levelData
matrice intactă. De exemplu, folosim levelData
și levelDataLive []
, clonează-l pe cel din urmă de la primul la începutul nivelului, și se schimbă levelDataLive []
în timpul jocului.
De exemplu, am dat naștere unui pickup aleatoriu pe o plăcuță de iarbă vacantă după fiecare preluare și incrementare pickupCount
. pickupItem
funcționează astfel.
funcția pickupItem () pickupCount ++; levelData [heroMapTile.y] [heroMapTile.x] = 0; // spawn next pickup spawnNewPickup ();
Ar trebui să observați că am verificat pentru pickups ori de câte ori caracterul este pe acea țiglă. Acest lucru se poate întâmpla de mai multe ori într-o secundă (verificăm doar atunci când utilizatorul se mișcă, dar putem merge și rotunde în interiorul unei plăci), dar logica de mai sus nu va eșua; deoarece am setat levelData
array date la 0
prima dată când detectăm un pickup, toate cele ulterioare onPickupTile ()
verificările vor reveni fals
pentru acea țiglă. Consultați exemplul interactiv de mai jos:
După cum sugerează și numele, dale de declanșare provoacă ceva ce se întâmplă atunci când jucătorul trece pe ele sau apasă o tastă atunci când se află pe ele. Aceștia ar putea teleporta jucătorul într-o altă locație, deschid o poartă sau aruncă un inamic, pentru a da câteva exemple. Într-un sens, pickup-urile sunt doar o formă specială de plăci de declanșare: atunci când jucătorul merge pe o țiglă care conține o monedă, moneda dispare și contorul de monede crește.
Să ne uităm la modul în care putem implementa o ușă care duce jucătorul la un nivel diferit. Placile de lângă ușă vor fi o placă de declanșare; când jucătorul apasă X cheie, vor trece la nivelul următor.
Pentru a schimba nivelele, tot ce trebuie să facem este să schimbăm curentul levelData
array cu cel al noului nivel și setați noul heroMapTile
poziția și direcția personajului eroului. Să presupunem că există două niveluri cu ușile care să permită trecerea între ele. Deoarece țigla de la sol de lângă ușă va fi tigla de declanșare în ambele niveluri, putem folosi aceasta ca noua poziție pentru caracter când apar în nivelul.
Logica de implementare aici este aceeași ca și pentru pickups și din nou folosim levelData
array pentru a stoca valorile de declanșare. Pentru exemplul nostru, 2
desemnează o placă de ușă, iar valoarea de lângă ea este declanșatorul. am folosit 101
și 102
cu convenția de bază că orice țiglă cu o valoare mai mare de 100 este o plăcuță de declanșare și valoarea minus 100 poate fi nivelul la care aceasta duce la:
var nivel1Data = [[1,1,1,1,1,1], [1,1,0,0,0,1], [1,0,0,0,0,1], [2,102,0 , 0,0,1], [1,0,0,0,1,1], [1,1,1,1,1,1]]; var nivel2Data = [[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0 , 0,0101,2], [1,0,1,0,0,1], [1,1,1,1,1,1]];
Codul pentru verificarea unui eveniment declanșator este prezentat mai jos:
var xKey = game.input.keyboard.addKey (Phaser.Keyboard.X); xKey.onUp.add (triggerListener); // adăugați un ascultător de semnal pentru funcția evenimentului sus triggerListener () var trigger = levelData [heroMapTile.y] [heroMapTile.x]; dacă (declanșa> 100) // declanșarea valorii de declanșare valide = = 100; dacă (declanșa == 1) // trece la nivelul 1 nivelData = nivel1Data; altfel // trecerea la nivel 2 nivelData = nivel2Data; pentru (var i = 0; i < levelData.length; i++) for (var j = 0; j < levelData[0].length; j++) trigger=levelData[i][j]; if(trigger>100) // găsiți noua placă de declanșare și puneți eroul acolo heroMapTile.y = j; heroMapTile.x = i; heroMapPos = noul Phaser.Point (heroMapTile.y * tileWidth, heroMapTile.x * tileWidth); heroMapPos.x + = (tileWidth / 2); heroMapPos.y + = (tileWidth / 2);
Functia triggerListener ()
verifică dacă valoarea matricei de date a declanșatorului la o coordonată dată este mai mare de 100. Dacă da, găsim la ce nivel trebuie să trecem la scăderea valorii de 100 de valori. Funcția găsește piesa de declanșare în noul levelData
, care va fi poziția spawn pentru eroul nostru. Am făcut ca declanșatorul să fie activat când X este eliberată; dacă ascultăm doar dacă cheia este apăsată atunci ajungem într-o buclă în care schimbăm între nivele atâta timp cât cheia este ținută în jos, caracterul întotdeauna apărând în noul nivel deasupra unei plăci de declanșare.
Aici este un demo de lucru. Încercați să ridicați obiecte umblând peste ele și schimbând nivelurile stând lângă ușă și lovind X.
A proiectil este ceva care se deplasează într-o anumită direcție cu o viteză specială, ca un glonț, o vrajă magică, o minge etc. Totul despre proiectil este același cu caracterul eroului, în afară de înălțime: mai degrabă decât de rulare de-a lungul solului, proiectile plutesc adesea deasupra ei la o anumita inaltime. Un glonț va călători deasupra nivelului taliei personajului și chiar o minge poate avea nevoie să sări în jur.
Un lucru interesant de observat este că înălțimea izometrică este aceeași cu înălțimea într-o vedere laterală 2D, deși mai mică în valoare. Nu sunt implicate conversii complicate. Dacă o minge are 10 pixeli deasupra solului în coordonate carteziene, ar putea fi de 10 sau 6 pixeli deasupra solului în coordonate izometrice. (În cazul nostru, axa relevantă este axa y.)
Să încercăm să punem în aplicare o minge care se învârte în pășunea noastră cu pereți. Ca o atingere a realismului, vom adăuga o umbră pentru minge. Tot ce trebuie să facem este să adăugăm valoarea înălțimii de înălțime la valoarea izometrică Y a mingii noastre. Valoarea înălțimii saltului se va schimba de la cadru la cadru, în funcție de gravitate, iar odată ce mingea atinge terenul, vom răsturna viteza curentului de-a lungul axei y.
Înainte de a ne lupta într-un sistem izometric, vom vedea cum îl putem implementa într-un sistem cartezian 2D. Să reprezentăm puterea de salt a mingii cu o variabilă zValue
. Imaginați-vă că, pentru început, mingea are o putere de salt de 100, deci zValue = 100
.
Vom folosi alte două variabile: incrementValue
, care începe la 0
, și gravitatie
, care are o valoare de -1
. Fiecare cadru, scădem incrementValue
din zValue
, și scădea gravitatie
din incrementValue
pentru a crea un efect de atenuare. Cand zValue
ajunge 0
, înseamnă că mingea a ajuns la pământ; în acest moment, ne întoarcem semnul incrementValue
prin înmulțirea lui cu -1
, transformându-l într-un număr pozitiv. Acest lucru înseamnă că mingea se va mișca în sus de la următorul cadru, ridicându-se astfel.
Iată cum arată acest lucru în cod:
dacă (game.input.keyboard.isDown (Phaser.Keyboard.X)) zValue = 100; incrementValue- = gravitate; zValue- = incrementValue; în cazul în care (zValue<=0) zValue=0; incrementValue*=-1;
Codul rămâne același pentru viziunea izometrică, cu diferența mică pe care o puteți utiliza pentru o valoare mai mică zValue
a începe cu. Vedeți mai jos cum zValue
se adaugă la izometrul y
valoarea mingii în timpul redării.
funcția drawBallIso () var isoPt = nou Phaser.Point (); // Nu se recomandă crearea punctelor în buclă de actualizare var ballCornerPt = nou Phaser.Point (ballMapPos.x-ball2DVolume.x / 2, ballMapPos.y-ball2DVolume .y / 2); isoPt = cartesianToIsometric (ballCornerPt); // găsiți o nouă poziție izometrică pentru eroul din poziția 2D a gameScene.renderXY (ballShadowSprite, isoPt.x + borderOffset.x + shadowOffset.x, isoPt.y + borderOffset.y + shadowOffset.y, false ); // trageți umbra pentru a face textura gameScene.renderXY (ballSprite, isoPt.x + borderOffset.x + ballOffset.x, isoPt.y + borderOffset.y-ballOffset.y-zValue, false); // trage eroul pentru a face textura
Consultați exemplul interactiv de mai jos:
Înțelegem că rolul jucat de umbra este unul foarte important, care adaugă la realismul acestei iluzii. De asemenea, rețineți că acum folosim cele două coordonate ale ecranului (x și y) pentru a reprezenta trei dimensiuni în coordonate izometrice - axa y în coordonatele ecranului este, de asemenea, axa z în coordonate izometrice. Acest lucru poate fi confuz!
Urmărirea și urmărirea traseului sunt procese destul de complicate. Există diferite abordări folosind algoritmi diferiți pentru a găsi calea între două puncte, ci ca și noi levelData
este o matrice 2D, lucrurile sunt mai simple decât ar putea fi altfel. Avem noduri bine definite și unice pe care jucătorul le poate ocupa și putem verifica cu ușurință dacă sunt în mișcare.
O privire de ansamblu detaliată a algoritmilor de traducere este în afara domeniului de aplicare al acestui articol, însă voi încerca să explic cele mai frecvente moduri în care funcționează: algoritmul de cale mai scurtă, al cărui algoritm A * și Dijkstra sunt implementări faimoase.
Scopul nostru este să găsim noduri care leagă un nod de pornire și un nod de terminare. Din nodul de pornire, vizităm toate cele opt noduri vecine și le marchem pe toate ca fiind vizitate; acest proces de bază este repetat pentru fiecare nod nou vizitat, recursiv.
Fiecare fir urmărește nodurile vizitate. Atunci când săriți la nodurile vecine, nodurile care au fost deja vizitate sunt sărite (reluarea se oprește); în caz contrar, procesul continuă până ajungem la nodul de terminare, unde se termină recursiunea și calea completă urmată este returnată ca o matrice de noduri. Uneori, nodul de sfârșit nu este niciodată atins, caz în care căderea de drum nu reușește. De obicei, ajungem să găsim căi multiple între cele două noduri, caz în care luăm pe cel cu cel mai mic număr de noduri.
Nu este înțelept să reinventăm roata atunci când vine vorba de algoritmi bine definiți, așadar am folosi soluțiile existente pentru scopurile noastre de căutare. Pentru a utiliza Phaser, avem nevoie de o soluție JavaScript, iar cea pe care am ales-o este EasyStarJS. Inițializăm motorul de identificare a traseului așa cum este descris mai jos.
easystar = noul EasyStar.js (); easystar.setGrid (levelData); easystar.setAcceptableTiles ([0]); easystar.enableDiagonals (); / / dorim ca traseul să aibă diagonale easystar.disableCornerCutting (); // nu există o cale diagonală atunci când mergem pe colțurile peretelui
Ca pe noi levelData
are doar 0
și 1
, îl putem transfera în mod direct ca matrice de noduri. Am stabilit valoarea 0
ca nodul walkable. Permitem capabilitățile diagonale de mers pe jos, dar dezactivați acest lucru atunci când vă plimbați în apropierea colțurilor plăcilor ce nu sunt pliabile.
Acest lucru se datorează faptului că, dacă este activat, eroul poate tăia țiglă care nu se poate învârti în timp ce face o plimbare pe diagonală. În acest caz, detectarea coliziunii nu va permite eroului să treacă. De asemenea, vă rugăm să fiți informat că în exemplul de față am eliminat complet detectarea coliziunii, deoarece nu mai este necesară pentru un exemplu de plimbare bazat pe AI.
Vom detecta robinetul pe orice placă liberă din interiorul nivelului și vom calcula calea folosind findPath
funcţie. Metoda de apel invers plotAndMove
primește matricea de noduri a căii rezultante. Marcăm minimap
cu calea nou descoperită.
game.input.activePointer.leftButton.onUp.add (findPath) funcția findPath () if (isFindingPath || isWalking) retur; var pos = game.input.activePointer.position; var isoPt = Phaser.Point nou (pos.x-borderOffset.x, pos.y-borderOffset.y); tapPos = isometricToCartesian (isoPt); tapPos.x- = tileWidth / 2; // ajustare pentru găsirea plăcii potrivite pentru eroare din cauza rotunjirii tapPos.y + = tileWidth / 2; tapPos = getTileCoordinates (tapPos, tileWidth); if (tapPos.x> -1 && tapPos.y> -1 && tapPos.x<7&&tapPos.y<7)//tapped within grid if(levelData[tapPos.y][tapPos.x]!=1)//not wall tile isFindingPath=true; //let the algorithm do the magic easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove); easystar.calculate(); function plotAndMove(newPath) destination=heroMapTile; path=newPath; isFindingPath=false; repaintMinimap(); if (path === null) console.log("No Path was found."); else path.push(tapPos); path.reverse(); path.pop(); for (var i = 0; i < path.length; i++) var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x); tmpSpr.tint=0x0000ff; //console.log("p "+path[i].x+":"+path[i].y);
Odată ce avem calea ca o matrice de noduri, trebuie să facem ca personajul să o urmeze.
Spuneți că vrem să facem caracterul să meargă la o țiglă pe care facem clic pe. Mai întâi trebuie să căutăm o cale între nodul ocupat în prezent și nodul în care am făcut clic. Dacă se găsește o cale de succes, atunci trebuie să mutăm caracterul la primul nod din matricea de noduri prin setarea ca destinație. Odată ce ajungem la nodul de destinație, verificăm dacă există mai multe noduri în matricea nodurilor și, dacă da, setați următorul nod ca destinație - și așa mai departe până când ajungem la nodul final.
De asemenea, vom schimba direcția player-ului pe baza nodului curent și a noului nod de destinație de fiecare dată când ajungem la un nod. Între noduri, mergem în direcția dorită până ajungem la nodul de destinație. Acesta este un AI foarte simplu, iar în exemplul acesta se face în metodă aiWalk
prezentate parțial mai jos.
funcția aiWalk () if (path.length == 0) // calea sa încheiat dacă (heroMapTile.x == destination.x && heroMapTile.y == destinație.y) dX = 0; dY = 0; isWalking = false; întoarcere; isWalking = true; dacă (heroMapTile.x == destination.x && heroMapTile.y == destinație.y) // a ajuns la destinația curentă, a stabilit o nouă direcție, a schimba // așteptați până când suntem câțiva pași în țiglă înainte să întoarcem pașiiTaken ++; în cazul în care (stepsTakendestinație.x) dX = -1; altceva dX = 0; dacă (heroMapTile.y destinație.y) dY = -1; altceva dY = 0; dacă (heroMapTile.x == destinație.x) dX = 0; altfel dacă (heroMapTile.y == destinație.y) dY = 0; ...
Noi do trebuie să filtrați punctele de clic valide, determinând dacă am făcut clic în zona de navigare, mai degrabă decât o plăcuță de perete sau o altă piesă care nu se învârte.
Un alt punct interesant pentru codarea AI: nu vrem ca personajul să se îndrepte spre fața următorului țiglă din matricea nodului de îndată ce a ajuns în actualul, deoarece o astfel de întoarcere imediată are ca rezultat caracterul nostru care merge pe marginea dale. În schimb, ar trebui să așteptăm până când caracterul este la câțiva pași în interiorul plăcii înainte de a căuta următoarea destinație. De asemenea, este mai bine să plasați manual eroul în mijlocul plăcii curente chiar înainte de a vă întoarce, pentru a vă face să vă simțiți perfect.
Verificați demo-ul de lucru de mai jos:
Când zona de nivel este mult mai mare decât suprafața de ecran disponibilă, va trebui să o facem sul.
Suprafața vizibilă a ecranului poate fi considerată drept un dreptunghi mai mic în interiorul dreptunghiului mai mare al suprafeței complete a nivelului. Deplasarea este, în esență, doar mișcarea dreptunghiului interior în interiorul celui mai mare. De obicei, atunci când apare o asemenea defilare, poziția eroului rămâne aceeași cu privire la dreptunghiul ecranului, de obicei la centrul ecranului. Interesant, tot ce trebuie să implementăm derularea este urmărirea punctului de colț al dreptunghiului interior.
Acest punct de colț, pe care îl reprezentăm în coordonate carteziene, se va încadra într-o țiglă în datele de nivel. Pentru derulare, incrementăm pozițiile x și y ale punctului de colț în coordonate carteziene. Acum putem converti acest punct la coordonate izometrice și îl putem folosi pentru a desena ecranul.
Valorile recent convertite, în spațiul izometric, trebuie să fie și colțul ecranului nostru, ceea ce înseamnă că acestea sunt noi (0, 0)
. Astfel, în timp ce analizăm și extragem datele de nivel, scădem această valoare din poziția izometrică a fiecărui tiglă și putem determina dacă noua poziție a plăcii intră în ecran.
În mod alternativ, putem decide că vom desena doar o singură dată X x Y placa de gresie izometrica pe ecran pentru a face ca bucla de desen sa fie eficienta pentru nivele mai mari.
Putem exprima acest lucru în pași astfel:
var cornerMapPos = nou Phaser.Point (0,0); var cornerMapTile = nou Phaser.Point (0,0); var visibleTiles = nou Phaser.Point (6,6); // ... actualizare funcție () // ... dacă (isWalkable ()) heroMapPos.x + = heroSpeed * dX; heroMapPos.y + = heroSpeed * dY; // mutați colțul în direcția opusă cornerMapPos.x - = heroSpeed * dX; cornerMapPos.y - = heroSpeed * dY; cornerMapTile = getTileCoordinates (cornerMapPos, tileWidth); // a obține noua placă de hartă a eroului heroMapTile = getTileCoordinates (heroMapPos, tileWidth); // depthsort & draw nouă scena renderScene (); renderScene () gameScene.clear (); // ștergeți cadrul anterior, apoi extrageți din nou var tileType = 0; // să ne limităm buclele în zona vizibilă var startTileX = Math.max (0,0-cornerMapTile.x); var startTileY = Math.max (0,0-cornerMapTile.y); var endTileX = Math.min (nivelData [0] .length, startTileX + visibleTiles.x); var endTileY = Math.min (nivelData.length, startTileY + visibleTiles.y); startTileX = Math.max (0, endTileX-visibleTiles.x); startTileY = Math.max (0, endTileY-visibleTiles.y); // verificați dacă condiția frontierei pentru (var i = startTileY; i < endTileY; i++) for (var j = startTileX; j < endTileX; j++) tileType=levelData[i][j]; drawTileIso(tileType,i,j); if(i==heroMapTile.y&&j==heroMapTile.x) drawHeroIso(); function drawHeroIso() var isoPt= new Phaser.Point();//It is not advisable to create points in update loop var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y); isoPt=cartesianToIsometric(heroCornerPt);//find new isometric position for hero from 2D map position gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//draw hero to render texture function drawTileIso(tileType,i,j)//place isometric level tiles var isoPt= new Phaser.Point();//It is not advisable to create point in update loop var cartPt=new Phaser.Point();//This is here for better code readability. cartPt.x=j*tileWidth+cornerMapPos.x; cartPt.y=i*tileWidth+cornerMapPos.y; isoPt=cartesianToIsometric(cartPt); //we could further optimise by not drawing if tile is outside screen. if(tileType==1) gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false); else gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false);
Rețineți că punctul de colț este incrementat în opus direcția spre actualizarea poziției eroului în timp ce se mișcă. Acest lucru face ca eroul să rămână acolo unde este cu privire la ecran. Consultați acest exemplu (utilizați săgețile pentru derulare, atingeți pentru a crește grila vizibilă).
Câteva note:
Această serie este destinată în special începătorilor care încearcă să exploreze lumi jocurilor isometrice. Multe dintre conceptele explicate au abordări alternative care sunt un pic mai complicate și am ales în mod deliberat pe cele mai ușoare.
Este posibil ca acestea să nu îndeplinească majoritatea scenariilor pe care le puteți întâlni, dar cunoștințele dobândite pot fi folosite pentru a construi aceste concepte pentru a crea soluții mai complicate. De exemplu, sortarea în profunzime simplificată va fi ruptă atunci când avem nivele cu mai multe etaje și plăci de platformă care se deplasează de la o poveste la alta.
Dar acesta este un tutorial pentru un alt moment.