第三十章:3D动画系统

第三十章:3D动画系统

动画是让3D游戏角色和物体栩栩如生的关键技术。Godot 4提供了强大的3D动画系统,包括骨骼动画、AnimationPlayer、AnimationTree状态机、程序化动画等功能。本章将全面讲解Godot的3D动画系统,帮助开发者创建流畅自然的游戏动画。

30.1 动画系统概述

30.1.1 动画基础架构

# animation_overview.gd
# 动画系统概述

extends Node3D

## Godot 3D动画系统组件
##
## 核心组件:
## - Skeleton3D: 骨骼系统,存储骨骼层次和姿态
## - AnimationPlayer: 动画播放器,管理动画资源
## - AnimationTree: 动画树,实现复杂动画混合和状态机
## - AnimationMixer: 动画混合器基类
##
## 动画类型:
## - 骨骼动画: 通过骨骼变换驱动网格变形
## - 属性动画: 动画化任何节点属性
## - 过程动画: 代码驱动的程序化动画
## - 物理动画: 物理驱动的动画效果

## 获取动画组件
func get_animation_components():
    var skeleton = get_node_or_null("Armature/Skeleton3D") as Skeleton3D
    var anim_player = get_node_or_null("AnimationPlayer") as AnimationPlayer
    var anim_tree = get_node_or_null("AnimationTree") as AnimationTree
    
    return {
        "skeleton": skeleton,
        "animation_player": anim_player,
        "animation_tree": anim_tree
    }

30.1.2 骨骼系统基础

# skeleton_basics.gd
# 骨骼系统基础

extends Node3D

@onready var skeleton: Skeleton3D = $Armature/Skeleton3D

func _ready():
    _explore_skeleton()

func _explore_skeleton():
    print("骨骼数量: ", skeleton.get_bone_count())
    
    # 遍历所有骨骼
    for i in range(skeleton.get_bone_count()):
        var bone_name = skeleton.get_bone_name(i)
        var parent_idx = skeleton.get_bone_parent(i)
        var parent_name = skeleton.get_bone_name(parent_idx) if parent_idx >= 0 else "ROOT"
        print("  骨骼 ", i, ": ", bone_name, " (父: ", parent_name, ")")

## 获取骨骼变换
func get_bone_transform(bone_name: String) -> Transform3D:
    var bone_idx = skeleton.find_bone(bone_name)
    if bone_idx < 0:
        push_error("Bone not found: " + bone_name)
        return Transform3D.IDENTITY
    return skeleton.get_bone_global_pose(bone_idx)

## 设置骨骼变换
func set_bone_transform(bone_name: String, transform: Transform3D):
    var bone_idx = skeleton.find_bone(bone_name)
    if bone_idx >= 0:
        skeleton.set_bone_global_pose_override(bone_idx, transform, 1.0, true)

## 重置骨骼姿态
func reset_bone_pose(bone_name: String):
    var bone_idx = skeleton.find_bone(bone_name)
    if bone_idx >= 0:
        skeleton.reset_bone_pose(bone_idx)

## 重置所有骨骼
func reset_all_bones():
    skeleton.reset_bone_poses()

## 获取骨骼世界位置
func get_bone_world_position(bone_name: String) -> Vector3:
    var bone_transform = get_bone_transform(bone_name)
    return skeleton.global_transform * bone_transform.origin

30.2 AnimationPlayer详解

30.2.1 基础动画播放

# animation_player_basics.gd
# AnimationPlayer基础使用

extends CharacterBody3D

@onready var anim_player: AnimationPlayer = $AnimationPlayer

## 动画名称常量
const ANIM_IDLE = "idle"
const ANIM_WALK = "walk"
const ANIM_RUN = "run"
const ANIM_JUMP = "jump"
const ANIM_FALL = "fall"
const ANIM_ATTACK = "attack"

func _ready():
    _connect_signals()
    play_animation(ANIM_IDLE)

func _connect_signals():
    anim_player.animation_finished.connect(_on_animation_finished)
    anim_player.animation_started.connect(_on_animation_started)
    anim_player.animation_changed.connect(_on_animation_changed)

## 播放动画
func play_animation(anim_name: String, blend_time: float = 0.2):
    if not anim_player.has_animation(anim_name):
        push_warning("Animation not found: " + anim_name)
        return
    
    anim_player.play(anim_name, blend_time)

## 播放动画(从头开始)
func play_from_start(anim_name: String):
    anim_player.stop()
    anim_player.play(anim_name)

## 队列播放
func queue_animation(anim_name: String):
    anim_player.queue(anim_name)

## 停止动画
func stop_animation(keep_state: bool = false):
    anim_player.stop(keep_state)

## 暂停/恢复
func pause_animation():
    anim_player.pause()

func resume_animation():
    anim_player.play()

## 设置播放速度
func set_speed(speed: float):
    anim_player.speed_scale = speed

## 跳转到特定时间
func seek_to(time: float):
    anim_player.seek(time, true)

## 信号处理
func _on_animation_finished(anim_name: String):
    print("Animation finished: ", anim_name)
    
    # 非循环动画结束后返回idle
    if anim_name in [ANIM_ATTACK, ANIM_JUMP]:
        play_animation(ANIM_IDLE)

func _on_animation_started(anim_name: String):
    print("Animation started: ", anim_name)

func _on_animation_changed(old_name: String, new_name: String):
    print("Animation changed: ", old_name, " -> ", new_name)

## 获取动画信息
func get_animation_info() -> Dictionary:
    return {
        "current": anim_player.current_animation,
        "position": anim_player.current_animation_position,
        "length": anim_player.current_animation_length,
        "playing": anim_player.is_playing(),
        "speed": anim_player.speed_scale
    }

30.2.2 动画混合与过渡

# animation_blending.gd
# 动画混合控制

extends CharacterBody3D

@onready var anim_player: AnimationPlayer = $AnimationPlayer

