O2. Game

Je gaat je eigen game programmeren!

Hulpmiddelen

We gebruiken in deze opdracht de volgende tools:

  1. GitHub met Codespaces en VS Code

Opdrachtbeschrijving

De opdracht in het kort is: Maak in groepjes van twee je eigen spel in JavaScript. Je gebruikt de startcode en maakt gebruik van de p5js-library. Om jezelf hierop voor te bereiden doe je eerst deel 1 van de opdracht.

Inschrijven
PO Deel 1
Werkwijze
Beoordeling
AI gebruik
Inleveren

Startcode

Je krijgt van de docent een kopie van onderstaande startcode.

Image

Stappenplan

Werk de planning af van boven naar beneden. Vul de planning aan en stel bij terwijl je aan de opdracht werkt.

Stap 1: Git oefening
Stap 2: ontwerp jullie spel
Stap 3: bouw de basis van het spel (MVP)
Stap 4: uitbreidingen toevoegen

Code voorbeelden

De voorbeelden hieronder helpen je op weg. Ze zijn niet compleet. Je moet zelf nadenken over wat er mist of hoe je ze aanpast voor jouw spel.


1. Speler bewegen

Optie A — speler kan overal naartoe bewegen

Voeg dit toe aan beweegAlles(). De speler beweegt met pijltjestoetsen of WASD. keyIsDown kijkt of een bepaalde toets is ingeduwd. Je kan op deze site zien welke toets aan welk nummer is gekopperd.

// beweeg omhoog
if (keyIsDown(UP_ARROW) || keyIsDown(87)) { // 87 = W
  spelerY = spelerY - 5;
}
// voeg zelf de andere drie richtingen toe

In plaats van altijd 5 te gebruiken kan je hier ook een variabele van maken die je later aanpast om de snelheid aan te kunnen passen van je speler.

Zorg dat de speler niet buiten het scherm kan. Gebruik een && in de if zodat de beweging alleen plaatsvindt als de speler nog binnen de grenzen is.

if ((keyIsDown(UP_ARROW) || keyIsDown(87)) && spelerY > ???) {
  spelerY = spelerY - 5;
}

Wat moet de grenswaarde zijn? De grenzen kan je vinden in createCanvas() daar definieer je hoe groot je speelveld is. Denk ook aan de grootte van de speler en waar die start.

Optie B — speler springt en valt

Voeg bovenaan toe:

var spelerSpringt = false; // is de speler aan het springen?
var spelerDaalt = false;   // is de speler aan het dalen?

Verwerk het springen in beweegAlles():

// spring als de speler op de grond staat en op spatie drukt
if (keyIsDown(32) && !spelerSpringt && !spelerDaalt) { // 32 = SPATIE
  spelerSpringt = ???;
}

// beweeg omhoog
if (spelerSpringt) {
  spelerY = spelerY - ???;
  if (spelerY <= ???) {  // hoogste punt bereikt
    spelerSpringt = ???;
    spelerDaalt = ???;
  }
}

// beweeg omlaag
if (spelerDaalt) {
  spelerY = spelerY + ???;
  if (spelerY >= ???) {  // grond bereikt
    spelerY = ???;
    spelerDaalt = ???;
  }
}

Vergeet spelerSpringt en spelerDaalt te resetten naar false bij het opnieuw starten van het spel (zie stap 4, game-over scherm).

Wil je een sprong die er natuurlijker uitziet? Gebruik dan een extra variabele snelheidY en zwaartekracht. De speler versnelt dan naar beneden in plaats van in een constant tempo te dalen.


2. Afbeelding gebruiken

Maak een map afbeeldingen/ in je repository en zet daar je plaatje in.

Voeg bovenaan sketch.js een variabele toe:

var spelerAfbeelding;

Laad de afbeelding in preload() (voeg deze functie toe boven setup()). Als je meerdere plaatjes wilt voeg je die allemaal toe in dezelfde preload() functie. Elk plaatje heeft wel zijn eigen variabele nodig.

function preload() {
  spelerAfbeelding = loadImage('afbeeldingen/speler.png');
}

Gebruik dan in tekenAlles(). Vervang de rect() van de speler door:

image(spelerAfbeelding, spelerX, spelerY, breedte, hoogte);

3. Score en health weergeven

Voeg bovenaan toe:

var punten = 0;

Teken onderaan tekenAlles(), na alles wat je al tekent:

