logo CoderDojo

Löve2D - Shooter

We gaan met 2D game “framework” Löve2D een shooter bouwen.

We gaan een eenvoudige shooter maken. Deze instructie is een vertaling en bewerking van een tutorial door >_OSMSTUDIOS.

Wat leer je?

Tijdens de Dojo’s over Scratch hebben jullie kennis gemaakt met variabelen en functies (blokken). Daar gaan we mee verder.

In Scratch kun je met drag-and-drop variabelen en functies combineren tot een programma, maar bij de meeste programmeertalen wordt het programma in tekst geschreven. Dat is ook het geval bij Löve. We gaan dus typen.

Omdat we er geen typecursus van willen maken, zijn een aantal functies, net als bij Scratch, al voor jullie gemaakt. Je hoeft deze dus niet helemaal zelf meer te maken. Je hoeft ze alleen maar te gebruiken. Dit zijn de functies: xRechterRand, yOnderRand, xWillekeurig, tekenVijanden, tekenKogels, maakNieuweVijand, maakNieuweKogel, spelerHeeftVijandGeraakt en kogelHeeftVijandGeraakt.

Als je deze in de uitleg tegenkomt, hoef je ze dus niet zelf te implementeren!

Benodigdheden

Om deze instructie te kunnen volgen, moet je Löve en een goede editor geïnstalleerd hebben. Verder moet je wat code downloaden om verder op te bouwen, maar dit wordt in stap 0 uitgelegd.

Installatie Löve

Hopelijk heb je de installatie al voorbereid na het ontvangen van de uitnodiging voor deze Dojo. Als dat zo is, kun je dit hoofdstukje overslaan en doorgaan naar Stap 1.

Windows

Löve is op Windows gemakkelijk te installeren via één van de installers. Als alternatief kun je kiezen voor één van de zipbestanden en deze op een gewenste locatie unzippen. Maak het jezelf gemakkelijk door Löve op een eenvoudig te onthouden plek te installeren, bijvoorbeeld in de map

c:\love\

Zie de Löve download pagina voor de installatie bestanden. Als je twijfelt tussen de 32- of 64-bits versie, kun je voor de zekerheid kiezen voor 32-bits versie of gewoon even checken welke versie je hebt.

Mac

Download het zip bestand voor Mac en unzip het op de gewenste locatie.

Linux

Voor Ubuntu kun je kiezen voor het toevoegen van de Löve PPA of voor de installatie van één van de .deb bestanden.
Onderstaand vind je de instructies voor het gebruiken van de AppImage:

Instructies voor het gebruiken van de Löve AppImage
  1. Download de Löve appimage x86_64. Zet deze bijvoorbeeld in je ~/Documents directory.

  2. Voer hier vervolgens de volgende commands op uit (vanuit ~/Documents). Je .AppImage kan een andere versie/naam hebben dan in het voorbeeld!

chmod u+x love-11.4-x86_64.AppImage
./love-11.4-x86_64.AppImage --appimage-extract
  1. Het resultaat is een directory ~/Documents/squashfs-root. Deze directory bevat het bestand AppRun, welke uitvoerbaar is.
  2. Zorg nu dat je weet waar je .lua bestanden staan (zie onderstaande stap 1). In ons voorbeeld is dit in ~/Documents/love2d-shooter-master/src/. Je voert de code dan op de volgende manier uit:
~/Documents/squashfs-root/AppRun ~/Documents/love2d-shooter-master/src/

Let op dat het scherm in eerste instantie zwart is, omdat we nog geen code geschreven hebben.
Mocht het niet lukken om de code op deze manier uit te voeren, vraag dan gerust om hulp!

Tekst editor

Als je gaat programmeren is het handig om een editor te installeren met meer mogelijkheden dan ‘kladblok’. Sublime Text snapt Lua en dus Löve. Als alternatief kun je kiezen voor Notepad++.

Stappen

TIP: de onderstaande stukken code hoef je niet over te typen. Je kunt ze natuurlijk gewoon copy-pasten in je editor.

