DEV Community

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

Posted on • Edited on

Een beat-em-up game maken met Godot - meppen!

Terug naar deel 5

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

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 ifs. 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
        [...]
Enter fullscreen mode Exit fullscreen mode

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

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

Select connect to animation_finished signal

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.

connect to animation_finished signal

We krijgen een nieuwe functie met pass (niks doen) als voorlopige inhoud.

func _on_animated_sprite_2d_animation_finished():
    pass # Replace with function body.
Enter fullscreen mode Exit fullscreen mode

Dit veranderen we naar:

func _on_animated_sprite_2d_animation_finished():
    state = State.READY
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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:

Connect minotaur animation finished

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

Het resultaat:

Meppen

Onze personages zijn nog 'onsterfelijk'... in het volgende deel zorgen we dat de speler en vijanden geraakt kunnen worden en sterven.

Top comments (0)