Saturday, April 16, 2016

Een race spel maken in Scratch

In deze post ga ik uitleggen hoe je een race spel met Scratch kunt maken.Scratch is een grafische programmeertaal waar men verhalen, spellen en animaties kan maken. Het is bedoeld voor de leeftijden 8 tot 16, maar kan voor iedereen een aardige introductie zijn in het maken van programma's. Toen ik van Scratch hoorde werd ik benieuwd. Een race spel maken in Scratch leek me een leuke manier om het te leren. 

Ik had vooraf de volgende wensen voor mijn race spel:
- je moest kunnen racen tegen een ander mens
- je moest kunnen racen tegen computergestuurde tegenstanders
- je ziet alles van bovenaf

Nu ik daar een tijdje mee aan de slag ben geweest, wilde ik delen hoe ik bepaalde dingen had gedaan. Het is een best lange post geworden, en om het niet nog langer te maken ga ik er van uit dat je al wat ervaring met de Scratch basisbegrippen hebt.

Het programma in volgevlucht

Speelveld 

Het speelveld van Scratch is als het ware een achtergrond. Je kunt het speelveld meerdere uiterlijken geven. In dit geval is er maar een uiterlijk: de racebaan inclusief startfinish lijn bevat. De rand van de racebaan mag niet gepasseerd worden, als je die raakt wordt je snelheid op nul gezet.




Auto sprite 

Binnen Scratch is een sprite als een poppetje die je kunt laten bewegen maar ook kunt programmeren om bepaald gedrag te vertonen als er dingen gebeuren. Iedere sprite heeft een of meerdere uiterlijken, dat bepaalt hoe hij er uit komt te zien in het spel. Voor de auto sprite kun je een plaatje van een bovenaanzicht van een auto op internet opzoeken en inladen.

De auto sprite is voor zowel menselijke als computer gestuurde auto's. De auto sprite bevat niet alleen een afbeelding van een auto, maar ook gekleurde vlakken voor botsing detectie. De auto sprite heeft 3 uiterlijken:


computer gestuurde auto
menselijk gestuurde auto
garage

Dit sprites zijn erg simpel, maar je kunt het zo mooi maken als je wilt. De 'garage' sprite heb ik gemaakt als zijnde de plek waar auto's vandaan komen en voorlopig maar als een paars vlak getekend.

Computer navigatie zones 

Dit zijn sprites die de computergestuurde tegenstanders helpen hun weg te vinden op de racebaan. De racebaan tekening alleen volstaat niet: de computer tegenstanders zouden dan aan de hand van botsingen met de rand sturen hetgeen ze zou vertragen. Bovendien ziet het er niet erg realistisch uit: menselijke tegenstanders volgen niet zozeer de racebaan rand maar de zogenaamde 'ideale lijn'.

Door de auto op deze lijn te houden bereiken echte coureurs de optimale snelheid en de snelste rondetijden. Menselijke coureurs vinden deze lijn door ervaring van anderen en zelf te experimenteren. De gevonden lijn onthouden ze voor tijdens de race en passen die zonodig ter plekke aan al naar gelang de omstandigheden, bijvoorbeeld regen.

Om dat gedrag deels na te bootsen geven we de computer tegenstanders kennis van de ideale lijn. Dat doen we door twee sprites te introduceren, die zo groot zijn als het speelveld zelf:


  • zone_links_van_ideale_lijn: hiermee kunnen computer tegenstanders weten wanneer ze zich links van de ideal lijn bevinden (en dus naar rechts moeten):





  • zone_rechts_van_ideale_lijn: hiermee kunnen computer tegenstanders weten wanneer ze zich rechts van de ideal lijn bevinden (en dus naar links moeten)



Daarnaast rijden coureurs scherpe bochten met een lagere snelheid dan de rechte stukken of flauwe bochten. Om de computer tegenstanders dit ook enigszins te laten doen, moeten ze op de een of andere manier weten dat ze zich in een bocht bevinden. Een simpele manier om dat te bereiken is door een derde navigatie zone sprite te maken:



Als je de racebaan en navigatie sprites combineert krijg je dit plaatje:


De bedoeling si dat de computer auto's langs een nauwe corridor rijden die de ideale lijn voorstelt, en op de roze stukken vaart zullen minderen.

De navigatie sprites zullen overigens onzichtbaar worden gemaakt door middels van het 'geest effect'. Hiermee kun je een sprite geheel of gedeeltelijk doorzichtig maken. We zullen de waarde op 100 zetten, hetgeen de navigatie zones geheel doorzichtig zal maken. Ze kunnen echter wel degelijk worden 'gezien' door Scratch, ze zijn enkel onzichtbaar voor het menselijk oog.

