Du verden så mye man lærer av å lage et skytespill uten rammeverk!

Attack of the Space Nerds er laget fra bunnen av i TypeScript.

Det er ingen tvil om at du kan spare masse tid ved å bruke rammeverk, i stedet for å “finne opp hjulet” på nytt. For eksempel pleier jeg selv å ty til Next.js hvis jeg skal lage en React-app eller en nettside. Og kanskje jeg i tillegg velger et CSS-rammeverk som Tailwind eller et komponentbibliotek som Chakra UI eller shadcn/ui for at det skal bli raskere å lage et lekkert brukergrensesnitt.

Men for hobbyprosjektene mine liker jeg å finne opp hjulet på nytt. Grunnen til at jeg har en masse hobbyprosjekter på gang, er jo ikke bare at jeg synes det er gøy – men at jeg vil lære noe nytt eller bli enda bedre på ett eller annet.

For å bli bedre i JavaScript, må du kode i Javascript

Den beste (eneste?) måten å bli bedre i et programmeringsspråk på, er å rett og slett bare lage noe selv. Det har jeg også innsett, etter å ha pløyet meg gjennom en masse nettbaserte kurs. Jeg spanderte for eksempel på meg Will Sentance’ sine fantastiske Frontend Masters-kurs “JavaScript: The Hard Parts” og “The New Hard parts” for å prøve å skjønne litt mer av hvordan JavaScript fungerer .

Det var etter det kurset at jeg innså at jeg rett og slett var for dårlig i “vanilla” JavaScript. De siste årene med koding hadde handlet mest om React, Next.js og Gatsby, og jeg hadde også lekt meg litt med andre rammeverk som for eksempel SvelteKit. Men jeg ville gjerne bli enda bedre i JavaScript.

Så jeg bestemte meg for at mitt neste hobbyprosjekt skulle lages fra bunnen av.

Det ble et spill!

Så hva skulle jeg lage? Inspirert av denne fine videoen fra Frank’s Laboratory og FreeCodeCamp fikk jeg lyst til å lage et 2D-skytespill i JavaScript.

Ettersom jeg benytter enhver anledning til å bli bedre i TypeScript, endte jeg riktignok til slutt opp med å lage alt sammen i TypeScript i stedet for "vanlig" JavaScript. Kan du TypeScript, kan du også JavaScript – men ikke motsatt, var min tanke. To fluer i én smekk!

Jeg har tidligere lekt meg med spillprogrammering med blant annet Unity og C# og med Lua og LÖVE-rammeverket. Men nå var altså poenget å prøve å lage noe fra bunnen av, uten ett av disse fantastiske rammeverkene som gjør alt så innmari enkelt. Mulig jeg liker å plage meg selv, men jeg synes faktisk det er mer gøy når ting er litt vanskelig. 🤯

Målet mitt var:

  • Å bli flinkere til å kode i vanilla JavaScript og TypeScript
  • Få litt mer erfaring med objektorientert programmering i JavaScript
  • Lære meg litt om HTML Canvas

Spillet jeg lagde ble temmelig annerledes enn spillet fra YouTube-videoen, siden jeg bare brukte grunnprinsippene og bygget alt sammen fra bunnen av selv. Jeg ville ha et 2D-skytespill av “den gode, gamle typen”, med parallax-scrolling.

Slik ble resultatet:

Ai-generert grafikk

Jeg kalte spillet mitt “Attack of the Space Nerds” og fikk litt AI-hjelp (Dall-E) til å lage grafikk, blant annet en “splash screen” med noen litt rare romnerder (hva nå det er for noe!). Og så lastet jeg ned litt gratis-grafikk fra den utmerkede nettsiden Kenney.

De AI-genererte “romnerdene” klippet jeg ut og fikk gjort om til noen enkle animerte sprites av ved hjelp av Figma. Disse kunne så animeres på HTML-canvaset med drawImage().

Jeg brukte Figma til å lage sprite-animasjoner med 24 frames per "fiende", og eksporterte disse som png-filer.
Jeg brukte Figma til å lage sprite-animasjoner med 24 frames per "fiende", og eksporterte disse som png-filer.

Siden det blir altfor omfattende å gå gjennom absolutt alt om hvordan spillet er laget, skal jeg bare gå kjapt igjennom hovedprinsippene for hvordan det er bygget opp. For detaljer, så ligger all kildekoden på min Github her: https://github.com/klekanger/attack-of-the-space-nerds

