24 KiB
Izgradnja svemirske igre, dio 3: Dodavanje kretanja
Razmislite o svojim omiljenim igrama – ono što ih čini privlačnima nisu samo lijepe grafike, već način na koji se sve kreće i reagira na vaše akcije. Trenutno je vaša svemirska igra poput prekrasne slike, ali uskoro ćemo dodati kretanje koje će je oživjeti.
Kada su NASA-ini inženjeri programirali računalo za navođenje Apollo misija, suočili su se sa sličnim izazovom: kako učiniti da svemirska letjelica reagira na unos pilota, a istovremeno automatski održava korekcije kursa? Principi koje ćemo danas naučiti odražavaju te iste koncepte – upravljanje kretanjem koje kontrolira igrač uz automatsko ponašanje sustava.
U ovoj lekciji naučit ćete kako učiniti da svemirski brodovi klize po ekranu, reagiraju na naredbe igrača i stvaraju glatke uzorke kretanja. Sve ćemo razložiti na razumljive koncepte koji se prirodno nadovezuju jedan na drugi.
Na kraju, igrači će moći upravljati svojim herojskim brodom po ekranu dok neprijateljski brodovi patroliraju iznad. Još važnije, razumjet ćete osnovne principe koji pokreću sustave kretanja u igrama.
Kviz prije predavanja
Razumijevanje kretanja u igrama
Igre oživljavaju kada se stvari počnu kretati, a postoje dva osnovna načina na koja se to događa:
- Kretanje kontrolirano od strane igrača: Kada pritisnete tipku ili kliknete mišem, nešto se pomakne. Ovo je izravna veza između vas i svijeta igre.
- Automatsko kretanje: Kada sama igra odluči pomaknuti stvari – poput onih neprijateljskih brodova koji trebaju patrolirati ekranom bez obzira na vaše radnje.
Pomicanje objekata na ekranu računala jednostavnije je nego što mislite. Sjećate li se onih x i y koordinata iz matematike? Upravo s njima radimo ovdje. Kada je Galileo 1610. pratio Jupiterove mjesece, u biti je radio isto – bilježio je položaje tijekom vremena kako bi razumio uzorke kretanja.
Pomicanje stvari na ekranu je poput stvaranja animacije u obliku flipbooka – trebate slijediti ova tri jednostavna koraka:
- Ažurirajte položaj – Promijenite gdje bi vaš objekt trebao biti (možda ga pomaknite 5 piksela udesno)
- Izbrišite stari okvir – Očistite ekran kako ne biste vidjeli sablasne tragove posvuda
- Nacrtajte novi okvir – Postavite svoj objekt na njegovo novo mjesto
Ako to radite dovoljno brzo, bum! Dobivate glatko kretanje koje se igračima čini prirodnim.
Evo kako to može izgledati u kodu:
// 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);
Što ovaj kod radi:
- Ažurira x-koordinatu heroja za 5 piksela kako bi se pomaknuo vodoravno
- Briše cijelo područje platna kako bi uklonio prethodni okvir
- Ispunjava platno crnom bojom pozadine
- Ponovno crta sliku heroja na njegovom novom položaju
✅ Možete li smisliti razlog zašto ponovno crtanje vašeg heroja mnogo puta u sekundi može uzrokovati troškove performansi? Pročitajte o alternativama ovom obrascu.
Rukovanje događajima na tipkovnici
Ovdje povezujemo unos igrača s akcijom u igri. Kada netko pritisne razmaknicu za ispaljivanje lasera ili pritisne strelicu za izbjegavanje asteroida, vaša igra mora otkriti i reagirati na taj unos.
Događaji na tipkovnici događaju se na razini prozora, što znači da cijeli prozor vašeg preglednika sluša te pritiske tipki. Klikovi mišem, s druge strane, mogu se vezati za određene elemente (poput klikanja na gumb). Za našu svemirsku igru, fokusirat ćemo se na kontrole tipkovnice jer one igračima daju taj klasični arkadni osjećaj.
Ovo me podsjeća na to kako su telegrafisti u 1800-ima morali prevoditi unos Morseove abecede u smislene poruke – radimo nešto slično, prevodimo pritiske tipki u naredbe igre.
Za rukovanje događajem trebate koristiti metodu addEventListener() prozora i pružiti joj dva ulazna parametra. Prvi parametar je naziv događaja, na primjer keyup. Drugi parametar je funkcija koja bi se trebala pozvati kao rezultat događaja.
Evo primjera:
window.addEventListener('keyup', (evt) => {
// evt.key = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
});
Razlaganje onoga što se ovdje događa:
- Sluša događaje na tipkovnici na cijelom prozoru
- Hvata objekt događaja koji sadrži informacije o tome koja je tipka pritisnuta
- Provjerava odgovara li pritisnuta tipka određenoj tipki (u ovom slučaju, strelici gore)
- Izvršava kod kada je uvjet ispunjen
Za događaje na tipkama postoje dva svojstva na događaju koja možete koristiti za provjeru koja je tipka pritisnuta:
key- ovo je tekstualni prikaz pritisnute tipke, na primjer'ArrowUp'keyCode- ovo je brojčani prikaz, na primjer37, što odgovaraArrowLeft
✅ Manipulacija događajima na tipkama korisna je i izvan razvoja igara. Koje druge primjene možete zamisliti za ovu tehniku?
Posebne tipke: upozorenje!
Neke tipke imaju ugrađene funkcije preglednika koje mogu ometati vašu igru. Strelice pomiču stranicu, a razmaknica skrolira prema dolje – ponašanja koja ne želite kada netko pokušava upravljati svojim svemirskim brodom.
Možemo spriječiti ova zadana ponašanja i dopustiti našoj igri da upravlja unosom. Ovo je slično tome kako su rani računalni programeri morali nadjačati sistemske prekide kako bi stvorili prilagođena ponašanja – mi to radimo na razini preglednika. Evo kako:
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);
Razumijevanje ovog koda za sprječavanje:
- Provjerava specifične kodove tipki koji bi mogli uzrokovati neželjeno ponašanje preglednika
- Sprječava zadanu akciju preglednika za strelice i razmaknicu
- Dopušta ostalim tipkama da normalno funkcioniraju
- Koristi
e.preventDefault()za zaustavljanje ugrađenog ponašanja preglednika
Kretanje inducirano igrom
Sada ćemo razgovarati o objektima koji se kreću bez unosa igrača. Razmislite o neprijateljskim brodovima koji krstare ekranom, mecima koji lete ravno ili oblacima koji lebde u pozadini. Ovo autonomno kretanje čini vaš svijet igre živim čak i kada nitko ne dodiruje kontrole.
Koristimo ugrađene JavaScript timere za ažuriranje položaja u redovitim intervalima. Ovaj koncept je sličan načinu na koji rade klatna u satovima – redoviti mehanizam koji pokreće dosljedne, vremenski određene radnje. Evo kako to može izgledati:
const id = setInterval(() => {
// Move the enemy on the y axis
enemy.y += 10;
}, 100);
Što ovaj kod za kretanje radi:
- Stvara timer koji se pokreće svakih 100 milisekundi
- Ažurira y-koordinatu neprijatelja za 10 piksela svaki put
- Pohranjuje ID intervala kako bismo ga kasnije mogli zaustaviti ako je potrebno
- Pomiče neprijatelja prema dolje na ekranu automatski
Petlja igre
Evo koncepta koji sve povezuje – petlja igre. Ako je vaša igra film, petlja igre bila bi projektor filma, prikazujući kadar za kadrom tako brzo da se sve čini kao glatko kretanje.
Svaka igra ima jednu od ovih petlji koja radi u pozadini. To je funkcija koja ažurira sve objekte igre, ponovno crta ekran i kontinuirano ponavlja ovaj proces. Ovo prati vašeg heroja, sve neprijatelje, sve lasere koji lete okolo – cijelo stanje igre.
Ovaj koncept me podsjeća na to kako su rani animatori poput Walta Disneya morali ponovno crtati likove kadar po kadar kako bi stvorili iluziju kretanja. Mi radimo isto, samo s kodom umjesto olovaka.
Evo kako tipična petlja igre može izgledati, izražena u kodu:
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);
Razumijevanje strukture petlje igre:
- Briše cijelo platno kako bi uklonila prethodni okvir
- Ispunjava pozadinu čvrstom bojom
- Crta sve objekte igre na njihovim trenutnim položajima
- Ponavlja ovaj proces svakih 200 milisekundi kako bi stvorila glatku animaciju
- Upravlja brzinom kadrova kontroliranjem intervalnog vremena
Nastavak svemirske igre
Sada ćemo dodati kretanje u statičnu scenu koju ste prethodno izgradili. Pretvorit ćemo je iz snimke zaslona u interaktivno iskustvo. Proći ćemo kroz ovo korak po korak kako bismo osigurali da se svaki dio nadovezuje na prethodni.
Preuzmite kod odakle smo stali u prethodnoj lekciji (ili započnite s kodom u Part II- starter mapi ako trebate svježi početak).
Evo što danas gradimo:
- Kontrole heroja: Strelice će upravljati vašim svemirskim brodom po ekranu
- Kretanje neprijatelja: Ti vanzemaljski brodovi će započeti svoj napad
Krenimo s implementacijom ovih značajki.
Preporučeni koraci
Pronađite datoteke koje su stvorene za vas u podmapi your-work. Trebale bi sadržavati sljedeće:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Svoj projekt započnite u mapi your-work upisivanjem:
cd your-work
npm start
Što ova naredba radi:
- Navigira do direktorija vašeg projekta
- Pokreće HTTP poslužitelj na adresi
http://localhost:5000 - Poslužuje datoteke vaše igre kako biste ih mogli testirati u pregledniku
Gore navedeno će pokrenuti HTTP poslužitelj na adresi http://localhost:5000. Otvorite preglednik i unesite tu adresu, trenutno bi trebao prikazati heroja i sve neprijatelje; ništa se još ne miče!
Dodavanje koda
-
Dodajte namjenske objekte za
heroja,neprijateljaiobjekt igre, oni bi trebali imati svojstvaxiy. (Sjetite se dijela o Nasljeđivanju ili kompoziciji).SAVJET
objekt igretrebao bi biti onaj sxiyte sposobnošću da se nacrta na platnu.Savjet: Započnite dodavanjem nove klase
GameObjects konstruktorom definiranim kao dolje, a zatim ga nacrtajte na platnu: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); } }Razumijevanje ove osnovne klase:
- Definira zajednička svojstva koja dijele svi objekti igre (položaj, veličina, slika)
- Uključuje zastavicu
deadza praćenje treba li objekt biti uklonjen - Pruža metodu
draw()koja prikazuje objekt na platnu - Postavlja zadane vrijednosti za sva svojstva koja podklase mogu nadjačati
Sada proširite ovaj
GameObjectkako biste stvoriliHeroiEnemy: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); } }Ključni koncepti u ovim klasama:
- Nasljeđuje od
GameObjectkoristeći ključnu riječextends - Poziva konstruktor roditelja s
super(x, y) - Postavlja specifične dimenzije i svojstva za svaku vrstu objekta
- Implementira automatsko kretanje za neprijatelje koristeći
setInterval()
-
Dodajte rukovatelje događajima na tipkama za upravljanje navigacijom (pomicanje heroja gore/dolje lijevo/desno)
ZAPAMTITE to je kartezijanski sustav, gornji lijevi kut je
0,0. Također zapamtite dodati kod za zaustavljanje zadanog ponašanjaSavjet: Kreirajte svoju funkciju
onKeyDowni povežite je s prozorom: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);Što ovaj rukovatelj događajima radi:
- Sluša događaje pritiska tipki na cijelom prozoru
- Bilježi kod tipke kako bi vam pomogao u otkrivanju koje se tipke pritišću
- Sprječava zadano ponašanje preglednika za strelice i razmaknicu
- Dopušta ostalim tipkama da normalno funkcioniraju
Provjerite konzolu preglednika u ovom trenutku i pratite pritiske tipki koji se bilježe.
-
Implementirajte Pub sub pattern, ovo će održati vaš kod čistim dok slijedite preostale dijelove.
Obrazac Publish-Subscribe pomaže organizirati vaš kod razdvajanjem otkrivanja događaja od rukovanja događajima. Ovo čini vaš kod modularnijim i lakšim za održavanje.
Za ovaj posljednji dio možete:
-
Dodati slušatelja događaja na prozor:
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); } });
Što ovaj sustav događaja radi:
- Otkriva unos s tipkovnice i pretvara ga u prilagođene događaje igre
- Razdvaja otkrivanje unosa od logike igre
- Omogućuje jednostavnu promjenu kontrola kasnije bez utjecaja na kod igre
- Dopušta više sustava da reagiraju na isti unos
-
Kreirajte klasu EventEmitter za objavljivanje i pretplatu na poruke:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } -
Dodajte konstante i postavite EventEmitter:
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();
Razumijevanje postavke:
- Definira konstante poruka kako bi se izbjegle pogreške u pisanju i olakšalo refaktoriranje
- Deklarira varijable za slike, kontekst platna i stanje igre
- Stvara globalni event emitter za sustav pub-sub
- Inicijalizira niz za pohranu svih objekata igre
-
Inicijalizirajte igru
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; });
-
-
Postavite petlju igre
Refaktorirajte funkciju
window.onloadkako biste inicijalizirali igru i postavili petlju igre na dobar interval. Također ćete dodati laserski zrak: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); };Razumijevanje postavke igre:
- Čeka da se stranica potpuno učita prije početka
- Dohvaća element platna i njegov 2D kontekst za crtanje
- Učitava sve slikovne resurse asinkrono koristeći
await - Pokreće petlju igre koja radi u intervalima od 100 ms (10 FPS)
- Briše i ponovno crta cijeli ekran svaki kadar
-
Dodajte kod za pomicanje neprijatelja u određenim intervalima
Refaktorirajte funkciju
createEnemies()kako biste stvorili neprijatelje i dodali ih u novu klasu gameObjects:function createEnemies() { const MONSTER_TOTAL = 5; const MONSTER_WIDTH = MONSTER_TOTAL * 98; const START_X = (canvas.width - MONSTER_WIDTH) / 2; const STOP_X = START_X + MONSTER_WIDTH; for (let x = START_X; x < STOP_X; x += 98) { for (let y = 0; y < 50 * 5; y += 50) { const enemy = new Enemy(x, y); enemy.img = enemyImg; gameObjects.push(enemy); } } }Što radi stvaranje neprijatelja:
- Izračunava položaje za centriranje neprijatelja na ekranu
- Stvara mrežu neprijatelja koristeći ugniježđene petlje
- Dodjeljuje sliku neprijatelja svakom objektu neprijatelja
- Dodaje svakog neprijatelja u globalni niz objekata igre
i dodajte funkciju createHero() koja će obaviti sličan proces za heroja.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
```
Što radi kreacija heroja:
- Pozicionira heroja na dno, u sredinu ekrana
- Dodjeljuje sliku heroja objektu heroja
- Dodaje heroja u niz objekata igre za prikaz
i na kraju, dodajte funkciju drawGameObjects() za početak crtanja:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
Razumijevanje funkcije crtanja:
- Iterira kroz sve objekte igre u nizu
- Poziva metodu
draw()na svakom objektu - Prosljeđuje kontekst platna kako bi se objekti mogli sami prikazati
Vaši neprijatelji trebali bi početi napredovati prema vašem svemirskom brodu heroja!
}
}
```
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 na kraju, dodajte funkciju drawGameObjects() za početak crtanja:
```javascript
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
```
Vaši neprijatelji trebali bi početi napredovati prema vašem svemirskom brodu heroja!
Izazov GitHub Copilot Agent 🚀
Evo izazova koji će poboljšati završnu obradu vaše igre: dodavanje granica i glatkih kontrola. Trenutno vaš heroj može odletjeti izvan ekrana, a kretanje može djelovati trzavo.
Vaša misija: Učinite da vaš svemirski brod djeluje realističnije implementacijom granica ekrana i fluidnog kretanja. To je slično načinu na koji NASA-ini sustavi za kontrolu leta sprječavaju svemirske letjelice da prekorače sigurne operativne parametre.
Što treba izgraditi: Stvorite sustav koji drži svemirski brod heroja na ekranu i učinite da kontrole djeluju glatko. Kada igrači drže pritisnutu tipku sa strelicom, brod bi trebao kliziti kontinuirano, umjesto da se kreće u diskretnim koracima. Razmislite o dodavanju vizualne povratne informacije kada brod dosegne granice ekrana – možda suptilni efekt koji označava rub područja igre.
Saznajte više o agent modu ovdje.
🚀 Izazov
Organizacija koda postaje sve važnija kako projekti rastu. Možda ste primijetili da vaš dokument postaje pretrpan funkcijama, varijablama i klasama koje su sve izmiješane. To me podsjeća na način na koji su inženjeri organizirali kod za misiju Apollo, stvarajući jasne, održive sustave na kojima su istovremeno mogle raditi različite ekipe.
Vaša misija:
Razmišljajte kao softverski arhitekt. Kako biste organizirali svoj kod tako da ga za šest mjeseci vi (ili vaš kolega) možete razumjeti? Čak i ako sve ostane u jednoj datoteci za sada, možete stvoriti bolju organizaciju:
- Grupiranje povezanih funkcija zajedno s jasnim zaglavljima komentara
- Razdvajanje odgovornosti - odvojite logiku igre od prikaza
- Korištenje dosljednih konvencija imenovanja za varijable i funkcije
- Stvaranje modula ili prostora imena za organizaciju različitih aspekata vaše igre
- Dodavanje dokumentacije koja objašnjava svrhu svakog glavnog dijela
Pitanja za razmišljanje:
- Koji su dijelovi vašeg koda najteži za razumjeti kada se vratite na njih?
- Kako biste mogli organizirati svoj kod da ga drugi lakše razumiju i doprinesu?
- Što bi se dogodilo ako biste željeli dodati nove značajke poput pojačanja ili različitih vrsta neprijatelja?
Kviz nakon predavanja
Pregled i samostalno učenje
Sve gradimo od nule, što je fantastično za učenje, ali evo male tajne – postoje nevjerojatni JavaScript okviri koji mogu obaviti velik dio posla za vas. Kada se osjećate ugodno s osnovama koje smo obradili, vrijedi istražiti što je dostupno.
Razmislite o okvirima kao o dobro opremljenoj kutiji s alatima, umjesto da svaki alat izrađujete ručno. Oni mogu riješiti mnoge od onih izazova organizacije koda o kojima smo govorili, plus ponuditi značajke koje bi vam inače trebale tjedni da ih sami izradite.
Stvari koje vrijedi istražiti:
- Kako game engine-i organiziraju kod – bit ćete zadivljeni pametnim obrascima koje koriste
- Trikovi za performanse kako bi igre na platnu radile glatko kao maslac
- Moderne značajke JavaScripta koje mogu učiniti vaš kod čišćim i lakšim za održavanje
- Različiti pristupi upravljanju objektima igre i njihovim odnosima
Zadatak
Izjava o odricanju odgovornosti:
Ovaj dokument je preveden pomoću AI usluge za prevođenje Co-op Translator. Iako nastojimo osigurati točnost, imajte na umu da automatski prijevodi mogu sadržavati pogreške ili netočnosti. Izvorni dokument na izvornom jeziku treba smatrati autoritativnim izvorom. Za ključne informacije preporučuje se profesionalni prijevod od strane čovjeka. Ne preuzimamo odgovornost za nesporazume ili pogrešna tumačenja koja proizlaze iz korištenja ovog prijevoda.