25 KiB
Construiește un Joc Spațial Partea 3: Adăugarea Mișcării
Gândește-te la jocurile tale preferate – ceea ce le face captivante nu sunt doar grafica frumoasă, ci modul în care totul se mișcă și răspunde la acțiunile tale. În acest moment, jocul tău spațial este ca o pictură frumoasă, dar suntem pe cale să adăugăm mișcare care să-l aducă la viață.
Când inginerii NASA au programat computerul de ghidare pentru misiunile Apollo, s-au confruntat cu o provocare similară: cum faci ca o navă spațială să răspundă la comenzile pilotului, menținând în același timp corecțiile automate de curs? Principiile pe care le vom învăța astăzi reflectă aceleași concepte – gestionarea mișcării controlate de jucător alături de comportamentele automate ale sistemului.
În această lecție, vei învăța cum să faci navele spațiale să alunece pe ecran, să răspundă la comenzile jucătorului și să creezi modele de mișcare fluide. Vom împărți totul în concepte gestionabile care se construiesc în mod natural unul pe celălalt.
La final, jucătorii vor putea să-și piloteze nava erou pe ecran, în timp ce navele inamice patrulează deasupra. Mai important, vei înțelege principiile de bază care stau la baza sistemelor de mișcare din jocuri.
Test înainte de lecție
Înțelegerea mișcării în jocuri
Jocurile prind viață atunci când lucrurile încep să se miște, iar în mod fundamental există două moduri prin care se întâmplă acest lucru:
- Mișcare controlată de jucător: Când apeși o tastă sau dai clic pe mouse, ceva se mișcă. Aceasta este conexiunea directă între tine și lumea jocului.
- Mișcare automată: Când jocul însuși decide să miște lucrurile – cum ar fi acele nave inamice care trebuie să patruleze pe ecran, indiferent dacă faci ceva sau nu.
A face obiectele să se miște pe un ecran de computer este mai simplu decât ai putea crede. Îți amintești de coordonatele x și y de la ora de matematică? Exact cu asta lucrăm aici. Când Galileo a urmărit lunile lui Jupiter în 1610, practic făcea același lucru – trasa poziții în timp pentru a înțelege modelele de mișcare.
Mișcarea obiectelor pe ecran este ca și cum ai crea o animație de tip flipbook – trebuie să urmezi acești trei pași simpli:
- Actualizează poziția – Schimbă locul unde ar trebui să fie obiectul (poate mută-l 5 pixeli spre dreapta)
- Șterge cadrul vechi – Curăță ecranul astfel încât să nu vezi urme fantomatice peste tot
- Desenează cadrul nou – Plasează obiectul în noua sa poziție
Fă acest lucru suficient de rapid și boom! Ai o mișcare fluidă care se simte natural pentru jucători.
Iată cum poate arăta acest lucru în cod:
// Set the hero's location
hero.x += 5;
// Clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
Ce face acest cod:
- Actualizează coordonata x a eroului cu 5 pixeli pentru a-l mișca orizontal
- Șterge întreaga zonă a canvas-ului pentru a elimina cadrul anterior
- Umple canvas-ul cu o culoare de fundal neagră
- Redesenează imaginea eroului în noua sa poziție
✅ Poți să te gândești la un motiv pentru care redesenarea eroului de mai multe ori pe secundă ar putea genera costuri de performanță? Citește despre alternative la acest model.
Gestionarea evenimentelor de tastatură
Aici conectăm input-ul jucătorului la acțiunea din joc. Când cineva apasă bara de spațiu pentru a trage un laser sau apasă o săgeată pentru a evita un asteroid, jocul tău trebuie să detecteze și să răspundă la acel input.
Evenimentele de tastatură se întâmplă la nivelul ferestrei, ceea ce înseamnă că întreaga fereastră a browserului ascultă acele apăsări de taste. Click-urile de mouse, pe de altă parte, pot fi legate de elemente specifice (cum ar fi clicul pe un buton). Pentru jocul nostru spațial, ne vom concentra pe controalele de tastatură, deoarece acestea oferă jucătorilor acea senzație clasică de arcade.
Acest lucru îmi amintește de cum operatorii de telegraf din anii 1800 trebuiau să traducă input-ul codului Morse în mesaje semnificative – facem ceva similar, traducând apăsările de taste în comenzi pentru joc.
Pentru a gestiona un eveniment, trebuie să folosești metoda addEventListener() a ferestrei și să îi oferi doi parametri de intrare. Primul parametru este numele evenimentului, de exemplu keyup. Al doilea parametru este funcția care ar trebui să fie invocată ca urmare a producerii evenimentului.
Iată un exemplu:
window.addEventListener('keyup', (evt) => {
// evt.key = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
});
Ce se întâmplă aici:
- Ascultă evenimentele de tastatură pe întreaga fereastră
- Capturează obiectul evenimentului care conține informații despre ce tastă a fost apăsată
- Verifică dacă tasta apăsată corespunde unei taste specifice (în acest caz, săgeata sus)
- Execută codul atunci când condiția este îndeplinită
Pentru evenimentele de tastatură, există două proprietăți pe obiectul evenimentului pe care le poți folosi pentru a vedea ce tastă a fost apăsată:
key- aceasta este o reprezentare sub formă de șir a tastei apăsate, de exemplu'ArrowUp'keyCode- aceasta este o reprezentare numerică, de exemplu37, corespunde cuArrowLeft
✅ Manipularea evenimentelor de tastatură este utilă și în afara dezvoltării de jocuri. La ce alte utilizări te poți gândi pentru această tehnică?
Taste speciale: un avertisment!
Unele taste au comportamente încorporate în browser care pot interfera cu jocul tău. Tastele săgeți derulează pagina, iar bara de spațiu sare în jos – comportamente pe care nu le dorești atunci când cineva încearcă să-și piloteze nava spațială.
Putem preveni aceste comportamente implicite și să lăsăm jocul nostru să gestioneze input-ul în schimb. Acest lucru este similar cu modul în care primii programatori de computere trebuiau să suprascrie întreruperile sistemului pentru a crea comportamente personalizate – doar că noi facem acest lucru la nivelul browserului. Iată cum:
const onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
Înțelegerea acestui cod de prevenire:
- Verifică codurile de taste specifice care ar putea cauza comportamente nedorite ale browserului
- Previne acțiunea implicită a browserului pentru tastele săgeți și bara de spațiu
- Permite altor taste să funcționeze normal
- Folosește
e.preventDefault()pentru a opri comportamentul încorporat al browserului
Mișcarea indusă de joc
Acum să vorbim despre obiectele care se mișcă fără input-ul jucătorului. Gândește-te la navele inamice care se deplasează pe ecran, gloanțele care zboară în linii drepte sau norii care se deplasează în fundal. Această mișcare autonomă face ca lumea jocului să pară vie, chiar și atunci când nimeni nu atinge controalele.
Folosim temporizatoarele încorporate ale JavaScript pentru a actualiza pozițiile la intervale regulate. Acest concept este similar cu modul în care funcționează ceasurile cu pendul – un mecanism regulat care declanșează acțiuni consistente, temporizate. Iată cât de simplu poate fi:
const id = setInterval(() => {
// Move the enemy on the y axis
enemy.y += 10;
}, 100);
Ce face acest cod de mișcare:
- Creează un temporizator care rulează la fiecare 100 de milisecunde
- Actualizează coordonata y a inamicului cu 10 pixeli de fiecare dată
- Stochează ID-ul intervalului pentru a-l putea opri mai târziu, dacă este necesar
- Mută inamicul în jos pe ecran automat
Bucla jocului
Iată conceptul care leagă totul – bucla jocului. Dacă jocul tău ar fi un film, bucla jocului ar fi proiectorul de film, arătând cadru după cadru atât de rapid încât totul pare să se miște lin.
Fiecare joc are una dintre aceste bucle care rulează în fundal. Este o funcție care actualizează toate obiectele din joc, redesenează ecranul și repetă acest proces continuu. Aceasta ține evidența eroului tău, a tuturor inamicilor, a oricăror lasere care zboară – întregul stadiu al jocului.
Acest concept îmi amintește de modul în care primii animatori de film, precum Walt Disney, trebuiau să redeseneze personajele cadru cu cadru pentru a crea iluzia mișcării. Facem același lucru, doar că folosim cod în loc de creioane.
Iată cum arată de obicei o buclă de joc, exprimată în cod:
const gameLoopId = setInterval(() => {
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}
gameLoop();
}, 200);
Înțelegerea structurii buclei jocului:
- Șterge întreaga zonă a canvas-ului pentru a elimina cadrul anterior
- Umple fundalul cu o culoare solidă
- Desenează toate obiectele jocului în pozițiile lor curente
- Repetă acest proces la fiecare 200 de milisecunde pentru a crea o animație fluidă
- Gestionează rata cadrelor prin controlul intervalului de timp
Continuarea Jocului Spațial
Acum vom adăuga mișcare scenei statice pe care ai construit-o anterior. Vom transforma aceasta dintr-un screenshot într-o experiență interactivă. Vom parcurge acest proces pas cu pas pentru a ne asigura că fiecare piesă se construiește pe cea anterioară.
Ia codul de unde am rămas în lecția anterioară (sau începe cu codul din folderul Part II- starter dacă ai nevoie de un început proaspăt).
Iată ce construim astăzi:
- Controale pentru erou: Tastele săgeți vor pilota nava ta spațială pe ecran
- Mișcarea inamicilor: Aceste nave extraterestre vor începe să avanseze
Să începem implementarea acestor funcționalități.
Pași recomandați
Găsește fișierele care au fost create pentru tine în subfolderul your-work. Ar trebui să conțină următoarele:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Începe proiectul tău în folderul your-work tastând:
cd your-work
npm start
Ce face această comandă:
- Navighează către directorul proiectului tău
- Pornește un server HTTP la adresa
http://localhost:5000 - Servește fișierele jocului tău astfel încât să le poți testa într-un browser
Comanda de mai sus va porni un server HTTP la adresa http://localhost:5000. Deschide un browser și introdu acea adresă, acum ar trebui să afișeze eroul și toți inamicii; nimic nu se mișcă – încă!
Adaugă cod
-
Adaugă obiecte dedicate pentru
hero,enemyșigame object, acestea ar trebui să aibă proprietățixșiy. (Amintește-ți porțiunea despre Moștenire sau compoziție).INDICAȚIE
game objectar trebui să fie cel care arexșiyși abilitatea de a se desena pe un canvas.Sfat: Începe prin a adăuga o nouă clasă
GameObjectcu constructorul său definit astfel, și apoi desenează-l pe canvas:class GameObject { constructor(x, y) { this.x = x; this.y = y; this.dead = false; this.type = ""; this.width = 0; this.height = 0; this.img = undefined; } draw(ctx) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); } }Înțelegerea acestei clase de bază:
- Definește proprietăți comune pe care toate obiectele jocului le împărtășesc (poziție, dimensiune, imagine)
- Include un indicator
deadpentru a urmări dacă obiectul ar trebui eliminat - Oferă o metodă
draw()care redă obiectul pe canvas - Setează valori implicite pentru toate proprietățile pe care clasele copil le pot suprascrie
Acum, extinde acest
GameObjectpentru a creaHeroșiEnemy:class Hero extends GameObject { constructor(x, y) { super(x, y); this.width = 98; this.height = 75; this.type = "Hero"; this.speed = 5; } }class Enemy extends GameObject { constructor(x, y) { super(x, y); this.width = 98; this.height = 50; this.type = "Enemy"; const id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y); clearInterval(id); } }, 300); } }Concepte cheie în aceste clase:
- Moștenește de la
GameObjectfolosind cuvântul cheieextends - Apelează constructorul părinte cu
super(x, y) - Setează dimensiuni și proprietăți specifice pentru fiecare tip de obiect
- Implementează mișcarea automată pentru inamici folosind
setInterval()
-
Adaugă gestionare de evenimente pentru taste pentru a gestiona navigarea cu tastele (mută eroul sus/jos, stânga/dreapta)
AMINTEȘTE-ȚI că este un sistem cartezian, colțul din stânga sus este
0,0. De asemenea, amintește-ți să adaugi cod pentru a opri comportamentul implicit.Sfat: Creează funcția ta
onKeyDownși atașeaz-o la fereastră:const onKeyDown = function (e) { console.log(e.keyCode); // Add the code from the lesson above to stop default behavior switch (e.keyCode) { case 37: case 39: case 38: case 40: // Arrow keys case 32: e.preventDefault(); break; // Space default: break; // do not block other keys } }; window.addEventListener("keydown", onKeyDown);Ce face acest handler de evenimente:
- Ascultă evenimentele de apăsare a tastelor pe întreaga fereastră
- Loghează codul tastei pentru a te ajuta să depanezi ce taste sunt apăsate
- Previne comportamentul implicit al browserului pentru tastele săgeți și bara de spațiu
- Permite altor taste să funcționeze normal
Verifică consola browserului în acest moment și urmărește apăsările de taste care sunt logate.
-
Implementează Modelul Pub-Sub, acesta va menține codul tău curat pe măsură ce urmezi părțile rămase.
Modelul Publish-Subscribe ajută la organizarea codului tău prin separarea detectării evenimentelor de gestionarea acestora. Acest lucru face codul mai modular și mai ușor de întreținut.
Pentru a face această ultimă parte, poți:
-
Adaugă un ascultător de evenimente pe fereastră:
window.addEventListener("keyup", (evt) => { if (evt.key === "ArrowUp") { eventEmitter.emit(Messages.KEY_EVENT_UP); } else if (evt.key === "ArrowDown") { eventEmitter.emit(Messages.KEY_EVENT_DOWN); } else if (evt.key === "ArrowLeft") { eventEmitter.emit(Messages.KEY_EVENT_LEFT); } else if (evt.key === "ArrowRight") { eventEmitter.emit(Messages.KEY_EVENT_RIGHT); } });
Ce face acest sistem de evenimente:
- Detectează input-ul de la tastatură și îl convertește în evenimente personalizate ale jocului
- Separă detectarea input-ului de logica jocului
- Face ușoară schimbarea controalelor mai târziu fără a afecta codul jocului
- Permite mai multor sisteme să răspundă la același input
-
Creează o clasă EventEmitter pentru a publica și a subscrie la mesaje:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } -
Adaugă constante și configurează EventEmitter-ul:
const Messages = { KEY_EVENT_UP: "KEY_EVENT_UP", KEY_EVENT_DOWN: "KEY_EVENT_DOWN", KEY_EVENT_LEFT: "KEY_EVENT_LEFT", KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT", }; let heroImg, enemyImg, laserImg, canvas, ctx, gameObjects = [], hero, eventEmitter = new EventEmitter();
Înțelegerea configurării:
- Definește constante de mesaje pentru a evita greșelile de scriere și pentru a face refactorizarea mai ușoară
- Declară variabile pentru imagini, contextul canvas-ului și starea jocului
- Creează un emitter global de evenimente pentru sistemul pub-sub
- Inițializează un array pentru a ține toate obiectele jocului
-
Inițializează jocul
function initGame() { gameObjects = []; createEnemies(); createHero(); eventEmitter.on(Messages.KEY_EVENT_UP, () => { hero.y -= 5; }); eventEmitter.on(Messages.KEY_EVENT_DOWN, () => { hero.y += 5; }); eventEmitter.on(Messages.KEY_EVENT_LEFT, () => { hero.x -= 5; });
-
-
Configurează bucla jocului
Refactorizează funcția
window.onloadpentru a inițializa jocul și a configura o buclă de joc la un interval bun. Vei adăuga și o rază laser:window.onload = async () => { canvas = document.getElementById("canvas"); ctx = canvas.getContext("2d"); heroImg = await loadTexture("assets/player.png"); enemyImg = await loadTexture("assets/enemyShip.png"); laserImg = await loadTexture("assets/laserRed.png"); initGame(); const gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100); };Înțelegerea configurării jocului:
- Așteaptă ca pagina să se încarce complet înainte de a începe
- Obține elementul canvas și contextul său de redare 2D
- Încarcă toate resursele de imagine asincron folosind
- Creează o grilă de inamici folosind bucle imbricate
- Atribuie imaginea inamicului fiecărui obiect inamic
- Adaugă fiecare inamic în array-ul global de obiecte ale jocului
și adaugă o funcție createHero() pentru a realiza un proces similar pentru erou.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
Ce face crearea eroului:
- Poziționează eroul în centrul de jos al ecranului
- Atribuie imaginea eroului obiectului erou
- Adaugă eroul în array-ul de obiecte ale jocului pentru redare
și, în final, adaugă o funcție drawGameObjects() pentru a începe desenarea:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
Înțelegerea funcției de desenare:
- Iterează prin toate obiectele jocului din array
- Apelează metoda
draw()pentru fiecare obiect - Transmite contextul canvas-ului astfel încât obiectele să se poată reda singure
Inamicii tăi ar trebui să înceapă să avanseze spre nava spațială a eroului tău!
}
}
```
and add a `createHero()` function to do a similar process for the hero.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
și, în final, adaugă o funcție drawGameObjects() pentru a începe desenarea:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
Inamicii tăi ar trebui să înceapă să avanseze spre nava spațială a eroului tău!
Provocarea Agentului GitHub Copilot 🚀
Iată o provocare care va îmbunătăți aspectul jocului tău: adăugarea limitelor și a controlului fluid. În prezent, eroul tău poate zbura în afara ecranului, iar mișcarea poate părea sacadată.
Misiunea ta: Fă ca nava spațială să se simtă mai realistă implementând limitele ecranului și o mișcare fluidă. Este similar cu modul în care sistemele de control al zborului NASA împiedică navele spațiale să depășească parametrii operaționali siguri.
Ce trebuie să construiești: Creează un sistem care menține nava spațială a eroului pe ecran și face ca mișcările să fie fluide. Când jucătorii țin apăsată o săgeată, nava ar trebui să alunece continuu, nu să se miște în pași discreți. Ia în considerare adăugarea unui feedback vizual atunci când nava ajunge la limitele ecranului – poate un efect subtil pentru a indica marginea zonei de joc.
Află mai multe despre modul agent aici.
🚀 Provocare
Organizarea codului devine din ce în ce mai importantă pe măsură ce proiectele cresc. Poate ai observat că fișierul tău devine aglomerat cu funcții, variabile și clase amestecate. Acest lucru îmi amintește de modul în care inginerii care organizau codul misiunii Apollo au trebuit să creeze sisteme clare și ușor de întreținut, pe care mai multe echipe să le poată folosi simultan.
Misiunea ta:
Gândește ca un arhitect software. Cum ți-ai organiza codul astfel încât, peste șase luni, tu (sau un coleg) să poată înțelege ce se întâmplă? Chiar dacă totul rămâne într-un singur fișier deocamdată, poți crea o organizare mai bună:
- Gruparea funcțiilor similare împreună cu anteturi clare de comentarii
- Separarea responsabilităților - păstrează logica jocului separată de redare
- Utilizarea convențiilor consistente de denumire pentru variabile și funcții
- Crearea de module sau spații de nume pentru organizarea diferitelor aspecte ale jocului
- Adăugarea de documentație care explică scopul fiecărei secțiuni majore
Întrebări de reflecție:
- Care părți ale codului tău sunt cele mai greu de înțeles când te întorci la ele?
- Cum ai putea să-ți organizezi codul pentru a fi mai ușor pentru altcineva să contribuie?
- Ce s-ar întâmpla dacă ai dori să adaugi funcționalități noi, cum ar fi power-ups sau tipuri diferite de inamici?
Test de evaluare după lecție
Recapitulare și studiu individual
Am construit totul de la zero, ceea ce este fantastic pentru învățare, dar iată un mic secret – există câteva framework-uri JavaScript uimitoare care pot face o mare parte din munca grea pentru tine. Odată ce te simți confortabil cu elementele de bază pe care le-am acoperit, merită să explorezi ce este disponibil.
Gândește-te la framework-uri ca la un set de unelte bine echipat, în loc să faci fiecare unealtă manual. Ele pot rezolva multe dintre acele provocări de organizare a codului despre care am vorbit, plus oferă funcționalități care ar dura săptămâni să le construiești singur.
Lucruri care merită explorate:
- Cum organizează motoarele de joc codul – vei fi uimit de modelele ingenioase pe care le folosesc
- Trucuri de performanță pentru a face jocurile pe canvas să ruleze fluid
- Funcționalități moderne ale JavaScript care pot face codul tău mai curat și mai ușor de întreținut
- Diferite abordări pentru gestionarea obiectelor jocului și a relațiilor dintre ele
Temă
Declinare de responsabilitate:
Acest document a fost tradus folosind serviciul de traducere AI Co-op Translator. Deși ne străduim să asigurăm acuratețea, vă rugăm să fiți conștienți că traducerile automate pot conține erori sau inexactități. Documentul original în limba sa maternă ar trebui considerat sursa autoritară. Pentru informații critice, se recomandă traducerea profesională realizată de oameni. Nu ne asumăm responsabilitatea pentru neînțelegerile sau interpretările greșite care pot apărea din utilizarea acestei traduceri.