Jeg tar gjerne imot innspill og forbedringsforslag – og det er selvfølgelig bare å kopiere eller “forke” spillet og gjøre hva du vil med det.

Klasser og enkle byggeklosser

Selv om erfaringen min med spillprogrammering er temmelig begrenset, har jeg skjønt såpass at det er objektorientert programmering som gjelder for at dette skal bli særlig effektivt. Hvis jeg plutselig vil ha hundrevis av romskip som flyr over skjermen, er det mye lettere å få til det når jeg bare kan lage et nytt objekt basert på en klasse som definerer hvordan dette romskipet skal se ut og hvordan det skal oppføre seg.

Inspirert av videoen fra Frank’s Laboratory valgte jeg derfor å la nesten alt i spillet være klasser.

Det at jeg hadde liten erfaring med klasser og den slags i JavaScript, var bare en bonus: Da får jeg jo mulighet til å bli bedre på det!

Ved å la alt i spillet basere seg på klasser kan jeg for eksempel pushe nye “bomber” til et array for å få de fiendtlige romskipene til å slippe bomber. Eksempel: enemyShoot i kodesnutten under er metode i en Game-klasse jeg har laget (kommer inn på det etterpå):

1enemyShoot(enemy: Enemy) {
2    this.enemyWave.push(
3      new EnemyBomb(this, enemy.x, enemy.y + enemy.height / 2)
4    );
5  }

Oppbyggingen av spillet

Selv om det ble ganske mange filer etter hvert som spillet vokste, er oppbyggingen ganske enkel.

Jeg opprettet prosjektet med Vite for å få “ut-av-boksen” støtte for TypeScript, en kjapp dev-server, “hot module reloading” og alt mulig annet snacks.

Jeg strukturerte prosjektet slik at alle klasser lå i egne filer under ./src/classes, grafikk under ./src/artwork, lydeffekter og musikk under ./src/audio, typedefinisjoner i ./src/types/index.ts, og så videre.

Filen ./src/main.ts er der alt starter. Her initialiserer jeg spillet, viser startskjermbildet, setter opp lyd, og så videre. Når det er gjort, starter jeg en gameloop som typisk kjører 60 ganger i sekundet (avhengig av nettleser) ved hjelp av requestAnimationFrame().

Det er én svær hovedklasse, Game (i ./src/classes/game.ts) som holder styr på nesten alt – som spilleren, fiender, kuler og bomber, poengsum, antall liv, og så videre.

Instanser av alle andre klasser opprettes inne i Game-klassen. For eksempel oppretter vi en ny spiller, eller rettere sagt romskipet til spilleren slik i konstruktøren til Game-klassen: this.player = new Player(this). Inne i Player-klassen definerer vi alt som har med spillerens romskip å gjøre, som grafikken til romskipet, samt metoder for å oppdatere posisjonen til romskipet og for å tegne opp romskipet på skjermen.

“Alt” skjer i update() og draw()

De fleste klasser har en draw() -metode som kjører for hvert skjermbilde (frame) for å tegne grafikk til HTML-canvasen. Det er vanligvis også en update()-metode som kjører for hvert skjermbilde.

I update()legger jeg alt av “business-logikk”, som å oppdatere x- og y-posisjoner, kalle metoder for kollisjonssjekk mellom ulike objekter, og så videre.

Her er en forenklet fremstilling av hvordan gameloopen i main.ts (blå) kaller Game-klassens (gul) game.update() og game.draw() for hver frame, og så kaller vi update- og draw-metodene for Enemy, Player og alt mulig annet (grønn) fra Game-klassen igjen.

Diagram som viser hvordan spillet er bygget opp.
En forenklet fremstilling av hvordan gameloopen i main.ts (blå) kaller Game-klassens (gul) game.update() og game.draw() for hver frame, og så kaller vi update- og draw-metodene for Enemy, Player og alt mulig annet (grønn) fra Game-klassen igjen.

I update()metodene for de ulike klassene er det også logikk for å fjerne objekter vi ikke trenger lenger. For eksempel pusher vi nye fiendtlige romskip til et array som ligger i Game-klassen. Hvert romskip er basert på Enemy-klassen.

Når et romskip enten har eksplodert eller beveget seg utenfor skjermbildet kan vi flagge at romskipet skal slettes. I Game-klassens update-metode kan vi da loope over arrayet med alle romskipene og fjerne de som ikke skal være der lenger:

1// ./src/classes/game.ts
2
3export class Game implements IGame {
4.
5.
6.
7	update(delta: number) {
8	.
9	.
10	.
11		for (const enemy of this.enemyWave) {
12		// Remove enemies that have been killed
13		    this.enemyWave = this.enemyWave.filter((enemy) => !enemy.markedForDeletion);
14		}
15	}
16}