## 混合配置
@export var default_blend_time: float = 0.25
@export var quick_blend_time: float = 0.1
@export var smooth_blend_time: float = 0.5

## 混合预设
var blend_presets: Dictionary = {
    # [from_anim, to_anim] = blend_time
    ["idle", "walk"]: 0.2,
    ["walk", "run"]: 0.15,
    ["run", "walk"]: 0.2,
    ["any", "attack"]: 0.1,
    ["attack", "idle"]: 0.3,
    ["any", "jump"]: 0.1,
    ["fall", "idle"]: 0.15,
}

func _ready():
    _setup_blend_times()

func _setup_blend_times():
    # 使用AnimationPlayer的set_blend_time设置预定义混合时间
    for key in blend_presets:
        var from_anim = key[0]
        var to_anim = key[1]
        var blend_time = blend_presets[key]
        
        if from_anim == "any":
            # 设置所有动画到目标动画的混合时间
            for anim_name in anim_player.get_animation_list():
                anim_player.set_blend_time(anim_name, to_anim, blend_time)
        else:
            anim_player.set_blend_time(from_anim, to_anim, blend_time)

## 智能播放(自动选择混合时间)
func smart_play(anim_name: String):
    var current = anim_player.current_animation
    var blend_time = _get_blend_time(current, anim_name)
    anim_player.play(anim_name, blend_time)

func _get_blend_time(from_anim: String, to_anim: String) -> float:
    # 检查特定组合
    var key = [from_anim, to_anim]
    if blend_presets.has(key):
        return blend_presets[key]
    
    # 检查通配符
    var any_key = ["any", to_anim]
    if blend_presets.has(any_key):
        return blend_presets[any_key]
    
    return default_blend_time

## 交叉淡入淡出
func crossfade_to(anim_name: String, duration: float = 0.3):
    # 使用Tween实现平滑交叉淡入淡出
    var current_anim = anim_player.current_animation
    var current_pos = anim_player.current_animation_position
    
    # 创建临时AnimationPlayer用于混合
    anim_player.play(anim_name, duration)

30.2.3 动画层与轨道

# animation_layers.gd
# 动画层系统

extends Node3D

## 多AnimationPlayer模拟层系统
@onready var base_anim: AnimationPlayer = $BaseAnimationPlayer
@onready var upper_body_anim: AnimationPlayer = $UpperBodyAnimationPlayer
@onready var additive_anim: AnimationPlayer = $AdditiveAnimationPlayer

## 层权重
var layer_weights: Dictionary = {
    "base": 1.0,
    "upper_body": 0.0,
    "additive": 0.0
}

func _ready():
    _setup_layers()

func _setup_layers():
    # 基础层始终播放
    base_anim.play("idle")
    
    # 上半身层配置为只影响特定骨骼
    # 这需要在动画资源中配置轨道过滤

## 设置层权重
func set_layer_weight(layer_name: String, weight: float):
    layer_weights[layer_name] = clamp(weight, 0.0, 1.0)
    
    match layer_name:
        "upper_body":
            # 调整上半身动画的影响
            pass
        "additive":
            # 调整附加动画的影响
            pass

## 播放上半身动画
func play_upper_body(anim_name: String):
    upper_body_anim.play(anim_name)
    set_layer_weight("upper_body", 1.0)

## 播放附加动画(如呼吸、受伤抖动)
func play_additive(anim_name: String, weight: float = 1.0):
    additive_anim.play(anim_name)
    set_layer_weight("additive", weight)

## 淡出层
func fadeout_layer(layer_name: String, duration: float = 0.3):
    var tween = create_tween()
    tween.tween_method(
        func(w): set_layer_weight(layer_name, w),
        layer_weights[layer_name],
        0.0,
        duration
    )

30.3 AnimationTree状态机

30.3.1 AnimationTree基础

# animation_tree_basics.gd
# AnimationTree基础使用

extends CharacterBody3D

@onready var anim_tree: AnimationTree = $AnimationTree
@onready var anim_state: AnimationNodeStateMachinePlayback

func _ready():
    anim_tree.active = true
    anim_state = anim_tree.get("parameters/playback")

## 状态机控制
func travel_to_state(state_name: String):
    """平滑过渡到目标状态"""
    anim_state.travel(state_name)

func start_state(state_name: String):
    """立即切换到状态(无过渡)"""
    anim_state.start(state_name)

func stop_state_machine():
    anim_state.stop()

## 获取当前状态
func get_current_state() -> String:
    return anim_state.get_current_node()

func is_playing() -> bool:
    return anim_state.is_playing()

## 参数控制
func set_blend_parameter(param_path: String, value: float):
    anim_tree.set("parameters/" + param_path, value)

func get_blend_parameter(param_path: String) -> float:
    return anim_tree.get("parameters/" + param_path)

## 常用参数设置
func set_movement_blend(value: float):
    # 控制idle/walk/run混合
    set_blend_parameter("movement/blend_position", value)

func set_aim_blend(horizontal: float, vertical: float):
    # 控制瞄准方向
    set_blend_parameter("aim/blend_position", Vector2(horizontal, vertical))

30.3.2 状态机配置

# state_machine_setup.gd
# 状态机配置与管理

extends CharacterBody3D

@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback

## 状态常量
const STATE_IDLE = "Idle"
const STATE_MOVE = "Move"
const STATE_JUMP = "Jump"
const STATE_FALL = "Fall"
const STATE_LAND = "Land"
const STATE_ATTACK = "Attack"
const STATE_HIT = "Hit"
const STATE_DEATH = "Death"

## 当前状态
var current_state: String = STATE_IDLE
var previous_state: String = ""

func _ready():
    anim_tree.active = true
    state_machine = anim_tree.get("parameters/StateMachine/playback")

func _physics_process(delta):
    _update_animation_state()
    _update_blend_parameters(delta)

func _update_animation_state():
    var new_state = _determine_state()
    
    if new_state != current_state:
        _transition_to_state(new_state)

