So far, we made only a generic unit (base Unit
). Although the scene (in previous tutorials) had a warrior character, but the script that was attached was Unit.gd. It only knew how to walk & stay idle (i.e., only had states WALKING
& IDLE
).
In this post, we will extend Unit, and create an actual Warrior. The warrior will have additional states such as FIGHTING
& FOLLOWING_TARGET
(chasing). The warrior will handle these states & its transitions.
Getting Started with our Military Unit
The core idea is to create a script named “Warrior.gd” (or “MilitaryUnit.gd”, whatever you prefer). And then attach this script to the warrior scene (which is same as older scene we used for the Unit
, if it has fighting animations). In the warrior script, extend from base Unit
class:
extends Unit
# Additional states for Warrior
enum WarriorState {
ATTACKING = 100, FOLLOWING_TARGET,
}
var attack_range = 5
var currently_attacking: Unit = null
func _ready():
super() # call base class _ready() function
func _process(delta):
super(delta) # call base class _process() function
func _physics_process(delta):
super(delta) # call base class _physics_process() function
Trigger the warrior to attack a Unit
In RTS games, we typically select a military (warrior) unit & order them to attack some other (enemy) unit. This is the reason why I declared currently_attacking
variable above. Add following code under the _physics_process
method to implement this logic:
func _physics_process(delta):
# ... PREVIOUS CODE...
if Input.is_action_just_pressed("left_mouse_button") and self in get_tree().get_nodes_in_group("selected_units"):
var target = RaycastSystem.get_raycast_hit_object(0b00000000_00000000_00000000_00000010)
if target:
if target in get_tree().get_nodes_in_group("units") and target != self:
currently_attacking = target
else:
currently_attacking = null
The above piece of code determines who we are currently_attacking
. Again, I used our raycast system which we made in earlier tutorials & passed collision_mask
for units in the parameter.
Handling Warrior States & Transitions
So far, we only assigned who to attack. But our warrior will likely do nothing more than moving to the target position, due to the influence of base class, which makes the current state to be MOVING
when we click somewhere in the map.
To make use of warrior states, we override _check_state_transitions
and _process_all_states
methods of our base Unit
class. Lets do this.
# @Override
func _check_state_transitions(delta: float):
super(delta) # Must call base class method so some states are handled by the base class
# Handle attacking and following
if currently_attacking:
if currently_attacking.health <= 0: # If enemy is dead
current_state = UnitState.IDLE # become idle
return # no need to process more states, return immediately
var distance = (self.global_position * Vector3(1, 0, 1)).distance_to(currently_attacking.global_position * Vector3(1, 0, 1))
if distance > attack_range:
current_state = WarriorState.FOLLOWING_TARGET
else:
current_state = WarriorState.ATTACKING
else:
# Reset state if not attacking
if current_state in [WarriorState.ATTACKING, WarriorState.FOLLOWING_TARGET]:
current_state = UnitState.IDLE
# @Override
func _process_all_states(delta: float):
super(delta)
_process_following_target_state()
_process_attacking_state(delta)
Again, I assigned the state based on different conditions (in _check_state_transitions
). There must be a condition that needs to be true
to lead to a transition (change of state). Also, I called super()
to make sure the base class handles the states that are not handled by the Warrior class.
_process_all_states
also handles the warrior states, but they are not implement right now. Lets implement them:
# Basically walking logic replicated. TODO: apply DRY principle
func _process_following_target_state():
if current_state == WarriorState.FOLLOWING_TARGET:
if current_agent_path_index < current_agent_path.size():
var target_pos = current_agent_path[current_agent_path_index]
target_pos.y = self.global_position.y
var direction = (target_pos - self.global_position).normalized()
var distance = self.global_position.distance_to(target_pos)
# Update velocity for move_and_slide
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Play walk animation
animation_player.play("Walk")
# Rotate towards the target only along y-axis
look_at(target_pos, Vector3.UP)
# Check if agent reached the current target position
if distance < 0.5:
# Move to the next waypoint
current_agent_path_index += 1 # Move to the next waypoint
print("Reached waypoint:", current_agent_path_index)
else:
calculate_path(currently_attacking.global_position)
func _process_attacking_state(delta):
if current_state == WarriorState.ATTACKING:
if currently_attacking:
# Attack the target
animation_player.play("Sword_Attack2")
animation_player.get_animation("Sword_Attack2").loop_mode = Animation.LOOP_LINEAR
look_at(currently_attacking.global_position, Vector3.UP)
currently_attacking.health -= 12 * delta # Damage per frame
Note: Notice the logic for following a target (in _process_following_target_state
) is same as unit moving. Now here comes the first mistake that I think I made. If we keep coding in this way, we will soon end up with tons of functions that implement the same logic. We are going against DRY principle in programming. – In my opinion, it will be better if we somehow dissolve the chasing state & make it an application of MOVING
state implemented by the base Unit
, since there are no differences in their logic, other than that the target is not static in case of FOLLOWING_TARGET
.
In the other function (_process_attacking_state
), it just looks at the enemy and keeps playing attack animation. It also reduce the health every frame by small amount.
Finally, a very slight change. We override _process_idle_state
of base Unit, only to make currently_attacking
null
in case state becomes IDLE
:
# @Override
func _process_idle_state():
super() # Rest of logic of idle remains same, thus calling super()
if current_state == UnitState.IDLE:
currently_attacking = null
Congrats, we created a warrior that can fight!