Denne update-metoden kaller vi fra main.ts inne i game-loopen slik: game.update(delta). Delta-verdien som vi sender inn er tiden som har gått siden forrige “frame”, og nødvendig for at spillet skal gå like fort uansett hvor rask eller treg PC du har.

Effekter, grafikk og lyd

Som nevnt innledningsvis brukte jeg grafikk fra diverse gratisnettsteder, i tillegg til å få Dall-E til å lage noe til meg. Musikken fant jeg på OpenGameArt.org.

Lydeffekter lagde jeg med det ZzFX (Zuper Zmall Zound Zynth). Jaja, det er vel kanskje ikke å lage det helt fra bunnen av – siden det er et bibliotek – men det får gå.

Et forholdsvis enkelt spill som dette kan fort se ganske traust og kjedelig ut, så jeg trengte noen fine eksplosjoner. I stedet for å prøve å animere eksplosjoner, eller bruke ferdige animasjoner som andre har laget, fant jeg ut at jeg heller ville bruke partikkeleffekter. Jeg hadde ingen anelse om hvordan slike partikkeleffekter fungerte, men etter litt googling fant jeg ut at det faktisk er enklere enn det ser ut til.

Jeg lagde en Particle-klasse som tar inn x- og y-posisjonen til for eksempel et fiendtlig romskip. Når romskipet blir truffet, kan jeg opprette en hel haug med Particle-objekter basert på denne klassen og la partiklene starte på den aktuelle x/y-posisjonen.

Hver partikkel er i praksis bare en fylt, halvgjennomsiktig sirkel på HTML-canvaset, i en tilfeldig størrelse og farge – og en tilfeldig retning fra 0 til 360 grader.

Prikkene som fyker utover er partikkeleffekter.
Prikkene som fyker utover er partikkeleffekter.

Alle partiklene som skal vises på canvaset lagres i array i et felt i Game-klassen. I update()-metoden i Game-klassen looper vi over alle partiklene i dette arrayet og kjører particle.update()-metoden for hver partikkel for å flytte x- og y-posisjonene til partikkelen. Og så kjører vi particle.draw() for å tegne partiklene på skjermen.

Når partiklene har forlatt skjermen, merker vi dem for sletting og fjerner dem fra partikkel-arrayet i Game-klassen neste gang game.update() kjøres.

Resultatet ble noen ganske fine eksplosjoner, om jeg må få si det selv!

Masse forbedringsmuligheter

Etter hvert som spillet vokste var planen å legge til enda flere romskip som skulle bevege seg på enda mer avanserte måter, men jeg får se hva jeg får tid til etter hvert.

For at spillet skal bli virkelig spillbart må det jobbes ganske mye med vanskelighetsgraden på hver enkel level. Nå skjer det kanskje litt lite, og spillet er altfor enkelt.

For øyeblikket fungerer spillet best på desktop, og det bør gjøres noe for å håndtere reskalering av nettleservinduet, bedre touch-kontroller så det blir mer spillbart på mobil, og så videre.

Jeg synes imidlertid at jeg har oppnådd målet mitt med å lage spillet:

  • Jeg har fått øvd meg på å jobbe med objektorientert programmering, som jeg ikke har drevet noe særlig med i JavaScript.
  • Jeg har måttet finne ut av en masse, som hvordan man kan animere ting på et HTML-canvas (aldri gjort før!) og hvordan man lager partikkeleffekter.
  • Jeg har virkelig sett betydningen av å planlegge og strukturere alt sammen skikkelig fra starten slik at det er enkelt å bygge ut spillet senere uten å miste oversikten.

Hvis du vil prøve spillet så ligger det en spillbar versjon på https://attack-of-the-space-nerds.netlify.app/ .

Kildekoden finner du på https://github.com/klekanger/attack-of-the-space-nerds

Jeg tar gjerne imot forbedringsforslag, eller innspill på ting jeg burde ha gjort annerledes. Kanskje du til og med vil forbedre og bygge ut spillet selv? Send meg gjerne en pull request!

Slik ser spillet ut visualisert med verktøyet Dependency Cruiser, som jeg har skrevet om på kode24.no.

Attack of the Space Nerds visualisert med Dependency Cruiser.
Attack of the Space Nerds visualisert med Dependency Cruiser.
Publisert: 09. juli 2023 (oppdatert: 15. juli 2023)