Stap 0: installatie voorbereidde code

Download the code van GitHub: Löve 2D shooter code De zip-file bevat een src/ folder. Kopieer de inhoud daarvan naar c:\games\shooter. Om de voorbereidde code uit te voeren, druk je op de Windows knop en typ je cmd. Je krijgt dan een zwart venster. Typ dan cd c:\games\shooter om naar de folder te gaan waar je de code hebt neergezet. Typ tenslotte c:\love\love.exe .\ om het programma te starten. Als alles goed gaat, krijg je een zwart venster.

Stap 1: teken het vliegtuig van de speler

In deze stap laden we het plaatje van het vliegtuig van de speler en tekenen we het plaatje op het scherm. Open main.lua in je tekst editor en type het volgende:

-- variabele om het plaatje in op te slaan
spelersVliegtuig = nil

function love.load(arg)
  -- laad het plaatje in de variabele
  spelersVliegtuig = love.graphics.newImage('plaatjes/spelersVliegtuig.png')
end

En vervolgens om op het scherm te tekenen:

function love.draw(dt)
  -- teken het plaatje op het scherm
  love.graphics.draw(spelersVliegtuig, 100, 100)
end

Start je programmaatje en zie het resultaat:

scherm met vliegtuig

x en y as

assen stelsel in Löve2D

In de code hebben we bij het aanroepen van functie love.graphics.draw twee keer het getal 100 gebruikt. Dit getal gebruiken we om de positie van het plaatje in het scherm te bepalen. Daarbij is positie (0,0) links bovenin. Bij (100,100) is het plaatje dus 100 stappen naar beneden en 100 naar rechts verschoven ten opzichte van (0,0). Voor de horizontale positie wordt vaak variabele naam x gebruikt en voor de verticale positie y. Een handige manier van opschrijven is (x, y), bijvoorbeeld (50, 30) als x = 50 en y = 30.

In Scratch komen x en y waarden voor de plaats van een Sprite ook terug. Hieronder staat Scratch op x = 31 en y = -36

x en y in Scratch

Stap 2: eigenschappen van de speler

Het vliegtuig van de speler staat nu nog stil op positie (100,100), maar zal natuurlijk van links naar rechts gaan bewegen als we wat verder zijn. Je zou kunnen zeggen dat de speler een aantal eigenschappen heeft:

Om die informatie bij elkaar te houden, vervangen we variabele spelersVliegtuig door een nieuwe variabele waarin meerdere eigenschappen opgeslagen kunnen worden. Deze variabele noemen we speler:

-- variabele om eigenschappen van de speler in op te slaan
speler = { x = 200, y = 560, plaatje = nil }

function love.load(arg)
  -- laad het plaatje in eigenschap plaatje van de variabele speler
  speler.plaatje = love.graphics.newImage('plaatjes/spelersVliegtuig.png')
end

Het tekenen van het plaatje moet ook aangepast worden, zodat we de eigenschappen van de nieuwe variabele speler gebruiken:

function love.draw(dt)
  -- teken het plaatje op het scherm
  love.graphics.draw(speler.plaatje, speler.x, speler.y)
end

Start het programma opnieuw op, en je zal zien dat het vliegtuig nu onderin het venster staat. De wijzigingen die je hebt gemaakt zorgen dat we het plaatje later kunnen laten bewegen.

Stap 3: de speler bewegen

Je programma toont nu alleen een stilstaand plaatje. Dat is natuurlijk niet erg interessant. In deze stap ga je het plaatje laten bewegen.

Bewegen met pijltjes

Om het plaatje horizontaal te laten bewegen, moet de speler.x eigenschap worden aangepast. De waarde van speler.x moet kleiner worden om naar links te bewegen en groter om naar rechts te bewegen. Je kunt dit ook eenvoudig uitproberen door de waarde 200 in de code groter of kleiner te maken en je programma te starten.

Om het plaatje te laten bewegen terwijl het programma draait, moet het programma gaan reageren op toetsen van je toetsenbord. De pijl naar links zorgt voor een beweging naar links en de pijl naar rechts zorgt voor een beweging naar rechts:

