Bgie
Posted on June 16, 2023
Terug naar deel 7
Onze speler is nog onsterfelijk, maar dat willen we niet. We gaan hem een beperkt aantal levens geven.
In het player.gd script voegen we helemaal vanboven (na de extends CharacterBody2D
) toe:
var health_points: int = 3
Wat wil dit zeggen?
-
var
hebben we al gebruikt en betekend: maak een variabele. -
health_points
is de naam de we hebben gekozen voor de variabele. -
: int
zegt dat onze variabele altijd een integer is (geheel getal zonder komma). Als we ons vergissen in de code, gaat Godot ons waarschuwen. Dit is een hulpmiddel. -
= 3
is de start waarde, onze speler begint met 3 levens.
We gaan deze variabele nu gebruiken in onze take_damage()
functie. Die zou er nog zo uit moeten zien:
func take_damage():
if state == State.READY:
state = State.HURT
sprite.play("hurt")
Die code zorgt enkel voor de juiste animatie als de speler geraakt wordt. We willen nu dat een aanval een leven kost.
func take_damage():
health_points = health_points - 1
if state == State.READY:
state = State.HURT
sprite.play("hurt")
De nieuwe code doet een eenvoudige berekening. De nieuwe health_points
(links) wordt gelijk aan de oude health_points
min 1.
Dus iedere keer als we in take_damage()
komen gaat de speler 1 leven minder krijgen.
We voegen nog een stukje voorlopige code toe, om nu snel te kunnen testen:
func take_damage():
health_points = health_points - 1
if health_points == 0:
self.queue_free()
if state == State.READY:
state = State.HURT
sprite.play("hurt")
Als de health_points
zijn verminderd tot nul, laten we de speler verdwijnen met self.queue_free()
, zoals bij de vijand. We gaan dadelijk een beter einde maken, maar deze code kunnen we nu al testen.
Het is vaak een goed idee je code in kleine stapjes aan te passen, en telkens te testen of het werkt. Schrijf je honderden regels ineens, zonder testen, dan kom je voor verrassingen te staan wanneer je het uiteindelijk wil testen. Dan is het moeilijker om de fout te vinden, tussen die honderden regels.
Dit is ons resultaat:
Wat we eigenlijk willen, is een death animation zoals bij de vijand. Daarvoor voegen we nog een state toe: DYING
.
enum State {READY, ATTACK, HURT, DYING}
Onze take_damage()
wordt ook uitgebreid:
if state == State.DYING:
return
health_points = health_points - 1
if health_points == 0:
state = State.DYING
sprite.play("die")
elif state == State.READY:
state = State.HURT
sprite.play("hurt")
Wat veranderd er?
- we starten met een
if
en eenreturn
, want als de speler al dood gaat, is een nieuwetake_damage
niet meer nodig.return
zorgt dat we onmiddellijk uit de functie gaan, de rest van de code wordt overgeslagen. - de tijdelijke
self.queue_free()
vervangen we. We zetten onze state opDYING
en starten de juiste animatie"die"
- de laatste
if
wordt eenelif
. Enkel als de eersteif
niet waar is, gaan we deze tweedeelif
doen.DYING
heeft voorrang opHURT
(staat eerst), en als de speler dood gaat doen we nooit eenHURT
.
Er komt nog 1 regel bij in _on_animated_sprite_2d_animation_finished
.
De huidige code dient om na een ATTACK
of een HURT
terug naar de gewone READY
te gaan, zodat we niet eeuwig blijven aanvallen of hurten.
Maar dood gaan is wel redelijk definitief... De nieuwe code:
func _on_animated_sprite_2d_animation_finished():
if state != State.DYING:
state = State.READY
Enkel voor states verschillend van DYING
zetten we de state terug op READY
.
Het resultaat:
... en onze zeer enthousiaste vijand blijft lekker doormeppen!
Wat er nu nog ontbreekt aan ons player health systeem, is een manier om te tonen hoeveel levens de speler nog heeft.
Daarvoor maken we een HUD (heads-up-display). Dit is een vaste laag bovenop alle graphics van de game wereld, die nooit met de camera mee beweegt. We willen linksboven in het scherm met een reeks hartjes tonen hoeveel levens de speler nog heeft.
We maken een nieuwe scene voor onze HUD. Klik met de rechtermuisknop op res://
in het FileSystem
paneel en kies New
en Scene
.
Vervolgens kiezen we als node type CanvasLayer
en we geven ook de naam 'HUD' aan onze scene.
We krijgen nu een nieuwe scene, gelijkaardig aan onze andere scenes. Het verschil tussen een CanvasLayer
en onze andere scenes, is dat de CanvasLayer
NIET beïnvloed wordt door de camera. Dit is nu van geen belang, omdat we de camera (nog) niet verschuiven in onze game. Maar doen we dit later wel, dan willen we dat onze HUD op een vaste plaats in het scherm blijft staan.
Bij onze graphics zitten nog geen hartjes. Maar je kan gemakkelijk png
afbeeldingen van het web gebruiken in je game.
Maak eerst een aparte map bij je assets. Je kan met rechtermuisknop klikken op de 'assets' map en via New
en Folder
een nieuwe map maken die we 'HUD' gaan noemen.
De nieuwe map komt onder je assets te staan:
Dit is het hartje dat we gaan gebruiken:
Afhankelijk van je web browser is dit iets verschillend, maar meestal kan je rechts klikken en is er een save image as
optie. Verander de naam naar iets duidelijker zoals 'heart.png'. Je kan de naam ook veranderen vanuit Godot.
Nu gaan we de hartjes toevoegen aan onze scene.
Zorg dat de 'HUD' node
is geselecteerd in je scene en klik de grote plus +
om een node toe te voegen. We kiezen voor een TextureRect
.
We veranderen de naam van onze TextureRect
naar 'Hearts', en voegen een Texture
toe. We kiezen load
en zoeken onze 'hearts.png' in de 'assets/HUD' map.
Dit levert ons 1 hartje op:
Er zijn meerdere manieren om nu meerdere hartjes te krijgen. Het ligt voor de hand om simpelweg meerdere TextureRect
nodes te maken. Maar we gaan een truc gebruiken, waarbij we de grafische kaart al het werk laten doen. De grafische kaart kan zonder veel moeite een texture herhalen, dat is iets wat bijvoorbeeld in 3D games voortdurend gebruikt wordt.
We veranderen:
-
Expand Mode
naarIgnore Size
. De grafische kaart negeert de vaste afmetingen van het hartje . -
Stretch Mode
zetten we opTile
. De grafische kaart gaat de afbeelding 'tegelen' (=herhalen). -
Custom Minimum Size
wordtx=360
eny=90
.
Het hartje zelf is 120x90 pixels groot. Omdat we tegelen opzetten en vragen om een gebied van 360x90
op te vullen, gaat de grafische kaart het hartje meermaals tekenen om het gevraagde gebied op te vullen. 360 is 3 x 120, we krijgen 3 hartjes.
Wat we nu nog gaan doen, is de hartjes in de linkerbovenhoek plaatsen, met een kleine marge zodat ze niet helemaal tegen de rand van het scherm plakken.
Verander de Anchor Preset
naar Custom
. Dan krijg je extra parameters, onder Anchor Points
, Anchor Offsets
en Grow Direction
.
Het systeem van anchors (anker-punten) leggen we later beter uit. Zorg nu gewoon dat de Anchor Offsets
voor Left
en Top
op 25 px
staan. Daarmee zorgen we voor een afstand van 25 pixels tussen de hartjes en de linker- en bovenrand van het scherm. Kijk even na of de andere waarden (aangeduid met groen) hetzelfde zijn.
De hartjes gaan lichtjes verspringen wanneer je de offsets intypt, maar verder zien we niet veel. We moeten onze HUD nog toevoegen aan de game scene.
Ga naar de 'game' scene en sleep 'HUD.tscn' beneden naar de root node 'Game'.
Onze hartjes verschijnen, maar staan niet linksboven!
Dit is verwarrend, maar toch komen de hartjes juist als we het spel starten.
Het beeld in de editor toont niet wat de camera ziet, maar wat er op de scene staat. Starten we de game, dan kijken we wel door de camera.
Nu rest er enkel nog wat code, om ook werkelijk het aantal levens van de speler te tonen in de HUD.
In de 'HUD' scene selecteren we de 'HUD' root node en voegen een nieuw script toe. Dit krijgt automatisch de naam 'HUD.gd', wat prima is.
Het script ziet er zo uit:
extends CanvasLayer
@onready var hearts: TextureRect = $Hearts
func set_health(health: int):
hearts.custom_minimum_size.x = health * 120
Wat doen we hier? De variabele 'hearts' verwijst naar de TextureRect die onze hartjes toont. De functie set_health
krijgt in de parameter 'health' mee hoeveel hartjes er getoond moeten worden. We weten dat een hartje 120 pixels breed is, en als we 120 maal 'health' doen, hebben we de breedte voor onze TextureRect
om het juiste aantal hartjes te tonen.
Dit moeten we nog verbinden met de player.
Omdat onze player en onze HUD twee aparte scenes zijn, die niets van mekaar afweten, gaan we de verbinding leggen in onze game scene.
We voegen ook een script toe aan de root node 'Game' van onze game scene.
De eenvoudigste code is als volgt:
extends Node2D
@onready var player: CharacterBody2D = $Arena/Player
@onready var hud: CanvasLayer = $HUD
func _process(_delta):
hud.set_health(player.health_points)
We maken de variabelen 'player' en 'hud' die de juiste node opzoeken en opslaan (via het $ symbool). De functie _process(_delta)
wordt bij elke frame opgeroepen. Hier vragen we player.health_points
op en geven dit door aan onze set_health
functie van de HUD.
Dit kan efficiënter als we player een signaal laten uitsturen enkel wanneer health_points wijzigt, maar voorlopig is dit goed genoeg.
Met onze HUD kunnen we nog meer doen. In het volgende deel maken we een "Game Over" scherm en de mogelijkheid om een nieuw spel te starten.
Posted on June 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.