第十九章:2D动画系统
"动画让游戏角色栩栩如生,让整个世界充满活力。"
动画是游戏表现力的核心。本章将全面讲解Godot的2D动画系统,包括AnimationPlayer、AnimatedSprite2D、Tween、骨骼动画等内容。
19.1 动画系统概述
19.1.1 Godot动画方案
Godot 2D动画方案:
├── AnimatedSprite2D - 帧动画(精灵表)
├── AnimationPlayer - 通用动画(属性动画)
├── AnimationTree - 动画状态机
├── Tween - 程序化补间动画
└── Skeleton2D - 2D骨骼动画
19.1.2 选择合适的方案
场景 推荐方案
───────────────────────────────────
角色走跑跳等帧动画 AnimatedSprite2D
UI动画、属性变化 AnimationPlayer / Tween
复杂状态切换 AnimationTree
程序化动效 Tween
角色骨骼动画 Skeleton2D
19.2 AnimatedSprite2D
19.2.1 基础使用
extends AnimatedSprite2D
func _ready():
# 播放动画
play("idle")
# 反向播放
play("walk", -1.0, true) # custom_speed, from_end
# 暂停/继续
pause()
play()
# 停止
stop()
# 设置帧
frame = 0
# 播放速度
speed_scale = 1.5
# 连接信号
func _ready():
animation_finished.connect(_on_animation_finished)
animation_changed.connect(_on_animation_changed)
frame_changed.connect(_on_frame_changed)
func _on_animation_finished():
if animation == "attack":
play("idle")
func _on_frame_changed():
if animation == "attack" and frame == 2:
apply_damage()
19.2.2 SpriteFrames资源
# 代码创建SpriteFrames
func create_sprite_frames() -> SpriteFrames:
var frames = SpriteFrames.new()
# 添加动画
frames.add_animation("walk")
frames.set_animation_speed("walk", 12) # 每秒12帧
frames.set_animation_loop("walk", true)
# 添加帧
for i in range(8):
var texture = load("res://sprites/walk_%d.png" % i)
frames.add_frame("walk", texture)
# 从精灵表添加帧
var spritesheet = load("res://sprites/character.png")
for i in range(4):
var atlas = AtlasTexture.new()
atlas.atlas = spritesheet
atlas.region = Rect2(i * 64, 0, 64, 64)
frames.add_frame("idle", atlas)
return frames
# 应用
func _ready():
sprite_frames = create_sprite_frames()
play("idle")
19.2.3 动画控制器
# animation_controller.gd
class_name AnimationController
extends AnimatedSprite2D
signal animation_event(event_name: String)
@export var default_animation: String = "idle"
var _queued_animation: String = ""
var _locked: bool = false
func _ready():
animation_finished.connect(_on_animation_finished)
frame_changed.connect(_on_frame_changed)
play(default_animation)
func play_animation(anim_name: String, lock: bool = false) -> void:
if _locked and animation != anim_name:
_queued_animation = anim_name
return
_locked = lock
play(anim_name)
func _on_animation_finished():
_locked = false
if _queued_animation != "":
play(_queued_animation)
_queued_animation = ""
else:
play(default_animation)
func _on_frame_changed():
# 在SpriteFrames的自定义数据中定义事件帧
var frame_name = sprite_frames.get_animation_names()[0] # 简化
# 实际实现需要更复杂的事件系统
19.3 AnimationPlayer
19.3.1 基础使用
extends Node2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready():
# 播放动画
anim_player.play("walk")
# 反向播放
anim_player.play_backwards("walk")
# 从特定位置播放
anim_player.play("walk")
anim_player.seek(0.5) # 跳到0.5秒
# 暂停/停止
anim_player.pause()
anim_player.stop()
# 播放速度
anim_player.speed_scale = 2.0
# 获取信息
var current = anim_player.current_animation
var position = anim_player.current_animation_position
var length = anim_player.current_animation_length
# 信号
func _ready():
anim_player.animation_finished.connect(_on_animation_finished)
anim_player.animation_started.connect(_on_animation_started)
anim_player.animation_changed.connect(_on_animation_changed)
19.3.2 创建动画轨道
# 代码创建动画
func create_animation() -> Animation:
var anim = Animation.new()
anim.length = 1.0
anim.loop_mode = Animation.LOOP_LINEAR
# 位置轨道
var track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, "Sprite2D:position")
anim.track_insert_key(track_idx, 0.0, Vector2(0, 0))
anim.track_insert_key(track_idx, 0.5, Vector2(100, 0))
anim.track_insert_key(track_idx, 1.0, Vector2(0, 0))
# 旋转轨道
track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, "Sprite2D:rotation")
anim.track_insert_key(track_idx, 0.0, 0)
anim.track_insert_key(track_idx, 1.0, TAU)
# 方法调用轨道
track_idx = anim.add_track(Animation.TYPE_METHOD)
anim.track_set_path(track_idx, ".")
anim.track_insert_key(track_idx, 0.5, {
"method": "spawn_effect",
"args": []
})
return anim
func setup_animation_player():
var library = AnimationLibrary.new()
library.add_animation("bounce", create_animation())
$AnimationPlayer.add_animation_library("", library)
19.3.3 动画轨道类型
# 轨道类型
Animation.TYPE_VALUE # 属性值变化
Animation.TYPE_POSITION_3D # 3D位置
Animation.TYPE_ROTATION_3D # 3D旋转
Animation.TYPE_SCALE_3D # 3D缩放
Animation.TYPE_BLEND_SHAPE # 混合形状
Animation.TYPE_METHOD # 方法调用
Animation.TYPE_BEZIER # 贝塞尔曲线
Animation.TYPE_AUDIO # 音频播放
Animation.TYPE_ANIMATION # 动画引用
# 插值模式
Animation.INTERPOLATION_NEAREST # 最近邻(离散)
Animation.INTERPOLATION_LINEAR # 线性
Animation.INTERPOLATION_CUBIC # 三次方
# 循环模式
Animation.LOOP_NONE # 不循环
Animation.LOOP_LINEAR # 线性循环
Animation.LOOP_PINGPONG # 往返循环
19.4 AnimationTree
19.4.1 状态机基础
extends CharacterBody2D
@onready var animation_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback
func _ready():
animation_tree.active = true
state_machine = animation_tree.get("parameters/playback")
func _physics_process(delta: float):
# 根据状态切换动画
if is_on_floor():
if velocity.length() > 10:
state_machine.travel("run")
else:
state_machine.travel("idle")
else:
if velocity.y < 0:
state_machine.travel("jump")
else:
state_machine.travel("fall")
# 状态机控制
func play_attack():
state_machine.travel("attack")
func get_current_state() -> String:
return state_machine.get_current_node()
func is_playing(state: String) -> bool:
return state_machine.get_current_node() == state
19.4.2 BlendSpace2D
extends CharacterBody2D
@onready var animation_tree: AnimationTree = $AnimationTree
func _physics_process(delta: float):
# 设置混合参数(用于8向移动动画)
var input_vector = Input.get_vector("left", "right", "up", "down")
if input_vector != Vector2.ZERO:
# 设置BlendSpace2D参数
animation_tree.set("parameters/Move/blend_position", input_vector)
animation_tree.set("parameters/Idle/blend_position", input_vector)
# BlendSpace1D(用于走跑混合)
func update_movement_blend(speed: float):
var blend = clamp(speed / max_speed, 0, 1)
animation_tree.set("parameters/WalkRun/blend_position", blend)
19.4.3 动画层
extends CharacterBody2D
@onready var animation_tree: AnimationTree = $AnimationTree
func _ready():
# 设置层权重
animation_tree.set("parameters/UpperBody/blend_amount", 1.0)
animation_tree.set("parameters/LowerBody/blend_amount", 1.0)
func play_shoot():
# 只在上半身层播放射击动画
var upper_state = animation_tree.get("parameters/UpperBody/playback")
upper_state.travel("shoot")
# 下半身继续当前动画(走/跑/站立)
19.5 Tween动画
19.5.1 基础使用
extends Node2D
func move_to(target: Vector2, duration: float = 0.5):
var tween = create_tween()
tween.tween_property(self, "position", target, duration)
func fade_out(duration: float = 0.3):
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0.0, duration)
tween.tween_callback(queue_free)
func scale_bounce():
var tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.2, 1.2), 0.1)
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)
19.5.2 Tween链式调用
extends Sprite2D
func complex_animation():
var tween = create_tween()
# 顺序执行
tween.tween_property(self, "position:x", 200, 0.5)
tween.tween_property(self, "position:y", 100, 0.3)
tween.tween_callback(func(): print("完成"))
# 并行执行
tween.set_parallel(true)
tween.tween_property(self, "position", Vector2(300, 200), 0.5)
tween.tween_property(self, "rotation", PI, 0.5)
tween.tween_property(self, "modulate", Color.RED, 0.5)
# 恢复顺序
tween.set_parallel(false)
tween.tween_interval(0.5) # 等待
tween.tween_property(self, "modulate", Color.WHITE, 0.3)
func shake_effect():
var tween = create_tween()
var original_pos = position
for i in range(5):
var offset = Vector2(randf_range(-10, 10), randf_range(-10, 10))
tween.tween_property(self, "position", original_pos + offset, 0.05)
tween.tween_property(self, "position", original_pos, 0.05)
19.5.3 缓动函数
extends Node2D
func tween_with_easing():
var tween = create_tween()
# 设置过渡类型
tween.set_trans(Tween.TRANS_SINE)
# TRANS_LINEAR, TRANS_SINE, TRANS_QUINT, TRANS_QUART
# TRANS_QUAD, TRANS_EXPO, TRANS_ELASTIC, TRANS_CUBIC
# TRANS_CIRC, TRANS_BOUNCE, TRANS_BACK, TRANS_SPRING
# 设置缓动类型
tween.set_ease(Tween.EASE_IN_OUT)
# EASE_IN, EASE_OUT, EASE_IN_OUT, EASE_OUT_IN
tween.tween_property(self, "position:x", 500, 1.0)
# 为单个属性设置缓动
func individual_easing():
var tween = create_tween()
tween.tween_property(self, "position", Vector2(500, 300), 1.0)\
.set_trans(Tween.TRANS_ELASTIC)\
.set_ease(Tween.EASE_OUT)
19.5.4 Tween工具类
# tween_utils.gd
class_name TweenUtils
static func flash(node: CanvasItem, color: Color = Color.WHITE, duration: float = 0.1) -> Tween:
var original = node.modulate
var tween = node.create_tween()
tween.tween_property(node, "modulate", color, duration / 2)
tween.tween_property(node, "modulate", original, duration / 2)
return tween
static func pop_in(node: Node2D, duration: float = 0.3) -> Tween:
node.scale = Vector2.ZERO
var tween = node.create_tween()
tween.tween_property(node, "scale", Vector2(1.2, 1.2), duration * 0.6)\
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(node, "scale", Vector2.ONE, duration * 0.4)
return tween
static func slide_in(node: Control, from: Vector2, duration: float = 0.3) -> Tween:
var target = node.position
node.position = from
node.modulate.a = 0
var tween = node.create_tween()
tween.set_parallel(true)
tween.tween_property(node, "position", target, duration)\
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
tween.tween_property(node, "modulate:a", 1.0, duration * 0.7)
return tween
static func typewriter(label: Label, text: String, speed: float = 0.05) -> Tween:
label.text = ""
label.visible_characters = 0
label.text = text
var tween = label.create_tween()
tween.tween_property(label, "visible_characters", text.length(), text.length() * speed)
return tween
19.6 骨骼动画
19.6.1 Skeleton2D基础
extends Node2D
@onready var skeleton: Skeleton2D = $Skeleton2D
func _ready():
# 获取骨骼
var bone = skeleton.get_bone(0)
print("骨骼名称:", bone.name)
# 修改骨骼变换
bone.transform = Transform2D(0.5, Vector2(10, 0))
# 程序化动画
func wiggle_bone(bone_idx: int, amount: float):
var bone = skeleton.get_bone(bone_idx)
var original_rotation = bone.rotation
var tween = create_tween().set_loops(3)
tween.tween_property(bone, "rotation", original_rotation + amount, 0.1)
tween.tween_property(bone, "rotation", original_rotation - amount, 0.1)
tween.tween_property(bone, "rotation", original_rotation, 0.1)
19.6.2 IK(逆向运动学)
extends Skeleton2D
@onready var ik: SkeletonModification2DFABRIK = null
func _ready():
# 设置IK
var stack = SkeletonModificationStack2D.new()
stack.modification_count = 1
stack.enabled = true
var fabrik = SkeletonModification2DFABRIK.new()
fabrik.target_nodepath = $"../IKTarget".get_path()
fabrik.fabrik_data_chain_length = 3
stack.set_modification(0, fabrik)
modification_stack = stack
# 让手臂跟随目标
func point_at(target_position: Vector2):
$"../IKTarget".global_position = target_position
19.7 特效动画
19.7.1 粒子动画配合
extends Node2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var particles: GPUParticles2D = $GPUParticles2D
func play_attack():
anim_player.play("attack")
# 在动画特定帧触发粒子
await get_tree().create_timer(0.2).timeout
particles.emitting = true
# 或在AnimationPlayer中使用方法调用轨道
func emit_particles():
$GPUParticles2D.emitting = true
func spawn_impact(pos: Vector2):
var effect = preload("res://effects/impact.tscn").instantiate()
effect.global_position = pos
get_tree().root.add_child(effect)
19.7.2 屏幕震动
# camera_shake.gd
extends Camera2D
var shake_intensity: float = 0.0
var shake_decay: float = 5.0
func _process(delta: float):
if shake_intensity > 0:
offset = Vector2(
randf_range(-shake_intensity, shake_intensity),
randf_range(-shake_intensity, shake_intensity)
)
shake_intensity = max(0, shake_intensity - shake_decay * delta)
else:
offset = Vector2.ZERO
func shake(intensity: float = 10.0, decay: float = 5.0):
shake_intensity = intensity
shake_decay = decay
# 使用Tween的版本
func shake_tween(intensity: float = 10.0, duration: float = 0.3):
var tween = create_tween()
var original = offset
for i in range(int(duration / 0.05)):
var shake_offset = Vector2(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity)
)
tween.tween_property(self, "offset", shake_offset, 0.05)
intensity *= 0.9
tween.tween_property(self, "offset", original, 0.05)
19.8 实际案例
19.8.1 完整角色动画系统
# character_animator.gd
class_name CharacterAnimator
extends Node
signal animation_event(event_name: String)
@export var animated_sprite: AnimatedSprite2D
@export var animation_tree: AnimationTree
enum State { IDLE, RUN, JUMP, FALL, ATTACK, HURT, DIE }
var current_state: State = State.IDLE
var state_machine: AnimationNodeStateMachinePlayback
var is_locked: bool = false
func _ready():
if animation_tree:
animation_tree.active = true
state_machine = animation_tree.get("parameters/playback")
if animated_sprite:
animated_sprite.animation_finished.connect(_on_animation_finished)
animated_sprite.frame_changed.connect(_on_frame_changed)
func update(velocity: Vector2, is_on_floor: bool) -> void:
if is_locked:
return
var new_state = _determine_state(velocity, is_on_floor)
if new_state != current_state:
_transition_to(new_state)
func _determine_state(velocity: Vector2, is_on_floor: bool) -> State:
if is_on_floor:
if abs(velocity.x) > 10:
return State.RUN
return State.IDLE
else:
if velocity.y < 0:
return State.JUMP
return State.FALL
func _transition_to(state: State) -> void:
current_state = state
if animation_tree:
_play_tree_state(state)
elif animated_sprite:
_play_sprite_animation(state)
func _play_tree_state(state: State) -> void:
match state:
State.IDLE: state_machine.travel("idle")
State.RUN: state_machine.travel("run")
State.JUMP: state_machine.travel("jump")
State.FALL: state_machine.travel("fall")
State.ATTACK: state_machine.travel("attack")
State.HURT: state_machine.travel("hurt")
State.DIE: state_machine.travel("die")
func _play_sprite_animation(state: State) -> void:
match state:
State.IDLE: animated_sprite.play("idle")
State.RUN: animated_sprite.play("run")
State.JUMP: animated_sprite.play("jump")
State.FALL: animated_sprite.play("fall")
State.ATTACK: animated_sprite.play("attack")
State.HURT: animated_sprite.play("hurt")
State.DIE: animated_sprite.play("die")
func play_attack() -> void:
is_locked = true
_transition_to(State.ATTACK)
func play_hurt() -> void:
is_locked = true
_transition_to(State.HURT)
func play_die() -> void:
is_locked = true
_transition_to(State.DIE)
func set_facing(direction: float) -> void:
if animated_sprite:
animated_sprite.flip_h = direction < 0
func _on_animation_finished() -> void:
match current_state:
State.ATTACK, State.HURT:
is_locked = false
State.DIE:
pass # 保持锁定
func _on_frame_changed() -> void:
# 动画事件系统
var anim = animated_sprite.animation
var frame = animated_sprite.frame
# 在特定动画的特定帧触发事件
if anim == "attack" and frame == 2:
animation_event.emit("attack_hit")
elif anim == "run" and (frame == 2 or frame == 6):
animation_event.emit("footstep")
19.8.2 UI动画管理器
# ui_animator.gd
class_name UIAnimator
extends Node
static func show_popup(popup: Control, duration: float = 0.3) -> void:
popup.scale = Vector2.ZERO
popup.modulate.a = 0
popup.visible = true
var tween = popup.create_tween()
tween.set_parallel(true)
tween.tween_property(popup, "scale", Vector2.ONE, duration)\
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(popup, "modulate:a", 1.0, duration * 0.7)
static func hide_popup(popup: Control, duration: float = 0.2) -> void:
var tween = popup.create_tween()
tween.set_parallel(true)
tween.tween_property(popup, "scale", Vector2(0.8, 0.8), duration)
tween.tween_property(popup, "modulate:a", 0.0, duration)
tween.set_parallel(false)
tween.tween_callback(func(): popup.visible = false)
static func button_hover(button: Control) -> void:
var tween = button.create_tween()
tween.tween_property(button, "scale", Vector2(1.1, 1.1), 0.1)
static func button_normal(button: Control) -> void:
var tween = button.create_tween()
tween.tween_property(button, "scale", Vector2.ONE, 0.1)
static func number_count(label: Label, from: int, to: int, duration: float = 1.0) -> void:
var tween = label.create_tween()
tween.tween_method(
func(value: int): label.text = str(value),
from, to, duration
)
本章小结
本章全面学习了Godot的2D动画系统:
- 动画系统概述:各种动画方案对比
- AnimatedSprite2D:帧动画、SpriteFrames
- AnimationPlayer:属性动画、轨道类型
- AnimationTree:状态机、BlendSpace、动画层
- Tween动画:链式调用、缓动函数、工具类
- 骨骼动画:Skeleton2D、IK
- 特效动画:粒子配合、屏幕震动
- 实际案例:角色动画系统、UI动画管理
下一章将学习TileMap与地图编辑。
上一章:碰撞检测
下一章:TileMap与地图编辑