We hadden al een attack animatie gemaakt voor onze held, die gaan we nu gebruiken. Ons script voor de 'Player' gaan we uitbreiden.
Om aanvallen mogelijk te maken, gaan we een concept gebruiken dat vaak terugkomt in games: een 'state machine'.
Een 'state' is een toestand waarin ons personage zich bevindt. Er bestaan meerdere states, maar er is er maar eentje actief. Afhankelijk van de state die actief is, wordt er andere code uitgevoerd.
Dit wordt duidelijker met een voorbeeld: we gaan een ATTACK
state toevoegen, en als de speler in de ATTACK
state zit, mag hij niet meer bewegen tot de aanval gedaan is. De gewone state noemen we READY
, dan mag de speler wel rondlopen. Later gaan we ook de state DEAD
gebruiken.
In het eerste stuk van ons 'Player' script voegen we volgende regels toe:
enum State {READY, ATTACK, DEAD}
var state: State = State.READY
De enum
is een opsomming (enumeratie) van states die we willen gebruiken. In de variabele state
slaan we de huidige state van onze speler op.
Het gebruiken van states kan heel eenvoudig, met gewone if
s. Vaak wordt in tutorials complexe code met klassen en polymorfisme gebruikt, maar zo moeilijk moet het daarom niet zijn.
Onze bestaande code om de speler te bewegen komt nu binnen een if
/elif
blok te zitten:
func _physics_process(_delta):
if state == State.ATTACK:
pass
elif state == State.READY:
velocity.x = Input.get_axis("ui_left", "ui_right") * SPEED
velocity.y = Input.get_axis("ui_up", "ui_down") * SPEED
[...]
Dit doet natuurlijk nog niets, want state == State.READY
gaat gewoon altijd waar zijn. We moeten nog code toevoegen om een overgang tussen states te maken.
We doen dat via keyboard input, en zetten van onze state
variabele op een andere waarde.
De uiteindelijke code in _physics_process
ziet er nu zo uit:
func _physics_process(_delta):
if state == State.ATTACK:
pass
elif state == State.READY:
if Input.is_action_just_pressed("ui_accept"):
state = State.ATTACK
sprite.play("attack")
else:
velocity.x = Input.get_axis("ui_left", "ui_right") * SPEED
velocity.y = Input.get_axis("ui_up", "ui_down") * SPEED
if velocity.x < 0:
sprite.scale.x = -1
elif velocity.x > 0:
sprite.scale.x = 1
if velocity:
move_and_slide()
sprite.play("walk")
else:
sprite.play("idle")
De input "ui_accept" is standaard gekoppeld aan zowel de SPACE als de ENTER toets.
Enkel als onze speler in de READY
state zit, gaan we kijken of er een aanval gestart moet worden. Daar zetten we de state
op State.ATTACK
. Alle volgende frames wanneer _physics_process
wordt opgeroepen, zal in de bovenste 'if' enkel het stukje code bij de ATTACK
state uitgevoerd worden. Alle code om rond te lopen wordt overgeslagen. De speler moet eerst zijn attack afwerken!
Als we deze code uitvoeren merken we dat er nog iets ontbreekt: als je 1 maal een attack start, blijft de speler meppen. Er is ook nog code nodig om de aanval te stoppen.
Daarvoor gebruiken we een signaal dat van de AnimatedSprite2D
komt: animation_finished()
. We gaan code hangen aan dit signaal. Open het Node
paneel en Klik rechts op animation_finished()
. Kies daar Connect...
.
In de volgende dialoog zorg je dat we het Player
script gebruiken (dit is de standaard optie). De voorgestelde naam on_animated_sprite_blabla
voor de functie (receiver method
) is prima. Klik op Connect
.
We krijgen een nieuwe functie met pass
(niks doen) als voorlopige inhoud.
func _on_animated_sprite_2d_animation_finished():
pass # Replace with function body.
Dit veranderen we naar:
func _on_animated_sprite_2d_animation_finished():
state = State.READY
Dit kunnen we in-game testen. Spatie-balk laat de speler 1 maal meppen. Blijven indrukken = blijven meppen. Dit kan later verbeterd worden met bijvoorbeeld een cooldown timer.
Voor onze vijand doen we ongeveer hetzelfde. We maken ook een eenvoudige state machine met een state
variabele:
enum State {READY, ATTACK}
var state: State = State.READY
De hele code in _physics_process
komt een niveau dieper te zitten in een if/elif
blok:
func _physics_process(_delta):
if state == State.ATTACK:
pass
elif state == State.READY:
if target != null:
var delta: Vector2 = (target.position - self.position)
var direction = delta.normalized()
var distance = delta.length()
if distance > ATTACK_DISTANCE_MAX:
velocity = direction * SPEED
elif distance < ATTACK_DISTANCE_MIN:
velocity = direction * -SPEED
else:
velocity = Vector2()
if delta.x < 0:
sprite.scale.x = -abs(sprite.scale.x)
elif delta.x > 0:
sprite.scale.x = abs(sprite.scale.x)
if velocity:
sprite.play("walk")
else:
sprite.play("idle")
move_and_slide()
Ook hier moeten zorgen dat onze vijand effectief wisselt van state om een aanval te starten. Maar we hebben al een if/else
waar we dit kunnen inschuiven! Als de vijand net ver genoeg staat, dus tussen ATTACK_DISTANCE_MAX
en ATTACK_DISTANCE_MIN
staat, dan starten we een aanval.
if distance > ATTACK_DISTANCE_MAX:
velocity = direction * SPEED
elif distance < ATTACK_DISTANCE_MIN:
velocity = direction * -SPEED
else:
velocity = Vector2()
state = State.ATTACK
Ook niet vergeten om onze animatie correct aan te passen. Waar we normaal enkel naar velocity kijken om te kiezen tussen "walk" of "idle", gaan we nu eerst kijken of we net naar de ATTACK
state zijn gegaan. In dat geval starten we de attack animatie. De regel if velocity
wordt nu een elif velocity
.
if state == State.ATTACK:
sprite.play("attack")
elif velocity:
sprite.play("walk")
else:
sprite.play("idle")
Net als bij de speler gaan we het animation_finished()
signaal gebruiken om de aanval af te sluiten. We verbinden het signaal zoals we eerder deden:
Onze nieuwe functie die we kregen van Godot passen we aan, we veranderen de state terug naar READY
als de aanval klaar is:
func _on_animated_sprite_2d_animation_finished():
state = State.READY
Het resultaat:
Onze personages zijn nog 'onsterfelijk'... in het volgende deel zorgen we dat de speler en vijanden geraakt kunnen worden en sterven.
Top comments (0)