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:
Nu gaan we code schrijven om onze vijand te laten bewegen. Open de minotaurus scene
en het script voor de vijand.
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
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")
Hoe werkt dit? We hebben in onze 'game' scene ervoor gezorgd dat zowel de 'Player' als de 'Minotaur' kinderen zijn van de 'Arena' 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")
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")
Als je nog nooit van vectoren of coördinaten hebt gehoord, geen probleem, hier in het kort wat er gebeurt:
De posities van de 2 personages kan je in getallen uitdrukken.
- De horizontale as (links-rechts) noemen we
x
. De speler staat opx
=4000. - De verticale as (boven-onder) noemen we
y
. De speler staat opy
=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()
Dit kunnen we testen met Run Project
of F5
. Daar komt ie!
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
... 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)
Het andere probleem, dat onze minotaurus eeuwig blijft lopen, kunnen we oplossen met een paar extra if
s.
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
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 if
s schrijven.
var distance = delta.length()
if distance > ATTACK_DISTANCE_MAX:
velocity = direction * SPEED
elif distance < ATTACK_DISTANCE_MIN:
velocity = direction * -SPEED
else:
velocity = Vector2()
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()
In het volgende deel starten we met het gevecht systeem.
Top comments (0)