fill('white');
noStroke();
textSize(20);
textAlign(LEFT, TOP);
text('Punten: ' + punten, 10, 10);
text('Health: ' + ???, ???, ???);

4. Startscherm en game-over scherm

Het template heeft al SPELEN, GAMEOVER en spelStatus. Voeg een derde toestand toe bovenaan:

const INTRO = 0;        // nieuw
const SPELEN = 1;
const GAMEOVER = 2;
var spelStatus = INTRO; // begin op het startscherm

Vul in draw() de lege blokken in:

if (spelStatus === INTRO) {
  // teken een startscherm
  ???

  // Als je in de INTRO fase bent en dan op spatie duwt ga je naar de SPELEN fase
  if (keyIsDown(32)) { // 32 = SPATIE
    spelStatus = SPELEN;
  }
}
if (spelStatus === GAMEOVER) {
  // teken een gameover scherm

  if (keyIsDown(ENTER)) { 
    // reset alle variabelen naar beginwaarden
    punten = ???;
    health = ???;
    spelerX = ???;
    spelerY = ???;
    spelStatus = ???;
  }
}

Vergeet niet ook de vijanden en kogel te resetten.

5. Vijanden

Gebruik arrays voor de x- en y-posities van alle vijanden. Voeg bovenaan toe:

var vijandenX = [200, 500, 900];
var vijandenY = [100, 300, 200];
var vijandenSnelheid = 2;

Beweeg de vijanden in beweegAlles() van de rechterkant van het scherm richting de linkerkant van het scherm. Dit doe je met een loop over alle elementen in de array:

for (var i = 0; i < vijandenX.length; i++) {
  vijandenX[i] = vijandenX[i] - vijandenSnelheid;

  // Als de vijand aan de rand van het scherm is gekomen moet hij terug naar de andere kant
  if (vijandenX[i] < ???) {
    vijandenX[i] = ???;
    vijandenY[i] = ???;
  }
}

Als je vijanden naar je speler toe wilt bewegen kijk dan naar code snippet 8 en pas bovenstaande code daarmee aan.

Teken ze in tekenAlles():

for (var i = 0; i < vijandenX.length; i++) {
  ???
}

6. Botsing (rechthoek)

In p5.js teken je met rect(x, y, breedte, hoogte) vanuit de linkerbovenhoek. Dus spelerX is de linkerrand, en spelerX + spelerBreedte is de rechterrand. spelerY is de bovenrand, en spelerY + spelerHoogte de onderrand. Datzelfde geldt voor je vijand. Twee rechthoeken botsen alleen als ze tegelijk horizontaal én verticaal overlappen. Het is dus eigenlijk twee losse checks: overlappen ze op de x-as, en overlappen ze op de y-as?

Zet bovenaan de maten van je speler en vijand klaar (pas de getallen aan naar jouw spel):

var spelerBreedte = ???;
var spelerHoogte = ???;
var vijandBreedte = ???;
var vijandHoogte = ???;

Je kan deze variabelen nu ook gebruiken op de plek waar je je speler en vijanden tekent. Zo wordt alles aangepast als je deze breedte en hoogte aanpast.

Voeg dit toe aan verwerkBotsing(). De horizontale check zijn de eerste twee regels. De verticale check zijn de laatste twee regels:

for (var i = 0; i < vijandenX.length; i++) {
  if (spelerX < vijandenX[i] + vijandBreedte &&   // speler-linkerrand vóór vijand-rechterrand
      spelerX + spelerBreedte > vijandenX[i] &&    // speler-rechterrand voorbij vijand-linkerrand
      spelerY < vijandenY[i] + ??? &&              // speler-bovenrand boven vijand-onderrand
      spelerY + ??? > vijandenY[i]) {              // speler-onderrand onder vijand-bovenrand
    health = health - 1;
  }
}

Werk je met een cirkelvormige speler of vijand? Dan klopt deze rechthoek-botsing niet helemaal. Voor cirkels meet je de afstand tussen de middelpunten en kijk je of die kleiner is dan de twee stralen samen.


7. Kogel afvuren

Optie A — meerdere kogels tegelijk

Gebruik lege arrays en voeg elke nieuwe kogel toe met push()

var kogelsX = [];
var kogelsY = [];

Voeg een nieuwe kogel toe bij het afvuren in tekenAlles()

if (keyIsDown(32) && spelStatus === SPELEN) { // 32 = SPATIE
  kogelsX.push(???);
  kogelsY.push(???);
}

