28/12/2022
Online spil har gennemgået en bemærkelsesværdig transformation. Hvor Flash engang var den ubestridte konge, har de seneste år set en eksplosion i spil udviklet med JavaScript og Canvas API'en. Denne ændring har åbnet dørene for utallige muligheder, hvilket gør det nemmere end nogensinde at bringe dine spilidéer til live direkte i browseren. Hvis du har en forkærlighed for klassiske spil og ønsker at udforske spiludviklingens verden, er du kommet til det rette sted. Tag dine JavaScript-færdigheder frem, og lad os sammen tage på en hurtig rundtur i spilskabelsens fascinerende univers.

I denne dybdegående guide vil vi trække den gamle, klodsede telefonfavorit fra fortiden – Snake – ind i den modige nye verden af smartphones og webbrowsere. Vi vil dække alt fra de grundlæggende HTML-opsætninger til kompleks JavaScript-logik, der får slangen til at bevæge sig, spise æbler og vokse sig lang. Gør dig klar til at kode, for vi skal bygge dette spil helt fra bunden!
Typisk er de fleste spil opbygget omkring en grundlæggende løkke, der gentages konstant for at drive spillet fremad. Denne spilløkke er kernen i enhver interaktiv oplevelse og består af nogle få, men afgørende trin:
- Kontrollér brugerinput: Registrér, om spilleren trykker på taster, klikker eller rører skærmen.
- Flyt spillere/objekter: Opdater positionerne for alle elementer i spillet – i vores tilfælde slangen, dens forskellige segmenter og det velsmagende røde æble.
- Kontrollér for kollisioner: Undersøg, om objekter støder sammen. Dette kan være slangen, der bider æblet, slangen, der bider sin egen hale, eller slangen, der kolliderer med skærmens grænser.
- Udfør passende handling: Baseret på kollisionen tages en handling, f.eks. at øge scoren, afslutte spillet eller flytte æblet.
- Gentag: Hele processen gentages igen og igen, mange gange i sekundet, for at skabe en flydende animation.
Denne konstante cyklus sikrer, at spillet reagerer dynamisk på spillerens handlinger og spillets interne logik.
Trin 1: Et Tomt Canvas – Grundlaget for dit Spil
For at starte har vi brug for en grundlæggende HTML-fil. Denne fil vil indeholde den minimale struktur, der er nødvendig for at vise vores spil. Vi tilføjer en smule CSS for grundlæggende styling, et <canvas>-tag, som er vores tegneflade, og et par knapper til venstre og højre bevægelse, især nyttige for mobilbrugere. Det er vigtigt at indlæse vores JavaScript-fil lige før det afsluttende </body>-tag. Dette sikrer, at JavaScript'en først køres, når hele HTML-dokumentet er indlæst og klar, hvilket eliminerer behovet for at kontrollere, om DOM'en er klar i JavaScript.
Den egentlige magi sker i JavaScript-filen, som vil håndtere alle aspekter af spillet – fra tegning på skærmen til kontrol af brugerinput og spillets logik. Åbn din foretrukne teksteditor, og lad os bygge spillet fra bunden.
Vi starter med en selveksekverende anonym funktion. Dette er en smart måde at indkapsle alle dine variabler og funktioner på uden at forurene det globale navnerum. Parenteserne efter den afsluttende krøllede parentes betyder: kør denne funktion nu.
(function () {
var canvas = document.getElementById('snakeCanvas'),
ctx = canvas.getContext('2d'),
score = 0,
hiScore = 20,
leftButton = document.getElementById('leftButton'),
rightButton = document.getElementById('rightButton'),
input = { left: false, right: false };
canvas.width = 320;
canvas.height = 350;
// check for keypress and set input properties
window.addEventListener('keyup', function(e) {
switch (e.keyCode) {
case 37: // Venstre piltast
input.left = true;
break;
case 39: // Højre piltast
input.right = true;
break;
}
}, false);
// resten af koden kommer her
}());Øverst i scriptet har vi deklareret nogle grundlæggende variabler. Forhåbentlig er det indlysende, hvad hver af dem betyder. I overensstemmelse med bedste praksis er det en god idé at deklarere disse i begyndelsen af dit program. De sidste par linjer fortæller browseren at kontrollere for tastaturinput. Hver tast har sin egen keyCode. Vi behøver kun at kontrollere for højre og venstre piletast, og hvis de er blevet trykket, sætter vi input-egenskaben til true for den pågældende tast.
Nu skal vi tilføje et simpelt objekt, der tager sig af tegning for os. Dette vil gøre det nemt at tegne spillets byggesten.
var draw = {
clear: function () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
},
rect: function (x, y, w, h, col) {
ctx.fillStyle = col;
ctx.fillRect(x, y, w, h);
},
circle: function (x, y, radius, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
},
text: function (str, x, y, size, col) {
ctx.font = 'bold ' + size + 'px monospace';
ctx.fillStyle = col;
ctx.fillText(str, x, y);
}
};Vi har oprettet et draw objekt, der giver os mulighed for at rydde canvas, tegne rektangler, cirkler og tekst. ctx er vores reference til canvas'en og giver os mulighed for at bruge Canvas API'en. Dette bruger object literal notation, som er en simpel måde at oprette en enkelt instans af et objekt på.
Trin 2: En Slange i Græsset – Skab Slangeklassen
Dernæst kommer vores Snake klasse. Vi skal spore slangens koordinater samt dens længde. Lad os også inkludere værdier som bredde, højde, farve osv., så vi nemt kan ændre dem senere. Hvilke handlinger skal vores slange udføre? Den skal flytte sig selv og hvert segment af sin slimede krop, tegne sig selv, og kontrollere for kollisioner med enten spillets grænser, æblet eller sin egen hale.
Bemærk: I modsætning til draw-objektet erklærer vi dette som en funktion (funktioner er førsteklasses objekter i JavaScript). Vi vil tilføje metoder til Snake-klassen via dens prototype.

