In this lesson, we will guide you through the process of creating mobs for your game.
Here’s what we will do:
The mob system is the most complex part of this project, so we will take the time to create the flying shield together, step-by-step.
First, let’s explore the provided Mob scene and script.
Open the file res://mobs/Mob.tscn
.
Prod around, and try to guess what each node is for.
The names and types of nodes should be enough to give you an idea of their purpose, but in case there’s a doubt, here’s a rundown of the scene.
Let’s start with the two areas:
The
of each area should be unique to each mob. Some mobs will detect the robot from very far away, and others will need to be close. Some will attack from far away, and some will only attack when the robot is next to them.Let’s now go over the other nodes:
is the way the mob looks. We will change it for each different mob.
The
has an Alert child (also a ), which is by default shown when the mob is alert (when the robot has entered the DetectionArea).DieSound and HurtSound are audio stream players for when the mob dies or gets hurt.
CoolDownTimer is the time between attacks. You can set the time differently on different mobs. Some attack often, and some have longer cooldowns.
BeforeAttackTimer is the time between when a mob has decided to attack and when the attack launches. This is typically less than a second and gives the player some time to avoid the attack.
contains a few animations:
Another scene to know is res://mobs/Cannon.tscn
and its
accompanying script, res://mobs/Cannon.gd
. Open them, and
take a look. You should be familiar with that type of code already.
The Cannon is just a
with a . It receives a bullet scene and can then fire it when needed. It’s supposed to be attached to mobs and allows them to fire.Note: The
has no texture. We could use a instead. But using a is convenient as we can assign a texture to see it and make sure it rotates correctly when debugging. We might also actually assign a cannon texture in a later mob.The code is similar to what you’ve programmed for the turrets in the
tower defense series. We use the look_at()
function to turn
towards a target, and then some code instantiates a bullet and orients
it in the proper direction.
Let’s now look at the mob code.
Open res://mobs/Mob.gd
.
Look at the left of the script, at the Outliner. This will give you a summary of all the functions available in this script.
Please take a moment to read the script yourself and try to see how much you understand.
Don’t be intimidated by the script’s length. We wrote it piece by piece; not all at once! You should also read it bit by bit without letting the size intimidate you.
The general way the script works is as follows:
_target
variable will update to point at the
player’s robot. When the robot enters the DetectionArea,
_target
is set, and the Active sprite becomes
visible._target_within_range
is a boolean that becomes
true
when the robot is within attack range (inside the
AttackArea
).take_damage()
is called,
which plays an animation and removes health. If no health remains,
_die()
is called, which plays another animation, and
removes the mob from the scene.There’s no other default behavior. The rest is up to inherited mob scripts to implement. By default, a mob doesn’t attack or do anything.
But the mob script also offers a few convenient functions to be used in your mobs:
follow(target_global_position: Vector2)
: moves the mob
in direction of the provided position. You will probably use this to
follow the robot, but you could also make it follow any other point of
interest, be it another mob or a pickup.orbit_target()
: makes the mob orbit around the robot.
It uses the exported orbit_distance
variable to decide how
far it will orbit.is_ready_to_attack()
: is a shortcut that makes sure the
mob can attack. A mob should only attack if the robot is in view and if
both CoolDownTimer and BeforeAttackTimer are not
running.Let’s now inherit the mob scene and script to code the shield. We’ll go step by step through creating this first mob, as this is undoubtedly the most challenging part of the project.
You will then have the opportunity to create new mobs by yourself.
Our first enemy will fly around the robot and shoot from time to time.
Right-click Mob.tscn
, and select New Inherited
Scene. Name the top node Shield, and save the scene inside
the res://mobs/shield/
directory.
Then, drag res://mobs/shield/shield_inactive.png
to the
, and
res://mobs/shield/shield_active.png
to the
Sprite/Active
node.
Drag the Cannon.tscn
scene to the Shield root
node.
Pick a bullet scene to feed the Cannon. Drag it to its Bullet Scene property.
Finally, extend the script.
Name the script Shield.gd
and open it.
As usual, we create a reference for the nodes we will need:
onready var _cannon := $Cannon
We write a small function that starts the winding-up timer. Without the timer, the mob would shoot as soon as the robot entered the attack area. That doesn’t look too good. Instead, we want the mob to orbit the player a bit first.
func _prepare_to_attack() -> void:
if not is_ready_to_attack():
return
_before_attack_timer.start()
We can call this function on each frame. Remember that
is_ready_to_attack()
will return false
if
BeforeAttackTimer is started. Therefore, every call to this
function after the first will return without doing anything.
Once the timer finishes counting, it runs
_on_BeforeAttackTimer_timeout()
. This function will perform
the attack:
# The wind-up timer has finished, we can attack.
func _on_BeforeAttackTimer_timeout() -> void:
# The target might have exited the range while the timeout was running, so
# we check again
if not _target:
return
# Finally, we shoot.
_cannon.shoot_at_target(_target)
_cooldown_timer.start()
After those two utility functions, we finally approach
_physics_process()
, where most of the code will go. First,
we check that there is a target. If there isn’t, the robot hasn’t
entered DetectArea, so we do nothing.
func _physics_process(delta: float) -> void:
if not _target:
return
If there is a target, we turn the cannon towards it and check if the robot has entered the AttackArea. If so, we orbit around the robot and we start the attack.
func _physics_process(delta: float) -> void:
#...
_cannon.look_at(_target.global_position)
if _target_within_range:
orbit_target()
# We can call this on every frame, it's not a problem; if the
# _before_attack_timer is already started, nothing will happen (on most
# frames, this function does nothing)
_prepare_to_attack()
If there is a target that isn’t within attack range, the robot is still within DetectArea but too far for orbiting or attacking. Then, we follow the robot:
func _physics_process(delta: float) -> void:
#...
else:
follow(_target.global_position)
Here’s the breakdown of what happens when the robot enters the AttackArea:
_prepare_to_attack()
will be called. It will run
BeforeAttackTimer_prepare_to_attack()
will be
called, but because BeforeAttackTimer started, the function
will do nothing._prepare_to_attack()
will also
be called, but because CoolDownTimer is started, the function
will still do nothing.1
.That’s it! You’ve made your first mob. Open
TestRoom.tscn
, and drop the mob there. Run the scene to see
the shield follow, orbit around the robot, and shoot at the player.
Before we challenge you to make more mobs, please take a time to tweak the shield’s properties in the Inspector and try see what feels right.
By doing this, you are shifting from code to game design. Tweaking the parameters of your game allows you to balance it and is part of the many tasks that game designers handle.
Here are some properties you can tweak:
orbit distance
of the shield.speed
value of the shield.health
value of the shield.bullet
scene.DetectionArea
and AttackArea
collision shapes’s sizes (or use different shapes!). They control the
mob’s ranges.These paramaters already allow you to change a lot about how the shield feels to play against. But you can do much more!
Challenge: Can you make a shield that never forgets
the robot? (hint: override
_on_DetectionArea_body_exited
)
Challenge: Can you make a shield that shoots bullets in a cone?
Challenge: Can you make a shield that doesn’t see
the robot when the robot is behind a wall? (hint: you will need to use
Raycast2D
)
Now that you have your first mob, you can build other ones.
We have prepared a few sprites and assets for you to do so.
In the directory mobs/bomb/
, you will find a
Bomb.tscn
scene.
Additionally to the normal Mob nodes, the bomb also has:
An
named ShockArea. This represents the Bomb’s explosion radius and should hurt the robot when the robot enters it.Two new animations:
We want to extend Mob.gd
with those behaviors:
queue_free()
.RESET
, then the “hover” animation.0
, unless the
“explode” animation is playing.To help you a bit, here’s the function
_on_AnimationPlayer_animation_finished()
.
Careful! The animation_finished
signal yourself.
func _on_AnimationPlayer_animation_finished(anim_name: String) -> void:
if anim_name == "will_explode":
_disable()
_die_sound.play()
_animation_player.play("explode")
elif anim_name == "explode":
queue_free()
elif anim_name == "RESET":
_animation_player.play("hover")
If you’re stuck, feel free to check out the bomb’s code files in the finished game project. They contain a functional bomb mob.
Challenge: Can you make a bomb that slows the player when the player enters the field?
Challenge: Can you make a bomb that can also shoot bullets?
If you open mobs/sword/
, you’ll find two sword images.
You’ll have to create an inherited Mob scene and assign them to the mob
sprites.
Here’s the functionality we want for the sword: when the robot enters the AttackArea, the sword waits a bit, then lunges towards the player.
That’s it!
Bonus points if:
Hint: You will need an
to damage the robot. We usually call this kind of area a Hurtbox.The sword is very hard to get perfectly right, so don’t be disappointed if you don’t manage to code it. We even had a few bugs ourselves while writing it!
Still, it’s a good challenge and even if you don’t nail it, an excellent practice.