Autos programmeren

Om autos te programmeren zullen we ze scripts toekennen. De scripts beschrijven het gedrag van de autos en worden door Scratch ingelezen om ze de instructies te laten uitvoeren. Om de scripts goed te doorgronden is het handig om eerst de structuur van de scripts van de Auto sprite op hoofdlijnen te begrijpen. 

Autos klonen

Voor het maken van iedere auto, ongeacht of het door mens of computer wordt bestuurd, maken we gebruik van sprite klonen. De 'Auto' sprite zelf wordt dan eigenlijk een soort van sjabloon waarmee we andere sprites zoals hem kunnen maken.

We hadden ook de Auto sprite kunnen kopieren vooraf met de functie 'kopie maken'. We zouden dan een kopie krijgen van de Auto sprite op dat moment, die we bijvoorbeeld 'Tegenstander' zouden kunnen noemen. Dit kent wel wat nadelen: ten eerste moet je voor iedere extra tegenstander deze handeling opnieuw doen. Maar er is nog een probleem: stel dat je een verandering aanbrengt in 'Auto' die ook in 'Tegenstander' moet komen? Dan moet je opnieuw een kopie maken. Maar als je 'Tegenstander' al aangepast had om hem een specifiek gedrag te geven, dan zul je die aanpassingen opnieuw moeten doorvoeren.

Daarom werken we met enkel de 'Auto' sprite. Het zijn allemaal auto's per slot van rekening. We willen ook de spelers visueel van elkaar kunnen onderscheiden. Scratch geeft de mogelijkheid iedere Sprite een aantal uiterlijken toe te kennen. We geven zo de auto's verschillende kleuren. De sjabloon wordt zo met andere parameters toegepast, waardoor we controle hebben over het gemeenschappelijke en afwijkende deel van hun gedrag.

Als het programma wordt gestart willen we de klonen aanmaken. Dat doen we door te reageren op het klikken van de groene vlag (start van het programma):


Hier worden een aantal variabelen van begin waardes voorzien. Ik zal een aantal nu uitleggen, de rest komt later aan de orde:

  • speler_auto: als 0, dan is de auto computer gestuurd, anders door een speler. Momenteel ondersteunt het programma maar 1 speler
  • draai_snelheid: de snelheid waarmee auto's van richting veranderen.
  • aantal_ronden: aantal ronden die de race telt
  • clone_id: unieke identificatie getal van een auto kloon. Om die reden wordt clone_id met 1 opgehoogd binnen de herhaal blok, anders krijgt iedere kloon dezelfde identificatie en is die niet uniek
In het herhaal blok daarna worden de klonen aangemaakt. Iedere keer dat de 'maak kloon van' blok wordt aangeroepen, wordt een kloon gemaakt.

Hoe de auto op hoofdlijnen werkt

Om de kloon te gaan programmeren en niet de Sprite zelf, moeten we reageren op de gebeurtenis 'wanneer ik als kloon start':




Wederom is er een begin stuk en een herhaal blok. Ik leg eerst kort uit een aantal dingen die in het stuk voor het herhaal blok gebeuren:

  • my_id: de kloon kopieert de waarde van clone_id op dat moment, en houdt hiermee zijn eigen id bij
  • nieuwe_richting: met deze variabele bepalen we welke richting de kloon moet gaan rijden. In het begin krijgt hij dezelfde richting als de Sprite (het sjabloon dus!). We kunnen de beginrichting bepalen door de Sprite zelf te slepen en te draaien en dat is wel zo makkelijk
  • topsnelheid: klonen met een hogere id hebben een hogere topsnelheid. Dit is puur gedaan om het spel interessanter te maken

Het belangrijkste is nu het 'herhaal' blok. Alles wat binnen deze blok gebeurt wordt continu herhaald, dus gebeurt niet alleen op het moment van kloon start maar zolang de kloon in het spel is.

Het 'herhaal' blok heeft een hoofdstructuur, waarbinnen andere blokken worden aangeroepen:
  • in het begin verplaatst de auto zich naar de nieuwe richting met de nieuwe snelheid
  • in het eerste 'als' blok worden deze eigenschappen bepaald voor de computer auto's door het aanroepen van de paarse blokken
  • in het tweede 'als' blok gebeurt dat voor de mens gestuurde auto's
  • de paarse blokken die daarvoor of daarna zitten, worden in beide gevallen aangeroepen
