17. 电影摄像机与过场动画
2026/4/14大约 13 分钟
电影摄像机与过场动画
电影摄像机概念
在电影里,导演通过控制摄像机的位置、角度和运动来讲述故事。在游戏里,你也可以像导演一样控制摄像机——这就是电影摄像机(Cinematic Camera)。
游戏中的电影摄像机常用于:
- 开场动画:游戏开始时展示游戏世界
- 剧情过场:角色对话时展示特写镜头
- Boss 出场:给 Boss 一个酷炫的登场展示
- 胜利画面:击败 Boss 后的庆祝镜头
Camera3D 的动画控制
通过 AnimationPlayer 节点,你可以对 Camera3D 的位置、旋转、FOV(视场角)等属性制作关键帧动画,就像在视频编辑软件里做动画一样。
基本概念
- 关键帧(Keyframe):在某个时间点记录摄像机的一个状态(位置、角度等)
- 插值(Interpolation):两个关键帧之间的过渡方式(线性、缓入缓出等)
- 时间轴(Timeline):所有关键帧按时间排列
C#
// 用代码控制摄像机动画
using Godot;
public partial class CameraAnimator : Node3D
{
private Camera3D _camera;
private AnimationPlayer _animPlayer;
private Animation _animation;
public override void _Ready()
{
_camera = GetNode<Camera3D>("Camera3D");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
// 创建动画
CreateCameraAnimation();
}
private void CreateCameraAnimation()
{
_animation = new Animation();
double duration = 5.0; // 动画持续5秒
_animation.Length = (float)duration;
// 轨道索引:控制摄像机位置
int posXTrack = _animation.AddTrack(Animation.TrackType.Value);
_animation.TrackSetPath(posXTrack, new NodePath("Camera3D:position:x"));
int posYTrack = _animation.AddTrack(Animation.TrackType.Value);
_animation.TrackSetPath(posYTrack, new NodePath("Camera3D:position:y"));
int posZTrack = _animation.AddTrack(Animation.TrackType.Value);
_animation.TrackSetPath(posZTrack, new NodePath("Camera3D:position:z"));
// 轨道索引:控制摄像机旋转
int rotXTrack = _animation.AddTrack(Animation.TrackType.Value);
_animation.TrackSetPath(rotXTrack, new NodePath("Camera3D:rotation_degrees:x"));
// 轨道索引:控制 FOV
int fovTrack = _animation.AddTrack(Animation.TrackType.Value);
_animation.TrackSetPath(fovTrack, new NodePath("Camera3D:fov"));
// === 关键帧 ===
// 起始位置:远处高处
SetKeyframe(posXTrack, 0.0, 0.0);
SetKeyframe(posYTrack, 0.0, 8.0);
SetKeyframe(posZTrack, 0.0, 15.0);
SetKeyframe(rotXTrack, 0.0, -30.0f);
SetKeyframe(fovTrack, 0.0, 40.0f);
// 2秒:推近
SetKeyframe(posXTrack, 2.0, 0.0);
SetKeyframe(posYTrack, 2.0, 3.0);
SetKeyframe(posZTrack, 2.0, 8.0);
SetKeyframe(rotXTrack, 2.0, -15.0f);
SetKeyframe(fovTrack, 2.0, 50.0f);
// 5秒:近处平视
SetKeyframe(posXTrack, 5.0, 0.0);
SetKeyframe(posYTrack, 5.0, 1.7);
SetKeyframe(posZTrack, 5.0, 3.0);
SetKeyframe(rotXTrack, 5.0, 0.0f);
SetKeyframe(fovTrack, 5.0, 60.0f);
// 添加动画到 AnimationPlayer
var lib = new AnimationLibrary();
lib.AddAnimation("camera_push_in", _animation);
_animPlayer.AddAnimationLibrary("", lib);
// 播放动画
_animPlayer.Play("camera_push_in");
GD.Print("摄像机动画开始播放!");
}
private void SetKeyframe(int trackIdx, double time, float value)
{
_animation.TrackInsertKey(trackIdx, (float)time, value);
}
}GDScript
# 用代码控制摄像机动画
extends Node3D
@onready var _camera: Camera3D = $Camera3D
@onready var _anim_player: AnimationPlayer = $AnimationPlayer
var _animation: Animation
func _ready():
create_camera_animation()
func create_camera_animation():
_animation = Animation.new()
var duration = 5.0 # 动画持续5秒
_animation.length = duration
# 轨道索引:控制摄像机位置
var pos_x_track = _animation.add_track(Animation.TYPE_VALUE)
_animation.track_set_path(pos_x_track, NodePath("Camera3D:position:x"))
var pos_y_track = _animation.add_track(Animation.TYPE_VALUE)
_animation.track_set_path(pos_y_track, NodePath("Camera3D:position:y"))
var pos_z_track = _animation.add_track(Animation.TYPE_VALUE)
_animation.track_set_path(pos_z_track, NodePath("Camera3D:position:z"))
# 轨道索引:控制摄像机旋转
var rot_x_track = _animation.add_track(Animation.TYPE_VALUE)
_animation.track_set_path(rot_x_track, NodePath("Camera3D:rotation_degrees:x"))
# 轨道索引:控制 FOV
var fov_track = _animation.add_track(Animation.TYPE_VALUE)
_animation.track_set_path(fov_track, NodePath("Camera3D:fov"))
# === 关键帧 ===
# 起始位置:远处高处
set_keyframe(pos_x_track, 0.0, 0.0)
set_keyframe(pos_y_track, 0.0, 8.0)
set_keyframe(pos_z_track, 0.0, 15.0)
set_keyframe(rot_x_track, 0.0, -30.0)
set_keyframe(fov_track, 0.0, 40.0)
# 2秒:推近
set_keyframe(pos_x_track, 2.0, 0.0)
set_keyframe(pos_y_track, 2.0, 3.0)
set_keyframe(pos_z_track, 2.0, 8.0)
set_keyframe(rot_x_track, 2.0, -15.0)
set_keyframe(fov_track, 2.0, 50.0)
# 5秒:近处平视
set_keyframe(pos_x_track, 5.0, 0.0)
set_keyframe(pos_y_track, 5.0, 1.7)
set_keyframe(pos_z_track, 5.0, 3.0)
set_keyframe(rot_x_track, 5.0, 0.0)
set_keyframe(fov_track, 5.0, 60.0)
# 添加动画到 AnimationPlayer
var lib = AnimationLibrary.new()
lib.add_animation("camera_push_in", _animation)
_anim_player.add_animation_library("", lib)
# 播放动画
_anim_player.play("camera_push_in")
print("摄像机动画开始播放!")
func set_keyframe(track_idx: int, time: float, value: float):
_animation.track_insert_key(track_idx, time, value)多机位切换
就像电视直播有多台摄像机一样,游戏中也可以放置多台 Camera3D,通过设置 Current = true 来切换当前激活的摄像机。
C#
// 多机位切换系统
using Godot;
using Godot.Collections;
public partial class CameraSwitcher : Node
{
private Camera3D[] _cameras;
private int _currentCameraIndex;
private double _autoSwitchInterval;
private double _switchTimer;
public override void _Ready()
{
// 收集场景中所有摄像机
var cameraList = new Array<Node>();
foreach (var child in GetTree().Root.GetChildren())
{
FindCameras(child, cameraList);
}
_cameras = new Camera3D[cameraList.Count];
for (int i = 0; i < cameraList.Count; i++)
{
_cameras[i] = cameraList[i] as Camera3D;
}
// 默认激活第一台摄像机
if (_cameras.Length > 0)
{
SwitchToCamera(0);
}
}
public override void _Process(double delta)
{
// 手动切换:按数字键 1-9
for (int i = 0; i < Mathf.Min(_cameras.Length, 9); i++)
{
if (Input.IsActionJustPressed($"camera_{i + 1}"))
{
SwitchToCamera(i);
}
}
// 自动轮播
if (_autoSwitchInterval > 0)
{
_switchTimer -= delta;
if (_switchTimer <= 0)
{
_switchTimer = _autoSwitchInterval;
SwitchToCamera((_currentCameraIndex + 1) % _cameras.Length);
}
}
}
/// <summary>切换到指定摄像机</summary>
public void SwitchToCamera(int index)
{
if (index < 0 || index >= _cameras.Length) return;
// 关闭所有摄像机
foreach (var cam in _cameras)
{
cam.Current = false;
}
// 激活目标摄像机
_cameras[index].Current = true;
_currentCameraIndex = index;
GD.Print($"切换到摄像机 {index + 1}:{_cameras[index].Name}");
}
/// <summary>设置自动切换间隔</summary>
public void SetAutoSwitch(double interval)
{
_autoSwitchInterval = interval;
_switchTimer = interval;
}
/// <summary>递归查找摄像机节点</summary>
private void FindCameras(Node node, Array<Node> list)
{
if (node is Camera3D cam)
{
list.Add(cam);
}
foreach (var child in node.GetChildren())
{
FindCameras(child, list);
}
}
}GDScript
# 多机位切换系统
extends Node
var _cameras: Array[Camera3D] = []
var _current_camera_index: int = 0
var _auto_switch_interval: float = 0.0
var _switch_timer: float = 0.0
func _ready():
# 收集场景中所有摄像机
find_all_cameras(get_tree().root)
# 默认激活第一台摄像机
if _cameras.size() > 0:
switch_to_camera(0)
func _process(delta: float):
# 手动切换:按数字键 1-9
for i in range(mini(_cameras.size(), 9)):
if Input.is_action_just_pressed("camera_%d" % (i + 1)):
switch_to_camera(i)
# 自动轮播
if _auto_switch_interval > 0:
_switch_timer -= delta
if _switch_timer <= 0:
_switch_timer = _auto_switch_interval
switch_to_camera((_current_camera_index + 1) % _cameras.size())
## 切换到指定摄像机
func switch_to_camera(index: int):
if index < 0 or index >= _cameras.size():
return
# 关闭所有摄像机
for cam in _cameras:
cam.current = false
# 激活目标摄像机
_cameras[index].current = true
_current_camera_index = index
print("切换到摄像机 %d:%s" % [index + 1, _cameras[index].name])
## 设置自动切换间隔
func set_auto_switch(interval: float):
_auto_switch_interval = interval
_switch_timer = interval
## 递归查找摄像机节点
func find_all_cameras(node: Node):
if node is Camera3D:
_cameras.append(node)
for child in node.get_children():
find_all_cameras(child)摄像机轨道
摄像机轨道就是让摄像机沿着一条预定路径移动——就像电影里的轨道摄影车。
使用 Path3D + PathFollow3D + Camera3D 三个节点来实现:
C#
// 摄像机轨道系统
using Godot;
public partial class CameraTrack : PathFollow3D
{
[Export] public float TrackSpeed { get; set; } = 0.1f;
[Export] public bool Loop { get; set; } = true;
[Export] public bool AutoPlay { get; set; } = true;
private Camera3D _camera;
private bool _isPlaying;
public override void _Ready()
{
_camera = GetNode<Camera3D>("Camera3D");
ProgressRatio = 0.0f;
_isPlaying = AutoPlay;
}
public override void _Process(double delta)
{
if (!_isPlaying) return;
// 沿路径前进
ProgressRatio += TrackSpeed * (float)delta;
if (ProgressRatio >= 1.0f)
{
if (Loop)
{
ProgressRatio = 0.0f;
}
else
{
ProgressRatio = 1.0f;
_isPlaying = false;
GD.Print("摄像机轨道播放完成");
}
}
// 让摄像机始终看向目标点
Node3D lookTarget = GetNodeOrNull<Node3D>("/root/MainScene/LookTarget");
if (lookTarget != null && _camera != null)
{
_camera.LookAt(lookTarget.GlobalPosition, Vector3.Up);
}
}
/// <summary>开始播放</summary>
public void Play()
{
ProgressRatio = 0.0f;
_isPlaying = true;
}
/// <summary>暂停</summary>
public void Pause()
{
_isPlaying = false;
}
/// <summary>恢复</summary>
public void Resume()
{
_isPlaying = true;
}
}GDScript
# 摄像机轨道系统
extends PathFollow3D
@export var track_speed: float = 0.1
@export var loop_enabled: bool = true
@export var auto_play: bool = true
var _camera: Camera3D
var _is_playing: bool = false
func _ready():
_camera = $Camera3D
progress_ratio = 0.0
_is_playing = auto_play
func _process(delta: float):
if not _is_playing:
return
# 沿路径前进
progress_ratio += track_speed * delta
if progress_ratio >= 1.0:
if loop_enabled:
progress_ratio = 0.0
else:
progress_ratio = 1.0
_is_playing = false
print("摄像机轨道播放完成")
# 让摄像机始终看向目标点
var look_target = get_node_or_null("/root/MainScene/LookTarget")
if look_target and _camera:
_camera.look_at(look_target.global_position, Vector3.UP)
## 开始播放
func play():
progress_ratio = 0.0
_is_playing = true
## 暂停
func pause():
_is_playing = false
## 恢复
func resume():
_is_playing = true运镜技巧表格
| 技巧 | 英文名 | 效果 | FOV | 速度 | 实现方式 |
|---|---|---|---|---|---|
| 推 | Push In | 摄像机向主体靠近,营造紧张感 | 逐渐变小(40->35) | 由慢到快 | 沿 Z 轴移动 |
| 拉 | Pull Out | 摄像机远离主体,展示全景 | 逐渐变大(40->60) | 由快到慢 | 沿 Z 轴后退 |
| 摇 | Pan | 摄像机左右/上下转动,展示环境 | 不变(50) | 匀速 | 改变 rotation |
| 跟 | Follow | 摄像机跟随角色移动 | 不变(50) | 与角色同步 | 绑定到角色 |
| 环绕 | Orbit | 摄像机绕主体旋转 | 不变(50) | 匀速 | 绕中心点旋转 |
C#
// 各种运镜技巧的代码实现
using Godot;
public partial class CameraTechniques : Node3D
{
private Camera3D _camera;
private Node3D _target;
public override void _Ready()
{
_camera = GetNode<Camera3D>("Camera3D");
_target = GetNode<Node3D>("/root/MainScene/Player");
}
/// <summary>推镜头:向目标靠近</summary>
public void PushIn(float distance, float duration)
{
var tween = CreateTween();
Vector3 targetPos = _target.GlobalPosition + Vector3.Back * distance;
targetPos.Y = _target.GlobalPosition.Y + 1.5f;
tween.TweenProperty(_camera, "global_position", targetPos, duration)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.In);
tween.Parallel().TweenProperty(_camera, "fov", 35.0f, duration);
}
/// <summary>拉镜头:远离目标</summary>
public void PullOut(float distance, float duration)
{
var tween = CreateTween();
Vector3 targetPos = _target.GlobalPosition + Vector3.Back * distance;
targetPos.Y = _target.GlobalPosition.Y + 3.0f;
tween.TweenProperty(_camera, "global_position", targetPos, duration)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.Out);
tween.Parallel().TweenProperty(_camera, "fov", 60.0f, duration);
}
/// <summary>摇镜头:左右转动</summary>
public void Pan(float angleDegrees, float duration)
{
var tween = CreateTween();
float targetRotY = _camera.RotationDegrees.Y + angleDegrees;
tween.TweenProperty(_camera, "rotation_degrees:y", targetRotY, duration)
.SetTrans(Tween.TransitionType.Linear);
}
/// <summary>跟镜头:跟随目标</summary>
public void FollowTarget(Vector3 offset)
{
var tween = CreateTween();
tween.SetLoops(); // 无限循环
tween.TweenCallback(Callable.From(() =>
{
Vector3 targetPos = _target.GlobalPosition + offset;
_camera.GlobalPosition = _camera.GlobalPosition.Lerp(targetPos, 0.05f);
_camera.LookAt(_target.GlobalPosition + Vector3.Up * 1.5f, Vector3.Up);
}));
tween.TweenInterval(0.016); // ~60fps
}
/// <summary>环绕镜头:绕目标旋转</summary>
public void Orbit(float radius, float height, float duration)
{
var tween = CreateTween();
float startAngle = 0;
float endAngle = Mathf.Tau; // 完整一圈
// 使用 Tween 的 step 方式模拟环绕
int steps = 120; // 总步数
float stepDuration = duration / steps;
for (int i = 0; i <= steps; i++)
{
float t = (float)i / steps;
float angle = Mathf.Lerp(startAngle, endAngle, t);
float x = _target.GlobalPosition.X + Mathf.Cos(angle) * radius;
float z = _target.GlobalPosition.Z + Mathf.Sin(angle) * radius;
float y = _target.GlobalPosition.Y + height;
var pos = new Vector3(x, y, z);
if (i == 0)
{
_camera.GlobalPosition = pos;
}
else
{
tween.TweenProperty(_camera, "global_position", pos, stepDuration);
}
}
}
}GDScript
# 各种运镜技巧的代码实现
extends Node3D
@onready var _camera: Camera3D = $Camera3D
@onready var _target: Node3D = %Player
## 推镜头:向目标靠近
func push_in(distance: float, duration: float):
var tween = create_tween()
var target_pos = _target.global_position + Vector3.BACK * distance
target_pos.y = _target.global_position.y + 1.5
tween.tween_property(_camera, "global_position", target_pos, duration)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN)
tween.parallel().tween_property(_camera, "fov", 35.0, duration)
## 拉镜头:远离目标
func pull_out(distance: float, duration: float):
var tween = create_tween()
var target_pos = _target.global_position + Vector3.BACK * distance
target_pos.y = _target.global_position.y + 3.0
tween.tween_property(_camera, "global_position", target_pos, duration)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_OUT)
tween.parallel().tween_property(_camera, "fov", 60.0, duration)
## 摇镜头:左右转动
func pan(angle_degrees: float, duration: float):
var tween = create_tween()
var target_rot_y = _camera.rotation_degrees.y + angle_degrees
tween.tween_property(_camera, "rotation_degrees:y", target_rot_y, duration)\
.set_trans(Tween.TRANS_LINEAR)
## 跟镜头:跟随目标
func follow_target(offset: Vector3):
var tween = create_tween()
tween.set_loops() # 无限循环
tween.tween_callback(func():
var target_pos = _target.global_position + offset
_camera.global_position = _camera.global_position.lerp(target_pos, 0.05)
_camera.look_at(_target.global_position + Vector3.UP * 1.5, Vector3.UP)
)
tween.tween_interval(0.016) # ~60fps
## 环绕镜头:绕目标旋转
func orbit(radius: float, height: float, duration: float):
var tween = create_tween()
var start_angle = 0.0
var end_angle = TAU # 完整一圈
# 使用 Tween 的 step 方式模拟环绕
var steps = 120 # 总步数
var step_duration = duration / steps
for i in range(steps + 1):
var t = float(i) / steps
var angle = lerpf(start_angle, end_angle, t)
var x = _target.global_position.x + cos(angle) * radius
var z = _target.global_position.z + sin(angle) * radius
var y = _target.global_position.y + height
var pos = Vector3(x, y, z)
if i == 0:
_camera.global_position = pos
else:
tween.tween_property(_camera, "global_position", pos, step_duration)Timeline 过场动画系统
Timeline(时间线)系统是一个简单的队列式播放器——把多个摄像机动画按顺序排队播放。
C#
// 简单的 Timeline 过场动画系统
using Godot;
using System.Collections.Generic;
public partial class CinematicTimeline : Node
{
[Signal] public delegate void CinematicStartedEventHandler();
[Signal] public delegate void CinematicFinishedEventHandler();
private struct Shot
{
public Camera3D Camera;
public float Duration;
public string AnimationName;
}
private readonly List<Shot> _shots = new();
private int _currentShotIndex;
private double _shotTimer;
private bool _isPlaying;
private Camera3D _gameplayCamera;
public override void _Ready()
{
_gameplayCamera = GetViewport().GetCamera3D();
}
public override void _Process(double delta)
{
if (!_isPlaying) return;
_shotTimer -= delta;
if (_shotTimer <= 0)
{
PlayNextShot();
}
}
/// <summary>添加一个镜头到时间线</summary>
public void AddShot(Camera3D camera, float duration, string animName = "")
{
_shots.Add(new Shot
{
Camera = camera,
Duration = duration,
AnimationName = animName
});
}
/// <summary>开始播放过场动画</summary>
public void Play()
{
if (_shots.Count == 0) return;
_currentShotIndex = 0;
_isPlaying = true;
// 暂停游戏逻辑
GetTree().Paused = true;
EmitSignal(SignalName.CinematicStarted);
PlayCurrentShot();
}
private void PlayCurrentShot()
{
if (_currentShotIndex >= _shots.Count)
{
Stop();
return;
}
var shot = _shots[_currentShotIndex];
// 切换摄像机
if (shot.Camera != null)
{
shot.Camera.Current = true;
}
// 播放动画(如果有)
if (!string.IsNullOrEmpty(shot.AnimationName))
{
var animPlayer = shot.Camera?.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
animPlayer?.Play(shot.AnimationName);
}
_shotTimer = shot.Duration;
GD.Print($"播放镜头 {_currentShotIndex + 1}/{_shots.Count},持续 {shot.Duration} 秒");
}
private void PlayNextShot()
{
_currentShotIndex++;
PlayCurrentShot();
}
/// <summary>停止过场动画,恢复游戏</summary>
public void Stop()
{
_isPlaying = false;
_shots.Clear();
// 恢复游戏摄像机
if (_gameplayCamera != null)
{
_gameplayCamera.Current = true;
}
// 恢复游戏逻辑
GetTree().Paused = false;
EmitSignal(SignalName.CinematicFinished);
GD.Print("过场动画结束,恢复游戏");
}
/// <summary>跳过过场动画</summary>
public void Skip()
{
GD.Print("跳过过场动画");
Stop();
}
}GDScript
# 简单的 Timeline 过场动画系统
extends Node
signal cinematic_started()
signal cinematic_finished()
class Shot:
var camera: Camera3D
var duration: float
var animation_name: String = ""
var _shots: Array[Shot] = []
var _current_shot_index: int = 0
var _shot_timer: float = 0.0
var _is_playing: bool = false
var _gameplay_camera: Camera3D
func _ready():
_gameplay_camera = get_viewport().get_camera_3d()
func _process(delta: float):
if not _is_playing:
return
_shot_timer -= delta
if _shot_timer <= 0:
play_next_shot()
## 添加一个镜头到时间线
func add_shot(camera: Camera3D, duration: float, anim_name: String = ""):
var shot = Shot.new()
shot.camera = camera
shot.duration = duration
shot.animation_name = anim_name
_shots.append(shot)
## 开始播放过场动画
func play_cinematic():
if _shots.size() == 0:
return
_current_shot_index = 0
_is_playing = true
# 暂停游戏逻辑
get_tree().paused = true
cinematic_started.emit()
play_current_shot()
func play_current_shot():
if _current_shot_index >= _shots.size():
stop()
return
var shot = _shots[_current_shot_index]
# 切换摄像机
if shot.camera:
shot.camera.current = true
# 播放动画(如果有)
if shot.animation_name != "":
var anim_player = shot.camera.get_node_or_null("AnimationPlayer")
if anim_player:
anim_player.play(shot.animation_name)
_shot_timer = shot.duration
print("播放镜头 %d/%d,持续 %.1f 秒" % [_current_shot_index + 1, _shots.size(), shot.duration])
func play_next_shot():
_current_shot_index += 1
play_current_shot()
## 停止过场动画,恢复游戏
func stop():
_is_playing = false
_shots.clear()
# 恢复游戏摄像机
if _gameplay_camera:
_gameplay_camera.current = true
# 恢复游戏逻辑
get_tree().paused = false
cinematic_finished.emit()
print("过场动画结束,恢复游戏")
## 跳过过场动画
func skip():
print("跳过过场动画")
stop()屏幕淡入淡出
屏幕淡入淡出是过场动画中最常见的过渡效果——画面从全黑慢慢变亮(淡入),或从画面慢慢变黑(淡出)。
C#
// 屏幕淡入淡出效果
using Godot;
public partial class ScreenFade : ColorRect
{
[Signal] public delegate void FadeInFinishedEventHandler();
[Signal] public delegate void FadeOutFinishedEventHandler();
[Export] public float FadeDuration { get; set; } = 1.0f;
public override void _Ready()
{
// 全屏覆盖
SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
MouseFilter = MouseFilterFlags.Ignore;
// 初始为完全透明
Color = new Color(0, 0, 0, 0);
}
/// <summary>淡入:从全黑到透明(场景显现)</summary>
public void FadeIn()
{
Color = new Color(0, 0, 0, 1); // 开始全黑
var tween = CreateTween();
tween.TweenProperty(this, "color:a", 0.0f, FadeDuration)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.InOut);
tween.TweenCallback(Callable.From(() =>
{
EmitSignal(SignalName.FadeInFinished);
GD.Print("淡入完成");
}));
}
/// <summary>淡出:从透明到全黑(场景消失)</summary>
public void FadeOut()
{
Color = new Color(0, 0, 0, 0); // 开始透明
var tween = CreateTween();
tween.TweenProperty(this, "color:a", 1.0f, FadeDuration)
.SetTrans(Tween.TransitionType.Sine)
.SetEase(Tween.EaseType.InOut);
tween.TweenCallback(Callable.From(() =>
{
EmitSignal(SignalName.FadeOutFinished);
GD.Print("淡出完成");
}));
}
/// <summary>淡出后执行动作再淡入</summary>
public async void FadeOutIn(Godot.Collections.Array actions = null)
{
// 淡出
FadeOut();
await ToSignal(this, SignalName.FadeOutFinished);
// 执行中间操作(比如切换场景)
if (actions != null)
{
foreach (var action in actions)
{
if (action is Callable callable)
{
callable.Call();
}
}
}
// 淡入
FadeIn();
}
}GDScript
# 屏幕淡入淡出效果
extends ColorRect
signal fade_in_finished()
signal fade_out_finished()
@export var fade_duration: float = 1.0
func _ready():
# 全屏覆盖
anchors_preset = Control.PRESET_FULL_RECT
# 初始为完全透明
color = Color(0, 0, 0, 0)
mouse_filter = Control.MOUSE_FILTER_IGNORE
## 淡入:从全黑到透明(场景显现)
func fade_in():
color = Color(0, 0, 0, 1) # 开始全黑
var tween = create_tween()
tween.tween_property(self, "color:a", 0.0, fade_duration)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN_OUT)
tween.tween_callback(func():
fade_in_finished.emit()
print("淡入完成")
)
## 淡出:从透明到全黑(场景消失)
func fade_out():
color = Color(0, 0, 0, 0) # 开始透明
var tween = create_tween()
tween.tween_property(self, "color:a", 1.0, fade_duration)\
.set_trans(Tween.TRANS_SINE)\
.set_ease(Tween.EASE_IN_OUT)
tween.tween_callback(func():
fade_out_finished.emit()
print("淡出完成")
)
## 淡出后执行动作再淡入
func fade_out_in(actions: Array[Callable] = []):
# 淡出
fade_out()
await fade_out_finished
# 执行中间操作(比如切换场景)
for action in actions:
action.call()
# 淡入
fade_in()常见问题
摄像机切换时画面闪烁
问题:切换 Camera3D 时画面出现闪烁或短暂黑屏。
解决方案:
- 确保新摄像机在禁用旧摄像机之前就设置好位置
- 使用 ScreenFade 淡入淡出来遮盖切换瞬间
- 避免在同一帧内禁用所有摄像机
摄像机轨道运动不平滑
问题:摄像机沿 Path3D 运动时有抖动。
解决方案:
- 增加路径曲线的控制点数量
- 使用
Cubic插值模式 - 确保摄像机看向目标的逻辑在
_process而不是_physics_process中
Tween 动画卡顿
问题:Tween 动画不流畅。
解决方案:
- 确保动画运行在
_process中而非_physics_process - 检查帧率是否稳定(目标 60fps)
- 避免在动画期间执行大量计算
过场动画的设计原则
- 保持简短:过场动画不要超过 10-15 秒,玩家想玩游戏而不是看电影
- 提供跳过选项:永远给玩家跳过过场动画的能力
- 叙事驱动:过场动画应该推动故事发展,不只是炫技
- 不要剥夺控制感:如果只是简单的对话,考虑用 HUD 对话框代替过场动画
本章小结
本章我们学习了电影摄像机和过场动画的完整技术:
| 技术 | 用途 | 关键节点 |
|---|---|---|
| 摄像机动画 | 位置/角度/FOV 关键帧动画 | AnimationPlayer + Camera3D |
| 多机位切换 | 多台摄像机之间切换 | Camera3D.Current |
| 摄像机轨道 | 沿路径移动摄像机 | Path3D + PathFollow3D |
| 运镜技巧 | 推/拉/摇/跟/环绕 | Tween 动画 |
| Timeline 系统 | 多镜头队列播放 | 自定义时间线脚本 |
| 淡入淡出 | 场景过渡效果 | ColorRect + Tween |
通过组合这些技术,你可以创造出专业的过场动画效果,让你的游戏更具电影感。
