DEV Community

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

Posted on • Updated on

Een beat-em-up game maken met Godot - bewegende vijand

Terug naar deel 4

Wat hebben we nu al gemaakt? Een wereld met een speler die kan rondlopen, en een vijand, die gewoon wat staat te staan... nog niet bepaald een leuke game...

Wat willen we: vijanden die net buiten beeld vertrekken en de speler komen aanvallen. We gaan ons werk in 3 delen splitsen:

  • vijanden moeten naar de speler toe lopen
  • vijanden en de speler moeten kunnen vechten en doodgaan.
  • net buiten beeld moeten telkens nieuwe vijanden 'spawnen'

We starten met het lopen van de vijand. Plaats eerst de speler en de vijand op een goede startpositie in de wereld. De speler start links-midden, en de vijand uiterst rechts net buiten beeld:

Starting positions

Nu gaan we code schrijven om onze vijand te laten bewegen. Open de minotaurus scene en het script voor de vijand.

Open minotaur script

Net als bij de speler gaan we een 'constante' maken voor de snelheid waarmee dit personage beweegt. We zetten hem op 250, langzamer dan de speler, zodat de speler nog kan weglopen. Deze regel komt in het bovenste deel van het script:

const SPEED = 250.0
Enter fullscreen mode Exit fullscreen mode

De beweging van onze vijand hangt niet af van het toetsenbord, zoals bij onze speler, maar van de positie van die speler tegenover onze vijand.

De code om te bewegen komt bij in onze bestaande _physics_process functie die we al hadden.

Eerste stap is het opzoeken van de node van onze player. Dit steken we in een nieuwe variabele 'target', het doelwit van onze minotaurus.

func _physics_process(_delta):
    var target = self.get_parent().get_node("Player")

    sprite.play("idle")
Enter fullscreen mode Exit fullscreen mode

Hoe werkt dit? We hebben in onze 'game' scene ervoor gezorgd dat zowel de 'Player' als de 'Minotaur' kinderen zijn van de 'Arena' node.

Arena parent node

In bovenstaande code starten we met self, de Minotaurus node (waar het script aanhangt). get_parent geeft dan de 'Arena' node erboven. Doen we dan get_node("Player") om tussen de child nodes van 'Arena' te zoeken naar een node met de naam 'Player'. Dat is degene die we willen hebben, en die slaan we op in onze nieuwe variabele var target.

Volgende stap is controleren of we wel een tegenstander hebben gevonden.

func _physics_process(_delta):
    var target = self.get_parent().get_node("Player")
    if target != null:
        # movement code 

    sprite.play("idle")
Enter fullscreen mode Exit fullscreen mode

Enkel als de node 'Player' gevonden is (misschien is de speler al dood?) dan gaan we onze minotaurus laten bewegen. null is een speciale waarde die 'niets' betekend. Enkel als onze target NIET null is, gaan we verder.

Nu volgt een stukje wiskunde, om de beweging van onze minotaurus te berekenen.

Denk je ooit tijdens de les wiskunde: waarom moet ik dit allemaal kennen? Wel, computergames gebruiken een hoop wiskunde! En schrijf je uiteindelijk een succesvolle game? Dan heb je ook wiskunde nodig om je hopen geld te kunnen tellen.

De code is:

func _physics_process(_delta):
    var target = self.get_parent().get_node("Player")
    if target != null:
        var delta = (target.position - self.position)
        var direction = delta.normalized()
        velocity = direction * SPEED

    sprite.play("idle")
Enter fullscreen mode Exit fullscreen mode

Als je nog nooit van vectoren of coördinaten hebt gehoord, geen probleem, hier in het kort wat er gebeurt:

Coordinates

De posities van de 2 personages kan je in getallen uitdrukken.

  • De horizontale as (links-rechts) noemen we x. De speler staat op x=4000.
  • De verticale as (boven-onder) noemen we y. De speler staat op y=500.
  • Samen noemen we dat de coördinaten van de speler (x,y). De speler staat op (4000, 500), de minotaurus staat op (5000, 1000).

We kunnen rekenen met coördinaten, zoals we met eenvoudige getallen doen.

  • Het verschil tussen de positie van de speler (target) en de minotaurus (self) geeft ons de blauwe pijl op de tekening.
  • In code uitgedrukt: target.position - self.position.
  • In ons voorbeeld is de uitkomst (4000,500) - (5000, 1000) = (-1000, -500).