Beweeg alle kogels in beweegAlles().

for (var i = kogelsX.length - 1; i >= 0; i--) {
  ???

  // verwijder de kogel als hij buiten het scherm is
  if (kogelsX[i] < ??? || kogelsX[i] > ??? ||
      kogelsY[i] < ??? || kogelsY[i] > ???) {
    kogelsX.splice(i, 1); // verwijder 1 element op positie i uit de x-array
    kogelsY.splice(i, 1); // verwijder 1 element op positie i uit de y-array
  }
}

De array groeit elke keer als je schiet. Met splice() haal je kogels buiten beeld uit de array. Als je dat niet doet zal je game uiteindelijk crashen omdat er teveel kogels zijn voor het computer geheugen.

Optie B — maximaal één kogel tegelijk

Gebruik twee variabelen voor de X en Y positie van de kogel en verder een boolean om te kijken of de kogel actief is

var kogelX = -100;
var kogelY = -100;
var kogelActief = false;

Voeg een nieuwe kogel toe als hij nog niet actief was in tekenAlles()

if (keyIsDown(32) && spelStatus === SPELEN && ???) { // 32 = SPATIE
  kogelX = ???;
  kogelY = ???;
  kogelActief = ???;
}

Beweeg de kogel in beweegAlles().

if (kogelActief) {
  kogelX = kogelX + 10;
  if (kogelX > width) {
    kogelActief = ???;
  }
}

In welke richting schiet jouw speler? Pas de beweging aan. Vergeet de kogels ook te tekenen in tekenAlles() en botsingen te verwerken in verwerkBotsing().

Optie C — riching van de speler meegeven aan je kogel

Gebruik dit als je kogels in verschillende richtingen wilt afvuren, bijvoorbeeld afhankelijk van welke kant de speler op beweegt. Sla de laatste bewegingsrichting op in twee variabelen:

var laatsteRichtingX = 1; // beginwaarde: schiet naar rechts
var laatsteRichtingY = 0;

Werk deze variabelen bij elke keer dat de speler beweegt in beweegAlles(). Gebruik losse if-blokken (geen else if) zodat je ook schuin kunt bewegen:

// reset richting elke keer voor je checkt welke toetsen ingedrukt zijn
var nieuweRichtingX = 0;
var nieuweRichtingY = 0;

if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) { // 68 = D
  spelerX = spelerX + 5;
  nieuweRichtingX = 1;
}
// hetzelfde voor alle andere richtingen

// sla de richting alleen op als de speler beweegt
if (nieuweRichtingX !== 0 || nieuweRichtingY !== 0) {
  laatsteRichtingX = nieuweRichtingX;
  laatsteRichtingY = nieuweRichtingY;
}

Voeg naast de positie ook de richting per kogel toe als aparte arrays (voor meerdere kogels) of losse waarde (voor één kogel):

// Optie A: meerdere kogels
var kogelsRichtingX = [];
var kogelsRichtingY = [];

// Optie B: één kogel
var kogelRichtingX = 1;
var kogelRichtingY = 0;

Sla bij het afvuren de richting op dat moment op, en gebruik die om de kogel te bewegen in beweegAlles():

// Optie A: meerdere kogels
kogelsRichtingX.push(laatsteRichtingX);
kogelsRichtingY.push(laatsteRichtingY);
// ...
kogelsX[i] = kogelsX[i] + kogelsRichtingX[i] * 10;
kogelsY[i] = kogelsY[i] + kogelsRichtingY[i] * 10;

// Optie B: één kogel
kogelRichtingX = laatsteRichtingX;
kogelRichtingY = laatsteRichtingY;
// ...
kogelX = kogelX + kogelRichtingX * 10;
kogelY = kogelY + kogelRichtingY * 10;

Let op: bij een schuine richting beweegt de kogel iets sneller dan puur horizontaal of verticaal. Wil je dat alle kogels exact even snel gaan, kijk dan naar snippet 8 over het normaliseren van een richting.


8. Richting bepalen

Soms wil je iets laten bewegen richting een ander punt. Bijvoorbeeld: een kogel richting de muis, of een vijand richting de speler. Je berekent het verschil in x en y, en schaalt dat bij tot de gewenste snelheid met behulp van de stelling van Pythagoras.