var Snake = function() {
this.init = function() {
this.dead = false;
this.len = 0; // længden af slangen (antal segmenter)
this.speed = 4; // antal pixels flyttet pr. frame
this.history = []; // vi skal holde styr på, hvor vi har været
this.dir = [
[0, -1], // op
[1, 0], // højre
[0, 1], // ned
[-1, 0] // venstre
];
this.x = 100;
this.y = 100;
this.w = this.h = 16;
this.currentDir = 2; // dvs. this.dir[2] = ned
this.col = 'darkgreen';
};
this.move = function() {
if (this.dead) {
return;
}
// kontroller om en knap er trykket
if (input.left) {
this.currentDir += 1;
if (this.currentDir > 3) {
this.currentDir = 0;
}
} else if (input.right) {
this.currentDir -= 1;
if (this.currentDir < 0) {
this.currentDir = 3;
}
}
// kontroller om uden for grænser
if (this.x < 0 || this.x > (canvas.width - this.w) || this.y < 0 || this.y > (canvas.height - this.h)) {
this.dead = true;
}
// opdater position
this.x += (this.dir[this.currentDir][0] * this.speed);
this.y += (this.dir[this.currentDir][1] * this.speed);
// gem denne position i history-arrayet
this.history.push({x: this.x, y: this.y, dir: this.currentDir});
};
this.draw = function () {
draw.rect(this.x, this.y, this.w, this.h, this.col); // tegn hoved
draw.rect(this.x + 4, this.y + 1, 3, 3, 'white'); // tegn øjne
draw.rect(this.x + 12, this.y + 1, 3, 3, 'white');
};
this.collides = function (obj) {
// denne sprites rektangel
this.left = this.x;
this.right = this.x + this.w;
this.top = this.y;
this.bottom = this.y + this.h;
// andet objekts rektangel
obj.left = obj.x;
obj.right = obj.x + obj.w;
obj.top = obj.y;
obj.bottom = obj.y + obj.h;
// afgør om de ikke overlapper
if (this.bottom < obj.top) { return false; }
if (this.top > obj.bottom) { return false; }
if (this.right < obj.left) { return false; }
if (this.left > obj.right) { return false; }
// ellers er det et hit
return true;
};
};Vores slange har nu fire metoder, som vi skal bruge til at manipulere den:
init(): Denne metode opsætter alle variabler (eller egenskaber), der er nødvendige for slangen; dens x- og y-koordinater, farve, længde, retning, hastighed, et historiearray af alle tidligere positioner osv. Vi skal kalde denne efter at have oprettet vores slange, eller når et nyt spil starter. Du undrer dig måske over, hvad der er medthis.dir-arrayet? Hvad vi har her, er et array, der indeholder fire arrays, der svarer til op, højre, ned og venstre.this.currentDirpeger på en indgang ithis.dir.move(): Dette er, hvor det meste af slange-aktionen finder sted. Her kontrollerer vi for brugerinput og justerer vores retning i overensstemmelse hermed, kontrollerer om vi stadig er på skærmen, og hvis ikke, markerer slangen som død. Dernæst opdaterer vi slangens position. For eksempel, hvisthis.currentDirer 0, er vores retningthis.dir[0], som indeholder et array med 0, -1 (op). Vi lægger den første værdi (0) til vores x-koordinat og ganger med hastighed. Den anden værdi (-1) lægges til y-koordinaten og ganges igen med hastighed. I dette tilfælde bevæger vi os -4 pixels op. Den sidste linje skubber en registrering af vores koordinater ind ihistory-arrayet. Dette vil være praktisk, når vi senere placerer hvert segment af slangens hale.draw(): Dette er meget ligetil. Vi foretager først et kald tildraw.rectbaseret på dyrets hoveds position. Derefter tegner vi yderligere to rektangler for at skildre øjnene.collides(): Her skal vi kontrollere, om slangens hoved rører et andet objekt. Denne metode tjekker grundlæggende, om to rektangler overlapper, og returnerertrue, hvis de gør.
Nu er vi klar til at teste vores kode. Vi skal blot tilføje en loop() funktion, der kalder sig selv periodisk:
var p1 = new Snake();
p1.init();
function loop() {
draw.clear(); // ryd vores canvas. den forrige løkkes tegninger er stadig der
p1.move();
p1.draw();
if (p1.dead) {
draw.text('Game Over', 100, 100, 12, 'black');
}
// vi skal nulstille højre og venstre, ellers fortsætter slangen med at dreje
input.right = input.left = false;
};
setInterval(loop, 30); // kald loop-funktionen hvert 30. millisekundTrin 3: Tilføj Æblet – Spillets Mål
Æbleklassen bliver meget enklere end dens slange-modstykke. Grundlæggende skal vi blot placere det tilfældigt på skærmen. Hvis det bliver spist, skal vi placere det et andet sted. Her er den grundlæggende klasse:
var Apple = function() {
this.x = 0;
this.y = 0;
this.w = 16;
this.h = 16;
this.col = 'red';
this.replace = 0; // antal spilrunder indtil vi flytter æblet et andet sted
this.draw = function() {
if (this.replace === 0) {
// tid til at flytte æblet et andet sted
this.relocate();
}
draw.rect(this.x, this.y, this.w, this.h, this.col);
this.replace -= 1;
};
this.relocate = function() {
this.x = Math.floor(Math.random() * (canvas.width - this.w));
this.y = Math.floor(Math.random() * (canvas.height - this.h));
this.replace = Math.floor(Math.random() * 200) + 200;
};
};Nu skal vi integrere æblet i spillets logik og bruge slangens kollisionsmetode til at se, om vi har slugt det. Kollisionsmetoden ser måske lidt skræmmende ud, men grundlæggende kontrollerer den, om to rektangler overlapper, og returnerer true, hvis de gør.
Vi skal opdatere loop-funktionen til at håndtere tegning af æblet og kontrol af kollision. Vi tilføjer også score- og highscore-visning.
// Opret æble-instansen uden for loop-funktionen
var apple = new Apple();
apple.relocate(); // Flyt æblet til en startposition
function loop() {
draw.clear();
p1.move();
p1.draw();
if (p1.collides(apple)) {
score += 1;
p1.len += 1;
apple.relocate();
// Her kunne en lydeffekt for at spise æblet afspilles (f.eks. foodSound.play();)
}
if (score > hiScore) {
hiScore = score;
}
apple.draw();
draw.text('Score: ' + score, 20, 20, 12, 'black');
draw.text('Hi: ' + hiScore, 260, 20, 12, 'black');
if (p1.dead === true) {
draw.text('Game Over', 100, 200, 20, 'black');
// Her kunne en lydeffekt for game over afspilles (f.eks. gameOverSound.play();)
if (input.right || input.left) {
p1.init(); // Genstart spillet
score = 0;
}
}
input.right = input.left = false;
}Trin 4: Se Slangen Vokse – Hale-Logikken
Hvert segment af halen vil altid være 'n' træk bag dens nærmeste nabo foran. Fra dette kan vi komme med følgende meget enkle algoritme:
- For hvert segment af slangen hentes dens position fra
history-arrayet. Mere præcist vil positionen være en funktion afi(segmentet) minus bredde divideret med hastighed. Dette betyder, at for det første segment skal vi flytte tilbage 1 * (16 / 4). - Tegn et rektangel på den position.
- Kontrollér, om slangens hoved er i kollision med dette segment, og hvis det er tilfældet, sæt slangens status til
dead.
Dette skal implementeres i slangens draw metode, så den tegner hele halen, ikke kun hovedet, og kontrollerer for kollisioner med sin egen krop.
this.draw = function () {
var i, offset, segPos, col;
// loop gennem hvert segment af slangen,
// tegning & kontrol af kollisioner
for (i = 1; i <= this.len; i += 1) {
// offset beregner placeringen i history-arrayet
offset = i * Math.floor(this.w / this.speed);
offset = this.history.length - offset;
// Sikrer at vi ikke forsøger at tilgå et indeks uden for arrayet
if (offset < 0) continue;
segPos = this.history[offset];
col = this.col;
// reducer området vi tjekker for kollision, for at være lidt
// mere tilgivende med små overlap
// Vi skaber en midlertidig kopi af segPos for kollisionstjekket
// for ikke at ændre de gemte historikdata.
var collisionSeg = {
x: segPos.x,
y: segPos.y,
w: this.w - this.speed,
h: this.h - this.speed
};
// i > 2 for at undgå kollision med de første par segmenter lige bag hovedet
// som ville udløse en game over med det samme.
if (i > 2 && this.collides(collisionSeg)) {
this.dead = true;
col = 'darkred'; // fremhæv ramte segmenter
// Her kunne en lydeffekt for game over afspilles
}
draw.rect(segPos.x, segPos.y, this.w, this.h, col);
}
draw.rect(this.x, this.y, this.w, this.h, this.col); // tegn hoved
draw.rect(this.x + 4, this.y + 1, 3, 3, 'white'); // tegn øjne
draw.rect(this.x + 12, this.y + 1, 3, 3, 'white');
// Ryd op i historikken, så den ikke vokser uendeligt
// Beholder kun de nødvendige segmenter plus en buffer
if (this.history.length > (this.len + 10) * (this.w / this.speed)) {
this.history.splice(0, this.history.length - (this.len + 10) * (this.w / this.speed));
}
};Bemærk, at vi tilføjede en lille justering til this.draw-metoden for at forhindre history-arrayet i at vokse uendeligt. Dette er en vigtig optimering for spillets ydeevne, da et for stort array kan føre til hukommelsesproblemer og langsommere spil over tid. Ved at fjerne gamle, unødvendige positioner holder vi arrayet på en håndterbar størrelse.
Trin 5: Mobil Tilpasning – Spil på Farten
Du tænker sikkert, hvad der skete med mobil-delen i titlen. Indtil videre har vi kun kontrolleret piletasterne for input. Vores HTML-fil indeholder et par kontrolknapper nederst på canvas'en. Alt, hvad vi skal gøre, er at kontrollere, om de er blevet trykket på.
// lad os antage, at vi ikke bruger en touch-kompatibel enhed
var clickEvent = 'click';
// nu prøver vi en simpel test for at se, om vi har touch
try {
document.createEvent('TouchEvent');
// det ser ud til, at vi gør, så vi bør kontrollere for det i stedet for click
clickEvent = 'touchend';
} catch(e) {
// browseren understøtter ikke TouchEvent, så vi bruger clickEvent
}
leftButton.addEventListener(clickEvent, function(e) {
e.preventDefault();
input.left = true;
}, false);
rightButton.addEventListener(clickEvent, function(e) {
e.preventDefault();
input.right = true;
}, false);Først deklarerer vi vores clickEvent-variabel, der som standard er sat til 'click'. Derefter bruger vi en try/catch-blok for at se, om vi har touch-kapacitet ved at oprette et TouchEvent. Fordelen ved at bruge try/catch er, at hvis browseren ikke er touch-kompatibel, vil den ikke afslutte med en ugenoprettelig fejl. Essentielt undertrykker vi eventuelle mulige fejl. Nu hvor vi ved, om clickEvent enten er standard 'click' eller 'touchend', kan vi tilføje event-listeners til højre og venstre knapper for at opdatere inputstatus.
Der er et par flere tricks, vi kan bruge til at forbedre oplevelsen for iPhone-brugere. Tilføj følgende meta-tags øverst i din HTML-fil, inden for <head>-sektionen:
<meta name="viewport" content="width=320, height=440, user-scalable=no, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-startup-image" href="iphonestartup.png" />Det første meta-tag deaktiverer vigtigt nok skalering. De næste to er, åbenlyst, iPhone-specifikke. Den første fjerner knaplinjerne og URL'en, den anden justerer statuslinjen. <link>-tagget giver os mulighed for at indstille et flot startskærmikon, hvis brugeren tilføjer spillet til sin startskærm.
En sidste ting, vi kan gøre, er at forsøge at skjule URL-linjen ved at rulle til den første pixel på skærmen. Dette opnås nemt med window.scrollTo(x, y), som kaldes lige før vi kalder setInterval.
window.scrollTo(0,0);
setInterval(loop, 30);På dette stadie har vi en nogenlunde spilbar Snake-klon. Ikke ligefrem banebrydende, men jeg håber, det har givet dig en idé om, hvad der er involveret i at skabe spil med JavaScript og Canvas API'en.
Videreudvikling og Inspiration
Dette afslutter denne vejledning, selvom der er masser af muligheder for forbedringer. Hvorfor ikke prøve kræfter med nogle af forslagene nedenfor? Husk, at i spiludvikling, som i mange andre felter, lærer man hurtigt, at man ikke skal genopfinde den dybe tallerken. Chancerne er, at en eller anden klog person allerede er kommet med en løsning. I den ånd foreslår jeg, at du tager et kig på nogle JavaScript-spilbiblioteker:
- ImpactJS: Koster omkring 100$, men er virkelig pengene værd. Kan prale af fremragende ydeevne, en niveau-editor, sprites og god lydsupport.
- Mibbu: Open source, letvægt og understøtter både DOM og Canvas.
- Akihabra: Rettet mod at skabe spil i retro-stil.
- Mere: En pænt samlet, omfattende liste over JavaScript-spilmotorer findes online, som kan give dig yderligere inspiration og værktøjer.
Mulige Forbedringer til Dit Snake-spil:
Du kan forbedre dit spil markant med nogle af disse forslag:
- Nem: Brug HTML5's LocalStorage API til at gemme highscores på tværs af sessioner, så spillerne kan konkurrere mod deres egne tidligere resultater.
- Nem: Brug HTML5's cache manifest for offline-spil. Dette gør, at spillet kan spilles, selv når brugeren ikke har internetadgang.
- Nem: Brug
circle-tegnefunktionen til at tegne et mere realistisk æble i stedet for et firkantet. - Medium: Tilføj et slangemønster til slangens krop for at gøre den visuelt mere interessant.
- Medium: Tilføj nogle lydeffekter for at forbedre spiloplevelsen. Tænk på lyde for at spise æble, game over og bevægelse. Som nævnt i den oprindelige tekst, kan du bruge:
const foodSound = new Audio('music/food.mp3');
const gameOverSound = new Audio('music/gameover.mp3');
const moveSound = new Audio('music/move.mp3');
const musicSound = new Audio('music/music.mp3');Disse kan integreres i de relevante dele af din
loop-funktion og slangensmove-metode. - Svær: Tilføj en spids hale til slangen. Tip: Du skal tilføje en trekantmetode til
draw-klassen og rotere den afhængigt af retning. Læs mere om at rotere canvas online.
Ofte Stillede Spørgsmål (FAQ)
- Hvad er HTML5 Canvas?
- HTML5 Canvas er et HTML-element, der giver dig mulighed for at tegne grafik på en webside ved hjælp af JavaScript. Det er perfekt til spil, diagrammer og animationer, da det giver en pixel-præcis kontrol over tegnefladen.
- Hvorfor bruge JavaScript til spil i stedet for andre teknologier?
- JavaScript er browserens indbyggede sprog, hvilket betyder, at det kan køre direkte i stort set enhver moderne webbrowser uden behov for plugins. Kombineret med Canvas API'en giver det kraftfulde værktøjer til at skabe interaktive og visuelt rige spil, der er tilgængelige for et bredt publikum.
- Er spillet responsivt, så det kan spilles på forskellige enheder?
- Ja, med den tilføjede mobiloptimering (viewport meta-tags og touch-event lyttere) samt den fleksible canvas-størrelse, kan spillet tilpasses forskellige skærmstørrelser og enheder, herunder mobiltelefoner og tablets. Dette sikrer en god brugeroplevelse uanset enheden. Den indledende information nævner også, at et lignende spil kan være "completely responsive which you can play on mobile, laptop and tablet", hvilket understøtter dette.
- Hvordan kan jeg tilføje lydeffekter til spillet?
- Du kan tilføje lydeffekter ved at oprette
Audio-objekter i JavaScript og afspille dem ved relevante begivenheder, f.eks. når slangen spiser et æble (foodSound.play()) eller kolliderer (gameOverSound.play()). Sørg for at have dine lydfiler (f.eks. .mp3) i en tilgængelig mappe. - Kan jeg gemme highscores, så de ikke forsvinder, når browseren lukkes?
- Ja, du kan bruge HTML5's LocalStorage API til dette. Når en ny highscore opnås, gemmer du den i
localStorage(f.eks.localStorage.setItem('snakeHiScore', hiScore);) og indlæser den ved spillets start (f.eks.hiScore = localStorage.getItem('snakeHiScore') || 0;). Dette sikrer, at dine bedste resultater bevares.
Vi håber, at denne vejledning har givet dig en solid forståelse af, hvordan du kan begynde at skabe dine egne spil med JavaScript og Canvas API'en. Selvom et Snake-spil måske ikke er det mest avancerede spil i dag, er principperne og teknikkerne, du har lært, fundamentale for stort set enhver browserbaseret spiludvikling. Bliv ved med at eksperimentere, bygge videre på det, du har lært, og udforsk de mange muligheder, der ligger i webbaseret spiludvikling. Lykke til med din spilskabelse!
Hvis du vil læse andre artikler, der ligner Skab dit eget klassiske Snake-spil med HTML5, kan du besøge kategorien Teknologi.