We slaan dit op in variabele delta.
Delta geeft ons de richting en afstand naar de target.

We willen enkel de richting gebruiken, en dan met een vaste snelheid een stukje bewegen in die richting. Zonder te veel details te geven, maar normalized doet net dat, en geeft als uitkomst de groene pijl.

De groene pijl is een pijl met lengte 1 met dezelfde richting als de blauwe pijl. Dit slaan we op in var direction.

De laatste regel velocity = direction * SPEED gebruikt de groene pijl (direction) en onze constante snelheid (SPEED) om de snelheid van bewegen voor onze minotaurus te bepalen. Dit steken we in velocity, een bestaande variabele die bij onze CharacterBody2D hoort.

Nu volgt enkel nog het bewegen zelf, met de juiste animatie.

Die code is gelijkaardig aan de code van de speler. Als onze minotaurus snelheid heeft (velocity is niet nul) dan spelen we de 'walk' animatie. Anders spelen we 'idle'. De move_and_slide() doet het feitelijke bewegen op basis van de velocity, rekening houdend met botsingen.

Alles te samen ziet onze code er nu zo uit:

extends CharacterBody2D

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

const SPEED = 250.0

func _physics_process(_delta):
    var target = self.get_parent().get_node("Player")
    if target != null:
        var delta = (target.position - self.position)
        var direction = delta.normalized()
        velocity = direction * SPEED

    if velocity:
        sprite.play("walk")
    else:
        sprite.play("idle")

    move_and_slide()
Enter fullscreen mode Exit fullscreen mode

Dit kunnen we testen met Run Project of F5. Daar komt ie!

Image description

Merk op dat onze vijand nogal 'plakkerig' is... hij stopt nooit en loopt tot tegen de speler, en blijft maar lopen.

Hij loopt ook achteruit, maar dat kennen we al en kunnen we oplossen zoals bij de speler. Er is een klein verschil, we hebben de scale van onze minotaurus op 1.5 gezet omdat hij wat klein was.

Als we dezelfde code gebruiken als bij de speler:

    if velocity.x < 0:
        sprite.scale.x = -1
    elif velocity.x > 0:
        sprite.scale.x = 1
Enter fullscreen mode Exit fullscreen mode

... dan gaat onze scale terug verspringen naar 1 of -1, en is onze minotaurus terug klein.

Met de wiskundige functie abs kunnen we dit oplossen. abs geeft de absolute waarde van een getal, wat wil zeggen dat een min-teken wordt weggelaten als dit er is, elk getal wordt positief gemaakt.

In de volgende code behouden we het getal dat in sprite.scale.x zit, en veranderen we enkel het teken van plus naar min of omgekeerd, zodat de scale van de minotaurus van 1.5 behouden blijft.

    if velocity.x < 0:
        sprite.scale.x = -abs(sprite.scale.x)
    elif velocity.x > 0:
        sprite.scale.x = abs(sprite.scale.x)
Enter fullscreen mode Exit fullscreen mode

Het andere probleem, dat onze minotaurus eeuwig blijft lopen, kunnen we oplossen met een paar extra ifs.

Als de minotaurus ver weg is, willen we dat hij naar de speler toeloopt. Is hij te dichtbij, dan moet hij naar achteren. Zit hij op de ideale afstand, dan mag hij blijven staan, dan gaat hij aanvallen. De afstanden steken we in een paar constanten:

const ATTACK_DISTANCE_MAX = 520.0
const ATTACK_DISTANCE_MIN = 480.0
Enter fullscreen mode Exit fullscreen mode

De logica zelf komt bij in onze _physics_process. We berekenen de afstand tussen onze minotaurus en de speler uit de variabele delta die we al hadden. Met die afstand kunnen we ifs schrijven.

        var distance = delta.length()
        if distance > ATTACK_DISTANCE_MAX:
            velocity = direction * SPEED
        elif distance < ATTACK_DISTANCE_MIN:
            velocity = direction * -SPEED
        else:
            velocity = Vector2()
Enter fullscreen mode Exit fullscreen mode

Het uiteindelijke script:

extends CharacterBody2D

@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D

const SPEED = 250.0
const ATTACK_DISTANCE_MAX = 520.0
const ATTACK_DISTANCE_MIN = 480.0

var target: Node2D

func _ready():
    target = self.get_parent().get_node("Player")

func _physics_process(_delta):
    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

In het volgende deel starten we met het gevecht systeem.

Top comments (0)