第三十章: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动画系统:
- 动画系统概述:骨骼系统、AnimationPlayer、AnimationTree构架
- AnimationPlayer:基础播放、混合过渡、动画层
- AnimationTree:状态机配置、混合空间使用
- 骨骼动画高级:IK系统、SkeletonIK3D、骨骼修改器
- 程序化动画:Tween系统、过程动画实现
- 动画事件:事件系统、通知回调
- 动画优化:LOD系统、缓存管理
- 实际案例:完整角色动画系统
动画是游戏角色的灵魂,好的动画系统能够显著提升游戏的沉浸感和反馈感。下一章我们将学习Godot的3D相机系统,控制玩家观察游戏世界的视角。