De paarse blokken zijn eigen gemaakte script blokken die bij elkaar worden gevoegd onder een specifieke naam. Hiermee zijn ze makkelijk herkenbaar en herbruikbaar. Zo zie je dat sommige paarse blokken voorkomen bij beide soorten auto's, andere weer niet.  En als je kijkt naar hoe die paarse blokken genoemd zijn, begrijp je dat hier het echte werk gebeurt: botsen met muren, snelheid aanpassen, rondes registreren. Met andere woorden, alle beslissingen die nodig zijn om een race te kunnen rijden. Een blik op deze blok is zo voldoende om te zien wat een auto kan. Laten we de blok daarom het 'gedrag blok' noemen.

Een mens gestuurde auto

We willen de auto zo maken dat hij reageert op onze invoer. Dat bepalen we in de 'stuur_mens' blok. We gebruiken het toetsenbord. Scratch heeft handige scripts die je daarvoor kunt gebruiken: 




De auto moet nog verkleind worden. Racebanen zijn meestal een aantal autobreedtes breed, tenzij het straatcircuits zijn. Laten we de auto tot ongeveer een vijfde van de breedte van de weg verkleinen.  Gebruik daarvoor de 'kleiner maken' knop in de grijze bovenbalk van Scratch (dit is een screenshot van de offline editor):



We kunnen nu de auto sturen, sneller laten gaan, stoppen en achteruit laten gaan. Het probleem nu nog is dat we door de muren van het circuit kunnen rijden. Dat zullen we bijstellen met het 'detecteer_muur' scriptblok:




Dit zorgt ervoor dat je snelheid nul is als je met de muur in aanraking komt. De auto weet dat dit zo is omdat een signaal stuur naar de auto sprite wordt gestuurd als hij een bepaalde kleur raakt. Daar zorgt Scratch voor. Je kunt het dus zien als een soort omroepmechanisme op kleur gebaseerd. Wij maken die kleur gelijk aan de kleur van de muur.

Dit blok wordt bewust aan het eind van het gedrag blok aangeroepen, zodat ongeacht wat de andere blokken hebben besloten, we nooit door muren heen kunnen rijden. Andere blokken kunnen zich dan concentreren op een ding en blijven op deze manier zo simpel mogelijk. Het maakt het ook makkelijker om ze te programmeren als je je niet hoeft te bekommeren met muur detectie in andere blokken.

Dit principe wordt consisten toegepast: ieder blok heeft zijn eigen taak, onafhankelijk van de andere. Door ze te combineren krijgt de auto zijn gedrag. Zo heeft het programma als geheel een duidelijke opbouw en is het tevens makkelijk om nieuw gedrag toe te voegen of specifieke delen aan te passen.


Rondjes tellen

Als we de rondjes niet kunnen tellen, loopt de race nooit af. We hebben daarvoor een groene lijn getekend, die als start-finish zal dienen. Iedere keer dat een auto start-finish passeert, wordt een ronde genoteerd voor die auto. Dat wordt bereikt met het volgende script blok:



Wederom maken we gebruik van kleuren om te weten wat er gebeurt: als de kleur van start-finish (groen) door de auto sprite wordt geraakt, wordt het signaal doorgegeven aan dit script blok en die registreert de ronde.

We moeten hier wel voorzichtig zijn: omdat de lijn dikte heeft, wordt dit signaal afgevuurd niet alleen als de auto voor het eerst te lijn raakt, maar ook tijdens het passeren van de lijn. Dat zou voor een heleboel overbodige ronde registraties zorgen.

Wij ondervangen dat door bij te houden of we al eerder start-finish gezien hadden met de variabele finish_lijn_gepaseerd. De variabele start met de waarde '0', hetgeen betekent 'niet gepasseerd'. Bij iedere aanraking met groen wordt  geverifieerd dat de waarde nog steeds '0' is en zo ja dan:
- wordt de variabele op '1' gezet
- de ronde wordt geregistreerd.
- als het maximum aantal rondes is gepaseerd, eindigt het spel (stop alle)

Bij de volgende aanraking met groen, dat tijdens het passeren van de start-finish lijn gebeurt, zullen we dit blok niet ingaan. Als we groen niet aanraken, wordt 'finish_lijn_gepaseerd' weer op '0' gezet voor de volgende ronde.

Computer gestuurde tegenstanders

Nu de computer tegenstanders. Murendetectie en ronde registratie is hetzelfde als bij de menselijk bestuurde auto's. Wat nu dus rest is logica voor het volgen van de ideale lijn, het afremmen in de bocht, en het vermijden van botsingen met andere auto's.

