|
3 weeks ago | |
---|---|---|
.. | ||
README.md | 3 weeks ago | |
assignment.md | 4 weeks ago |
README.md
Byg et rumspil del 3: Tilføj bevægelse
Quiz før forelæsning
Spil er ikke særlig sjove, før du har rumvæsener, der bevæger sig rundt på skærmen! I dette spil vil vi gøre brug af to typer bevægelser:
- Tastatur/mus-bevægelse: når brugeren interagerer med tastaturet eller musen for at flytte et objekt på skærmen.
- Spilinduceret bevægelse: når spillet flytter et objekt med et bestemt tidsinterval.
Så hvordan flytter vi ting på en skærm? Det handler alt sammen om kartesiske koordinater: vi ændrer objektets placering (x, y) og tegner derefter skærmen igen.
Typisk har du brug for følgende trin for at opnå bevægelse på en skærm:
- Indstil en ny placering for et objekt; dette er nødvendigt for at opfatte objektet som flyttet.
- Ryd skærmen, skærmen skal ryddes mellem tegningerne. Vi kan rydde den ved at tegne et rektangel, som vi fylder med en baggrundsfarve.
- Tegn objektet igen på den nye placering. Ved at gøre dette opnår vi endelig at flytte objektet fra en placering til en anden.
Sådan kan det se ud i kode:
//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);
✅ Kan du tænke på en grund til, at det kan medføre ydeevneomkostninger at tegne din helt mange gange i sekundet? Læs om alternativer til dette mønster.
Håndtering af tastaturhændelser
Du håndterer hændelser ved at knytte specifikke hændelser til kode. Tastaturhændelser udløses på hele vinduet, mens musehændelser som et klik
kan forbindes til at klikke på et specifikt element. Vi vil bruge tastaturhændelser gennem hele dette projekt.
For at håndtere en hændelse skal du bruge vinduets addEventListener()
-metode og give den to inputparametre. Den første parameter er navnet på hændelsen, for eksempel keyup
. Den anden parameter er den funktion, der skal kaldes som et resultat af, at hændelsen finder sted.
Her er et eksempel:
window.addEventListener('keyup', (evt) => {
// `evt.key` = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
})
For tastaturhændelser er der to egenskaber på hændelsen, du kan bruge til at se, hvilken tast der blev trykket på:
key
, dette er en strengrepræsentation af den trykkede tast, for eksempelArrowUp
.keyCode
, dette er en numerisk repræsentation, for eksempel37
, som svarer tilArrowLeft
.
✅ Manipulation af tastaturhændelser er nyttig uden for spiludvikling. Hvilke andre anvendelser kan du komme i tanke om for denne teknik?
Specielle taster: en advarsel
Der er nogle specielle taster, der påvirker vinduet. Det betyder, at hvis du lytter til en keyup
-hændelse og bruger disse specielle taster til at flytte din helt, vil det også udføre horisontal rulning. Af den grund vil du måske slå fra denne indbyggede browseradfærd, mens du bygger dit spil. Du har brug for kode som denne:
let 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);
Koden ovenfor sikrer, at piletasterne og mellemrumstasten får deres standard adfærd slået fra. Deaktiveringsmekanismen sker, når vi kalder e.preventDefault()
.
Spilinduceret bevægelse
Vi kan få ting til at bevæge sig af sig selv ved at bruge timere som funktionerne setTimeout()
eller setInterval()
, der opdaterer objektets placering ved hvert tick eller tidsinterval. Sådan kan det se ud:
let id = setInterval(() => {
//move the enemy on the y axis
enemy.y += 10;
})
Spil-loopet
Spil-loopet er et koncept, der i bund og grund er en funktion, der kaldes med regelmæssige intervaller. Det kaldes spil-loopet, fordi alt, hvad der skal være synligt for brugeren, tegnes i løbet af loopet. Spil-loopet gør brug af alle spilobjekter, der er en del af spillet, og tegner dem, medmindre de af en eller anden grund ikke længere skal være en del af spillet. For eksempel, hvis et objekt er en fjende, der blev ramt af en laser og eksploderer, er det ikke længere en del af det aktuelle spil-loop (du vil lære mere om dette i de følgende lektioner).
Sådan kan et spil-loop typisk se ud, udtrykt i kode:
let 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();
}, 200);
Loopet ovenfor kaldes hvert 200
millisekund for at tegne lærredet igen. Du har mulighed for at vælge det bedste interval, der giver mening for dit spil.
Fortsættelse af rumspillet
Du vil tage den eksisterende kode og udvide den. Enten start med den kode, du færdiggjorde under del I, eller brug koden i Del II - starter.
- Flyt helten: du vil tilføje kode for at sikre, at du kan flytte helten ved hjælp af piletasterne.
- Flyt fjender: du skal også tilføje kode for at sikre, at fjenderne bevæger sig fra top til bund med en given hastighed.
Anbefalede trin
Find de filer, der er blevet oprettet til dig i undermappen your-work
. Den bør indeholde følgende:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
Du starter dit projekt i mappen your_work
ved at skrive:
cd your-work
npm start
Ovenstående starter en HTTP-server på adressen http://localhost:5000
. Åbn en browser og indtast den adresse. Lige nu bør den vise helten og alle fjenderne; intet bevæger sig - endnu!
Tilføj kode
-
Tilføj dedikerede objekter for
hero
,enemy
oggame object
, de skal havex
- ogy
-egenskaber. (Husk afsnittet om Arv eller komposition).TIP
game object
bør være det, der harx
ogy
og evnen til at tegne sig selv på et lærred.tip: start med at tilføje en ny GameObject-klasse med dens constructor defineret som nedenfor, og tegn den derefter på lærredet:
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); } }
Udvid nu denne GameObject for at oprette Hero og Enemy.
class Hero extends GameObject { constructor(x, y) { ...it needs an x, y, type, and speed } }
class Enemy extends GameObject { constructor(x, y) { super(x, y); (this.width = 98), (this.height = 50); this.type = "Enemy"; let id = setInterval(() => { if (this.y < canvas.height - this.height) { this.y += 5; } else { console.log('Stopped at', this.y) clearInterval(id); } }, 300) } }
-
Tilføj tastaturhændelses-håndterere for at håndtere navigation med taster (flyt helten op/ned, venstre/højre).
HUSK det er et kartesisk system, øverst til venstre er
0,0
. Husk også at tilføje kode for at stoppe standardadfærd.tip: opret din onKeyDown-funktion og tilknyt den til vinduet:
let onKeyDown = function (e) { console.log(e.keyCode); ...add the code from the lesson above to stop default behavior } }; window.addEventListener("keydown", onKeyDown);
Tjek din browsers konsol på dette tidspunkt, og se tastetrykkene blive logget.
-
Implementer Pub-sub-mønsteret, dette vil holde din kode ren, mens du følger de resterende dele.
For at gøre denne sidste del kan du:
-
Tilføj en hændelseslytter på vinduet:
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); } });
-
Opret en EventEmitter-klasse for at publicere og abonnere på beskeder:
class EventEmitter { constructor() { this.listeners = {}; } on(message, listener) { if (!this.listeners[message]) { this.listeners[message] = []; } this.listeners[message].push(listener); } emit(message, payload = null) { if (this.listeners[message]) { this.listeners[message].forEach((l) => l(message, payload)); } } }
-
Tilføj konstanter og opsæt 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();
-
Initialiser spillet
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; }); eventEmitter.on(Messages.KEY_EVENT_RIGHT, () => { hero.x += 5; }); }
-
-
Opsæt spil-loopet
Refaktorer window.onload-funktionen for at initialisere spillet og opsætte et spil-loop med et passende interval. Du vil også tilføje en laserstråle:
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(); let gameLoopId = setInterval(() => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); drawGameObjects(ctx); }, 100) };
-
Tilføj kode for at flytte fjender med et bestemt interval.
Refaktorer
createEnemies()
-funktionen for at oprette fjenderne og skubbe dem ind i den nye gameObjects-klasse: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); } } }
og tilføj en
createHero()
-funktion for at gøre en lignende proces for helten.function createHero() { hero = new Hero( canvas.width / 2 - 45, canvas.height - canvas.height / 4 ); hero.img = heroImg; gameObjects.push(hero); }
og til sidst tilføj en
drawGameObjects()
-funktion for at starte tegningen:function drawGameObjects(ctx) { gameObjects.forEach(go => go.draw(ctx)); }
Dine fjender bør begynde at nærme sig dit rumskib!
🚀 Udfordring
Som du kan se, kan din kode blive til 'spaghetti-kode', når du begynder at tilføje funktioner, variabler og klasser. Hvordan kan du bedre organisere din kode, så den er mere læsbar? Skitser et system til at organisere din kode, selvom den stadig befinder sig i én fil.
Quiz efter forelæsning
Gennemgang & Selvstudie
Mens vi skriver vores spil uden at bruge frameworks, findes der mange JavaScript-baserede canvas-frameworks til spiludvikling. Brug lidt tid på at læse om disse.
Opgave
Ansvarsfraskrivelse:
Dette dokument er blevet oversat ved hjælp af AI-oversættelsestjenesten Co-op Translator. Selvom vi bestræber os på nøjagtighed, skal du være opmærksom på, at automatiserede oversættelser kan indeholde fejl eller unøjagtigheder. Det originale dokument på dets oprindelige sprog bør betragtes som den autoritative kilde. For kritisk information anbefales professionel menneskelig oversættelse. Vi påtager os intet ansvar for misforståelser eller fejltolkninger, der måtte opstå som følge af brugen af denne oversættelse.