| name | bygg-tredee-spill |
| description | Bygg et avansert, polert 3D-arkadespill i én enkelt selvstendig HTML-fil med Three.js. Bruk denne når noen vil lage et 3D-spill / nettleserspill i én fil, f.eks. "Eikesuset" – det flygende ekornet som samler eikenøtter. Dekker prosjektoppsett, robust game-loop, highscore, skjermer, ytelse og en obligatorisk automatiserings-kontrakt som gjør spillet testbart. |
| metadata | {"tags":"3d, threejs, spill, game, webgl, single-file, arkade, highscore"} |
Bygg et avansert 3D-spill i én HTML-fil
Denne skillen gir deg oppskriften på et polert, spillbart 3D-arkadespill som ligger i
én eneste .html-fil og kjører rett i nettleseren – ingen byggesteg, ingen npm install,
ingen ressursfiler ved siden av. Du bygger med Three.js lastet fra CDN.
Standard filnavn er spill.html i rotmappa. Verifiseringen (npm run verifiser) og
demo-manuset forventer akkurat det navnet. Bruk det med mindre noe annet er sagt.
Når du skal bruke den
Når noen ber om et 3D-spill, nettleserspill eller "et spill i én HTML-fil". Følg da kravene
under slavisk – de er det som skiller et leketøy fra noe som faktisk er gøy å spille og
som består verifiseringen.
Gyldne regler (ikke-forhandlingsbare)
- Alt i én fil. All HTML, CSS og JavaScript ligger i
spill.html. Ingen eksterne filer
utenom Three.js fra CDN. Fila skal kunne dobbeltklikkes og spilles.
- Det skal faktisk være gøy og se bra ut. Ekte 3D, ikke en flat boks. Lys, tåke, dybde,
bevegelse, fart som øker, fin følelse i styringen. Sikt mot "wow" på 10 sekunder.
- Highscore som overlever omstart. Lagre beste poengsum i
localStorage og vis den på
start- og slutt-skjermen. Feir ny rekord tydelig.
- Tre tilstander:
menu → playing → gameover, med en skjerm for hver. Man skal alltid
kunne starte på nytt uten å laste siden på nytt.
- Den OBLIGATORISKE automatiserings-kontrakten under MÅ være på plass (se eget avsnitt).
Uten den feiler verifiseringen, og demoen faller fra hverandre.
- Robust. Den skal ikke kaste feil i konsollen, den skal håndtere vindusendring, pause når
fanen er skjult, og vise en vennlig melding hvis WebGL mangler.
Three.js fra CDN – bruk import map (pinnet versjon)
Pinn versjonen. Ikke bruk @latest – en live-demo skal ikke avhenge av at en ny versjon ikke
har brukket noe.
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
</script>
Anbefalt struktur på spill.html
<!doctype html>
<html lang="nb">
<head> meta, <title>, <style> (fullskjerm canvas, HUD-overlay, skjermer) </style> </head>
<body>
<div id="hud"> poeng, highscore, liv </div>
<div id="start-skjerm"> tittel, hvordan spille, highscore, "Trykk for å starte" </div>
<div id="slutt-skjerm"> poeng, highscore, evt. "Ny rekord!", "Spill igjen" </div>
<div id="webgl-feil" hidden> vennlig melding hvis WebGL ikke støttes </div>
<script type="importmap"> ... </script>
<script type="module">
// 1) WebGL-sjekk i try/catch -> vis #webgl-feil og stopp hvis det feiler
// 2) Scene, kamera, renderer, lys, tåke, bakgrunn (gradient)
// 3) Spiller (bygd av primitiver), verden (bakke + kulisser), objekt-pools
// 4) Input: tastatur (piltaster/WASD) + peker/touch
// 5) Game-loop med requestAnimationFrame og klemt delta-tid
// 6) Tilstandsmaskin: menu / playing / gameover
// 7) Highscore via localStorage
// 8) window.GAME-kontrakten (se under) <-- IKKE hopp over
// 9) Resize + visibilitychange (pause)
</script>
</body>
</html>
Bygg figurer og verden fra primitiver (BoxGeometry, SphereGeometry, CapsuleGeometry,
ConeGeometry, CylinderGeometry, IcosahedronGeometry). Ikke last eksterne 3D-modeller –
det bryter "én fil" og er en feilkilde i en live-demo.
Bevegelse og kollisjon (mønster som er robust)
- La verden bevege seg mot spilleren: spilleren holder seg rundt
z ≈ 0, objekter spawner
langt foran (-z) og flyttes mot +z hver frame, og resirkuleres når de er bak kameraet.
- Styring flytter spilleren i
x (og gjerne litt i y), klemt innenfor banen. Bank/tilt
figuren etter sidefart – det gir liv.
- Kameraet MÅ følge spilleren mykt (tredjepersons jaktkamera) – både til siden og i høyden.
Et statisk kamera er en klassisk felle: når spilleren svinger, glir figuren ut i hjørnet
mens hindrene spawner midt på skjermen, og spillet blir nesten umulig å sikte med. Lerp både
kameraets posisjon og blikkpunktet (
lookAt) mot spilleren hver frame, og hold kameraet litt
bak og over. Følg gjerne bare ~60 % av sidebevegelsen, så man fortsatt fornemmer svingen. Bruk
en moderat synsvinkel (FOV ~60), ikke vid – det gjør det lettere å bedømme avstand.
- Kollisjon trenger ikke fysikkmotor: når et objekt passerer nær
z ≈ 0, sjekk avstand i
x/y mot spilleren. Innenfor radius = treff (hinder) eller plukk (samleobjekt).
- Poeng øker mens man spiller (distanse), pluss bonus for samleobjekter. At poeng vokser av
seg selv mens
state() === 'playing' er et krav – verifiseringen sjekker nettopp det.
- Fart og spawn-rate øker gradvis med distanse. Klem til et tak så det forblir spillbart.
- Bruk delta-tid (klemt, f.eks.
Math.min(dt, 0.05)) så fysikken ikke hopper etter en pause.
Ytelse og robusthet
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)).
- Gjenbruk geometri/materialer; bruk object pools for hinder og samleobjekter i stedet for å
opprette/slette mesh hele tiden.
- Håndter
resize (oppdater kamera-aspect, updateProjectionMatrix(), renderer.setSize).
- Pause loopen på
visibilitychange/blur og nullstill delta ved retur, så man ikke "dør i
pausen".
- Lyd (valgfritt, men koselig): WebAudio-pip, opprettet på første brukerklikk (start), pakket i
try/catch slik at blokkert autoplay aldri stopper spillet.
🤝 OBLIGATORISK: automatiserings-kontrakten (window.GAME)
Spillet MÅ legge dette objektet på window så snart det er klart. Det er det som lar
verifiseringen (Playwright) drive spillet uten en mus, og det er kjernen i hele demoen
("se – spillet sjekker seg selv"). Bygg det inn fra start, ikke som et etterpåklokt tillegg.
window.GAME = {
ready: true,
state: () => state,
start: () => startGame(),
reset: () => goToMenu(),
score: () => Math.floor(score),
highscore: () => highscore,
lives: () => lives,
playerX: () => player.position.x,
forceGameOver: () => endGame(),
};
Krav til kontrakten:
- Tilstandsstrengene er nøyaktig
'menu', 'playing', 'gameover'.
- Styring skjer via ekte tastatur-events (
keydown/keyup på window), slik at en test
kan trykke ArrowLeft/ArrowRight og se playerX() endre seg. Venstre skal gi lavere
playerX(), høyre høyere.
score() returnerer et heltall og vokser mens man spiller.
- Etter
forceGameOver() er state() === 'gameover', og highscore() er minst like høy som
poengsummen i runden. Highscore lagres i localStorage slik at den overlever en
side-omlasting.
Hold kontrakten tynn og ærlig: funksjonene skal speile spillets faktiske tilstand, ikke late
som. Dette er testbarhet bygget inn i produktet – samme tankegang som resten av fagdagen.
Når du er ferdig
Si i klartekst hva du lagde, og kjør så verifiseringen (se skillen verifiser-spill eller
npm run verifiser). Fiks det som er rødt og kjør på nytt til alt er grønt. Ikke meld
"ferdig" før verifiseringen er grønn.