func _determine_state() -> String:
    # 优先级最高的状态检查
    if is_dead():
        return STATE_DEATH
    
    if is_hit():
        return STATE_HIT
    
    if is_attacking():
        return STATE_ATTACK
    
    # 空中状态
    if not is_on_floor():
        if velocity.y > 0:
            return STATE_JUMP
        else:
            return STATE_FALL
    
    # 着陆检查
    if previous_state in [STATE_JUMP, STATE_FALL] and is_on_floor():
        return STATE_LAND
    
    # 地面移动
    if velocity.length() > 0.1:
        return STATE_MOVE
    
    return STATE_IDLE

func _transition_to_state(new_state: String):
    previous_state = current_state
    current_state = new_state
    
    # 使用travel进行平滑过渡
    state_machine.travel(new_state)
    
    # 状态进入回调
    _on_state_entered(new_state)

func _on_state_entered(state: String):
    match state:
        STATE_ATTACK:
            # 攻击状态可能需要特殊处理
            pass
        STATE_LAND:
            # 着陆后自动过渡到idle或move
            await get_tree().create_timer(0.2).timeout
            if is_on_floor():
                state_machine.travel(STATE_IDLE if velocity.length() < 0.1 else STATE_MOVE)

func _update_blend_parameters(delta: float):
    # 更新移动混合
    var speed = Vector2(velocity.x, velocity.z).length()
    var normalized_speed = clamp(speed / 10.0, 0.0, 1.0)
    
    var current_blend = anim_tree.get("parameters/Move/blend_position")
    var target_blend = normalized_speed
    var new_blend = lerp(current_blend, target_blend, 10.0 * delta)
    anim_tree.set("parameters/Move/blend_position", new_blend)

## 状态检查辅助函数
func is_dead() -> bool:
    return false  # 根据实际游戏逻辑实现

func is_hit() -> bool:
    return false

func is_attacking() -> bool:
    return false

30.3.3 混合空间

# blend_spaces.gd
# 混合空间使用

extends CharacterBody3D

@onready var anim_tree: AnimationTree = $AnimationTree

## 1D混合空间参数
func set_1d_blend(parameter_path: String, value: float):
    """
    1D混合空间用于:
    - 移动速度(idle -> walk -> run)
    - 伤害程度(轻微 -> 严重)
    """
    anim_tree.set("parameters/" + parameter_path + "/blend_position", value)

## 2D混合空间参数
func set_2d_blend(parameter_path: String, position: Vector2):
    """
    2D混合空间用于:
    - 八方向移动(前、后、左、右及对角)
    - 瞄准方向(水平和垂直)
    """
    anim_tree.set("parameters/" + parameter_path + "/blend_position", position)

## 移动动画混合示例
func update_movement_animation():
    # 获取移动方向(相对于角色朝向)
    var local_velocity = global_transform.basis.inverse() * velocity
    var move_direction = Vector2(local_velocity.x, -local_velocity.z)
    
    # 归一化速度
    var max_speed = 10.0
    move_direction = move_direction / max_speed
    move_direction = move_direction.limit_length(1.0)
    
    set_2d_blend("Locomotion", move_direction)

## 瞄准动画混合示例
func update_aim_animation(aim_direction: Vector3):
    # 计算瞄准角度
    var local_aim = global_transform.basis.inverse() * aim_direction
    
    # 水平角度(左右)
    var horizontal = atan2(local_aim.x, -local_aim.z) / PI
    
    # 垂直角度(上下)
    var vertical = asin(clamp(local_aim.y, -1.0, 1.0)) / (PI / 2.0)
    
    set_2d_blend("AimDirection", Vector2(horizontal, vertical))

30.4 骨骼动画高级技术

30.4.1 IK(反向动力学)

# ik_system.gd
# 反向动力学系统

extends Node3D

@onready var skeleton: Skeleton3D = $Armature/Skeleton3D

## IK目标
@export var foot_ik_enabled: bool = true
@export var hand_ik_enabled: bool = true
@export var look_at_enabled: bool = true

## 骨骼索引
var left_foot_bone: int
var right_foot_bone: int
var left_hand_bone: int
var right_hand_bone: int
var head_bone: int

## IK目标位置
var left_foot_target: Vector3
var right_foot_target: Vector3
var left_hand_target: Vector3
var right_hand_target: Vector3
var look_at_target: Vector3

## 射线检测
@onready var left_foot_ray: RayCast3D = $LeftFootRay
@onready var right_foot_ray: RayCast3D = $RightFootRay

func _ready():
    _cache_bone_indices()

func _cache_bone_indices():
    left_foot_bone = skeleton.find_bone("LeftFoot")
    right_foot_bone = skeleton.find_bone("RightFoot")
    left_hand_bone = skeleton.find_bone("LeftHand")
    right_hand_bone = skeleton.find_bone("RightHand")
    head_bone = skeleton.find_bone("Head")

func _physics_process(delta):
    if foot_ik_enabled:
        _update_foot_ik(delta)
    if look_at_enabled:
        _update_look_at(delta)

func _update_foot_ik(delta):
    # 左脚射线检测
    if left_foot_ray.is_colliding():
        left_foot_target = left_foot_ray.get_collision_point()
        _apply_foot_ik(left_foot_bone, left_foot_target, left_foot_ray.get_collision_normal())
    
    # 右脚射线检测
    if right_foot_ray.is_colliding():
        right_foot_target = right_foot_ray.get_collision_point()
        _apply_foot_ik(right_foot_bone, right_foot_target, right_foot_ray.get_collision_normal())