function love.update(dt)
  -- als pijltje naar links wordt ingedrukt
  if love.keyboard.isDown('left') then

	-- dan doe een stap naar links
	speler.x = speler.x - 1

  -- als pijltje naar rechts wordt ingedrukt
  elseif love.keyboard.isDown('right') then

	-- dan doe een stap naar rechts
	speler.x = speler.x + 1
  end
end

Als je voor het eerst dit soort code leest ziet het bovenstaande voorbeeld er ingewikkeld uit. Maar: vergelijk het eens met het bewegen via pijltjes in Scratch. love.update(dt) wordt steeds opnieuw uitgevoerd en is daarom vergelijkbaar met de herhaal loop in Scratch. if ... then is gelijk aan de als ... dan keuze in Scratch. In Scratch zou dit er ongeveer als volgt hebben uitgezien:

wanneer groene vlag wordt aangeklikt herhaal als <toets [pijltje links v] ingedrukt?> dan verander x met (-1) end als <toets [pijltje rechts v] ingedrukt?> dan verander x met (1) end end

Als je de code hebt ingevoerd (in Löve2D, niet in Scratch 😀), kun je het programma starten om uit te proberen of het werkt.

Probeer ook eens een andere stapgrootte dan 1. Doet dat wat je ervan verwacht? Wat gebeurt er als je er een negatief getal van maakt? Kun je de code aanpassen, zodat je ook naar boven en beneden kunt bewegen (gebruik toetsen up voor naar boven en down voor naar beneden)? Wat gebeurt er als je bijvoorbeeld het pijltje naar rechts lang ingedrukt houdt?

Stoppen bij de rand

Om te voorkomen dat het plaatje het venster kan verlaten, moeten we voorkomen dat het verder beweegt als het aan de randen van het venster komt. Aan de linker kant is dat makkelijk: beweeg niet verder als speler.x gelijk is aan 0. In code ziet dat er als volgt uit:

-- als pijltje naar links ingedrukt
if love.keyboard.isDown('left') then

  -- en linker rand is nog niet bereikt
  if speler.x > 0 then

	-- dan doe een stap naar links
	speler.x = speler.x - 1

  end

Aan de rechter zijde is dat lastiger. Daar hangt de maximale x af van de breedte van het venster. De maximale waarde voor x is gelijk aan de breedte van het venster, of xRechterRand(). Let op: xRechterRand is een van de kant en klare functies!

-- als pijltje naar rechts ingedrukt
elseif love.keyboard.isDown('right') then

  -- en de rechter rand is nog niet bereikt
  if speler.x < xRechterRand() then

	-- dan doe een stap naar rechts
	speler.x = speler.x + 1

  end
end

Pas de code die je eerder bij stap 3 (bewegen met pijltjes) hebt geschreven aan, zodat je stopt bij de rand. Controleer ook of het plaatje nu niet aan de rechterkant verdwijnt. Voor de duidelijkheid hebben we de Scratch code ook aangepast en bijgevoegd:

wanneer groene vlag wordt aangeklikt herhaal als <toets [pijltje links v] ingedrukt?> dan als <(x-positie) > (0)> dan verander x met (-1) end end als <toets [pijltje rechts v] ingedrukt?> dan als <(x-positie) < (240)> dan verander x met (1) end end end

Stap 4: de vijand

Het spel draait natuurlijk niet alleen om het heen en weer bewegen van een vliegtuigje. Er zijn ook vijanden. In deze stap gaan we die vijanden maken.

We beginnen met het tekenen van één vijand. Daarvoor voegen we één regel toe (bijna bovenaan je code) en we voegen een regel code toe aan de love.load() en love.draw() functies:

-- variabelen om eigenschappen van de speler en vijand in op te slaan
speler = { x = 200, y = 560, plaatje = nil }
vijand = { x = 200, y = 0, plaatje = nil }

