Gallery index · Boars · Albino station · Raw notes (.md) · Archive

Godot 4.6 — links and context

Official 4.6 docs first. Then Po Runner facts the animation station was built against. Researched 2026-09-07. Do not use Godot 3 recipes.

Official 4.6

2D sprite animation Tutorial — individual images → AnimatedSprite2D docs.godotengine.org/en/4.6/tutorials/2d/2d_sprite_animation.html AnimatedSprite2D play, play_backwards, animation_finished docs.godotengine.org/en/4.6/classes/class_animatedsprite2d.html SpriteFrames add_frame, set_animation_speed, set_animation_loop docs.godotengine.org/en/4.6/classes/class_spriteframes.html

Use the 4.6 URLs. Older blogs and Godot 3 pages still show AnimatedSprite / frames=. Po Runner is 4.6.

On this gallery

What Po Runner already does

FactValue
EngineGodot 4.6 (config/features includes "4.6")
Viewport480×270, stretch/mode=canvas_items
Player animationAnimatedSprite2D + SpriteFrames in main.tscn
Play call$AnimatedSprite2D.play(current_anim) in player.gd
Finished signalanimation_finished for land and run kick (non-looping one-shots)
Run cycle&"run", loop true, speed 17.0, each frame duration 1.0
Obstacle rocksSprite2D at native pixel size. Collision is a handmade box, not the PNG bounds
Wildboar PNGsExist at res://assets/obstacles/wildboar.png (+ _01 / _02) — not wired in the spawner yet
Ship ruleMitchell 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

StationStriploopExtra
Loopas orderedtrue—
Reverse loopreversedtruebaked into PATHS
Ping-pongA B C D C Btruebaked (no engine flag)
Hold lastas orderedfalseengine pauses on last frame
Once (reset)as orderedfalsefinished → 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

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".