func _apply_foot_ik(bone_idx: int, target_pos: Vector3, normal: Vector3):
    if bone_idx < 0:
        return
    
    # 获取当前骨骼变换
    var bone_transform = skeleton.get_bone_global_pose(bone_idx)
    
    # 计算新位置
    var skeleton_transform = skeleton.global_transform
    var local_target = skeleton_transform.inverse() * target_pos
    
    # 创建新变换
    var new_transform = bone_transform
    new_transform.origin = local_target
    
    # 根据地面法线调整旋转
    var up = normal
    var forward = -new_transform.basis.z
    var right = up.cross(forward).normalized()
    forward = right.cross(up).normalized()
    new_transform.basis = Basis(right, up, forward)
    
    # 应用IK
    skeleton.set_bone_global_pose_override(bone_idx, new_transform, 1.0, true)

func _update_look_at(delta):
    if head_bone < 0:
        return
    
    var head_transform = skeleton.get_bone_global_pose(head_bone)
    var skeleton_transform = skeleton.global_transform
    var world_head_pos = skeleton_transform * head_transform.origin
    
    # 计算看向方向
    var look_direction = (look_at_target - world_head_pos).normalized()
    var local_look_dir = skeleton_transform.basis.inverse() * look_direction
    
    # 创建看向变换
    var new_transform = head_transform.looking_at(
        head_transform.origin + local_look_dir,
        Vector3.UP
    )
    
    # 限制旋转角度
    # ...
    
    skeleton.set_bone_global_pose_override(head_bone, new_transform, 0.5, true)

## 设置IK目标
func set_hand_target(left: Vector3, right: Vector3):
    left_hand_target = left
    right_hand_target = right

func set_look_at_target(target: Vector3):
    look_at_target = target

30.4.2 SkeletonIK3D节点

# skeleton_ik_usage.gd
# SkeletonIK3D使用

extends Node3D

@onready var skeleton: Skeleton3D = $Armature/Skeleton3D
@onready var left_arm_ik: SkeletonIK3D = $Armature/Skeleton3D/LeftArmIK
@onready var right_arm_ik: SkeletonIK3D = $Armature/Skeleton3D/RightArmIK

## IK目标节点
@onready var left_hand_target: Node3D = $LeftHandTarget
@onready var right_hand_target: Node3D = $RightHandTarget

## IK权重
var left_arm_weight: float = 0.0
var right_arm_weight: float = 0.0

func _ready():
    _setup_ik()

func _setup_ik():
    # 配置左舣IK
    left_arm_ik.root_bone = "LeftShoulder"
    left_arm_ik.tip_bone = "LeftHand"
    left_arm_ik.target_node = left_hand_target.get_path()
    left_arm_ik.use_magnet = true
    left_arm_ik.magnet = Vector3(0, 0, -1)  # 胘彯方向
    
    # 配置右舣IK
    right_arm_ik.root_bone = "RightShoulder"
    right_arm_ik.tip_bone = "RightHand"
    right_arm_ik.target_node = right_hand_target.get_path()
    right_arm_ik.use_magnet = true
    right_arm_ik.magnet = Vector3(0, 0, -1)

func _physics_process(_delta):
    _update_ik_weights()

func _update_ik_weights():
    left_arm_ik.interpolation = left_arm_weight
    right_arm_ik.interpolation = right_arm_weight
    
    left_arm_ik.start()
    right_arm_ik.start()

## 启用/禁用IK
func enable_left_arm_ik(weight: float = 1.0):
    left_arm_weight = weight

func enable_right_arm_ik(weight: float = 1.0):
    right_arm_weight = weight

func disable_arm_ik():
    left_arm_weight = 0.0
    right_arm_weight = 0.0

## 设置IK目标位置
func set_left_hand_position(pos: Vector3):
    left_hand_target.global_position = pos

func set_right_hand_position(pos: Vector3):
    right_hand_target.global_position = pos

## 双手持武器示例
func grip_weapon(weapon: Node3D):
    var left_grip = weapon.get_node("LeftGrip")
    var right_grip = weapon.get_node("RightGrip")
    
    left_hand_target.global_transform = left_grip.global_transform
    right_hand_target.global_transform = right_grip.global_transform
    
    enable_left_arm_ik()
    enable_right_arm_ik()

30.4.3 骨骼修改器

# bone_modifiers.gd
# 骨骼修改器系统

extends Node3D

@onready var skeleton: Skeleton3D = $Armature/Skeleton3D

## 修改器类型
enum ModifierType {
    ROTATION,       # 旋转修改
    POSITION,       # 位置修改
    SCALE,          # 缩放修改
    CONSTRAINT,     # 约束
    SPRING,         # 弹簧物理
    LOOK_AT         # 看向
}

## 修改器数据
class BoneModifier:
    var bone_name: String
    var bone_index: int
    var modifier_type: ModifierType
    var weight: float = 1.0
    var parameters: Dictionary = {}
    var enabled: bool = true

var modifiers: Array[BoneModifier] = []

## 添加旋转修改器
func add_rotation_modifier(
    bone_name: String,
    rotation: Vector3,
    weight: float = 1.0
):
    var modifier = BoneModifier.new()
    modifier.bone_name = bone_name
    modifier.bone_index = skeleton.find_bone(bone_name)
    modifier.modifier_type = ModifierType.ROTATION
    modifier.weight = weight
    modifier.parameters = {"rotation": rotation}
    modifiers.append(modifier)

## 添加看向修改器
func add_look_at_modifier(
    bone_name: String,
    target: Node3D,
    weight: float = 1.0
):
    var modifier = BoneModifier.new()
    modifier.bone_name = bone_name
    modifier.bone_index = skeleton.find_bone(bone_name)
    modifier.modifier_type = ModifierType.LOOK_AT
    modifier.weight = weight
    modifier.parameters = {"target": target}
    modifiers.append(modifier)

## 添加弹簧修改器(头发、尾巴等)
func add_spring_modifier(
    bone_name: String,
    stiffness: float = 100.0,
    damping: float = 10.0
):
    var modifier = BoneModifier.new()
    modifier.bone_name = bone_name
    modifier.bone_index = skeleton.find_bone(bone_name)
    modifier.modifier_type = ModifierType.SPRING
    modifier.parameters = {
        "stiffness": stiffness,
        "damping": damping,
        "velocity": Vector3.ZERO,
        "current_rotation": Vector3.ZERO
    }
    modifiers.append(modifier)

