Official 4.6
Use the 4.6 URLs. Older blogs and Godot 3 pages still show AnimatedSprite / frames=. Po Runner is 4.6.
On this gallery
- Albino ×32 animation station — pick frames, FPS, order, playback; copy GDScript / .tscn / JSON
- Albino keyframes — charcoal vs lock
- Po Runner archive — charcoal station and earlier rounds
- Same notes as raw Markdown (agent / context dump)
What Po Runner already does
| Fact | Value |
|---|---|
| Engine | Godot 4.6 (config/features includes "4.6") |
| Viewport | 480×270, stretch/mode=canvas_items |
| Player animation | AnimatedSprite2D + SpriteFrames in main.tscn |
| Play call | $AnimatedSprite2D.play(current_anim) in player.gd |
| Finished signal | animation_finished for land and run kick (non-looping one-shots) |
| Run cycle | &"run", loop true, speed 17.0, each frame duration 1.0 |
| Obstacle rocks | Sprite2D at native pixel size. Collision is a handmade box, not the PNG bounds |
| Wildboar PNGs | Exist at res://assets/obstacles/wildboar.png (+ _01 / _02) — not wired in the spawner yet |
| Ship rule | Mitchell names winners before anything is dropped into assets/ |
Player sprite scale is for Po, not obstacles. Boars stay 76×34 native (75×34 refs padded one transparent column). STILL 256×256 does not go in this viewport.
SpriteFrames API the station emits
add_animation(anim: StringName) add_frame(anim: StringName, texture: Texture2D, duration: float = 1.0, at_position: int = -1) set_animation_speed(anim: StringName, fps: float) set_animation_loop(anim: StringName, loop: bool)
Duration is relative. Absolute seconds: relative_duration / (animation_fps * abs(playing_speed)). Po puts FPS on "speed" and leaves duration at 1.0. There is no ping-pong flag.
AnimatedSprite2D API
play(name: StringName = &"", custom_speed: float = 1.0, from_end: bool = false) play_backwards(name: StringName = &"") pause() stop() is_playing() -> bool
animation_finished fires at the end (or start if reverse). It does not emit if the animation loops. Nearest filter: texture_filter = TEXTURE_FILTER_NEAREST (= 1 in .tscn).
How the station maps playback
| Station | Strip | loop | Extra |
|---|---|---|---|
| Loop | as ordered | true | — |
| Reverse loop | reversed | true | baked into PATHS |
| Ping-pong | A B C D C B | true | baked (no engine flag) |
| Hold last | as ordered | false | engine pauses on last frame |
| Once (reset) | as ordered | false | finished → frame 0, stop |
Ping-pong without baking: loop = false and toggle play() / play_backwards() on animation_finished. If loop stays true, that signal never fires.
Path convention
- Gallery PNGs are review files. Godot must
load("res://...")from the game project. - Station default prefix:
res://assets/obstacles/boar/ - Copy named 76×34 files there after winners are named. Do not load gallery URLs from the game.
Concrete GDScript (Godot 4.6)
extends AnimatedSprite2D
const ANIM := &"boar"
const FPS := 8.0
const LOOP := true
const PATHS: PackedStringArray = [
"res://assets/obstacles/boar/p32-01-contact.png",
"res://assets/obstacles/boar/p32-02-gather.png",
"res://assets/obstacles/boar/p32-03-push.png",
]
func _ready() -> void:
texture_filter = TEXTURE_FILTER_NEAREST
var frames := SpriteFrames.new()
frames.add_animation(ANIM)
frames.set_animation_speed(ANIM, FPS)
frames.set_animation_loop(ANIM, LOOP)
for path in PATHS:
var tex := load(path) as Texture2D
if tex == null:
push_error("Missing boar frame: %s" % path)
continue
frames.add_frame(ANIM, tex, 1.0)
sprite_frames = frames
play(ANIM)
Same stack as the player: $AnimatedSprite2D.play(&"boar").
.tscn SpriteFrames shape (matches main.tscn)
[sub_resource type="SpriteFrames" id="SpriteFrames_boar"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("1_boar")
}, {
"duration": 1.0,
"texture": ExtResource("2_boar")
}],
"loop": true,
"name": &"boar",
"speed": 8.0
}]
[node name="Boar" type="AnimatedSprite2D"]
texture_filter = 1
sprite_frames = SubResource("SpriteFrames_boar")
animation = &"boar"
autoplay = "boar"
autoplay is a String. animation is a StringName. Do not “fix” autoplay to &"boar".