Een beat-em-up game maken met Godot - meppen!
Tags: beginners, gamedev, godot, tutorial
Published: 2023-05-06 10:05:19

[Terug naar deel 5](https://dev.to/bgie/een-beat-em-up-game-maken-met-godot-4-deel-5-1n6k)

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:

```gdscript
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:

```gdscript
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: 

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

![Select connect to animation_finished signal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pi956iqptyqavalv63bs.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x820f380ws9gg9lmix0n.png)

We krijgen een nieuwe functie met `pass` (niks doen) als voorlopige inhoud.
```gdscript
func _on_animated_sprite_2d_animation_finished():
	pass # Replace with function body.
```

Dit veranderen we naar:

```gdscript
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:

```gdscript
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:

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

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

```gdscript
		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:

![Connect minotaur animation finished](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vgf50zdosvdvpmx9z84.png)

Onze nieuwe functie die we kregen van Godot passen we aan, we veranderen de state terug naar `READY` als de aanval klaar is:

```gdscript
func _on_animated_sprite_2d_animation_finished():
	state = State.READY
```

Het resultaat:

![Meppen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t0znjxfoiu6rdwisrmd1.gif)

Onze personages zijn nog 'onsterfelijk'... in het [volgende deel](https://dev.to/bgie/een-beat-em-up-game-maken-met-godot-4-deel-7-1h1l) zorgen we dat de speler en vijanden geraakt kunnen worden en sterven.