第三十一章:3D相机系统
相机是玩家观察游戏世界的窗口,直接影响游戏的沉浸感和操控体验。Godot 4提供了功能丰富的3D相机系统,支持多种视角模式、平滑跟随、碰撞检测、特效处理等功能。本章将全面讲解Godot的3D相机系统,帮助开发者打造专业级的游戏视角。
31.1 Camera3D基础
31.1.1 相机属性详解
# camera_basics.gd
# 相机基础配置
extends Camera3D
## 投影模式
enum ProjectionType {
PERSPECTIVE, # 透视投影
ORTHOGONAL, # 正交投影
FRUSTUM # 自定义视锥
}
@export var projection_type: ProjectionType = ProjectionType.PERSPECTIVE:
set(value):
projection_type = value
_apply_projection()
## 透视投影参数
@export_group("Perspective")
@export_range(10, 170) var field_of_view: float = 75.0:
set(value):
field_of_view = value
if projection_type == ProjectionType.PERSPECTIVE:
fov = value
## 正交投影参数
@export_group("Orthogonal")
@export var ortho_size: float = 10.0:
set(value):
ortho_size = value
if projection_type == ProjectionType.ORTHOGONAL:
size = value
## 裁剪平面
@export_group("Clipping")
@export var clip_near: float = 0.05
@export var clip_far: float = 1000.0
func _ready():
_apply_projection()
near = clip_near
far = clip_far
func _apply_projection():
match projection_type:
ProjectionType.PERSPECTIVE:
projection = Camera3D.PROJECTION_PERSPECTIVE
fov = field_of_view
ProjectionType.ORTHOGONAL:
projection = Camera3D.PROJECTION_ORTHOGONAL
size = ortho_size
ProjectionType.FRUSTUM:
projection = Camera3D.PROJECTION_FRUSTUM
## 设置为当前相机
func activate():
current = true
func deactivate():
current = false
## 屏幕坐标转世界射线
func screen_to_world_ray(screen_pos: Vector2) -> Dictionary:
var from = project_ray_origin(screen_pos)
var direction = project_ray_normal(screen_pos)
return {"origin": from, "direction": direction}
## 世界坐标转屏幕坐标
func world_to_screen(world_pos: Vector3) -> Vector2:
if is_position_behind(world_pos):
return Vector2(-1, -1) # 在相机后方
return unproject_position(world_pos)
## 检查点是否在视野内
func is_in_view(world_pos: Vector3) -> bool:
if is_position_behind(world_pos):
return false
var screen_pos = unproject_position(world_pos)
var viewport_rect = get_viewport().get_visible_rect()
return viewport_rect.has_point(screen_pos)
31.1.2 视锥体与可见性
# frustum_culling.gd
# 视锥体检测
extends Camera3D
## 获取视锥体平面
func get_frustum_planes() -> Array[Plane]:
return get_frustum()
## 检查AABB是否在视锥内
func is_aabb_visible(aabb: AABB) -> bool:
var planes = get_frustum()
for plane in planes:
# 获取AABB在平面法线方向上最远的点
var positive_vertex = Vector3(
aabb.end.x if plane.normal.x >= 0 else aabb.position.x,
aabb.end.y if plane.normal.y >= 0 else aabb.position.y,
aabb.end.z if plane.normal.z >= 0 else aabb.position.z
)
# 如果最远点在平面外侧,则AABB不可见
if plane.distance_to(positive_vertex) < 0:
return false
return true
## 检查球体是否在视锥内
func is_sphere_visible(center: Vector3, radius: float) -> bool:
var planes = get_frustum()
for plane in planes:
if plane.distance_to(center) < -radius:
return false
return true
## 检查点是否在视锥内
func is_point_in_frustum(point: Vector3) -> bool:
var planes = get_frustum()
for plane in planes:
if plane.distance_to(point) < 0:
return false
return true
## 获取视锥体角点(用于调试绘制)
func get_frustum_corners(distance: float) -> Array[Vector3]:
var corners: Array[Vector3] = []
var viewport = get_viewport()
var rect = viewport.get_visible_rect()
var screen_corners = [
Vector2(rect.position.x, rect.position.y),
Vector2(rect.end.x, rect.position.y),
Vector2(rect.end.x, rect.end.y),
Vector2(rect.position.x, rect.end.y)
]
for corner in screen_corners:
var origin = project_ray_origin(corner)
var direction = project_ray_normal(corner)
corners.append(origin + direction * distance)
return corners
31.2 第三人称相机
31.2.1 基础跟随相机
# third_person_camera.gd
# 第三人称相机
extends Node3D
## 跟随目标
@export var target: Node3D
@export var follow_speed: float = 5.0
@export var rotation_speed: float = 3.0
## 相机偏移
@export var offset: Vector3 = Vector3(0, 2, 5)
@export var look_offset: Vector3 = Vector3(0, 1.5, 0)
## 旋转限制
@export var min_pitch: float = -80.0
@export var max_pitch: float = 80.0
## 当前旋转
var camera_rotation: Vector2 = Vector2.ZERO
@onready var camera: Camera3D = $Camera3D
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event):
if event is InputEventMouseMotion:
_handle_mouse_input(event.relative)
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _handle_mouse_input(motion: Vector2):
camera_rotation.x -= motion.y * 0.002 * rotation_speed
camera_rotation.y -= motion.x * 0.002 * rotation_speed
# 限制俯仰角
camera_rotation.x = clamp(
camera_rotation.x,
deg_to_rad(min_pitch),
deg_to_rad(max_pitch)
)
func _physics_process(delta):
if not target:
return
_update_position(delta)
_update_rotation(delta)
func _update_position(delta):
# 计算相机位置
var target_pos = target.global_position + look_offset
# 应用旋转后的偏移
var rotated_offset = offset.rotated(Vector3.UP, camera_rotation.y)
rotated_offset = rotated_offset.rotated(
Vector3.RIGHT.rotated(Vector3.UP, camera_rotation.y),
camera_rotation.x
)
var desired_pos = target_pos + rotated_offset
# 平滑跟随
global_position = global_position.lerp(desired_pos, follow_speed * delta)
func _update_rotation(delta):
# 相机始终看向目标
var look_target = target.global_position + look_offset
camera.look_at(look_target, Vector3.UP)
## 设置跟随目标
func set_target(new_target: Node3D):
target = new_target
## 重置相机位置
func reset_camera():
if target:
camera_rotation = Vector2.ZERO
global_position = target.global_position + offset
31.2.2 带碰撞检测的相机
# collision_camera.gd
# 碰撞检测相机
extends Node3D
## 目标设置
@export var target: Node3D
@export var offset: Vector3 = Vector3(0, 2, 5)
@export var look_offset: Vector3 = Vector3(0, 1.5, 0)
## 碰撞设置
@export_group("Collision")
@export var collision_enabled: bool = true
@export var collision_margin: float = 0.3
@export var collision_mask: int = 1
## 平滑设置
@export_group("Smoothing")
@export var position_smooth: float = 10.0
@export var collision_smooth: float = 20.0
@export var recovery_smooth: float = 5.0
## 内部状态
var desired_distance: float
var current_distance: float
var camera_rotation: Vector2 = Vector2.ZERO
@onready var camera: Camera3D = $Camera3D
@onready var spring_arm: SpringArm3D = $SpringArm3D
func _ready():
desired_distance = offset.length()
current_distance = desired_distance
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
_setup_spring_arm()
func _setup_spring_arm():
spring_arm.spring_length = desired_distance
spring_arm.collision_mask = collision_mask
spring_arm.margin = collision_margin
func _input(event):
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
camera_rotation.x -= event.relative.y * 0.003
camera_rotation.y -= event.relative.x * 0.003
camera_rotation.x = clamp(camera_rotation.x, deg_to_rad(-85), deg_to_rad(85))
func _physics_process(delta):
if not target:
return
_update_transform(delta)
_check_collision(delta)
func _update_transform(delta):
# 更新位置到目标
var target_pos = target.global_position + look_offset
global_position = global_position.lerp(target_pos, position_smooth * delta)
# 更新旋转
rotation = Vector3(camera_rotation.x, camera_rotation.y, 0)
func _check_collision(delta):
if not collision_enabled:
current_distance = desired_distance
return
var target_pos = target.global_position + look_offset
var camera_direction = -global_transform.basis.z
var desired_camera_pos = target_pos + camera_direction * desired_distance
# 射线检测
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
target_pos,
desired_camera_pos
)
query.collision_mask = collision_mask
query.exclude = [target.get_rid()] if target is CollisionObject3D else []
var result = space_state.intersect_ray(query)
if result:
# 发生碰撞,缩短距离
var hit_distance = target_pos.distance_to(result.position) - collision_margin
current_distance = lerp(current_distance, hit_distance, collision_smooth * delta)
else:
# 无碰撞,恢复距离
current_distance = lerp(current_distance, desired_distance, recovery_smooth * delta)
# 应用距离
camera.position = Vector3(0, 0, current_distance)
## 调整相机距离
func set_distance(distance: float):
desired_distance = distance
func zoom_in(amount: float):
desired_distance = max(desired_distance - amount, 1.0)
func zoom_out(amount: float):
desired_distance = min(desired_distance + amount, 20.0)
31.2.3 肩越相机(战斗模式)
# shoulder_camera.gd
# 肩越相机(战斗/瞄准模式)
extends Node3D
## 目标
@export var target: Node3D
## 相机偏移
@export_group("Offsets")
@export var normal_offset: Vector3 = Vector3(0.8, 1.8, 3.0)
@export var aim_offset: Vector3 = Vector3(0.5, 1.6, 1.5)
@export var sprint_offset: Vector3 = Vector3(0, 2.0, 4.0)
## 视野角度
@export_group("FOV")
@export var normal_fov: float = 75.0
@export var aim_fov: float = 50.0
@export var sprint_fov: float = 85.0
## 平滑参数
@export_group("Smoothing")
@export var offset_smooth: float = 8.0
@export var fov_smooth: float = 10.0
@export var rotation_smooth: float = 15.0
## 状态
enum CameraMode { NORMAL, AIM, SPRINT }
var current_mode: CameraMode = CameraMode.NORMAL
var current_offset: Vector3
var camera_rotation: Vector2 = Vector2.ZERO
@onready var camera: Camera3D = $Camera3D
func _ready():
current_offset = normal_offset
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event):
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
camera_rotation.x -= event.relative.y * 0.002
camera_rotation.y -= event.relative.x * 0.002
camera_rotation.x = clamp(camera_rotation.x, deg_to_rad(-80), deg_to_rad(80))
func _physics_process(delta):
if not target:
return
_update_mode()
_update_offset(delta)
_update_fov(delta)
_update_transform(delta)
func _update_mode():
if Input.is_action_pressed("aim"):
current_mode = CameraMode.AIM
elif Input.is_action_pressed("sprint"):
current_mode = CameraMode.SPRINT
else:
current_mode = CameraMode.NORMAL
func _update_offset(delta):
var target_offset: Vector3
match current_mode:
CameraMode.NORMAL:
target_offset = normal_offset
CameraMode.AIM:
target_offset = aim_offset
CameraMode.SPRINT:
target_offset = sprint_offset
current_offset = current_offset.lerp(target_offset, offset_smooth * delta)
func _update_fov(delta):
var target_fov: float
match current_mode:
CameraMode.NORMAL:
target_fov = normal_fov
CameraMode.AIM:
target_fov = aim_fov
CameraMode.SPRINT:
target_fov = sprint_fov
camera.fov = lerp(camera.fov, target_fov, fov_smooth * delta)
func _update_transform(delta):
# 计算目标位置
var target_pos = target.global_position
# 应用旋转
var rotated_offset = current_offset.rotated(Vector3.UP, camera_rotation.y)
rotated_offset = rotated_offset.rotated(
Vector3.RIGHT.rotated(Vector3.UP, camera_rotation.y),
camera_rotation.x
)
global_position = global_position.lerp(
target_pos + rotated_offset,
rotation_smooth * delta
)
# 看向目标前方
var look_target = target_pos + Vector3.UP * 1.5
camera.look_at(look_target, Vector3.UP)
## 切换肩膀侧
var shoulder_side: int = 1 # 1=右肩,-1=左肩
func switch_shoulder():
shoulder_side *= -1
normal_offset.x *= -1
aim_offset.x *= -1
## 模式切换
func set_aim_mode(enabled: bool):
current_mode = CameraMode.AIM if enabled else CameraMode.NORMAL
31.3 第一人称相机
31.3.1 基础第一人称相机
# first_person_camera.gd
# 第一人称相机
extends Node3D
## 灵敏度设置
@export_group("Sensitivity")
@export var mouse_sensitivity: float = 0.002
@export var controller_sensitivity: float = 2.0
## 视角限制
@export_group("Limits")
@export var min_pitch: float = -89.0
@export var max_pitch: float = 89.0
## 头部摇摆
@export_group("Head Bob")
@export var head_bob_enabled: bool = true
@export var head_bob_frequency: float = 2.0
@export var head_bob_amplitude: float = 0.05
## 倾斜效果
@export_group("Tilt")
@export var tilt_enabled: bool = true
@export var tilt_amount: float = 3.0
@export var tilt_speed: float = 5.0
var camera_rotation: Vector2 = Vector2.ZERO
var head_bob_time: float = 0.0
var current_tilt: float = 0.0
var base_position: Vector3
@onready var camera: Camera3D = $Camera3D
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
base_position = camera.position
func _input(event):
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_camera(event.relative)
func rotate_camera(motion: Vector2):
camera_rotation.x -= motion.y * mouse_sensitivity
camera_rotation.y -= motion.x * mouse_sensitivity
camera_rotation.x = clamp(
camera_rotation.x,
deg_to_rad(min_pitch),
deg_to_rad(max_pitch)
)
_apply_rotation()
func _apply_rotation():
rotation.y = camera_rotation.y
camera.rotation.x = camera_rotation.x
func _physics_process(delta):
_process_controller_input(delta)
_update_head_bob(delta)
_update_tilt(delta)
func _process_controller_input(delta):
var look_input = Vector2(
Input.get_axis("look_left", "look_right"),
Input.get_axis("look_up", "look_down")
)
if look_input.length() > 0.1:
rotate_camera(look_input * controller_sensitivity * 10.0)
func _update_head_bob(delta):
if not head_bob_enabled:
return
var parent = get_parent()
if parent is CharacterBody3D:
var velocity = parent.velocity
var horizontal_speed = Vector2(velocity.x, velocity.z).length()
if horizontal_speed > 0.5 and parent.is_on_floor():
head_bob_time += delta * head_bob_frequency * horizontal_speed / 5.0
var bob_offset = Vector3(
sin(head_bob_time) * head_bob_amplitude * 0.5,
abs(sin(head_bob_time)) * head_bob_amplitude,
0
)
camera.position = base_position + bob_offset
else:
head_bob_time = 0.0
camera.position = camera.position.lerp(base_position, 10.0 * delta)
func _update_tilt(delta):
if not tilt_enabled:
return
var move_input = Input.get_axis("move_left", "move_right")
var target_tilt = -move_input * tilt_amount
current_tilt = lerp(current_tilt, target_tilt, tilt_speed * delta)
camera.rotation.z = deg_to_rad(current_tilt)
## 视野震动
func shake(intensity: float = 0.1, duration: float = 0.3):
var tween = create_tween()
var original_pos = camera.position
for i in range(int(duration / 0.05)):
var shake_offset = Vector3(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity),
0
)
tween.tween_property(camera, "position", original_pos + shake_offset, 0.05)
tween.tween_property(camera, "position", original_pos, 0.05)
## 后坐力效果
func apply_recoil(vertical: float, horizontal: float):
camera_rotation.x -= deg_to_rad(vertical)
camera_rotation.y += deg_to_rad(randf_range(-horizontal, horizontal))
_apply_rotation()
31.3.2 武器相机系统
# weapon_camera.gd
# 武器/手臂相机系统
extends Node3D
## 主相机
@onready var main_camera: Camera3D = $MainCamera
## 武器相机(用于防止武器穿墙)
@onready var weapon_camera: Camera3D = $WeaponViewport/WeaponCamera
@onready var weapon_viewport: SubViewport = $WeaponViewport
## 武器显示设置
@export var weapon_fov: float = 60.0
@export var weapon_near_clip: float = 0.01
## 武器摇摆设置
@export_group("Weapon Sway")
@export var sway_amount: float = 0.02
@export var sway_speed: float = 5.0
@export var max_sway: float = 0.1
## 武器节点
@onready var weapon_holder: Node3D = $WeaponHolder
var sway_offset: Vector2 = Vector2.ZERO
var last_mouse_motion: Vector2 = Vector2.ZERO
func _ready():
_setup_weapon_camera()
func _setup_weapon_camera():
# 设置武器相机参数
weapon_camera.fov = weapon_fov
weapon_camera.near = weapon_near_clip
# 武器视口设置
weapon_viewport.transparent_bg = true
weapon_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
func _input(event):
if event is InputEventMouseMotion:
last_mouse_motion = event.relative
func _process(delta):
_sync_camera_transforms()
_update_weapon_sway(delta)
last_mouse_motion = Vector2.ZERO
func _sync_camera_transforms():
# 同步武器相机与主相机
weapon_camera.global_transform = main_camera.global_transform
func _update_weapon_sway(delta):
# 鼠标移动引起的摇摆
var target_sway = Vector2(
-last_mouse_motion.x * sway_amount,
-last_mouse_motion.y * sway_amount
)
target_sway = target_sway.limit_length(max_sway)
sway_offset = sway_offset.lerp(target_sway, sway_speed * delta)
# 应用摇摆
weapon_holder.rotation.y = sway_offset.x
weapon_holder.rotation.x = sway_offset.y
## 武器动画
func play_weapon_animation(anim_name: String):
var anim_player = weapon_holder.get_node_or_null("AnimationPlayer")
if anim_player:
anim_player.play(anim_name)
## 后坐力动画
func apply_weapon_recoil(kick_back: float, kick_up: float):
var tween = create_tween()
# 后退
tween.tween_property(
weapon_holder, "position:z",
weapon_holder.position.z + kick_back, 0.05
)
tween.parallel().tween_property(
weapon_holder, "rotation:x",
weapon_holder.rotation.x - deg_to_rad(kick_up), 0.05
)
# 恢复
tween.tween_property(weapon_holder, "position:z", 0.0, 0.2)
tween.parallel().tween_property(weapon_holder, "rotation:x", 0.0, 0.2)
31.4 相机特效
31.4.1 相机震动系统
# camera_shake.gd
# 相机震动系统
extends Node3D
## 震动参数
@export var decay_rate: float = 1.5
@export var max_offset: float = 0.5
@export var max_rotation: float = 5.0
## 噪声设置
@export_group("Noise")
@export var noise_speed: float = 30.0
var noise: FastNoiseLite
## 当前震动状态
var trauma: float = 0.0
var noise_y: float = 0.0
@onready var camera: Camera3D = $Camera3D
var base_position: Vector3
var base_rotation: Vector3
func _ready():
base_position = camera.position
base_rotation = camera.rotation
# 初始化噪声
noise = FastNoiseLite.new()
noise.seed = randi()
noise.frequency = 0.5
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
func _process(delta):
_update_shake(delta)
func _update_shake(delta):
if trauma <= 0:
camera.position = camera.position.lerp(base_position, 10.0 * delta)
camera.rotation = camera.rotation.lerp(base_rotation, 10.0 * delta)
return
# 衰减
trauma = max(trauma - decay_rate * delta, 0.0)
# 使用trauma的平方获得更自然的效果
var shake_amount = trauma * trauma
# 更新噪声采样位置
noise_y += delta * noise_speed
# 计算偏移
var offset = Vector3(
max_offset * shake_amount * noise.get_noise_2d(0, noise_y),
max_offset * shake_amount * noise.get_noise_2d(100, noise_y),
max_offset * shake_amount * noise.get_noise_2d(200, noise_y) * 0.5
)
# 计算旋转
var rotation_offset = Vector3(
max_rotation * shake_amount * noise.get_noise_2d(300, noise_y),
max_rotation * shake_amount * noise.get_noise_2d(400, noise_y),
max_rotation * shake_amount * noise.get_noise_2d(500, noise_y)
)
camera.position = base_position + offset
camera.rotation = base_rotation + rotation_offset * 0.01
## 添加震动
func add_trauma(amount: float):
trauma = min(trauma + amount, 1.0)
## 预设震动效果
func shake_light():
add_trauma(0.2)
func shake_medium():
add_trauma(0.4)
func shake_heavy():
add_trauma(0.7)
func shake_explosion():
add_trauma(1.0)
## 持续震动
func start_continuous_shake(intensity: float):
trauma = intensity
func stop_continuous_shake():
trauma = 0.0
31.4.2 FOV与镜头效果
# camera_effects.gd
# 相机镜头效果
extends Camera3D
## 基础设置
@export var base_fov: float = 75.0
## 速度线效果
@export_group("Speed Effect")
@export var speed_fov_enabled: bool = true
@export var max_speed_fov: float = 90.0
@export var speed_fov_threshold: float = 10.0
## 瞄准效果
@export_group("Aim Effect")
@export var aim_fov: float = 50.0
@export var aim_transition_speed: float = 10.0
## 受伤效果
@export_group("Damage Effect")
@export var damage_vignette_intensity: float = 0.5
@export var damage_flash_duration: float = 0.2
var target_fov: float
var is_aiming: bool = false
var current_speed: float = 0.0
func _ready():
fov = base_fov
target_fov = base_fov
func _process(delta):
_update_fov(delta)
func _update_fov(delta):
# 确定目标FOV
if is_aiming:
target_fov = aim_fov
elif speed_fov_enabled and current_speed > speed_fov_threshold:
var speed_factor = (current_speed - speed_fov_threshold) / 20.0
speed_factor = clamp(speed_factor, 0.0, 1.0)
target_fov = lerp(base_fov, max_speed_fov, speed_factor)
else:
target_fov = base_fov
# 平滑过渡
fov = lerp(fov, target_fov, aim_transition_speed * delta)
## 设置瞄准模式
func set_aiming(aiming: bool):
is_aiming = aiming
## 更新速度(由角色控制器调用)
func set_current_speed(speed: float):
current_speed = speed
## 冲击效果(如爆炸)
func impact_effect(intensity: float = 1.0):
var original_fov = fov
var tween = create_tween()
# 快速扩大
tween.tween_property(self, "fov", fov + 15.0 * intensity, 0.05)
# 恢复
tween.tween_property(self, "fov", original_fov, 0.3)
## 慢动作效果
func slow_motion(time_scale: float, duration: float):
Engine.time_scale = time_scale
# 调整FOV增强效果
var slow_mo_fov = base_fov - 10.0
var tween = create_tween()
tween.set_ignore_time_scale(true)
tween.tween_property(self, "fov", slow_mo_fov, 0.1)
tween.tween_interval(duration * time_scale)
tween.tween_property(self, "fov", base_fov, 0.2)
tween.tween_callback(func(): Engine.time_scale = 1.0)
## 聚焦效果
func focus_on(target_position: Vector3, duration: float = 0.5):
var tween = create_tween()
tween.tween_method(
func(t): look_at(global_position.lerp(target_position, t), Vector3.UP),
0.0, 1.0, duration
)
31.5 相机管理系统
31.5.1 多相机切换
# camera_manager.gd
# 相机管理器
extends Node
## 所有相机
var cameras: Dictionary = {}
var current_camera: Camera3D
var previous_camera: Camera3D
## 过渡设置
var transition_tween: Tween
var is_transitioning: bool = false
signal camera_changed(from: Camera3D, to: Camera3D)
## 注册相机
func register_camera(id: String, camera: Camera3D):
cameras[id] = camera
if cameras.size() == 1:
current_camera = camera
camera.current = true
## 注销相机
func unregister_camera(id: String):
if cameras.has(id):
var camera = cameras[id]
cameras.erase(id)
if current_camera == camera and not cameras.is_empty():
switch_to(cameras.keys()[0])
## 切换相机(无过渡)
func switch_to(id: String):
if not cameras.has(id):
push_error("Camera not found: " + id)
return
var new_camera = cameras[id]
if current_camera:
previous_camera = current_camera
current_camera.current = false
current_camera = new_camera
current_camera.current = true
emit_signal("camera_changed", previous_camera, current_camera)
## 平滑切换相机
func transition_to(id: String, duration: float = 1.0, ease_type: Tween.EaseType = Tween.EASE_IN_OUT):
if not cameras.has(id):
push_error("Camera not found: " + id)
return
if is_transitioning:
return
var target_camera = cameras[id]
if target_camera == current_camera:
return
is_transitioning = true
previous_camera = current_camera
# 创建过渡
if transition_tween:
transition_tween.kill()
transition_tween = create_tween()
transition_tween.set_ease(ease_type)
# 保存起始变换
var start_transform = current_camera.global_transform
var start_fov = current_camera.fov
# 激活目标相机
target_camera.current = true
current_camera.current = false
current_camera = target_camera
# 从旧位置过渡到新位置
var end_transform = target_camera.global_transform
var end_fov = target_camera.fov
target_camera.global_transform = start_transform
target_camera.fov = start_fov
transition_tween.tween_property(target_camera, "global_transform", end_transform, duration)
transition_tween.parallel().tween_property(target_camera, "fov", end_fov, duration)
transition_tween.tween_callback(func():
is_transitioning = false
emit_signal("camera_changed", previous_camera, current_camera)
)
## 返回上一个相机
func switch_to_previous(duration: float = 0.0):
if previous_camera:
var prev_id = cameras.find_key(previous_camera)
if prev_id:
if duration > 0:
transition_to(prev_id, duration)
else:
switch_to(prev_id)
## 获取当前相机
func get_current_camera() -> Camera3D:
return current_camera
## 获取相机ID
func get_camera_id(camera: Camera3D) -> String:
return cameras.find_key(camera)
31.5.2 电影相机系统
# cinematic_camera.gd
# 电影相机系统
extends Node3D
## 相机路径
@export var camera_path: Path3D
@export var look_at_target: Node3D
## 播放设置
@export_group("Playback")
@export var duration: float = 5.0
@export var auto_play: bool = false
@export var loop: bool = false
## 曲线控制
@export_group("Curves")
@export var speed_curve: Curve
@export var fov_curve: Curve
@export var base_fov: float = 75.0
@export var fov_range: float = 20.0
var path_follow: PathFollow3D
var is_playing: bool = false
var playback_time: float = 0.0
@onready var camera: Camera3D = $Camera3D
signal cinematic_started
signal cinematic_finished
func _ready():
_setup_path()
if auto_play:
play()
func _setup_path():
if camera_path:
path_follow = PathFollow3D.new()
path_follow.loop = loop
camera_path.add_child(path_follow)
func _process(delta):
if not is_playing:
return
_update_playback(delta)
func _update_playback(delta):
# 计算进度
var progress = playback_time / duration
# 应用速度曲线
if speed_curve:
var speed_mult = speed_curve.sample(progress)
playback_time += delta * speed_mult
else:
playback_time += delta
# 更新路径位置
if path_follow:
path_follow.progress_ratio = clamp(progress, 0.0, 1.0)
camera.global_position = path_follow.global_position
# 更新朝向
if look_at_target:
camera.look_at(look_at_target.global_position, Vector3.UP)
elif path_follow:
camera.global_rotation = path_follow.global_rotation
# 更新FOV
if fov_curve:
var fov_factor = fov_curve.sample(progress)
camera.fov = base_fov + fov_range * fov_factor
# 检查结束
if playback_time >= duration:
if loop:
playback_time = 0.0
else:
stop()
## 播放控制
func play():
is_playing = true
playback_time = 0.0
camera.current = true
emit_signal("cinematic_started")
func pause():
is_playing = false
func resume():
is_playing = true
func stop():
is_playing = false
playback_time = 0.0
emit_signal("cinematic_finished")
func seek(time: float):
playback_time = clamp(time, 0.0, duration)
## 关键帧系统
class CameraKeyframe:
var time: float
var position: Vector3
var rotation: Vector3
var fov: float
var keyframes: Array[CameraKeyframe] = []
func add_keyframe(time: float, pos: Vector3, rot: Vector3, camera_fov: float):
var kf = CameraKeyframe.new()
kf.time = time
kf.position = pos
kf.rotation = rot
kf.fov = camera_fov
keyframes.append(kf)
keyframes.sort_custom(func(a, b): return a.time < b.time)
func _interpolate_keyframes(time: float) -> Dictionary:
if keyframes.is_empty():
return {}
# 找到当前时间的关键帧区间
var prev_kf = keyframes[0]
var next_kf = keyframes[-1]
for i in range(keyframes.size() - 1):
if keyframes[i].time <= time and keyframes[i + 1].time > time:
prev_kf = keyframes[i]
next_kf = keyframes[i + 1]
break
# 计算插值
var t = 0.0
if next_kf.time != prev_kf.time:
t = (time - prev_kf.time) / (next_kf.time - prev_kf.time)
return {
"position": prev_kf.position.lerp(next_kf.position, t),
"rotation": prev_kf.rotation.lerp(next_kf.rotation, t),
"fov": lerp(prev_kf.fov, next_kf.fov, t)
}
31.6 实际案例:完整相机系统
# complete_camera_system.gd
# 完整相机系统
extends Node3D
## 相机模式
enum CameraMode {
THIRD_PERSON,
FIRST_PERSON,
TOP_DOWN,
CINEMATIC,
FREE
}
@export var default_mode: CameraMode = CameraMode.THIRD_PERSON
@export var target: CharacterBody3D
## 相机节点
@onready var third_person_cam: Node3D = $ThirdPersonCamera
@onready var first_person_cam: Node3D = $FirstPersonCamera
@onready var top_down_cam: Node3D = $TopDownCamera
@onready var cinematic_cam: Node3D = $CinematicCamera
@onready var free_cam: Node3D = $FreeCamera
## 当前状态
var current_mode: CameraMode
var active_camera: Camera3D
var is_transitioning: bool = false
## 相机特效
var shake_intensity: float = 0.0
var shake_decay: float = 0.0
signal mode_changed(old_mode: CameraMode, new_mode: CameraMode)
func _ready():
set_mode(default_mode)
func _process(delta):
_update_shake(delta)
_process_mode_input()
func _process_mode_input():
if Input.is_action_just_pressed("camera_mode_1"):
set_mode(CameraMode.THIRD_PERSON)
elif Input.is_action_just_pressed("camera_mode_2"):
set_mode(CameraMode.FIRST_PERSON)
elif Input.is_action_just_pressed("camera_mode_3"):
set_mode(CameraMode.TOP_DOWN)
func set_mode(mode: CameraMode, transition_time: float = 0.5):
if mode == current_mode or is_transitioning:
return
var old_mode = current_mode
current_mode = mode
_activate_camera_for_mode(mode, transition_time)
emit_signal("mode_changed", old_mode, mode)
func _activate_camera_for_mode(mode: CameraMode, transition_time: float):
var target_node: Node3D
match mode:
CameraMode.THIRD_PERSON:
target_node = third_person_cam
CameraMode.FIRST_PERSON:
target_node = first_person_cam
CameraMode.TOP_DOWN:
target_node = top_down_cam
CameraMode.CINEMATIC:
target_node = cinematic_cam
CameraMode.FREE:
target_node = free_cam
if not target_node:
return
var new_camera = target_node.get_node("Camera3D") as Camera3D
if transition_time > 0 and active_camera:
_transition_camera(active_camera, new_camera, transition_time)
else:
if active_camera:
active_camera.current = false
new_camera.current = true
active_camera = new_camera
func _transition_camera(from: Camera3D, to: Camera3D, duration: float):
is_transitioning = true
var start_transform = from.global_transform
var start_fov = from.fov
to.global_transform = start_transform
to.fov = start_fov
to.current = true
from.current = false
var tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_CUBIC)
# 获取目标变换
var end_transform = to.global_transform
var end_fov = to.fov
tween.tween_property(to, "global_transform", end_transform, duration)
tween.parallel().tween_property(to, "fov", end_fov, duration)
tween.tween_callback(func():
is_transitioning = false
active_camera = to
)
func _update_shake(delta):
if shake_intensity <= 0:
return
shake_intensity = max(0, shake_intensity - shake_decay * delta)
if active_camera:
var offset = Vector3(
randf_range(-1, 1) * shake_intensity,
randf_range(-1, 1) * shake_intensity,
0
)
active_camera.h_offset = offset.x
active_camera.v_offset = offset.y
## 公共API
func add_shake(intensity: float, decay: float = 5.0):
shake_intensity = min(shake_intensity + intensity, 1.0)
shake_decay = decay
func get_current_mode() -> CameraMode:
return current_mode
func get_active_camera() -> Camera3D:
return active_camera
func set_target(new_target: CharacterBody3D):
target = new_target
# 更新各相机的目标
if third_person_cam.has_method("set_target"):
third_person_cam.set_target(target)
if first_person_cam:
first_person_cam.get_parent().remove_child(first_person_cam)
target.add_child(first_person_cam)
31.7 本章小结
本章全面讲解了Godot 4的3D相机系统:
- Camera3D基础:投影模式、视锥体、坐标转换
- 第三人称相机:基础跟随、碰撞检测、肩越视角
- 第一人称相机:头部摇摆、武器相机、后坐力效果
- 相机特效:震动系统、FOV效果、慢动作
- 相机管理:多相机切换、电影相机、过渡动画
- 完整案例:多模式相机系统实现
相机是游戏体验的关键,好的相机系统能够增强玩家的沉浸感和操控感。第四部分"3D游戏开发"到此完成,下一部分我们将学习用户界面开发,为游戏添加菜单、HUD和交互界面。