第十九章:2D动画系统

第十九章: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动画系统:

  1. 动画系统概述:各种动画方案对比
  2. AnimatedSprite2D:帧动画、SpriteFrames
  3. AnimationPlayer:属性动画、轨道类型
  4. AnimationTree:状态机、BlendSpace、动画层
  5. Tween动画:链式调用、缓动函数、工具类
  6. 骨骼动画:Skeleton2D、IK
  7. 特效动画:粒子配合、屏幕震动
  8. 实际案例:角色动画系统、UI动画管理

下一章将学习TileMap与地图编辑。


上一章:碰撞检测

下一章:TileMap与地图编辑

← 返回目录