De ideale lijn volgen bereiken we met het volgende code blok. Doordat we de zones hebben gemaakt, is het een hele eenvoudige blok omdat Scratch ook voor Sprite detectie handige scripts aanlevert:




Afremmen in de bocht volgt hetzelfde principe van sprite detectie. We moeten nu er wel op letten dat auto's na de bocht weer gas geven, anders blijven ze langzaam rijden. De auto's hebben een topsnelheid variabele dat aangeeft hoe snel ze kunnen rijden. De topsnelheid is buiten de bocht afhankelijk van de auto_id, zodat er auto's van verschillende snelheden zijn. In de bocht mag die topsnelheid niet groter dan 2 zijn, dus als dat zo is dan wordt de topsnelheid bijgesteld.

De laatste twee 'als' blokken zorgen ervoor dat de snelheid stapsgewijs aan de topsnelheid wordt aangepast: als de huidige snelheid te hoog is wordt er afgeremd, als te laag wordt er gas gegeven.



We hadden ook de snelheid gelijk kunnen zetten op 2, en daarna op 'topsnelheid'. Dat is echter wel erg onnatuurlijk en lijkt niet op hoe echte coureurs het doen.

Computergestuurde auto's moeten ook elkaar kunnen ontwijken. Om dat te kunnen programmeren moeten we de het proces van botsen wat beter bekijken. Bij botsingen zijn er twee klonen van dezelfde Sprite er mee gemoeid, allebei auto's. Alle auto's werken volgens dezelfde basisprincipes, maar hun gedrag kan verschillen afhankelijk van de situatie. De truc is om de gebeurtenis te bekijken van uit de auto zelf, alsof je de bestuurder bent van die auto.

Om een auto te laten weten dat er een botsing dreigt maken we weer gebruik van kleurgebaseerde detectie.De botsing detectie vlakken zijn onderverdeeld in links (paars), rechts (geel) en achter (blauw). Voor het ontwijken van een botsing kijken we allen naar botsingen op de voorkant, dus als paars of geel wordt geraakt. De regels die de auto's moeten volgen zijn dan:
  • als paars wordt geraakt door een andere auto, stuurt de bestuurder de auto naar rechts
  • als geel wordt geraakt door een andere auto, stuurt de bestuurder de auto naar links
De bestuurder reageert dus niet als blauw wordt geraakt, omdat het niet van voren is (vanaf de bestuurder gezien). De blauwe kleur is bedoeld om te weten of we een andere auto van achteren raken en dan te reageren. Dus niet om zelf te reageren als een andere auto ons van achteren dreigt te raken, want in dat geval doen we niets. De 'ontwijk_autos' blok wordt dan:





Let op dat dit niet botsing detectie tussen auto's is, dit is juist een methode om botsingen te voorkomen. In sommige situaties kan het echter voorkomen dat auto's niet snel genoeg kunnen draaien, en dan zullen ze 'botsen'. Je ziet dan een overlap in de auto afbeeldingen van de klonen. Dit is dus nog niet helemaal klaar maar is een aardig begin.

Het nadeel aan deze kleur gebaseerde detectie is ook dat de detectie vlakken zichtbaar zijn terwijl het eigenlijk beter was geweest om ze onzichtbaar te maken zoals de navigatie zones. Ik heb nog niet goed kunnen nadenken hoe dat voor elkaar te krijgen. We kunnen op deze manier wel ervaring opdoen en de logica goed krijgen.

Conclusie

Ik heb besloten dit te maken omdat ik met Scratch bekend wilde raken en een racespel leek me wel aardig. En ik heb een hoop van Scratch en diens (on)mogelijkheden geleerd: klonen, detectie op kleur en op klonen, eigen gemaakt script blokken, het was een hoop gegoogle en gepuzzel af en toe.

Wat ik met name krachtig en leuk aan Scratch vond is het combineren van visuele elementen en het programmeren ervan. Zo kan dit spel makkelijk met extra circuits worden uitgebreid door ze eenvoudigweg te gaan tekenen, terwijl het programma er verder voor  zorgt dat het spel gaat werken. Het was ook verbazend makkelijk om de computer auto's autonoom te laten rijden (al gaat het niet altijd vlekkeloos). Het reageren op gebeurtenissen geeft een makkelijk en flexibele manier van indelen van je programma's, heel geschikt voor spelletjes.

Het spel is ook nog niet af: het was de bedoeling om tegen een ander mens te kunnen racen. Dat zal ik later moeten toevoegen, en misschien nog wat andere verbeteringen. Als je meer wilt weten post een vraag of beklijk dit project is online op https://scratch.mit.edu/projects/99557843/