7. 2.5D摄像机系统
2.5D摄像机系统
摄像机是什么?
想象你在看一场电影。你看到的画面,不是演员直接站在你面前,而是通过摄影机拍摄后呈现在屏幕上的。摄影机的位置、角度、焦距,决定了观众看到的画面。
游戏中的摄像机也是一样的道理。摄像机就是玩家的"眼睛",它决定了玩家能看到游戏世界的哪个部分、从什么角度看。
在2.5D游戏中,摄像机的设置尤为重要,因为它直接决定了游戏的"视觉风格":
- 横版游戏(如《空洞骑士》):摄像机从侧面看,跟随玩家左右移动
- 俯视游戏(如《塞尔达传说》早期作品):摄像机从上往下看
- 等角视角游戏(如《暗黑破坏神》):摄像机从45度角斜着看
Godot中的Camera3D节点
在Godot 4.x中,3D场景中的摄像机使用 Camera3D 节点。
添加摄像机
- 在场景树中,右键点击根节点
- 选择"添加子节点"(Add Child Node)
- 搜索"Camera3D",双击添加
Camera3D的主要属性
在检查器(Inspector)中,你会看到以下重要属性:
Projection(投影模式):决定摄像机如何将3D世界投影到2D屏幕上
FOV(视野角度):只在透视投影模式下有效,数值越大,视野越宽
Size(尺寸):只在正交投影模式下有效,控制可见区域的大小
Near / Far(近裁剪面 / 远裁剪面):摄像机能看到的最近和最远距离
投影模式详解
透视投影(Perspective)
透视投影模拟了人眼的视觉效果:近大远小。
就像你站在一条笔直的铁路上往前看,两条铁轨会在远处"汇聚"成一个点。这就是透视效果。
特点:
- 有深度感,画面更真实
- 远处的物体看起来比近处的小
- 适合第一人称、第三人称游戏
适用场景:
- 横版游戏(摄像机稍微有点透视感,让场景更有立体感)
- 需要强调深度感的游戏
正交投影(Orthogonal)
正交投影没有透视效果:无论远近,物体大小相同。
就像你看建筑图纸,图纸上的线条是平行的,不会因为"远近"而改变大小。
特点:
- 没有近大远小的效果
- 画面更"平",有2D感
- 适合俯视游戏、等角视角游戏
适用场景:
- 俯视游戏(从正上方看)
- 等角视角游戏(45度角)
- 像素风格游戏
如何选择?
- 如果你的游戏是横版动作游戏,用透视投影,FOV设置在30-60度之间
- 如果你的游戏是俯视或等角视角,用正交投影
2.5D游戏的常见摄像机设置
横版游戏摄像机
横版游戏(Side-Scroller)的摄像机从侧面看,玩家在屏幕上左右移动。
摄像机设置:
- 投影模式:透视(Perspective)或正交(Orthogonal)
- 位置:在玩家侧面,比如
(0, 2, 10)(X=0, Y=2高度, Z=10距离) - 旋转:稍微向下倾斜,比如 X轴旋转 -10度
跟随方式:
- 跟随玩家的X轴(左右)移动
- Y轴(上下)可以固定,也可以跟随(取决于游戏设计)
- Z轴(前后)固定不动
俯视游戏摄像机
俯视游戏(Top-Down)的摄像机从正上方往下看。
摄像机设置:
- 投影模式:正交(Orthogonal)
- 位置:在玩家正上方,比如
(0, 20, 0) - 旋转:X轴旋转 -90度(朝下看)
跟随方式:
- 跟随玩家的X轴和Z轴(水平面)移动
- Y轴(高度)固定
等角视角摄像机
等角视角(Isometric)是一种特殊的俯视角度,摄像机从45度角斜着看。
摄像机设置:
- 投影模式:正交(Orthogonal)
- 位置:在玩家斜上方,比如
(10, 10, 10) - 旋转:X轴旋转 -35.26度,Y轴旋转 45度(标准等角角度)
跟随方式:
- 跟随玩家在水平面上的移动
- 保持固定的角度和高度
摄像机跟随脚本
下面是一个完整的摄像机跟随脚本,支持横版和俯视两种模式:
横版游戏摄像机跟随
using Godot;
/// <summary>
/// 横版游戏摄像机控制器
/// 摄像机会平滑跟随玩家的水平移动
/// </summary>
public partial class SideScrollCamera : Camera3D
{
// 要跟随的目标(通常是玩家)
[Export] private Node3D _target;
// 跟随速度(0=不跟随,1=立即跟随,0.1=缓慢跟随)
[Export] private float _followSpeed = 5.0f;
// 摄像机相对于目标的偏移量
[Export] private Vector3 _offset = new Vector3(0, 2, 10);
// 是否跟随Y轴(上下)
[Export] private bool _followY = false;
// Y轴的固定高度(当不跟随Y轴时使用)
[Export] private float _fixedY = 2.0f;
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 计算目标位置
Vector3 targetPos = _target.GlobalPosition + _offset;
// 如果不跟随Y轴,使用固定高度
if (!_followY)
{
targetPos.Y = _fixedY + _offset.Y;
}
// 平滑移动到目标位置(Lerp = 线性插值,让移动更平滑)
GlobalPosition = GlobalPosition.Lerp(targetPos, (float)(_followSpeed * delta));
}
}extends Camera3D
## 横版游戏摄像机控制器
## 摄像机会平滑跟随玩家的水平移动
# 要跟随的目标(通常是玩家)
@export var target: Node3D
# 跟随速度(0=不跟随,1=立即跟随,0.1=缓慢跟随)
@export var follow_speed: float = 5.0
# 摄像机相对于目标的偏移量
@export var offset: Vector3 = Vector3(0, 2, 10)
# 是否跟随Y轴(上下)
@export var follow_y: bool = false
# Y轴的固定高度(当不跟随Y轴时使用)
@export var fixed_y: float = 2.0
func _physics_process(delta: float) -> void:
if not target:
return
# 计算目标位置
var target_pos = target.global_position + offset
# 如果不跟随Y轴,使用固定高度
if not follow_y:
target_pos.y = fixed_y + offset.y
# 平滑移动到目标位置(lerp = 线性插值,让移动更平滑)
global_position = global_position.lerp(target_pos, follow_speed * delta)俯视游戏摄像机跟随
using Godot;
/// <summary>
/// 俯视游戏摄像机控制器
/// 摄像机从上方跟随玩家移动
/// </summary>
public partial class TopDownCamera : Camera3D
{
[Export] private Node3D _target;
[Export] private float _followSpeed = 5.0f;
[Export] private float _height = 20.0f; // 摄像机高度
public override void _Ready()
{
// 设置正交投影
Projection = ProjectionType.Orthogonal;
Size = 15.0f; // 可见区域大小
// 朝下看
RotationDegrees = new Vector3(-90, 0, 0);
}
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 目标位置:在玩家正上方
Vector3 targetPos = new Vector3(
_target.GlobalPosition.X,
_height,
_target.GlobalPosition.Z
);
// 平滑跟随
GlobalPosition = GlobalPosition.Lerp(targetPos, (float)(_followSpeed * delta));
}
}extends Camera3D
## 俯视游戏摄像机控制器
## 摄像机从上方跟随玩家移动
@export var target: Node3D
@export var follow_speed: float = 5.0
@export var height: float = 20.0 # 摄像机高度
func _ready() -> void:
# 设置正交投影
projection = Camera3D.PROJECTION_ORTHOGONAL
size = 15.0 # 可见区域大小
# 朝下看
rotation_degrees = Vector3(-90, 0, 0)
func _physics_process(delta: float) -> void:
if not target:
return
# 目标位置:在玩家正上方
var target_pos = Vector3(
target.global_position.x,
height,
target.global_position.z
)
# 平滑跟随
global_position = global_position.lerp(target_pos, follow_speed * delta)摄像机边界限制
在很多游戏中,摄像机不能超出地图边界,否则玩家会看到地图外面的空白区域。
就像拍电影时,摄影机不能拍到摄影棚的墙壁,需要限制在布景范围内。
using Godot;
/// <summary>
/// 带边界限制的横版摄像机
/// </summary>
public partial class BoundedCamera : Camera3D
{
[Export] private Node3D _target;
[Export] private float _followSpeed = 5.0f;
[Export] private Vector3 _offset = new Vector3(0, 2, 10);
// 地图边界(X轴)
[Export] private float _minX = -50.0f;
[Export] private float _maxX = 50.0f;
// 地图边界(Y轴,如果跟随Y轴)
[Export] private float _minY = 0.0f;
[Export] private float _maxY = 20.0f;
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
Vector3 targetPos = _target.GlobalPosition + _offset;
// 限制X轴范围(Clamp = 夹紧,不让值超出范围)
targetPos.X = Mathf.Clamp(targetPos.X, _minX, _maxX);
// 限制Y轴范围
targetPos.Y = Mathf.Clamp(targetPos.Y, _minY, _maxY);
GlobalPosition = GlobalPosition.Lerp(targetPos, (float)(_followSpeed * delta));
}
}extends Camera3D
## 带边界限制的横版摄像机
@export var target: Node3D
@export var follow_speed: float = 5.0
@export var offset: Vector3 = Vector3(0, 2, 10)
# 地图边界(X轴)
@export var min_x: float = -50.0
@export var max_x: float = 50.0
# 地图边界(Y轴,如果跟随Y轴)
@export var min_y: float = 0.0
@export var max_y: float = 20.0
func _physics_process(delta: float) -> void:
if not target:
return
var target_pos = target.global_position + offset
# 限制X轴范围(clamp = 夹紧,不让值超出范围)
target_pos.x = clamp(target_pos.x, min_x, max_x)
# 限制Y轴范围
target_pos.y = clamp(target_pos.y, min_y, max_y)
global_position = global_position.lerp(target_pos, follow_speed * delta)摄像机震动效果
摄像机震动是一种常见的游戏反馈效果,当玩家受到攻击、发生爆炸时,摄像机会短暂震动,增强游戏的冲击感。
就像地震时,你感受到的那种震动感。
using Godot;
/// <summary>
/// 摄像机震动效果
/// 可以在任何地方调用 Shake() 方法触发震动
/// </summary>
public partial class ShakeCamera : Camera3D
{
[Export] private Node3D _target;
[Export] private float _followSpeed = 5.0f;
[Export] private Vector3 _offset = new Vector3(0, 2, 10);
// 震动参数
private float _shakeStrength = 0.0f; // 当前震动强度
private float _shakeDuration = 0.0f; // 剩余震动时间
private float _shakeDecay = 5.0f; // 震动衰减速度
private Vector3 _basePosition; // 基础位置(没有震动时的位置)
/// <summary>
/// 触发摄像机震动
/// </summary>
/// <param name="strength">震动强度(0.1=轻微,0.5=中等,1.0=强烈)</param>
/// <param name="duration">震动持续时间(秒)</param>
public void Shake(float strength, float duration)
{
_shakeStrength = strength;
_shakeDuration = duration;
}
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 计算基础跟随位置
_basePosition = _target.GlobalPosition + _offset;
// 如果有震动
if (_shakeDuration > 0)
{
_shakeDuration -= (float)delta;
// 生成随机偏移(就像手抖一样)
Vector3 shakeOffset = new Vector3(
(float)GD.RandRange(-1.0, 1.0) * _shakeStrength,
(float)GD.RandRange(-1.0, 1.0) * _shakeStrength,
0
);
GlobalPosition = _basePosition + shakeOffset;
// 震动强度随时间衰减
_shakeStrength = Mathf.Lerp(_shakeStrength, 0, (float)(_shakeDecay * delta));
}
else
{
// 没有震动时,正常跟随
GlobalPosition = GlobalPosition.Lerp(_basePosition, (float)(_followSpeed * delta));
}
}
}extends Camera3D
## 摄像机震动效果
## 可以在任何地方调用 shake() 方法触发震动
@export var target: Node3D
@export var follow_speed: float = 5.0
@export var offset: Vector3 = Vector3(0, 2, 10)
# 震动参数
var shake_strength: float = 0.0 # 当前震动强度
var shake_duration: float = 0.0 # 剩余震动时间
var shake_decay: float = 5.0 # 震动衰减速度
var base_position: Vector3 # 基础位置(没有震动时的位置)
## 触发摄像机震动
## strength: 震动强度(0.1=轻微,0.5=中等,1.0=强烈)
## duration: 震动持续时间(秒)
func shake(strength: float, duration: float) -> void:
shake_strength = strength
shake_duration = duration
func _physics_process(delta: float) -> void:
if not target:
return
# 计算基础跟随位置
base_position = target.global_position + offset
# 如果有震动
if shake_duration > 0:
shake_duration -= delta
# 生成随机偏移(就像手抖一样)
var shake_offset = Vector3(
randf_range(-1.0, 1.0) * shake_strength,
randf_range(-1.0, 1.0) * shake_strength,
0
)
global_position = base_position + shake_offset
# 震动强度随时间衰减
shake_strength = lerp(shake_strength, 0.0, shake_decay * delta)
else:
# 没有震动时,正常跟随
global_position = global_position.lerp(base_position, follow_speed * delta)如何使用震动效果:
// 在玩家受到攻击时触发震动
public void TakeDamage(int damage)
{
// ... 扣血逻辑 ...
// 触发摄像机震动
var camera = GetTree().GetFirstNodeInGroup("camera") as ShakeCamera;
camera?.Shake(0.3f, 0.2f); // 强度0.3,持续0.2秒
}# 在玩家受到攻击时触发震动
func take_damage(damage: int) -> void:
# ... 扣血逻辑 ...
# 触发摄像机震动
var camera = get_tree().get_first_node_in_group("camera")
if camera:
camera.shake(0.3, 0.2) # 强度0.3,持续0.2秒平滑插值详解
在上面的代码中,我们多次用到了 Lerp(线性插值)。这是一个非常重要的概念,值得单独解释。
Lerp 的含义:在两个值之间按比例取中间值。
比如:Lerp(0, 100, 0.3) = 30(从0到100,取30%的位置)
在摄像机跟随中:
Lerp(当前位置, 目标位置, 速度 × 时间)- 每帧都向目标位置移动一点点
- 距离越近,移动越慢(因为差值越小)
- 这就产生了"缓慢靠近"的平滑效果
调整跟随手感
follow_speed = 2.0:非常缓慢,有"漂浮感"follow_speed = 5.0:适中,推荐值follow_speed = 10.0:较快,接近即时跟随follow_speed = 20.0:非常快,几乎没有延迟
等角视角摄像机
等角视角是2.5D游戏中很有特色的一种视角,常见于《暗黑破坏神》、《文明》等游戏。
using Godot;
/// <summary>
/// 等角视角摄像机
/// 使用正交投影,从45度角斜着看
/// </summary>
public partial class IsometricCamera : Camera3D
{
[Export] private Node3D _target;
[Export] private float _followSpeed = 5.0f;
[Export] private float _height = 15.0f;
[Export] private float _distance = 15.0f;
public override void _Ready()
{
// 设置正交投影
Projection = ProjectionType.Orthogonal;
Size = 12.0f;
// 设置等角角度
// X轴旋转:-35.26度(标准等角角度)
// Y轴旋转:45度(斜45度看)
RotationDegrees = new Vector3(-35.26f, 45.0f, 0);
}
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 等角视角的偏移:斜上方
Vector3 targetPos = _target.GlobalPosition + new Vector3(_distance, _height, _distance);
GlobalPosition = GlobalPosition.Lerp(targetPos, (float)(_followSpeed * delta));
}
}extends Camera3D
## 等角视角摄像机
## 使用正交投影,从45度角斜着看
@export var target: Node3D
@export var follow_speed: float = 5.0
@export var height: float = 15.0
@export var distance: float = 15.0
func _ready() -> void:
# 设置正交投影
projection = Camera3D.PROJECTION_ORTHOGONAL
size = 12.0
# 设置等角角度
# X轴旋转:-35.26度(标准等角角度)
# Y轴旋转:45度(斜45度看)
rotation_degrees = Vector3(-35.26, 45.0, 0)
func _physics_process(delta: float) -> void:
if not target:
return
# 等角视角的偏移:斜上方
var target_pos = target.global_position + Vector3(distance, height, distance)
global_position = global_position.lerp(target_pos, follow_speed * delta)常见问题
摄像机抖动怎么办?
如果摄像机跟随时出现抖动,通常是因为:
在
_process中移动摄像机,但玩家在_physics_process中移动- 解决:把摄像机移动也放在
_physics_process中
- 解决:把摄像机移动也放在
跟随速度太高
- 解决:降低
follow_speed值
- 解决:降低
浮点数精度问题
- 解决:使用
MoveToward代替Lerp
- 解决:使用
摄像机穿墙怎么办?
如果摄像机会穿过墙壁,可以使用 RayCast3D 检测障碍物,动态调整摄像机距离。这是一个进阶话题,我们会在后续章节中详细讲解。
如何让摄像机有"预判"效果?
预判效果是指摄像机会稍微超前于玩家,让玩家能看到前方更多的内容。
// 在跟随脚本中添加预判偏移
private Vector3 _lookAheadOffset = Vector3.Zero;
private float _lookAheadDistance = 3.0f;
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 获取玩家的移动方向
var playerBody = _target as CharacterBody3D;
if (playerBody != null)
{
// 根据玩家速度方向,计算预判偏移
Vector3 velocity = playerBody.Velocity;
if (velocity.Length() > 0.1f)
{
_lookAheadOffset = new Vector3(
velocity.Normalized().X * _lookAheadDistance,
0, 0
);
}
}
Vector3 targetPos = _target.GlobalPosition + _offset + _lookAheadOffset;
GlobalPosition = GlobalPosition.Lerp(targetPos, (float)(_followSpeed * delta));
}var look_ahead_offset: Vector3 = Vector3.ZERO
var look_ahead_distance: float = 3.0
func _physics_process(delta: float) -> void:
if not target:
return
# 获取玩家的移动方向
if target is CharacterBody3D:
var velocity = target.velocity
if velocity.length() > 0.1:
# 根据玩家速度方向,计算预判偏移
look_ahead_offset = Vector3(
velocity.normalized().x * look_ahead_distance,
0, 0
)
var target_pos = target.global_position + offset + look_ahead_offset
global_position = global_position.lerp(target_pos, follow_speed * delta)小结
在这一章中,我们学习了:
- ✅ 摄像机的概念(玩家的"眼睛")
- ✅ Camera3D节点的主要属性
- ✅ 透视投影 vs 正交投影(有无近大远小效果)
- ✅ 三种常见的2.5D摄像机设置(横版、俯视、等角)
- ✅ 摄像机跟随脚本(平滑跟随玩家)
- ✅ 摄像机边界限制(不超出地图范围)
- ✅ 摄像机震动效果(增强游戏反馈)
- ✅ 平滑插值(Lerp)的原理和使用
摄像机是游戏体验的重要组成部分,一个好的摄像机系统能让玩家感觉更舒适、更沉浸。接下来,我们将学习2.5D游戏中的光照与阴影系统!
