| sidebar_position | 5 |
|---|---|
| title | Lesson 5 · The player scene and animation |
Reference checkpoint: node scripts/course-create.mjs --lesson 5 builds this lesson's runnable end state beside your project — compare, never overwrite. See Practice checkpoints.
Three beacons stand in the night forest. Now we make the person who will stand in that forest.
This lesson we do not move. We only stand. Instead we make standing properly — casting a shadow, rising and falling like a breath, catching firelight beside a beacon.
Moving is Lesson 6. But the facing direction we lock in now, so that when we attach the stick in Lesson 6 we only have to call one function.
We do not draw the player directly inside the arena. We extract player.tscn.
Same reason we extracted the beacon in Lesson 2.
- When we make enemies in Lesson 8 we use the same structure
- When we make enemies in Lesson 8 we use that same structure as-is (everything that moves looks like this)
- We can edit the player without opening the arena
player.tscn
Player (CharacterBody2D) ← the body. Lesson 6 puts velocity here
Shadow (Sprite2D) ← shadow at the feet
Sprite (AnimatedSprite2D) ← 4-direction idle and walk
Body (CollisionShape2D) ← the collision range
Breathe(AnimationPlayer) ← the breathing rise and fall
Why the root is
CharacterBody2DIn Lesson 6 we move withmove_and_slide(). We put what that needs on the root now. Change the root type later and every child you attached and every script shakes.
We look at Godot's official Step by step / Creating the player scene
together.
| Official docs | What we take from them |
|---|---|
| Creating the player scene | Player as a separate scene · AnimatedSprite2D |
| 2D sprite animation | Cutting a sprite sheet into SpriteFrames |
The official sample puts two animations on AnimatedSprite2D.
We put 4 directions × 2 states = 8. It is top-down, so we need direction.
We use NinjaBlue from the Ninja Adventure pack. We bring only two files.
| File | Size | Layout |
|---|---|---|
ninja_walk.png |
64 × 64 | 16px cells 4 columns × 4 rows |
ninja_idle.png |
64 × 16 | 16px cells 4 columns × 1 row |
Columns are direction and rows are frames. Get that backwards and the facing changes while walking.
| Column | Direction | How you can tell |
|---|---|---|
| 0 | Down | You can see the face |
| 1 | Up | You can see the back of the head |
| 2 | Left | Side view |
| 3 | Right | The other side view |
:::tip Zoom the sheet and confirm with your eyes Do not trust the layout written in a document. Open it. Packs differ, and even inside the same pack characters can differ. At 16px you have to scale 4–6× to see it. :::
Make eight animations in SpriteFrames.
| Name | Frames | Speed |
|---|---|---|
idle_down idle_up idle_left idle_right |
1 frame | 1 |
walk_down walk_up walk_left walk_right |
4 frames | 9 |
Idle.png has one frame per direction. Leave it and it freezes
completely.
It does not look alive.
We solve it without drawing more frames. Raise the sprite 1px and lower it.
[sub_resource type="Animation" id="Animation_breathe"]
length = 1.8
loop_mode = 1
tracks/0/path = NodePath("Sprite:offset")
"values": [Vector2(0, -8), Vector2(0, -9), Vector2(0, -8)]1px over 1.8 seconds. Internal resolution 808 × 360 grows 3× on the phone, so on screen it rises and falls 3px. Not noticeable, and not frozen.
In pixel art, 1px is a large value 1px on a 16px character is 1/16 of their height. On a person that is 10cm. Do 2px and it looks like hopping, not breathing.
The default ninja in this course has a one-frame idle, so this animation is
needed. The shipping 48×64 heroes have a 4-frame idle, so we pin the baseline
with Breathe.stop() and Sprite.offset = Vector2(0, -8). Layer a frame
animation on a whole-body offset and on a scaled-up screen you see two bodies
overlapping.
Make one soft 24 × 10 ellipse and put it at the feet.
:::warning Light must not reach the shadow
Set the shadow sprite's light_mask to 0.
Otherwise the shadow brightens beside a beacon. Closer to the fire, the shadow gets lighter — which is the opposite of reality.
Same judgment as giving the flame light_mask = 0 in Lesson 2.
Do not shine light on things that glow on their own, or on things that
block light.
:::
The player is a sibling of the forest. The night tint we gave the forest does not flow down. Leave it and a daytime ninja is standing in a night forest.
Put the night tint on the Player root's Modulate.
| R | G | B |
|---|---|---|
| 0.315 | 0.35 | 0.57 |
Same treatment as the fire pit in Lesson 2 and the clearing in Lesson 3.
There is one difference from the fire pit. Leave the sprite's
light_mask at the default 1. It has to receive beacon light.
That fire-side is bright in a night forest is this whole game. If the player does not receive that light, there is no reason to light a beacon.
We make this function ahead of time so that when we attach the stick in Lesson 6 we only have to call it.
enum Facing { DOWN, UP, LEFT, RIGHT }
## Lesson 6 passes the stick direction as-is. If the length is near 0, do not change facing.
func face_toward(direction: Vector2) -> void:
if direction.length() < 0.01:
return
if absf(direction.x) > absf(direction.y):
facing = Facing.RIGHT if direction.x > 0.0 else Facing.LEFT
else:
facing = Facing.DOWN if direction.y > 0.0 else Facing.UPWhich way is a diagonal Push the stick at 45 degrees and
xandyare similar. Split withabsf(x) > absf(y)and follow the larger one. We only have 4-direction sprites, so we have to pick one.Not changing facing when the length is near 0 is also important. The moment you release the stick the value drops to 0, and if you change facing then, the character looks down every time you lift your hand.
Match the enum order to the column order of the sprite sheet and you can
use it as-is when you build the name.
const FACING_NAMES: Array[StringName] = [&"down", &"up", &"left", &"right"]
func _play_current() -> void:
var prefix: String = "walk_" if _walking else "idle_"
_sprite.play(prefix + FACING_NAMES[facing]):::info The assembled file — replace, do not append
Section 5-4 showed face_toward() and _play_current() in pieces to explain
them. The file below assembles everything once: select the Player root,
Attach Script, save it as res://scripts/player.gd, and make the whole
file match this — do not type any part twice.
extends CharacterBody2D
enum Facing { DOWN, UP, LEFT, RIGHT }
const FACING_NAMES: Array[StringName] = [&"down", &"up", &"left", &"right"]
@export var facing: Facing = Facing.DOWN
@onready var _sprite: AnimatedSprite2D = $Sprite
var _walking: bool = false
func _ready() -> void:
_play_current()
func face_toward(direction: Vector2) -> void:
if direction.length() < 0.01:
return
if absf(direction.x) > absf(direction.y):
facing = Facing.RIGHT if direction.x > 0.0 else Facing.LEFT
else:
facing = Facing.DOWN if direction.y > 0.0 else Facing.UP
func set_walking(value: bool) -> void:
_walking = value
_play_current()
func _play_current() -> void:
var prefix: String = "walk_" if _walking else "idle_"
_sprite.play(prefix + FACING_NAMES[facing])Two things the pieces above did not show: extends CharacterBody2D (the
script runs on the player body from section 2), and _ready() calling
_play_current() so the exported facing actually shows when the scene
runs. Nothing else changed — the two functions match section 5-4 exactly.
:::
Open player.tscn and select the Player root first: set
collision_layer = 2 and collision_mask = 0. Value 2 (inspector checkbox
2) means "this is the player" — Lesson 7's beacon sensor and Lesson 8's
spirit hitbox both look for exactly this value, so set it now and never
touch it again.
Then attach player.tscn as an instance on arena.tscn and put it at
(330, 300).
Near the west beacon. You can see firelight catch the moment the beacon
lights.
godot --headless --path . --import(godot is your Lesson 1 GODOT_BIN invocation, quoted when the
path has spaces. The import validates that the editor can open the
project and that every resource imports: exit code 0 with no
ERROR lines in the output. It does not compile your scripts or
run gameplay — press Play in the editor for that, then close the
window to quit. The reference verifier additionally compiles every
script and drives each gameplay check headless.)
player.tscnexists and has 8 animations- A ninja stands in the arena with a shadow at their feet
- When the beacon is unlit the ninja is dark
- When the beacon lights the ninja brightens
- The shadow does not brighten
- Left alone, they rise and fall like a breath
① Set Player's Modulate back to white
→ A daytime ninja stands in a night forest. They look like they are in a
different world by themselves.
② Set the shadow's light_mask to 1
→ Beside a beacon the shadow gets lighter. Closer to the fire, the shadow
disappears.
③ Stretch breathe's 1px to 4px
→ It hops instead of breathing. You can see how large 1px is in pixel art.
④ Delete if direction.length() < 0.01: return from face_toward()
→ It does not show yet. Attach the stick in Lesson 6 and they look down
every time you lift your hand.
That is why we put it in now.
- [ ]
player.tscnexists as a separate file - [ ]
SpriteFrameshas 8 animations (4 idle · 4 walk) - [ ] A ninja stands in the arena
- [ ] There is a shadow at their feet, and it does not brighten when a beacon lights
- [ ] The ninja receives the night tint, and also receives beacon light
- [ ] They rise and fall like a breath
- [ ]
godot --headless --path . --importfinishes with exit code 0 and no errors in its output - [ ] The same screen appears on the phone
-
Set
walk_downtoautoplayand run. They march in place. This is a preview of when that animation will appear in Lesson 6. After you check, put it back toidle_down. -
In the inspector, change
FacingtoUP,LEFT, andRIGHT. It does not change in the editor; you have to run. Unlike the Lesson 4 beacon, we did not put@toolon it. -
Move the ninja near the north beacon at
(414, 196). That beacon is the third to light. Watch how dark the ninja is until then.