DEV Community

Cover image for Een beat-em-up game maken met Godot - health
Bgie
Bgie

Posted on

Een beat-em-up game maken met Godot - health

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")

Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

Player death gone animation

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}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Wat veranderd er?

  • we starten met een if en een return, want als de speler al dood gaat, is een nieuwe take_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 op DYING en starten de juiste animatie "die"
  • de laatste if wordt een elif. Enkel als de eerste if niet waar is, gaan we deze tweede elif doen. DYING heeft voorrang op HURT (staat eerst), en als de speler dood gaat doen we nooit een HURT.

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
Enter fullscreen mode Exit fullscreen mode

Enkel voor states verschillend van DYING zetten we de state terug op READY.

Het resultaat:

Player death animated

... 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.

Add new scene

Vervolgens kiezen we als node type CanvasLayer en we geven ook de naam 'HUD' aan onze scene.

Create new CanvasLayer 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.

Image description

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.

Add new folder to assets

De nieuwe map komt onder je assets te staan:

new hud folder

Dit is het hartje dat we gaan gebruiken:

Heart

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.

Heart.png asset

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.

Add texture rect node

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.

Rename texture rect etc

Dit levert ons 1 hartje op:

Single heart

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.

Tile heart texture

We veranderen:

  • Expand Mode naar Ignore Size. De grafische kaart negeert de vaste afmetingen van het hartje .
  • Stretch Mode zetten we op Tile. De grafische kaart gaat de afbeelding 'tegelen' (=herhalen).
  • Custom Minimum Size wordt x=360 en y=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.

Anchor topleft with margins

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'.

drag hud onto game scene root node

Onze hartjes verschijnen, maar staan niet linksboven!

Hearts on scene

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.

Run game with HUD

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.

Create hud script

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
Enter fullscreen mode Exit fullscreen mode

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.

Add script to game scene root node

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)
Enter fullscreen mode Exit fullscreen mode

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.

Hud hearts animation

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.

Top comments (0)