var dx = doelX - beginX;           // verschil in x
var dy = doelY - beginY;           // verschil in y
var afstand = sqrt(dx*dx + dy*dy); // afstand tussen de twee punten
var sX = dx / afstand * snelheid;  // stap in x-richting
var sY = dy / afstand * snelheid;  // stap in y-richting

Voor een kogel richting de muis sla je sX en sY op bij het afvuren. Voor vijanden die richting de speler bewegen gebruik je hetzelfde idee in de bestaande loop.

Wat gebeurt er als afstand gelijk is aan 0? Wanneer kan dat voorkomen en hoe voorkom je een fout?

9. Timer

Een timer gebruik je bijvoorbeeld om af te tellen (“je hebt 30 seconden”) of om elke paar seconden iets te laten gebeuren (een nieuwe vijand). p5.js heeft hiervoor millis(): het aantal milliseconden (1000 ms = 1 seconde) sinds je spel gestart is. We gebruiken millis() en niet het tellen van frames, omdat millis() ook klopt als je spel een keer hapert.

Maak een nieuwe functie aan die je aanroept in je draw() function voor de timer.

Optie A — aftellende klok

Onthoud wanneer het spel begon. Voeg bovenaan toe:

var startTijd;
var speelDuur = 30; // aantal seconden dat het spel duurt

Zet de starttijd in setup():

var startTijd = millis();

Maak een eigen functie voor de timer:

function timer() {
  var verstreken = (millis() - startTijd) / 1000; // seconden sinds de start
  var resterend = ???;          // tijd die nog over is

  fill('white');
  noStroke();
  textSize(20);
  text('Tijd: ' + floor(resterend), 10, 40); // floor rond het getal af zodat je geen komma getallen ziet

  if (resterend <= ???) {
    spelStatus = GAMEOVER;
  }
}

Vergeet niet de timer te resetten bij het opnieuw spelen anders staat de klok meteen weer op 0.

Optie B — elke x seconden iets laten gebeuren

Bijvoorbeeld elke 2 seconden een nieuwe vijand. Onthoud wanneer de laatste keer was:

var laatsteSpawn = 0;
var spawnInterval = 2000; // elke 2000 ms = 2 seconden

Maak hiervoor een eigen functie:

function spawnVijanden() {
  if (millis() - laatsteSpawn > spawnInterval) {
    // voeg een nieuwe vijand toe
    ???
    laatsteSpawn = millis(); // onthoud wanneer deze verscheen
  }
}

Maak spawnInterval kleiner naarmate het spel vordert. Dan komen er steeds sneller vijanden bij en wordt het spel vanzelf moeilijker.

10. Functies gebruiken

Stel: je vijand bestaat uit een lichaam, een oog en een mond. Je tekent hem op drie plekken in je code. Dan schrijf je dit drie keer de volgende code:

// lichaam
fill('red');
rect(x, y, 30, 30);
// oog
fill('white');
circle(x + 10, y + 8, 8);
fill('black');
circle(x + 12, y + 8, 4);
// mond
fill('black');
rect(x + 6, y + 18, 12, 4);

Als je later iets wilt veranderen, moet je dat op drie plekken aanpassen. Dat is foutgevoelig. De oplossing: stop het in een functie.

function tekenVijand(x, y) {
  // lichaam
  fill('red');
  rect(x, y, 30, 30);
  // oog
  fill('white');
  circle(x + 10, y + 8, 8);
  fill('black');
  circle(x + 12, y + 8, 4);
  // mond
  fill('black');
  rect(x + 6, y + 18, 12, 4);
}

Wil je de mond aanpassen? Dat doe je nu op één plek, in de functie. Je roept tekenVijand() aan vanuit tekenAlles(), die zelf weer aangeroepen wordt vanuit draw(). Zo ontstaat een ketting van functies die weer andere functies aanroepen. Elke keer dat p5.js draw() aanroept, wordt automatisch de hele ketting uitgevoerd. Je hoeft zelf alleen maar tekenVijand() aan te roepen op de juiste plek:

function tekenAlles() {
  // ... andere dingen tekenen

  for (var i = 0; i < vijandenX.length; i++) {
    tekenVijand(vijandenX[i], vijandenY[i]);
  }
}

Dit geldt ook voor beweegAlles() en verwerkBotsing(). Hoe meer je je spel uitbreidt, hoe meer functies je toevoegt aan de ketting. draw() blijft altijd het startpunt.

Herken je stukken code die je meerdere keren schrijft? Dat is een signaal dat je een functie kunt maken.

Uitlegvideo’s