GodotCourse/week6
2025-05-26 17:33:42 -04:00
..
example/UIDemo ui example 2024-08-12 17:45:20 -04:00
original_project week 6 files scene and game manager 2024-08-19 11:21:33 -04:00
updated_project adding player hurt 2024-10-07 19:20:42 -04:00
README.md added documentation on how to damage enemies 2025-05-26 17:33:42 -04:00

Resources

Resources are essentially data containers. Much like nodes, they can hold functions, properties, and signals.

Resources do nothing on their own. They must be a property of a node for their data to be accessed.

The resoource only gets loaded into memory once. Any nodes referencing a node are accessing the SAME data. This behavior can be changed so that the resource is unique for each node referencing it.

Lessons:

  • Create a script to use as a data object
  • Create a resource based on the script for the player, and the enemy
  • Learn to load resources

Lessons:

  • Create a script to be the game controller, set it to autoload

Relevance to UI

A good UI is essentially a graphical representation of the state of the game. Once that state is centralized in one place it becomes easier to make UI that accurately reflects all the data in the game (such as player health, coins collected, points, timers etc.)

Our goals for UI:

  • Create a Canvas Layer to float the UI over the viewport
  • Create a Control node to contain our UI nodes
  • Create Container nodes (vbox and hbox)
  • Use a MarginContainer node to wrap our UI nodes

Custom Resources

Exercise 1: Use a custom resource to track the player health.

First create a script to use as the basis for the custom resource. This script will include any relevant data we might want to keep track of for characters in our game.


class_name CharacterStats extends Resource

@export var max_health:int = 100
@export var starting_health:int = 100
@export var health:int = 100

@export var meleeDamage:int = 10
@export var rangeDamage:int = 8

Once the base class is created, custom resources are used to fill in specifics. In this case we can create custom resources for the player and the slime bad guys.

Create a custom resource called playerstats that extends CharacterStats. Assign health and damage values as you wish through the Godot UI.

In gamecontroller load in the player resource at the ready function.

player = load("res://scripts/res/playerStats.tres")

You can also load in the slime resource if you have created that.

This will be used for all the data related to the player.

  1. In the reset function, return the player to full health.

playerHealth = player.max_health

  1. In the player damage function, have the player's health get removed based on the melee attack strength of the enemy.

playerHealth -= slime.meleeDamage

Update our Bad Guy

We should update our slime so they don't run away from the player!

func _process(delta: float) -> void:
	if not right_down_cast.is_colliding():
		direction = -1
		sprite.flip_h  = true
	if right_side_cast.is_colliding():
		if not right_side_cast.get_collider() is Player:
			direction = -1
			sprite.flip_h  = true
			
	if not left_down_cast.is_colliding():
		direction = 1
		sprite.flip_h = false
	if left_side_cast.is_colliding():
		if not left_side_cast.get_collider() is Player:
			direction = 1
			sprite.flip_h = false
	position.x += direction * speed * delta

Damage and Death Animations

Time to update the character so they have a little life when they die. As a reminder, the GameController emits two custom signals - one for player damage, and another for player death.

The SceneManager has wired up those signals to make their way to the player.

Lets update the player controller to add on hurt and death animations.

First we should add two additional states to our state machine.

enum State{IDLE, RUN, JUMP, FALLING, MELEE, HURT, DEATH}

Now we can update the handlers for the signals coming from the game controller.

func _player_damage():
	print("Player taking damage!")
	current_state = State.HURT
func _player_death():
	print("Player dead!!")
	current_state = State.DEATH

Then we can update the state animations to play the hurt or death animations. In the update_animations function:

State.HURT:
	player_graphic.play("hurt")
State.DEATH:
	player_graphic.play("death")

Lastly we can update our listener function that knows when animations have completed. In _on_animation_finished:

State.HURT:
	current_state = State.IDLE
State.DEATH:
	deathComplete.emit()

The only other thing to do in the player code is to ensure that the player can not keep moving around while they are stunned or dying. This is pretty straightforward. We can do it by simply ignoring user input while they are being damaged.

In _physics_process:

if current_state != State.HURT and current_state != State.DEATH:
		handle_input()

Creating a UI

A good UI is a reflection of all the data in the game controller, and we have some good data to work with now. This includes:

  • Number of coins collected
  • Player health
  • Number of crates remaining
  • Time remaining
  • Level number

With the way we have set our architecture, getting this information to the UI is simply a matter of wiring up listeners in the SceneManager.

As an example, we could start with the time remaining.

The process to hook this up looks like this:

  1. Create a custom signal in the Gamecontroller
  2. Create a handler function in the UI
  3. In the SceneManager, connect the custom signal to the handler.
  4. Send out the custom signal in the Gamecontroller

Damaging Bad Guys (or anything)

While we have only one player character, we have many bad guys.

As our player runs around, they will damage various enemies at various times. How can the Gamecontroller keep track of it all?

The answer is to use a collection called a Dictionary.

This function is added to the Gamecontroller:

func addEnemyToLevel(enemy):
	#assign a health using the enemy as a key
	var enemyStat = {
		"health": slime.health,
		"damage": slime.meleeDamage
	}
	enemiesDict[enemy] = enemyStat

At the SceneManager, we call this function to assign every bad guy into the dictionary.

if enemies:
		for obj in enemies.get_children():
			if obj is Slime:
				obj.playerDamageSignal.connect(Gamecontroller._on_player_damage)
				Gamecontroller.addEnemyToLevel(obj)

Now we can update our function in the Gamecontroller that knows when a bullet makes contact.

func bulletDamage(body, bullet):
	print("Game controller knows about bullet hit")
	if body is Slime:
		enemiesDict[body]["health"] -= player.rangeDamage
		if enemiesDict[body]["health"] <=0:
			destroySignal.emit(body)
			enemiesDict.erase(body)

With this set up, we can now track the health of individual bad guys in the level, and we can damage them based on the attack strength of our player!