Creating the Battler
NOTE:A precise responsibility can lead to either large or small classes, depending
on the context. For example, our Battler class will represent a battler as a
whole, and in a complete game project, it may grow in size throughout
development. You can learn more in our video Programming Principles: Single
Responsibility in Godot.
Using exported properties for behavior
BattlerStats
, BattlerAI
, and ActionData
classes that define the battler's behavior and abilities in other lessons.extends Node2D
class_name Battler
# Resource that manages both the base and final stats for this battler.
export var stats: Resource
# If the battler has an `ai_scene`, we will instantiate it and let the AI make decisions.
# If not, the player controls this battler. The system should allow for ally AIs.
export var ai_scene: PackedScene
# Each action's data stored in this array represents an action the battler can perform.
# These can be anything: attacks, healing spells, etc.
export var actions: Array
NOTE:When prototyping, I often start with properties like these as they help me
build a map of the classes and systems I will have to code. For instance, in
an RPG game like this one, I know that the battler needs stats, actions, and
could be AI-controlled.
# If `true`, this battler is part of the player's party and it targets enemy units
export var is_party_member := false
Knowing when the battler is ready to act
_readiness
value that goes up every frame until it reaches 100.0
._readiness
on a battler's turn.# The turn queue will change this property when another battler is acting.
var time_scale := 1.0 setget set_time_scale
# When this value reaches `100.0`, the battler is ready to take their turn.
var _readiness := 0.0 setget _set_readiness
_readiness
and _set_readiness
is a convention meaning the variable is pseudo-private: you are only meant to use it inside the Battler.gd
script.- When
_readiness
is greater or equal to100.0
, which means the battler is ready to act. - When
_readiness
changes, to notify the user interface.
# Emitted when the battler is ready to take a turn.
signal ready_to_act
# Emitted when the battler's `_readiness` changes.
signal readiness_changed(new_value)
_process()
and the setter functions.func _process(delta: float) -> void:
# Increments the `_readiness`. Note stats.speed isn't defined yet.
# You can also write this self._readiness += ...
_set_readiness(_readiness + stats.speed * delta * time_scale)
# We will later need to propagate the time scale to status effects, which is why we use a
# setter function.
func set_time_scale(value) -> void:
time_scale = value
# Setter for the `_readiness` variable. Emits signals when the value changes and when the battler
# is ready to act.
func _set_readiness(value: float) -> void:
_readiness = value
emit_signal("readiness_changed", _readiness)
if _readiness >= 100.0:
emit_signal("ready_to_act")
# When the battler is ready to act, we pause the process loop. Doing so prevents _process from triggering another call to this function.
set_process(false)
var is_active: bool = true setget set_is_active
# ...
func set_is_active(value) -> void:
is_active = value
set_process(is_active)
Selecting the battler
# Emitted when modifying `is_selected`. The user interface will react to this for player-controlled battlers.
signal selection_toggled(value)
# If `true`, the battler is selected, which makes it move forward.
var is_selected: bool = false setget set_is_selected
# If `false`, the battler cannot be targeted by any action.
var is_selectable: bool = true setget set_is_selectable
func set_is_selected(value) -> void:
# This defensive check helps us ensure we don't attempt to change `is_selected` if the battler isn't selectable.
if value:
assert(is_selectable)
is_selected = value
emit_signal("selection_toggled", is_selected)
func set_is_selectable(value) -> void:
is_selectable = value
if not is_selectable:
set_is_selected(false)
# Returns `true` if the battler is controlled by the player.
func is_player_controlled() -> bool:
return ai_scene == null
The code so far
Battler.gd
should look so far.# Character or monster that's participating in combat.
# Any battler can be given an AI and turn into a computer-controlled ally or a foe.
extends Node2D
class_name Battler
signal ready_to_act
signal readiness_changed(new_value)
signal selection_toggled(value)
export var stats: Resource
export var ai_scene: PackedScene
export var actions: Array
export var is_party_member := false
var time_scale := 1.0 setget set_time_scale
var is_active: bool = true setget set_is_active
var is_selected: bool = false setget set_is_selected
var is_selectable: bool = true setget set_is_selectable
var _readiness := 0.0 setget _set_readiness
func _process(delta: float) -> void:
_set_readiness(_readiness + stats.speed * delta * time_scale)
func is_player_controlled() -> bool:
return ai_scene == null
func set_time_scale(value) -> void:
time_scale = value
func set_is_active(value) -> void:
is_active = value
set_process(is_active)
func set_is_selected(value) -> void:
if value:
assert(is_selectable)
is_selected = value
emit_signal("selection_toggled", is_selected)
func set_is_selectable(value) -> void:
is_selectable = value
if not is_selectable:
set_is_selected(false)
func _set_readiness(value: float) -> void:
_readiness = value
emit_signal("readiness_changed", _readiness)
if _readiness >= 100.0:
emit_signal("ready_to_act")
set_process(false)
Footnotes
-
Composition over inheritance is a principle in Object-Oriented Programming (OOP) that states that to make an object flexible and reuse code, we should favor instancing other classes that implement desired behavior over inheriting functionality from a parent class. ↩ -
Aggregation is a particular kind of composition where an object's children or the objects it references can function independently from it. Godot's scene system is built for aggregation: you can run any scene in isolation in the editor, by pressing F6. ↩
Lesson Q&A
Use this space for questions related to what you're learning. For any other type of support (website, learning platform, payments, etc...) please get in touch using the contact form.