func _physics_process(delta):
    _apply_modifiers(delta)

func _apply_modifiers(delta):
    for modifier in modifiers:
        if not modifier.enabled:
            continue
        
        if modifier.bone_index < 0:
            continue
        
        match modifier.modifier_type:
            ModifierType.ROTATION:
                _apply_rotation_modifier(modifier)
            ModifierType.LOOK_AT:
                _apply_look_at_modifier(modifier)
            ModifierType.SPRING:
                _apply_spring_modifier(modifier, delta)

func _apply_rotation_modifier(modifier: BoneModifier):
    var bone_transform = skeleton.get_bone_pose(modifier.bone_index)
    var rotation = modifier.parameters.rotation as Vector3
    
    # 应用加性旋转
    var additional_rotation = Basis.from_euler(rotation * modifier.weight)
    bone_transform.basis = bone_transform.basis * additional_rotation
    
    skeleton.set_bone_pose_rotation(modifier.bone_index, bone_transform.basis.get_rotation_quaternion())

func _apply_look_at_modifier(modifier: BoneModifier):
    var target = modifier.parameters.target as Node3D
    if not target:
        return
    
    var bone_global = skeleton.global_transform * skeleton.get_bone_global_pose(modifier.bone_index)
    var direction = (target.global_position - bone_global.origin).normalized()
    var local_direction = bone_global.basis.inverse() * direction
    
    # 计算旋转
    var look_rotation = Quaternion(Vector3.FORWARD, local_direction)
    var current_rotation = skeleton.get_bone_pose_rotation(modifier.bone_index)
    var blended_rotation = current_rotation.slerp(look_rotation, modifier.weight)
    
    skeleton.set_bone_pose_rotation(modifier.bone_index, blended_rotation)

func _apply_spring_modifier(modifier: BoneModifier, delta: float):
    var stiffness = modifier.parameters.stiffness
    var damping = modifier.parameters.damping
    var velocity = modifier.parameters.velocity as Vector3
    var current = modifier.parameters.current_rotation as Vector3
    
    # 获取目标旋转(原始动画)
    var target_rotation = skeleton.get_bone_pose_rotation(modifier.bone_index).get_euler()
    
    # 弹簧物理
    var force = (target_rotation - current) * stiffness
    velocity += force * delta
    velocity -= velocity * damping * delta
    current += velocity * delta
    
    # 更新状态
    modifier.parameters.velocity = velocity
    modifier.parameters.current_rotation = current
    
    # 应用旋转
    skeleton.set_bone_pose_rotation(
        modifier.bone_index,
        Quaternion.from_euler(current)
    )

30.5 程序化动画

30.5.1 Tween动画

# tween_animations.gd
# Tween程序化动画

extends Node3D

## 基础Tween动画
func animate_position(target: Vector3, duration: float) -> Tween:
    var tween = create_tween()
    tween.tween_property(self, "global_position", target, duration)
    return tween

func animate_rotation(target: Vector3, duration: float) -> Tween:
    var tween = create_tween()
    tween.tween_property(self, "rotation_degrees", target, duration)
    return tween

func animate_scale(target: Vector3, duration: float) -> Tween:
    var tween = create_tween()
    tween.tween_property(self, "scale", target, duration)
    return tween

## 组合动画
func jump_animation(height: float, duration: float):
    var tween = create_tween()
    var start_pos = global_position
    var peak_pos = start_pos + Vector3.UP * height
    
    # 上升
    tween.tween_property(self, "global_position", peak_pos, duration / 2)\
        .set_ease(Tween.EASE_OUT)\
        .set_trans(Tween.TRANS_QUAD)
    
    # 下降
    tween.tween_property(self, "global_position", start_pos, duration / 2)\
        .set_ease(Tween.EASE_IN)\
        .set_trans(Tween.TRANS_QUAD)
    
    return tween

## 并行动画
func parallel_animation():
    var tween = create_tween()
    tween.set_parallel(true)
    
    tween.tween_property(self, "position", Vector3(5, 0, 0), 1.0)
    tween.tween_property(self, "rotation_degrees:y", 360.0, 1.0)
    tween.tween_property(self, "scale", Vector3(2, 2, 2), 1.0)
    
    return tween

## 序列动画
func sequence_animation():
    var tween = create_tween()
    
    # 步骤1: 移动
    tween.tween_property(self, "position", Vector3(5, 0, 0), 0.5)
    # 步骤2: 等待
    tween.tween_interval(0.2)
    # 步骤3: 旋转
    tween.tween_property(self, "rotation_degrees:y", 180.0, 0.3)
    # 步骤4: 回调
    tween.tween_callback(func(): print("动画完成"))
    
    return tween

## 循环动画
func loop_animation(times: int = 0):
    var tween = create_tween()
    tween.set_loops(times)  # 0 = 无限循环
    
    tween.tween_property(self, "rotation_degrees:y", 360.0, 2.0)\
        .from(0.0)
    
    return tween

## 摇摆动画(往返)
func ping_pong_animation():
    var tween = create_tween()
    tween.set_loops()
    
    tween.tween_property(self, "position:x", 5.0, 1.0)
    tween.tween_property(self, "position:x", -5.0, 1.0)
    
    return tween

