Terug naar deel 6
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':
Vervolgens gaan we de hurtbox voor het wapen maken. Bij de speler voegen we 2 nodes toe, een Area2D
en daaronder een CollisionShape2D
:
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.
Zoals we al eerder deden, moeten we nu een Shape
toevoegen aan onze CollisionShape2D
. Hiervoor gebruiken we een CapsuleShape2D
.
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.
Onze hurtbox noemt nog Area2D
, de standaard naam. Het is duidelijker als we deze van naam te veranderen:
Onze code hangen we aan het signaal body_entered(body: Node2D)
dat hoort bij onze hurtbox. (In de screenshot heet de hurtbox nog Area2D
)
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:
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:
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.
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:
Vanboven in de code van de speler maken we een tweede @onready
variabele om aan onze hurtbox te kunnen:
@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.
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:
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
.
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.
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:
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.
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
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.
We verbinden ook het body_entered(body: Node2D)
signaal.
We kunnen hier dezelfde code gebruiken als voor onze speler:
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.
Dan verbinden we frame_changed()
van de AnimatedSprite2D
met ons script.
Net als bij de speler maken we een nieuwe @onready
variabele om aan de hurtbox te kunnen vanuit de Enemy.gd code:
@onready var hurtbox: Area2D = $HurtBox
De functie voor frame_changed
is identiek aan die van de speler, enkel de frame nummers zijn verschillend:
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:
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!
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.
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:
@onready var hurtbox: Area2D = $HurtBox
... wordt nu ...
@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.
Er schuilt echter nog een subtiele bug in ons spel. Slaat de speler naar links met zijn staf, dan verdwijnt hij zelf!
Dit is een goede gelegenheid om een debug optie van Godot te demonstreren. In het debug
menu kan je Visible Collision Shapes
opzetten.
Dan krijg je de botsingen te zien in de game, en dan zien we dit:
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
.
Alles lijkt nu te werken!
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:
enum State {READY, ATTACK, HURT}
Onze take_damage
maken we minder fataal:
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
.
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.
Dankzij de vertraging kan de speler nog weglopen wanneer de minotaurus begint te meppen, maar hij krijgt toch een stevig pak rammel!
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 implementeren we een basis 'health' systeem voor onze player.
Top comments (0)