godot-2d-animation by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-2d-animation为 Godot 中的逐帧动画和骨骼 2D 动画提供专家级指导。
kill()。animation_finished — 该信号仅在非循环动画结束时触发。对于循环检测,请改用 animation_looped。play() 并期望状态立即改变 — AnimatedSprite2D 在下一个处理帧才应用 play()。如果你需要同步的属性更新(例如,同时更改动画和 flip_h),请在 play() 之后立即调用 。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
advance(0)frame — 设置 frame 会将 frame_progress 重置为 0.0。在帧中切换动画时,使用 set_frame_and_progress(frame, progress) 来保持平滑过渡。@onready var anim_sprite — 在像 _physics_process() 这样的热点路径中,节点查找获取器的速度出奇地慢。始终使用 @onready。强制要求:在实现相应模式之前,请阅读对应的脚本。
用于帧精确逻辑(音效/视觉特效、碰撞框)的方法轨道触发器、信号驱动的异步游戏流程编排以及 AnimationTree 混合空间管理。在将游戏事件与动画帧同步时使用。
带有过渡队列的帧精确状态驱动动画 - 对于响应式角色动画至关重要。
通过 AnimationPlayer 属性轨道动画化 ShaderMaterial 的 uniform 变量。涵盖击中闪光、溶解效果以及用于批量精灵的实例 uniform。用于与动画状态关联的视觉反馈。
动态物理驱动的变形。提供 lerp 逻辑,用于平滑处理突然冲击产生的挤压和基于高速移动的方向性拉伸。
程序化骨骼装配管理。调整 FABRIK/CCDIK 修改堆栈并在运行时更新骨骼的静止姿势,以实现程序化的肢体目标到达。
专家级状态机控制。利用 AnimationNodeStateMachinePlayback.travel() 来利用引擎内部的 A* 寻路算法进行多状态过渡。
通过使用 advance(0) 强制引擎立即应用动画姿势以及 flip_h 等属性更改,从而消除“单帧故障”。
用于绕过 GPU 填充率瓶颈的架构模式。演示何时将大型精灵转换为专门的 2D 网格以避免透明像素开销。
针对数千个实体的优化。将动画逻辑(正弦波、飞行模式)卸载到 GPU 顶点着色器,以消除 CPU 节点处理。
安全且内存高效的 Tween 编排。处理快节奏游戏循环中的中断清理和属性冲突预防。
extends CharacterBody2D
@onready var anim: AnimatedSprite2D = $AnimatedSprite2D
func _ready() -> void:
# ✅ 正确:对重复动画使用 animation_looped
anim.animation_looped.connect(_on_loop)
# ✅ 正确:仅对一次性动画使用 animation_finished
anim.animation_finished.connect(_on_finished)
anim.play("run") # 循环动画
func _on_loop() -> void:
# 每次循环迭代时触发
emit_particle_effect("dust")
func _on_finished() -> void:
# 仅对非循环动画触发
anim.play("idle")
# 帧精确事件系统(攻击、脚步声等)
extends AnimatedSprite2D
signal attack_hit
signal footstep
# 定义每个动画的事件帧
const EVENT_FRAMES := {
"attack": {3: "attack_hit", 7: "attack_hit"},
"run": {2: "footstep", 5: "footstep"}
}
func _ready() -> void:
frame_changed.connect(_on_frame_changed)
func _on_frame_changed() -> void:
var events := EVENT_FRAMES.get(animation, {})
if frame in events:
emit_signal(events[frame])
当同时更新动画和精灵属性(例如 flip_h + 动画更改)时,play() 直到下一帧才会应用,导致 1 帧的视觉故障。
# ❌ 错误:会出现 1 帧的故障
func change_direction(dir: int) -> void:
anim.flip_h = (dir < 0)
anim.play("run") # 在下一帧应用
# 结果:1 帧的错误动画但翻转正确
# ✅ 正确:强制立即同步
func change_direction(dir: int) -> void:
anim.flip_h = (dir < 0)
anim.play("run")
anim.advance(0) # 强制立即更新
在动画中途切换动画而不产生视觉卡顿时使用:
# 示例:在动画中途无缝切换皮肤
func swap_skin(new_skin: String) -> void:
var current_frame := anim.frame
var current_progress := anim.frame_progress
# 加载新的 SpriteFrames 资源
anim.sprite_frames = load("res://skins/%s.tres" % new_skin)
# ✅ 保留精确的动画状态
anim.play(anim.animation) # 重新应用动画
anim.set_frame_and_progress(current_frame, current_progress)
# 结果:在动画中途无缝切换皮肤
| 场景 | 推荐节点 | 专家见解 |
|---|---|---|
| 独立的、纯粹的逐帧精灵表 | AnimatedSprite2D | 简单有效,但无法动画化非视觉属性、操作变换或触发外部方法。 |
| 剪纸动画、非视觉同步、音频/粒子 | AnimationPlayer | 当需要操作多个子精灵的变换、驱动 2D 网格变形或将方法/粒子与视觉帧同步时必需。 |
| 复杂状态机、混合、运动 | AnimationTree | 对于混合移动方向至关重要。它本身不持有动画;它是一个驱动底层 AnimationPlayer 的逻辑图。 |
| 程序化、动态、一次性 UI/特效 | Tween | 目标值在运行时计算。比 AnimationPlayer 轻量得多;设计为通过脚本创建和丢弃。 |
| 数千个实体的集群(蝙蝠、鱼群) | MultiMeshInstance2D + Shader | 完全绕过节点系统。在 GPU 顶点着色器上计算正弦波/移动,以避免巨大的 CPU 瓶颈。 |
# 物理驱动的挤压/拉伸以增强游戏手感
extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D
var _base_scale := Vector2.ONE
func _physics_process(delta: float) -> void:
var prev_velocity := velocity
move_and_slide()
# 落地时挤压
if not is_on_floor() and is_on_floor():
var impact_strength := clamp(abs(prev_velocity.y) / 800.0, 0.0, 1.0)
_squash_and_stretch(Vector2(1.0 + impact_strength * 0.3, 1.0 - impact_strength * 0.3))
# 跳跃时拉伸
elif velocity.y < -200:
sprite.scale = _base_scale.lerp(Vector2(0.9, 1.1), delta * 5.0)
else:
sprite.scale = sprite.scale.lerp(_base_scale, delta * 10.0)
func _squash_and_stretch(target_scale: Vector2) -> void:
var tween := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(sprite, "scale", target_scale, 0.08)
tween.tween_property(sprite, "scale", _base_scale, 0.12)
对于复杂的骨骼动画,使用 Bone2D 而不是手动设置 Sprite2D 父子关系:
Player (Node2D)
└─ Skeleton2D
├─ Bone2D (Root - Torso)
│ ├─ Sprite2D (Body)
│ └─ Bone2D (Head)
│ └─ Sprite2D (Head)
├─ Bone2D (ArmLeft)
│ └─ Sprite2D (Arm)
└─ Bone2D (ArmRight)
└─ Sprite2D (Arm)
# 在 AnimationPlayer 中设置骨骼旋转的关键帧
# 轨道:
# - "Skeleton2D/Bone2D:rotation"
# - "Skeleton2D/Bone2D/Bone2D2:rotation" (head)
# - "Skeleton2D/Bone2D3:rotation" (arm left)
为什么使用 Bone2D 而不是手动父子关系?
# ✅ 正确:在多个实例间共享 SpriteFrames 资源
const SHARED_FRAMES := preload("res://characters/player_frames.tres")
func _ready() -> void:
anim_sprite.sprite_frames = SHARED_FRAMES
# 所有玩家实例在内存中共享同一资源
# ❌ 错误:每个实例单独加载
func _ready() -> void:
anim_sprite.sprite_frames = load("res://characters/player_frames.tres")
# 每个实例在内存中复制资源
# 像素艺术纹理在像素之间居中对齐时可能显得模糊
# 解决方案 1:禁用居中
anim_sprite.centered = false
anim_sprite.offset = Vector2.ZERO
# 解决方案 2:启用全局像素对齐(项目设置)
# rendering/2d/snap/snap_2d_vertices_to_pixel = true
# rendering/2d/snap/snap_2d_transforms_to_pixel = true
# 问题:SpriteFrames 使用双线性过滤(对像素艺术来说模糊)
# 解决方案:在导入选项卡中为每个纹理设置:
# - 过滤器:Nearest(用于像素艺术)
# - Mipmaps:关闭(防止在远处混合)
# 或者在项目设置中全局设置:
# rendering/textures/canvas_textures/default_texture_filter = Nearest
不要局限于一种风格。使用 AnimationPlayer 来装配 2D 骨骼并动画化骨骼(剪纸动画),同时为特定子精灵的 texture 或 frame 属性设置关键帧。这允许对身体进行高效的基于变换的动画,同时使用传统的手绘赛璐珞动画来有选择地交换手部形状或面部表情。
具有大面积透明区域(如树叶)的精灵会浪费 GPU 填充率。在 Godot 中将 Sprite2D 转换为 MeshInstance2D。这会生成一个紧密包裹不透明像素的 2D 多边形,绕过了透明区域。虽然这会略微增加顶点处理时间,但在复杂的 2D 场景中能极大改善 GPU 填充率。
游戏状态变化迅速。确保在启动新的 Tween 之前正确清理旧的 Tween,以避免内存泄漏和动画冲突。
extends Node2D
var _tween: Tween
func animate_damage_flash() -> void:
# 防止反模式:在覆盖现有 Tween 之前始终将其终止。
if _tween:
_tween.kill()
_tween = create_tween()
# 链接 Tween 并使用循环进行快速、可预测的动画
_tween.set_loops(3)
_tween.tween_property($Sprite2D, "modulate", Color.RED, 0.1).set_trans(Tween.TRANS_SINE)
_tween.tween_property($Sprite2D, "modulate", Color.WHITE, 0.1).set_trans(Tween.TRANS_SINE)
在 AnimationTree 中使用 AnimationNodeStateMachine 时,你不直接“播放”动画。你命令状态机计算到达新状态的最短路径(使用 A*)。
extends CharacterBody2D
@onready var animation_tree: AnimationTree = $AnimationTree
# 从 AnimationTree 属性中获取播放对象
@onready var state_machine: AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")
func _ready() -> void:
# 状态机在旅行前必须启动
state_machine.start("idle")
func _physics_process(delta: float) -> void:
if velocity.length() > 0:
# 旅行到奔跑状态。内部的 A* 将无缝播放中间过渡。
state_machine.travel("run")
else:
state_machine.travel("idle")
每周安装量
98
代码仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
已安装于
gemini-cli97
codex97
opencode97
kimi-cli95
amp95
github-copilot95
Expert-level guidance for frame-based and skeletal 2D animation in Godot.
kill() on the previous instance before creating a new one.animation_finished for looping animations — The signal only fires on non-looping animations. Use animation_looped instead for loop detection.play() and expect instant state changes — AnimatedSprite2D applies play() on the next process frame. Call advance(0) immediately after play() if you need synchronous property updates (e.g., when changing animation + flip_h simultaneously).frame directly when preserving animation progress — Setting frame resets frame_progress to 0.0. Use set_frame_and_progress(frame, progress) to maintain smooth transitions when swapping animations mid-frame.@onready var anim_sprite — The node lookup getter is surprisingly slow in hot paths like _physics_process(). Always use @onready.MANDATORY : Read the appropriate script before implementing the corresponding pattern.
Method track triggers for frame-perfect logic (SFX/VFX hitboxes), signal-driven async gameplay orchestration, and AnimationTree blend space management. Use when syncing gameplay events to animation frames.
Frame-perfect state-driven animation with transition queueing - essential for responsive character animation.
Animating ShaderMaterial uniforms via AnimationPlayer property tracks. Covers hit flash, dissolve effects, and instance uniforms for batched sprites. Use for visual feedback tied to animation states.
Dynamic physics-driven deformation. Provides lerp logic for smoothing out sudden impact squashes and directional stretches based on high-velocity movement.
Programmatic rig management. Tuning FABRIK/CCDIK modification stacks and updating bone rest poses at runtime for procedural limb goal-reaching.
Expert state machine control. Utilizes AnimationNodeStateMachinePlayback.travel() to leverage the engine's internal A* pathfinding for multi-state transitions.
Eliminates the "One-Frame Glitch" by using advance(0) to force the engine to apply animation poses immediately alongside property changes like flip_h.
Architectural pattern for bypassing GPU fill-rate bottlenecks. Demonstrates when to convert large sprites into specialized 2D meshes to avoid transparent pixel overhead.
Optimization for thousands of entities. Offloads animation logic (sine waves, flight patterns) to the GPU vertex shader to eliminate CPU node processing.
Safe and memory-efficient Tween orchestration. Handles interruption cleanup and property-fight prevention in fast-paced gameplay loops.
extends CharacterBody2D
@onready var anim: AnimatedSprite2D = $AnimatedSprite2D
func _ready() -> void:
# ✅ Correct: Use animation_looped for repeating animations
anim.animation_looped.connect(_on_loop)
# ✅ Correct: Use animation_finished ONLY for one-shots
anim.animation_finished.connect(_on_finished)
anim.play("run") # Looping animation
func _on_loop() -> void:
# Fires every loop iteration
emit_particle_effect("dust")
func _on_finished() -> void:
# Only fires for non-looping animations
anim.play("idle")
# Frame-perfect event system (attacks, footsteps, etc.)
extends AnimatedSprite2D
signal attack_hit
signal footstep
# Define event frames per animation
const EVENT_FRAMES := {
"attack": {3: "attack_hit", 7: "attack_hit"},
"run": {2: "footstep", 5: "footstep"}
}
func _ready() -> void:
frame_changed.connect(_on_frame_changed)
func _on_frame_changed() -> void:
var events := EVENT_FRAMES.get(animation, {})
if frame in events:
emit_signal(events[frame])
When updating both animation and sprite properties (e.g., flip_h + animation change), play() doesn't apply until next frame, causing a 1-frame visual glitch.
# ❌ BAD: Glitches for 1 frame
func change_direction(dir: int) -> void:
anim.flip_h = (dir < 0)
anim.play("run") # Applied NEXT frame
# Result: 1 frame of wrong animation with correct flip
# ✅ GOOD: Force immediate sync
func change_direction(dir: int) -> void:
anim.flip_h = (dir < 0)
anim.play("run")
anim.advance(0) # Force immediate update
Use when changing animations mid-animation without visual stutter:
# Example: Skin swapping without animation reset
func swap_skin(new_skin: String) -> void:
var current_frame := anim.frame
var current_progress := anim.frame_progress
# Load new SpriteFrames resource
anim.sprite_frames = load("res://skins/%s.tres" % new_skin)
# ✅ Preserve exact animation state
anim.play(anim.animation) # Re-apply animation
anim.set_frame_and_progress(current_frame, current_progress)
# Result: Seamless skin swap mid-animation
| Scenario | Recommended Node | Expert Insight |
|---|---|---|
| Isolated, pure frame-by-frame spritesheets | AnimatedSprite2D | Simple and effective, but cannot animate non-visual properties, manipulate transforms, or trigger external methods. |
| Cutout animations, non-visual sync, audio/particles | AnimationPlayer | Required when manipulating transforms of many child sprites, driving 2D mesh deformations, or syncing methods/particles to visual frames. |
| Complex state machines, blending, locomotion | AnimationTree | Essential for blending movement directions. Does not hold animations itself; it's a logic graph driving an underlying AnimationPlayer. |
| Procedural, dynamic, fire-and-forget UI/fx | Tween | Target values calculated at runtime. Far more lightweight than AnimationPlayer; designed to be created and discarded via script. |
| Swarms of thousands of entities (bats, fish) | MultiMeshInstance2D + Shader | Bypasses the node system entirely. Calculate sine waves/movement on the GPU vertex shader to avoid massive CPU bottlenecks. |
# Physics-driven squash/stretch for game feel
extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D
var _base_scale := Vector2.ONE
func _physics_process(delta: float) -> void:
var prev_velocity := velocity
move_and_slide()
# Squash on landing
if not is_on_floor() and is_on_floor():
var impact_strength := clamp(abs(prev_velocity.y) / 800.0, 0.0, 1.0)
_squash_and_stretch(Vector2(1.0 + impact_strength * 0.3, 1.0 - impact_strength * 0.3))
# Stretch during jump
elif velocity.y < -200:
sprite.scale = _base_scale.lerp(Vector2(0.9, 1.1), delta * 5.0)
else:
sprite.scale = sprite.scale.lerp(_base_scale, delta * 10.0)
func _squash_and_stretch(target_scale: Vector2) -> void:
var tween := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(sprite, "scale", target_scale, 0.08)
tween.tween_property(sprite, "scale", _base_scale, 0.12)
For complex skeletal animation, use Bone2D instead of manual Sprite2D parenting:
Player (Node2D)
└─ Skeleton2D
├─ Bone2D (Root - Torso)
│ ├─ Sprite2D (Body)
│ └─ Bone2D (Head)
│ └─ Sprite2D (Head)
├─ Bone2D (ArmLeft)
│ └─ Sprite2D (Arm)
└─ Bone2D (ArmRight)
└─ Sprite2D (Arm)
# Key bone rotations in AnimationPlayer
# Tracks:
# - "Skeleton2D/Bone2D:rotation"
# - "Skeleton2D/Bone2D/Bone2D2:rotation" (head)
# - "Skeleton2D/Bone2D3:rotation" (arm left)
Why Bone2D over manual parenting?
# ✅ GOOD: Share SpriteFrames resource across instances
const SHARED_FRAMES := preload("res://characters/player_frames.tres")
func _ready() -> void:
anim_sprite.sprite_frames = SHARED_FRAMES
# All player instances share same resource in memory
# ❌ BAD: Each instance loads separately
func _ready() -> void:
anim_sprite.sprite_frames = load("res://characters/player_frames.tres")
# Duplicates resource in memory per instance
# Pixel art textures can appear blurry when centered between pixels
# Solution 1: Disable centering
anim_sprite.centered = false
anim_sprite.offset = Vector2.ZERO
# Solution 2: Enable global pixel snapping (Project Settings)
# rendering/2d/snap/snap_2d_vertices_to_pixel = true
# rendering/2d/snap/snap_2d_transforms_to_pixel = true
# Problem: SpriteFrames uses bilinear filtering (blurry for pixel art)
# Solution: In Import tab for each texture:
# - Filter: Nearest (for pixel art)
# - Mipmaps: Off (prevents blending at distance)
# Or set globally in Project Settings:
# rendering/textures/canvas_textures/default_texture_filter = Nearest
Do not limit yourself to just one style. Use AnimationPlayer to rig a 2D skeleton and animate the bones (Cutout animation), while simultaneously keyframing the texture or frame properties of specific child sprites. This allows highly efficient transform-based animation for the body, while selectively swapping hand shapes or facial expressions using traditional hand-drawn cel animation.
Sprites with large transparent areas (like tree leaves) waste GPU fill rate. Convert a Sprite2D into a MeshInstance2D in Godot. This generates a 2D polygon that tightly hugs the opaque pixels, bypassing transparent areas. While this slightly increases vertex processing time, it massively improves GPU fill rate on complex 2D scenes.
Game states change rapidly. Ensure Tweens are properly cleaned up before starting new ones to avoid memory leaks and conflicting animations.
extends Node2D
var _tween: Tween
func animate_damage_flash() -> void:
# Anti-pattern prevention: Always kill the existing tween before overwriting it.
if _tween:
_tween.kill()
_tween = create_tween()
# Chain tweeners and use loops for rapid, predictable animation
_tween.set_loops(3)
_tween.tween_property($Sprite2D, "modulate", Color.RED, 0.1).set_trans(Tween.TRANS_SINE)
_tween.tween_property($Sprite2D, "modulate", Color.WHITE, 0.1).set_trans(Tween.TRANS_SINE)
When using an AnimationNodeStateMachine within an AnimationTree, you do not directly "play" animations. You command the state machine to compute the shortest path (using A*) to a new state.
extends CharacterBody2D
@onready var animation_tree: AnimationTree = $AnimationTree
# Retrieve the playback object from the AnimationTree properties
@onready var state_machine: AnimationNodeStateMachinePlayback = animation_tree.get("parameters/playback")
func _ready() -> void:
# The state machine must be started before traveling
state_machine.start("idle")
func _physics_process(delta: float) -> void:
if velocity.length() > 0:
# Travel to the run state. Internal A* will seamlessly play intermediate transitions.
state_machine.travel("run")
else:
state_machine.travel("idle")
Weekly Installs
98
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli97
codex97
opencode97
kimi-cli95
amp95
github-copilot95
ESLint迁移到Oxlint完整指南:JavaScript/TypeScript项目性能优化工具
1,700 周安装
Adaptyv 云实验室平台:自动化蛋白质测试与验证服务,21天交付实验结果
186 周安装
RCSB PDB 数据库 API 使用指南:搜索、下载与分析蛋白质三维结构数据
185 周安装
免费在线表情包生成器 | 使用 memegen.link API 快速创建和自定义表情包
184 周安装
COBRApy Python库:代谢模型约束性重构与分析工具,用于系统生物学研究
186 周安装
Solid JSON 渲染库 | 细粒度响应式组件树 | Vercel Labs @json-render/solid
187 周安装
Android XML 转 Compose 迁移指南:布局映射、状态迁移与增量策略
189 周安装