## 缓动曲线示例
func easing_examples():
    var tween = create_tween()
    
    # 线性
    tween.tween_property(self, "position:x", 10.0, 1.0)\
        .set_trans(Tween.TRANS_LINEAR)
    
    # 次方缓出(常用于移动)
    tween.tween_property(self, "position:x", 0.0, 1.0)\
        .set_trans(Tween.TRANS_QUAD)\
        .set_ease(Tween.EASE_OUT)
    
    # 弹性缓出(常用于UI)
    tween.tween_property(self, "scale", Vector3(1.5, 1.5, 1.5), 0.5)\
        .set_trans(Tween.TRANS_ELASTIC)\
        .set_ease(Tween.EASE_OUT)
    
    # 回弹缓出
    tween.tween_property(self, "scale", Vector3.ONE, 0.5)\
        .set_trans(Tween.TRANS_BOUNCE)\
        .set_ease(Tween.EASE_OUT)
    
    return tween

30.5.2 过程动画系统

# procedural_animation.gd
# 过程动画系统

extends Node3D

## 头部跟踪
class HeadTracking:
    var target: Node3D
    var head_bone: int
    var skeleton: Skeleton3D
    var max_angle: float = 70.0
    var tracking_speed: float = 5.0
    var current_rotation: Quaternion = Quaternion.IDENTITY
    
    func update(delta: float):
        if not target or head_bone < 0:
            return
        
        # 获取头部位置
        var head_global = skeleton.global_transform * skeleton.get_bone_global_pose(head_bone)
        var head_pos = head_global.origin
        
        # 计算目标方向
        var direction = (target.global_position - head_pos).normalized()
        var local_dir = head_global.basis.inverse() * direction
        
        # 计算目标旋转
        var target_rotation = Quaternion(Vector3.FORWARD, local_dir)
        
        # 限制角度
        var angle = target_rotation.get_angle()
        if angle > deg_to_rad(max_angle):
            target_rotation = Quaternion.IDENTITY.slerp(target_rotation, deg_to_rad(max_angle) / angle)
        
        # 平滑跟踪
        current_rotation = current_rotation.slerp(target_rotation, tracking_speed * delta)
        
        # 应用
        var bone_pose = skeleton.get_bone_pose(head_bone)
        bone_pose.basis = Basis(current_rotation)
        skeleton.set_bone_pose_rotation(head_bone, current_rotation)

## 呼吸动画
class BreathingAnimation:
    var skeleton: Skeleton3D
    var spine_bones: Array[int] = []
    var breath_speed: float = 2.0
    var breath_intensity: float = 0.02
    var time: float = 0.0
    
    func setup(skel: Skeleton3D, bones: Array[String]):
        skeleton = skel
        for bone_name in bones:
            var idx = skeleton.find_bone(bone_name)
            if idx >= 0:
                spine_bones.append(idx)
    
    func update(delta: float):
        time += delta * breath_speed
        
        var breath_value = sin(time) * breath_intensity
        
        for i in range(spine_bones.size()):
            var bone_idx = spine_bones[i]
            var weight = 1.0 - float(i) / float(spine_bones.size())
            
            var scale = Vector3(1.0, 1.0 + breath_value * weight, 1.0)
            skeleton.set_bone_pose_scale(bone_idx, scale)

## 行走腰部摇摆
class HipSwayAnimation:
    var skeleton: Skeleton3D
    var hip_bone: int
    var sway_amount: float = 3.0
    var current_sway: float = 0.0
    var sway_speed: float = 5.0
    
    func update(velocity: Vector3, delta: float):
        if hip_bone < 0:
            return
        
        var speed = Vector2(velocity.x, velocity.z).length()
        var target_sway = sin(Time.get_ticks_msec() / 200.0) * sway_amount * min(speed / 5.0, 1.0)
        
        current_sway = lerp(current_sway, target_sway, sway_speed * delta)
        
        var rotation = Quaternion.from_euler(Vector3(0, 0, deg_to_rad(current_sway)))
        skeleton.set_bone_pose_rotation(hip_bone, rotation)

## 主控制器
var head_tracking: HeadTracking
var breathing: BreathingAnimation
var hip_sway: HipSwayAnimation

@onready var skeleton: Skeleton3D = $Armature/Skeleton3D

func _ready():
    _setup_procedural_animations()

func _setup_procedural_animations():
    # 头部跟踪
    head_tracking = HeadTracking.new()
    head_tracking.skeleton = skeleton
    head_tracking.head_bone = skeleton.find_bone("Head")
    
    # 呼吸
    breathing = BreathingAnimation.new()
    breathing.setup(skeleton, ["Spine", "Spine1", "Spine2"])
    
    # 腰部摇摆
    hip_sway = HipSwayAnimation.new()
    hip_sway.skeleton = skeleton
    hip_sway.hip_bone = skeleton.find_bone("Hips")

func _process(delta):
    breathing.update(delta)
    head_tracking.update(delta)

func _physics_process(delta):
    var velocity = Vector3.ZERO  # 从角色控制器获取
    hip_sway.update(velocity, delta)

func set_look_target(target: Node3D):
    head_tracking.target = target

30.6 动画事件与回调

30.6.1 动画事件系统

# animation_events.gd
# 动画事件系统

extends CharacterBody3D

@onready var anim_player: AnimationPlayer = $AnimationPlayer

## 事件信号
signal footstep(foot: String)
signal attack_hit
signal attack_end
signal sound_effect(sound_name: String)
signal particle_spawn(particle_name: String, position: Vector3)

func _ready():
    _setup_animation_callbacks()

func _setup_animation_callbacks():
    # 连接动画信号
    anim_player.animation_finished.connect(_on_animation_finished)

## 动画轨道调用的方法(在动画编辑器中设置)
func on_footstep_left():
    emit_signal("footstep", "left")
    _play_footstep_sound("left")
    _spawn_footstep_particle("left")

func on_footstep_right():
    emit_signal("footstep", "right")
    _play_footstep_sound("right")
    _spawn_footstep_particle("right")

func on_attack_hit_frame():
    emit_signal("attack_hit")
    _check_attack_collision()

func on_attack_end():
    emit_signal("attack_end")

func on_play_sound(sound_name: String):
    emit_signal("sound_effect", sound_name)

