Een beat-em-up game maken met Godot - hurtboxes en hitboxes
Tags: beginners, gamedev, godot, tutorial
Published: 2023-06-02 21:03:33

Terug naar [deel 6](https://dev.to/bgie/een-beat-em-up-game-maken-met-godot-4-deel-6-19ci)

Het vecht-systeem is gebaseerd op 'hitboxes' en 'hurtboxes'. 

Het slachtoffer van een aanval heeft een hitbox. Het wapen van de aanvaller heeft een hurtbox. Als de hurtbox van de aanvaller de hitbox van het slachtoffer raakt, krijgt het slachtoffer 'damage'. 

Beiden gaan we maken met `CollisionShape2D` objecten, waar we ook onze botsingen mee maakten.

We hadden al een `CollisionShape2D` op de voeten geplaatst van onze personages. Dit kunnen we ook gebruiken voor als hitbox. Om dit duidelijk te maken, veranderen we naam van deze nodes, zowel voor de 'Player' als voor de 'Minotaur':

![Rename to hitbox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axtb7v1z8y12or924qju.png)

Vervolgens gaan we de hurtbox voor het wapen maken. Bij de speler voegen we 2 nodes toe, een `Area2D` en daaronder een `CollisionShape2D`:

![Add player area2d](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ac2hz33lsfvg1hm6k09x.png)

Om de hurtbox op de juiste plaats te kunnen zetten, gaan we tijdelijk onze `AnimatedSprite2D` verzetten naar de juiste frame (5) voor de "attack" animatie. Zo zien we waar het wapen neerkomt.

![Attack animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8oik9vja8chtlesxka7.png)

Zoals we al eerder deden, moeten we nu een `Shape` toevoegen aan onze `CollisionShape2D`. Hiervoor gebruiken we een `CapsuleShape2D`.

![Add shape to hurtbox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kc0gponn82zyvbarxn34.png)

We zetten enkele waardes van onze `CapsuleShape2D` juist: de `Radius` wordt `130 px`, de `Height` wordt `400 px` en onze `Position` krijgt een `x` van `300`. Het is ook mogelijk de kleur (`Debug Color`) aan te passen, maar dit zie je niet in de game zelf, enkel in de editor tijdens het ontwerpen.

![Set hurtbox properties](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cwt09zcfxv2au1p9xgst.png)

Onze hurtbox noemt nog `Area2D`, de standaard naam. Het is duidelijker als we deze van naam te veranderen:

![Rename hurtbox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1m8bongjy5so1adpjrk.png)

Onze code hangen we aan het signaal `body_entered(body: Node2D)` dat hoort bij onze hurtbox. (In de screenshot heet de hurtbox nog `Area2D`)

![Connect body_entered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92ts4zayat3l8n6xv5ig.png)

De functie die we nu krijgen van Godot heeft een parameter `body`. Dit is een variabele waarin het object zit waar we mee botsen. Allerlei objecten kunnen botsen, en niet elk object kunnen we aanvallen. 

Onze vijand, die kan aangevallen worden, gaan we dadelijk een "take_damage" functie geven. In onze `_on_hurt_box_body_entered` van de speler kunnen we aan Godot vragen of de `body` waar we mee botsen zo een functie "take_damage" heeft.
Indien ja, dan roepen we die aan.

We passen onze `_on_hurt_box_body_entered` functie aan:

```gdscript
func _on_hurt_box_body_entered(body):
	if body.has_method("take_damage"):
		body.take_damage()
```

Het "take_damage" gedeelte komt in het script van de vijand. We voegen daar deze nieuwe functie toe. `self.queue_free()` is de instructie om de vijand te laten verdwijnen. We zorgen straks voor een 'sterf' animatie.

In het script van de vijand:
```gdscript
func take_damage():
	self.queue_free()
```

Als we dit testen, merken we dat er nog een probleem is. De vijand verdwijnt wel degelijk, maar onze speler moet niet eens aanvallen! Om dit correct te doen, moeten we de hurtbox enkel actief maken wanneer het wapen neerkomt.

We zorgen dat onze hurtbox in het begin UIT staat, en geen botsingen gaat detecteren. We zetten `Monitoring` en `Monitorable` allebei uit. 
We zetten ook `Layer 1` uit bij het collision gedeelte. Dit zorgt ervoor dat onze hurbox niet meetelt als 'hard' object voor botsingen. Andere objecten in laag 1 kunnen gewoon door onze hurtbox heen bewegen. Enkel `Mask` staat op, wat wil zeggen dat onze hurbox wel gaat kijken naar andere objecten.

![Set hurtbox properties](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gr5o6fz5xvpb2s62ccc8.png)

Nu willen we onze hurtbox AAN zetten wanneer het wapen neerkomt. Als we gaan kijken in onze `AnimatedSprite2D` bij de "attack" animatie, zien we alle frames en hun nummers. In frame 4 tot 7 komt het wapen neer. We verbinden het signaal `frame_changed()` aan ons script:

![Frames with attack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g344ayc2g5t5g89cehi1.png)

Vanboven in de code van de speler maken we een tweede `@onready` variabele om aan onze hurtbox te kunnen:

```gdscript
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var hurtbox: Area2D = $HurtBox
```

De code voor onze `_on_animated_sprite_2d_frame_changed` bestaat uit enkele eenvoudige `if` instructies. Als de "attack" animatie aan frame 4 komt, zetten we `monitoring` van de hurtbox aan. Bij frame 7 terug uit.

```gdscript
func _on_animated_sprite_2d_frame_changed():
	if sprite.animation == "attack":
		if sprite.frame == 4:
			hurtbox.monitoring = true
		elif sprite.frame == 7:
			hurtbox.monitoring = false
```

De `frame_changed()` functie wordt altijd opgeroepen, zelfs als de grafische kaart te traag is om het beeld feitelijk te tonen (lage FPS).

We hebben nu dit:

![Attack enemy gone](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a87a79fmocil0jbdj4u.gif)

De vijand verdwijnt gewoon, terwijl we een sterf animatie voor onze vijand hebben. Die willen we natuurlijk gebruiken!

Het is eenvoudig om dit aan onze state machine toe te voegen. In het script van de vijand voegen we een extra state toe, `DYING`.

```gdscript
enum State {READY, ATTACK, DYING}
```

In de `take_damage()` functie gaan we niet onmiddellijk de vijand laten verdwijnen, maar gaan we naar onze nieuwe `DYING` state switchen en de juiste animatie starten.

```gdscript
func take_damage():
	if state != State.DYING:
		sprite.play("die")
		state = State.DYING
```

Dit kan je testen, het werkt, maar na de dramatische val komt onze vijand gewoon terug recht. Dat komt door onze `animation_finished` code, die we hebben geschreven voor het aanvallen.

We passen de `_on_animated_sprite_2d_animation_finished` functie dus ook aan:
```gdscript
func _on_animated_sprite_2d_animation_finished():
	if state == State.DYING:
		self.queue_free()
	else:
		state = State.READY
```

Nu verdwijnt onze vijand na het omvallen.

Het is pas helemaal af met een fade-out animatie. Een manier om een eenvoudige animatie te starten vanuit code is met een `tween`. We maken een `tween`, en voegen een aantal animatie stappen toe, die de tween dan erna gaat uitvoeren. De animatie is als volgt: eerst 3 seconden wachten, dan de kleur van onze sprite geleidelijk over laten gaan naar transparant, gedurende 1 seconde. Dan pas volgt de `queue_free()` waarmee onze vijand verdwijnt. De animatie wordt samengesteld in onze code, maar start pas nadat onze functie `_on_animated_sprite_2d_animation_finished` afgelopen is.

```gdscript
func _on_animated_sprite_2d_animation_finished():
	if state == State.DYING:
		var tween = create_tween()
		tween.tween_interval(3)
		tween.tween_property(sprite, "modulate", Color.TRANSPARENT, 1)
		tween.tween_callback(self.queue_free)
	else:
		state = State.READY
```

![Attack with fade out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iv0hdiri5jdnziq1gtdj.gif)

Het is niet erg fair dat onze vijand zich niet kan verdedigen. We gaan dezelfde stappen herhalen voor de vijand, zodat de vijand onze speler ook pijn kan doen.

In het `Scene` paneel van de minotaurus voegen we ook een `Area2D` toe met daaronder een `CollisionShape2D`. De `Area2D` geven we een ander naam: `HurtBox`.
De `CollisionShape2D` krijgt een `CapsuleShape2D` als vorm, met een `Radius` van `150 px`, een `Height` van `500 px` en een `Position` met `x` = `400 px`. De `Debug Color` kleur mag je ook aanpassen als je wil.

![Enemy hurtbox properties](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1uceqpt1o55j3mq3qb3n.png)

We verbinden ook het `body_entered(body: Node2D)` signaal.

![Connect body entered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kj43waz40uebhydpjhsw.png)

We kunnen hier dezelfde code gebruiken als voor onze speler:
```gdscript
func _on_hurt_box_body_entered(body):
	if body.has_method("take_damage"):
		body.take_damage()
```

We moeten straks gewoon zorgen dat onze speler ook een "take_damage" functie heeft.

Eerst gaan we zorgen dat de onze hurtbox enkel actief is wanneer de knuppel neerkomt.

De hurtbox van onze minotaurus passen we aan: `Monitoring` en `Monitorable` moeten UIT, en `Layer 1` moet ook UIT.

![Disable enemy hurtbox monitoring](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d39e8e6ouax9sge76yc6.png)

Dan verbinden we `frame_changed()` van de `AnimatedSprite2D` met ons script.

![Connect enemy frame_changed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uwnarc4yewqlt2g8adwm.png)

Net als bij de speler maken we een nieuwe `@onready` variabele om aan de hurtbox te kunnen vanuit de Enemy.gd code:

```gdscript
@onready var hurtbox: Area2D = $HurtBox
```

De functie voor `frame_changed` is identiek aan die van de speler, enkel de frame nummers zijn verschillend:

```gdscript
func _on_animated_sprite_2d_frame_changed():
	if sprite.animation == "attack":
		if sprite.frame == 8:
			hurtbox.monitoring = true
		elif sprite.frame == 11:
			hurtbox.monitoring = false
```

Om dit stuk al te testen, maken we tijdelijk een "take_damage" functie in het Player.gd script:

```gdscript
func take_damage():
	self.queue_free()
```

Als we dit testen, dan lijkt het niet te werken. Er is een fout in de code geslopen, blijkbaar werkt de hurtbox alleen maar aan de rechterzijde van de vijand!

![Hurtbox bug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kce4ats9jyqq94eowcm6.gif)

Dit probleem had ik zelf niet voorzien, en ik wil de vorige screenshots niet opnieuw maken :(

De oplossing is gelukkig niet moeilijk. Het probleem zit in het spiegelen van de personages als ze naar links lopen, met de code `sprite.scale.x = -1`. Daar spiegelen we enkel de `AnimatedSprite2D`. Maar de hurtbox blijft natuurlijk op dezelfde plaats staan, aan de rechterzijde van de sprite.

We lossen dit op door de hurtbox van onze minotaurus te verslepen met de muis, en een kind te maken van de `AnimatedSprite2D`. Als we dan spiegelen, worden de kind nodes ook mee gespiegeld. De hurtbox komt dan ook aan de linkerzijde te liggen.

![Move hurtbox node](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uy58c58mg0x4zb373hli.png)

Er is een kleine wijziging nodig in onze Enemy.gd code, omdat we de hurtbox gebruiken in onze code. De plaats waar de hurtbox staat is nu anders, hij is een kind geworden van de `AnimatedSprite2D`. De regel:

```gdscript
@onready var hurtbox: Area2D = $HurtBox
```

... wordt nu ...

```gdscript
@onready var hurtbox: Area2D = $AnimatedSprite2D/HurtBox
```

De bug zit ook in onze player code. Ook daar moeten we de hurtbox node verslepen in de `Scene` en de code aanpassen.

![Attack by enemy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fj8rqz388bzpdipiv96r.gif)

Er schuilt echter _nog_ een subtiele bug in ons spel. Slaat de speler naar links met zijn staf, dan verdwijnt hij zelf!

![Attack with suicide](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvlie71hjm62fe9i8pj4.gif)

Dit is een goede gelegenheid om een debug optie van Godot te demonstreren. In het `debug` menu kan je `Visible Collision Shapes` opzetten. 

![Enable visible collision shapes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4d373fo0ubuv6ctqkr4g.png)

Dan krijg je de botsingen te zien _in_ de game, en dan zien we dit:

![Hurtbox touching own hitbox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vps7fk08f7v4khb12ybw.png)

Omdat de tekening van de engel niet helemaal in het midden staat, hebben we een probleem als we spiegelen. De hurtbox van de speler raakt zijn eigen hitbox!

Er is een manier om dit op te lossen door verschillende lagen te gebruiken voor de botsingen, maar daar gaan we nu niet op ingaan.

De eenvoudigste manier is, om in de `CollisionShape2D` van de speler de `CapsuleShape2D` wat smaller te maken. We veranderen  de `Radius` naar `120 px`.

![Reduce capsule radius](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ns5vulu33r23l4wwz3rg.png)

Alles lijkt nu te werken!

![Attack with collision shapes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vm0i1uvfry8mlxx8lqem.gif)

In de plaats van een instant-death voor onze speler, willen we misschien liever meerdere levens. Krijgt de speler een mep, dan spelen we de "hurt" animatie in plaats van te verdwijnen.

We voegen een nieuwe state toe in Player.gd:
```gdscript
enum State {READY, ATTACK, HURT}
```

Onze `take_damage` maken we minder fataal:
```gdscript
func take_damage():
	if state == State.READY:
		state = State.HURT
		sprite.play("hurt")
```

Een andere kleine wijziging is ook nodig: de vijand mept veel te snel, en de speler krijgt geen kans om te ontsnappen.

Open de `2D` view van de minotaurus, en open de `SpriteFrames` van de `AnimatedSprite2D`. In de "attack" animatie gaan we de laatste frame (11) langer laten duren. We zetten de `Frame Duration` op `20`.

![Add delay to minotaur attack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thk001vucdla0p1xeqwq.png)

Hetzelfde kunnen we doen voor de speler, maar hier gebruiken we `10` frames als vertraging. De staf van de speler is wat sneller dan de knuppel van de minotaurus.

![Add delay to player attack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lqln4i02kd3xl3q8253v.png)

Dankzij de vertraging kan de speler nog weglopen wanneer de minotaurus begint te meppen, maar hij krijgt toch een stevig pak rammel!

![Taking a beating](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mrb5od5qwjyi03han1w.gif)

In de volgende delen gaan we nog toevoegen:
- nieuwe vijanden die spawnen
- een beperkt aantal levens voor de speler
- game over en een high-score systeem

In het [volgende deel](https://dev.to/bgie/een-beat-em-up-game-maken-met-godot-health-b8d) implementeren we een basis 'health' systeem voor onze player.