Make hearts change animation when player takes damage

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By BuddyGames

So, in my 2D platformer I have the player take one damage out of 9 (3 hearts which have a full, half depleted and depleted state) every time the player comes into contact with an enemy. I have created a hearts scene separate from the player because I don’t want the hearts to move along with the player’s camera, but rather stay with the background. I’m finding it hard to update the animation of the hearts in the level from the player’s script.

Here’s how the damage system works so far:

var health: int = 



func _on_EnemyDetector_body_entered(body: PhysicsBody2D):
	print(body.name)
	if not body == self:
		health = health -1
	
	if health == 0:
		timer.start()
		$AnimationPlayer.play("Death")
		set_physics_process(false)

I don’t really want to use a health bar, I prefer Hearts. Any suggestions on what I can do to change the animations of the other nodes in the other scene?

:bust_in_silhouette: Reply From: Jimmio92

I sat down to solve your problem and I realized your real problem here: it’s a different problem entirely. 3 hearts total, and each heart has three states, one being none. So each heart is worth two. So your three hearts can represent 0-6 inclusive, not through 9. This is possibly where you’re having trouble.

Alternatively with 4 hearts, you can represent 0-8 inclusive, or 5 hearts 0-10 inclusive.

You can set the states of your three hearts using clamp and subtraction.

var heart_end = clamp(health-4, 0, 2)
var heart_mid = clamp(health-2, 0, 2)
var heart_beg = clamp(health, 0, 2)

heart_end, mid, and beg will all be 0-2 inclusive representing which “frame” to display, 2 full, 1 half, 0 empty.

I’ll try this if the other guy’s solution doesn’t work after some extra footering, but thanks for making me realise my brain fart with the number of lives lol

BuddyGames | 2021-01-29 22:27

:bust_in_silhouette: Reply From: HauntedWindow

You might want to consider using an event bus. So your player damage system node will get your singleton “event bus” node. When they get hurt, you’ll make the event bus emit a “on_player_damage” signal. A node in your hearts scene will also get your “event bus” node and connect to its “on_player_damage” signal and play the proper animation. That way your player scene and your hearts scene don’t have to know anything about each-other.

So I watched the tutorial you linked and I’ve tried it, and I’m not entirely sure what I’m doing wrong. Here’s a walkthrough:

This is in the Slime enemy scene. At the top of the script I put

signal Slime_attacked(damage)

This is in the _physics_process(delta: float) func of the Slime scene.

if $AttackRaycast1.is_colliding() or $AttackRaycast2.is_colliding():
	$AnimationPlayer.play("Attack")
	emit_signal("Slime_attacked", damage)

This is in the script for the root node of the level scene where the player, hearts and enemy instances are:

var health: int = 6
   func _ready():
    $Background/AnimationPlayer.play("GameHeart1Full")
    $Background/AnimationPlayer.play("GameHeart2Full")
    $Background/AnimationPlayer.play("GameHeart3Full")
  $Background.connect("Slime_attacked",$Enemies/Slime1,"_on_Slime_attacked")

And this is the signal func here:

func _on_Slime_attacked():
	print("Boo")
	health= health -1
	
	if health == 6:
		$Background/AnimationPlayer.play("GameHeart1Full")
		$Background/AnimationPlayer.play("GameHeart2Full")
		$Background/AnimationPlayer.play("GameHeart3Full")

I really have no experience with this side of Godot so some extra help to finish this would be amazing :slight_smile:

BuddyGames | 2021-01-29 22:26

Ok, we basically have 5 scenes:

  1. The enemy slime scene
  2. The player scene
  3. The hearts scene
  4. The level scene that contains the enemy, the player, and the hearts
  5. The autoloaded event bus scene

Let’s go over what happens when the slime attacks:
When your slime attacks, you’ll use your raycast nodes to determine if they hit the player and if they did, you’ll get the Player’s collider:

var player_collider = null
if $AttackRaycast1.is_colliding():
    player_collider = $AttackRaycast1.get_collider()
elif $AttackRaycast2.is_colliding():
    player_collider = $AttackRaycast2.get_collider()
    

After that, you’ll want the slime to communicate directly with the player to let it know it’s been damaged. However, you’ll want to do this in a way that assumes very little about the node structure in the player scene. So we’ll use propagate_call so that any node inside the player scene that has a take_damage method will have that method called:

if player_collider != null:
    player_collider.propagate_call("take_damage", [damage_amount])

OK, so inside your player scene you’ll make a node and call it something like player_health. That node will have a take_damage method that looks something like this:

func take_damage(amount):
    health -= amount
    $event_bus.emit_signal("on_player_damaged", health)

That event bus is an autoloaded scene that has a single node. The event bus will have a single node that has this in it:

signal on_player_damaged(new_health)

Your event bus acts a a global messaging system. Nodes can emit these “global” events by emitting them from the event bus. Nodes that need to receive these events connect to the signal on the event bus. This allows your player to notify your hearts that the player has been damaged without the hearts needing to know anything about the player. So your hearts will have some code that looks something like this:

_ready():
    $event_bus.connect("on_player_damaged", self, "_on_player_damaged")

func _on_player_damaged(health):
    play the heart animation

As your game develops, you’ll add more signals to the event bus as you need them.

HauntedWindow | 2021-01-31 20:10