func on_spawn_particle(particle_name: String):
    var bone_name = "RightHand"  # 或从参数获取
    var bone_idx = $Armature/Skeleton3D.find_bone(bone_name)
    var bone_pos = $Armature/Skeleton3D.get_bone_global_pose(bone_idx).origin
    var world_pos = $Armature/Skeleton3D.global_transform * bone_pos
    emit_signal("particle_spawn", particle_name, world_pos)

## 内部实现
func _play_footstep_sound(foot: String):
    # 播放脚步声
    pass

func _spawn_footstep_particle(foot: String):
    # 生成脚步粒子
    pass

func _check_attack_collision():
    # 检查攻击碰撞
    var hitbox = $AttackHitbox as Area3D
    var bodies = hitbox.get_overlapping_bodies()
    for body in bodies:
        if body.has_method("take_damage"):
            body.take_damage(10)

func _on_animation_finished(anim_name: String):
    match anim_name:
        "attack", "attack_heavy":
            on_attack_end()

30.6.2 动画通知系统

# animation_notifier.gd
# 动画通知器

extends Node

## 通知数据
class AnimationNotify:
    var animation_name: String
    var time: float
    var callback: Callable
    var one_shot: bool = false
    var triggered: bool = false

var notifies: Array[AnimationNotify] = []
var anim_player: AnimationPlayer

func setup(player: AnimationPlayer):
    anim_player = player

## 添加通知
func add_notify(
    anim_name: String,
    time: float,
    callback: Callable,
    one_shot: bool = false
):
    var notify = AnimationNotify.new()
    notify.animation_name = anim_name
    notify.time = time
    notify.callback = callback
    notify.one_shot = one_shot
    notifies.append(notify)

## 添加百分比通知
func add_notify_percent(
    anim_name: String,
    percent: float,
    callback: Callable,
    one_shot: bool = false
):
    var anim = anim_player.get_animation(anim_name)
    if anim:
        var time = anim.length * percent
        add_notify(anim_name, time, callback, one_shot)

func _process(_delta):
    if not anim_player or not anim_player.is_playing():
        return
    
    var current_anim = anim_player.current_animation
    var current_time = anim_player.current_animation_position
    
    for notify in notifies:
        if notify.animation_name != current_anim:
            continue
        
        if notify.one_shot and notify.triggered:
            continue
        
        # 检查是否到达通知时间
        if current_time >= notify.time and not notify.triggered:
            notify.callback.call()
            notify.triggered = true
    
    # 重置循环动画的通知
    for notify in notifies:
        if notify.animation_name == current_anim:
            if current_time < notify.time:
                notify.triggered = false

## 清除通知
func clear_notifies(anim_name: String = ""):
    if anim_name.is_empty():
        notifies.clear()
    else:
        notifies = notifies.filter(func(n): return n.animation_name != anim_name)

30.7 动画优化

30.7.1 动画LOD系统

# animation_lod.gd
# 动画LOD系统

extends Node3D

## LOD设置
@export var lod_distances: Array[float] = [10.0, 25.0, 50.0]

## LOD配置
enum AnimationLOD {
    FULL,           # 完整动画
    REDUCED,        # 减少帧率
    SIMPLE,         # 简化动画
    STATIC          # 静态姿态
}

var current_lod: AnimationLOD = AnimationLOD.FULL
var camera: Camera3D

@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var anim_tree: AnimationTree = $AnimationTree

func _ready():
    camera = get_viewport().get_camera_3d()

func _process(delta):
    _update_lod()

func _update_lod():
    if not camera:
        return
    
    var distance = global_position.distance_to(camera.global_position)
    var new_lod = _get_lod_level(distance)
    
    if new_lod != current_lod:
        current_lod = new_lod
        _apply_lod(current_lod)

func _get_lod_level(distance: float) -> AnimationLOD:
    if distance < lod_distances[0]:
        return AnimationLOD.FULL
    elif distance < lod_distances[1]:
        return AnimationLOD.REDUCED
    elif distance < lod_distances[2]:
        return AnimationLOD.SIMPLE
    else:
        return AnimationLOD.STATIC

func _apply_lod(lod: AnimationLOD):
    match lod:
        AnimationLOD.FULL:
            # 完整动画
            anim_player.playback_process_mode = AnimationPlayer.ANIMATION_PROCESS_PHYSICS
            anim_player.speed_scale = 1.0
            if anim_tree:
                anim_tree.active = true
        
        AnimationLOD.REDUCED:
            # 降低更新频率
            anim_player.playback_process_mode = AnimationPlayer.ANIMATION_PROCESS_IDLE
            anim_player.speed_scale = 1.0
            if anim_tree:
                anim_tree.active = true
        
        AnimationLOD.SIMPLE:
            # 简化动画
            anim_player.speed_scale = 0.5
            if anim_tree:
                anim_tree.active = false
        
        AnimationLOD.STATIC:
            # 停止动画
            anim_player.stop(true)
            if anim_tree:
                anim_tree.active = false

30.7.2 动画缓存与预加载

# animation_cache.gd
# 动画缓存系统

extends Node

## 动画缓存
var animation_cache: Dictionary = {}
var preloaded_animations: Array[String] = []

## 预加载动画
func preload_animations(anim_player: AnimationPlayer, animations: Array[String]):
    for anim_name in animations:
        if anim_player.has_animation(anim_name):
            var anim = anim_player.get_animation(anim_name)
            animation_cache[anim_name] = anim
            preloaded_animations.append(anim_name)

## 获取缓存的动画
func get_cached_animation(anim_name: String) -> Animation:
    return animation_cache.get(anim_name)

## 清理缓存
func clear_cache():
    animation_cache.clear()
    preloaded_animations.clear()

