第三十一章:3D相机系统

第三十一章: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相机系统:

  1. Camera3D基础:投影模式、视锥体、坐标转换
  2. 第三人称相机:基础跟随、碰撞检测、肩越视角
  3. 第一人称相机:头部摇摆、武器相机、后坐力效果
  4. 相机特效:震动系统、FOV效果、慢动作
  5. 相机管理:多相机切换、电影相机、过渡动画
  6. 完整案例:多模式相机系统实现

相机是游戏体验的关键,好的相机系统能够增强玩家的沉浸感和操控感。第四部分"3D游戏开发"到此完成,下一部分我们将学习用户界面开发,为游戏添加菜单、HUD和交互界面。

← 返回目录