function love.load(arg)

  -- laad het plaatje in eigenschap plaatje van de variabele speler
  speler.plaatje = love.graphics.newImage('plaatjes/spelersVliegtuig.png')
  vijand.plaatje = love.graphics.newImage('plaatjes/vijandsVliegtuig.png')
end

function love.draw(dt)

  -- teken het plaatje op het scherm
  love.graphics.draw(speler.plaatje, speler.x, speler.y)
  love.graphics.draw(vijand.plaatje, vijand.x, vijand.y)
end

Probeer bovenstaande code uit.

Er is nu één vijand en deze staat recht tegenover de speler. Een stilstaande vijand is niet zo interessant, dus we laten ’m naar de speler bewegen. Dat kan vrij eenvoudig door het volgende aan de code toe te voegen aan je love.update(dt) functie:

function love.update(dt)

  -- laat de vijand een stapje naar beneden doen 
  vijand.y = vijand.y + 1

Bovenstaande code werkt hetzelfde als het verplaatsen van de speler. Nu bewegen we echter verticaal, van boven naar beneden. Daarom moeten we de y-positie in plaats van de x-positie veranderen. De positie van de vijand wordt steeds met 1 stapje naar beneden verzet. Zo beweegt de vijand naar de speler toe.

Om het plaatje van buiten het venster tevoorschijn te laten komen, moet de y-positie aangepast worden naar -100. Pas de code bovenin je script aan door y = 0 te veranderen naar y = -100:

vijand = { x = 200, y = -100, plaatje = nil }

Onder aan het scherm vliegt de vijand het venster uit en is dan verdwenen. Het is natuurlijk leuker als er aan de bovenkant een nieuwe vijand verschijnt. Daarvoor moet je, net als bij het bewegend maken van de vijand, onderstaande code toevoegen aan love.update(dt):

-- als de vijand de onderrand heeft bereikt
if vijand.y > yOnderRand() then

  -- verplaats het dan uit het zicht boven het venster
  vijand.y = -100

end

Om je te helpen met het begrijpen van de code: het verplaatsen van de vijand in Scratch zou er ongeveer zo uitzien:

verander y met (1) als <(y-positie) > (180)> dan maak y (-200) end

Om het nog interessanter te maken, zou het leuk zijn als de vijand niet steeds op dezelfde plaats van boven naar beneden beweegt. De waarde van vijand.x (horizontale positie) zou iedere keer anders moeten zijn. Gebruik daarvoor de functie xWillekeurig():

-- als de vijand de onderrand heeft bereikt
if vijand.y > yOnderRand() then

  -- verplaats het dan uit het zicht boven het venster
  vijand.y = -100

  -- en zet de horizontale positie naar een willekeurige waarde
  vijand.x = xWillekeurig()

end

Wat je in Scratch zou doen:

verander y met (1) als <(y-positie) > (180)> dan maak y (-200) maak x (willekeurig getal tussen (-240) en (240)) end

Stap 5: meerdere vijanden

We hebben nu een enkele vijand. Die is natuurlijk makkelijk te ontwijken. Het wordt veel spannender met meerdere vijanden. Daarvoor moeten we het één en ander aanpassen, de code die je eerder hebt geschreven voor één vijand, gaan we vervangen met code voor meerdere vijanden. Je moet hier en daar dus code verwijderen!

Allereerst hebben we een lijst met vijanden nodig, die voegen we bovenaan de code toe, bij speler. Variabelen staan vaak bovenaan in code:

-- variabelen om eigenschappen van de speler in op te slaan
speler = { x = 200, y = 560, plaatje = nil }

-- lijst om vijanden in op te slaan
vijanden = {}

Een andere variabele die we nodig hebben is het plaatje van de vijand. Eerder deden we dat in de functie love.draw(dt) met speler.plaatje. Die regel mag nu verwijderd worden en in plaats daarvan zetten we bij de variabelen speler en vijanden ook het plaatje voor de vijand:

-- laad het plaatje van de vijand
vijandPlaatje = love.graphics.newImage('plaatjes/vijandsVliegtuig.png')

De vijanden moeten ook getekend worden, zorg ervoor dat jouw love.draw(dt) functie eruit ziet zoals hieronder:

function love.draw(dt)

  -- teken het plaatje op het scherm
  love.graphics.draw(speler.plaatje, speler.x, speler.y)

  -- teken de vijanden in de lijst
  tekenVijanden(vijanden)

end

Er worden alleen vijanden getekend die bestaan, daarom moeten vijanden ook aangemaakt worden:

function love.update(dt)

  maakNieuweVijand(vijanden)

In de vorige stap hebben we de vijand laten bewegen. Deze code moeten we nu voor iedere vijand in de lijst uitvoeren:

function love.update(dt)

  maakNieuweVijand(vijanden) 

  -- voor elke vijand in de lijst  
  for index, vijand in ipairs(vijanden) do

	-- laat de vijand een stapje naar beneden doen  
	vijand.y = vijand.y + 2

	-- als de vijand de onderrand heeft bereikt
	if vijand.y > yOnderRand() then

	  -- verwijder de vijand
	  table.remove(vijanden, index)

	end
  end

Het maken en verwijderen van vijanden zoals we dat hierboven doen is in Scratch heel lastig. We geven daarom geen voorbeeld van het bovenstaande in Scratch. Waar het kan doen we dat wel.

Voer het programma uit. Als het goed is ziet het eruit als in onderstaande plaatje:

scherm met vijandelijke vliegtuigen

Stap 6: de vijand ontwijken

Als de speler een vijand raakt, is het spel afgelopen. Om te zien of de speler een vijand geraakt heeft, hebben we functie spelerHeeftVijandGeraakt().

Als het resultaat van de functie waar (true) is, dan moet het spel gestopt worden.

Voeg daarvoor de volgende code toe aan functie love.update(dt):

-- als de speler een vijand geraakt heeft
if spelerHeeftVijandGeraakt() then

  -- is het spel afgelopen
  spelIsAfgelopen = true

end

In Scratch zou het er zo uitzien:

als <raak ik [Vijand v]?> dan maak[spelIsAfgelopen v] (1) end

In variabele spelIsAfgelopen wordt bijgehouden of het spel nog loopt of al is afgelopen. Als het is afgelopen, wordt de speler niet langer getekend. Pas daarvoor de functie love.draw(dt) aan door het tekenen van de speler binnen een if statement te plaatsen:

function love.draw(dt)

  -- als het spel nog niet is afgelopen
  if spelIsAfgelopen == false then

	-- teken het plaatje op het scherm
	love.graphics.draw(speler.plaatje, speler.x, speler.y)

  end

In Scratch zou het er zo uitzien:

als <(spelIsAfgelopen) = (0)> dan verschijn end

Vergeet niet om bovenaan het bestand de variabele toe te voegen:

spelIsAfgelopen = false

Als je nu het spel speelt, wordt het spelersvliegtuig niet meer getekend als je een vijand hebt geraakt.

Stap 7: schieten

De stap schieten lijkt op een combinatie van stappen 4 en 5.

Er moet een lijst worden toegevoegd voor de kogels:

-- variabelen om eigenschappen van de speler in op te slaan
speler = { x = spelerX, y = spelerY, plaatje = nil }

-- lijst om vijanden in op te slaan
vijanden = {}

-- lijst om kogels in op te slaan
kogels = {}

Het plaatje van de kogel moet worden geladen:

-- laad het plaatje van de vijand
vijandPlaatje = love.graphics.newImage('plaatjes/vijandsVliegtuig.png')

-- laad het plaatje van de kogel
kogelPlaatje = love.graphics.newImage('plaatjes/kogel.png')

En het plaatje moet worden getekend, dat doe je in de love.draw(dt) functie:

-- teken de kogels in de lijst
tekenKogels(kogels)

In tegenstelling tot de vijanden, worden nieuwe kogels alleen aangemaakt als de speler de spatiebalk indrukt. In de love.update(dt) functie voeg je het volgende toe:

if spelIsAfgelopen == false then

	-- als de spatiebalk wordt ingedrukt
	if love.keyboard.isDown('space') then

	  -- schiet dan
	  maakNieuweKogel(kogels, speler)

	end

	maakNieuweVijand(vijanden)  

Als je de code tot nu toe uitprobeert, krijg je wel kogels te zien, maar die bewegen niet. Dit ziet eruit als het plaatje hieronder.

scherm met vijandelijke vliegtuigen en kogels

Aan de love.update(dt) functie moet extra code worden toegevoegd om de kogels te laten bewegen. Elke kogel in de lijst laten we telkens vier stappen omhoog gaan. Als een kogel uit beeld verdwijnt, halen we de kogel ook uit de lijst:

if spelIsAfgelopen == false then
	-- als de spatiebalk wordt ingedrukt
	if love.keyboard.isDown('space') then
		-- schiet dan
		maakNieuweKogel(kogels, speler)
	end

	-- voor elke kogel in de lijst
	for index, kogel in ipairs(kogels) do

		-- beweeg de kogel naar boven
		kogel.y = kogel.y - (2 * stapGrootte)

		-- als de kogel de bovenrand heeft bereikt
		if kogel.y < -20 then
			-- verwijder het uit de lijst
			table.remove(kogels, index)
		end
	end

	maakNieuweVijand(vijanden)  

Dat ziet er beter uit!

scherm met vijandelijke vliegtuigen en bewegende kogels

Stap 8: raken en scoren

De vijanden vliegen, je kunt je vliegtuig besturen en je kan schieten. Maar je schoten raken nog niets en de vijanden vliegen door. Daar gaan we in deze stap wat aan doen.

Functie kogelHeeftVijandGeraakt() laat ons weten of een kogel een vijand geraakt heeft. Ook verwijdert deze functie de geraakte vijand en de kogel:

-- als de kogel een vijand geraakt heeft
if kogelHeeftVijandGeraakt() then

  -- heb je een punt gescoord
  score = score + 1

end

Voeg bovenstaande code toe aan love.update(dt). Om de score in op te slaan, moet je ook nog een variabele aanmaken boven aan het bestand:

score = 0

In Scratch ziet het er zo uit:

maak [score v] (0) als <raak ik [Kogel v]?> dan verander [score v] met (1) end

We willen natuurlijk de score kunnen zien. Voeg daarvoor in love.draw(dt) het volgende toe:

-- zet de tekstkleur op wit
love.graphics.setColor(255, 255, 255)

-- en druk de score af
love.graphics.print("SCORE: " .. tostring(score), 400, 10)

Stap 9: opnieuw starten

Mocht je een vijand aanraken, dan is het spel afgelopen, maar dan wil je natuurlijk wel opnieuw kunnen beginnen. Daarvoor gaan we code toevoegen aan love.update(dt):

-- als de o van opnieuw wordt ingedrukt
if love.keyboard.isDown('o') then

	-- wordt het spel opnieuw gestart
	kogels = {}
	vijanden = {}
	score = 0
	spelIsAfgelopen = false

end

Als je op de toets o drukt begint je spel opnieuw. Het spel is nu klaar!

Voorbereide functies

Bij het maken van het spelletje heb je gebruik gemaakt van een aantal voorbereide functies. Als je je nu afvraagt hoe die functies zijn gemaakt, dan kun je eens kijken in het bestand functies.lua.

Voorbeeld code

Mocht je nu ook nog een voorbeeld van de volledige code willen bekijken, dan kan je eens kijken in voorbeeld.lua.

Conclusie

Het spelletje is af en je hebt geleerd hoe je een 2D shooter kunt maken met Löve.

Vervolg

Als je meer wilt maken met Löve, dan zijn er meerdere instructies (in het Engels: tutorials) op het internet te vinden. Helaas zijn die wel vrijwel allemaal Engelstalig.

Een overzicht vind je op site van Löve zelf: https://love2d.org/wiki/Category:Tutorials.

Licentie

Deze instructies worden, net als alle andere instructies van CoderDojo Nijmegen, aangeboden onder een Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Licentie.

Creative Commons License