## 动画池(用于动态加载)
class AnimationPool:
    var pool: Dictionary = {}  # animation_path: Animation
    var max_size: int = 50
    var access_order: Array[String] = []
    
    func get_animation(path: String) -> Animation:
        if pool.has(path):
            # 更新访问顺序
            access_order.erase(path)
            access_order.append(path)
            return pool[path]
        
        # 加载动画
        var anim = load(path) as Animation
        if anim:
            _add_to_pool(path, anim)
        return anim
    
    func _add_to_pool(path: String, anim: Animation):
        # 检查池大小
        while pool.size() >= max_size:
            var oldest = access_order.pop_front()
            pool.erase(oldest)
        
        pool[path] = anim
        access_order.append(path)

30.8 实际案例:完整角色动画系统

# complete_character_animation.gd
# 完整的角色动画系统

extends CharacterBody3D

## 组件引用
@onready var skeleton: Skeleton3D = $Armature/Skeleton3D
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback

## 动画状态
enum AnimState {
    IDLE,
    WALK,
    RUN,
    JUMP,
    FALL,
    LAND,
    ATTACK,
    HIT,
    DEATH
}

var current_state: AnimState = AnimState.IDLE
var is_attacking: bool = false
var is_dead: bool = false

## IK目标
var look_target: Node3D
var left_hand_ik_target: Vector3
var right_hand_ik_target: Vector3
var foot_ik_enabled: bool = true

## 动画参数
var movement_blend: float = 0.0
var aim_direction: Vector2 = Vector2.ZERO

func _ready():
    anim_tree.active = true
    state_machine = anim_tree.get("parameters/StateMachine/playback")
    _setup_animation_callbacks()

func _setup_animation_callbacks():
    # 设置动画事件回调
    pass

func _physics_process(delta):
    _update_animation_state()
    _update_blend_parameters(delta)
    _update_ik(delta)

func _update_animation_state():
    if is_dead:
        _set_state(AnimState.DEATH)
        return
    
    if is_attacking:
        return  # 攻击状态不中断
    
    if not is_on_floor():
        if velocity.y > 0:
            _set_state(AnimState.JUMP)
        else:
            _set_state(AnimState.FALL)
        return
    
    var speed = Vector2(velocity.x, velocity.z).length()
    if speed < 0.5:
        _set_state(AnimState.IDLE)
    elif speed < 5.0:
        _set_state(AnimState.WALK)
    else:
        _set_state(AnimState.RUN)

func _set_state(new_state: AnimState):
    if new_state == current_state:
        return
    
    var old_state = current_state
    current_state = new_state
    
    var state_name = AnimState.keys()[new_state]
    state_machine.travel(state_name)
    
    _on_state_changed(old_state, new_state)

func _on_state_changed(old_state: AnimState, new_state: AnimState):
    # 状态变化回调
    pass

func _update_blend_parameters(delta: float):
    # 移动混合
    var speed = Vector2(velocity.x, velocity.z).length()
    var target_blend = clamp(speed / 10.0, 0.0, 1.0)
    movement_blend = lerp(movement_blend, target_blend, 10.0 * delta)
    anim_tree.set("parameters/Locomotion/blend_position", movement_blend)
    
    # 方向混合(用于扫射瞄准)
    if look_target:
        var aim_dir = _calculate_aim_direction()
        aim_direction = aim_direction.lerp(aim_dir, 5.0 * delta)
        anim_tree.set("parameters/AimLayer/blend_position", aim_direction)

func _calculate_aim_direction() -> Vector2:
    if not look_target:
        return Vector2.ZERO
    
    var direction = (look_target.global_position - global_position).normalized()
    var local_dir = global_transform.basis.inverse() * direction
    
    var horizontal = atan2(local_dir.x, -local_dir.z) / PI
    var vertical = asin(clamp(local_dir.y, -1.0, 1.0)) / (PI / 2.0)
    
    return Vector2(horizontal, vertical)

func _update_ik(delta: float):
    if not foot_ik_enabled:
        return
    
    # 脚部IK
    _update_foot_ik(delta)
    
    # 头部看向
    if look_target:
        _update_head_ik(delta)

func _update_foot_ik(delta: float):
    # 简化的脚部IK实现
    pass

func _update_head_ik(delta: float):
    var head_bone = skeleton.find_bone("Head")
    if head_bone < 0:
        return
    
    var head_global = skeleton.global_transform * skeleton.get_bone_global_pose(head_bone)
    var direction = (look_target.global_position - head_global.origin).normalized()
    var local_dir = head_global.basis.inverse() * direction
    
    var target_rotation = Quaternion(Vector3.FORWARD, local_dir)
    var current_rotation = skeleton.get_bone_pose_rotation(head_bone)
    var new_rotation = current_rotation.slerp(target_rotation, 5.0 * delta)
    
    skeleton.set_bone_pose_rotation(head_bone, new_rotation)

## 公共API
func play_attack():
    if is_attacking or is_dead:
        return
    
    is_attacking = true
    state_machine.travel("Attack")
    
    # 等待动画完成
    await get_tree().create_timer(0.8).timeout
    is_attacking = false

func play_hit():
    state_machine.travel("Hit")

func play_death():
    is_dead = true
    state_machine.travel("Death")

func set_look_target(target: Node3D):
    look_target = target

func set_foot_ik(enabled: bool):
    foot_ik_enabled = enabled

30.9 本章小结

本章全面讲解了Godot 4的3D动画系统:

  1. 动画系统概述:骨骼系统、AnimationPlayer、AnimationTree构架
  2. AnimationPlayer:基础播放、混合过渡、动画层
  3. AnimationTree:状态机配置、混合空间使用
  4. 骨骼动画高级:IK系统、SkeletonIK3D、骨骼修改器
  5. 程序化动画:Tween系统、过程动画实现
  6. 动画事件:事件系统、通知回调
  7. 动画优化:LOD系统、缓存管理
  8. 实际案例:完整角色动画系统

动画是游戏角色的灵魂,好的动画系统能够显著提升游戏的沉浸感和反馈感。下一章我们将学习Godot的3D相机系统,控制玩家观察游戏世界的视角。

← 返回目录