Godot 4.x 3D相机系统实战:从第一人称到第三人称的构建与优化
1. 项目概述:为什么3D相机系统是游戏体验的基石
在Godot Engine里折腾3D项目,无论是第一人称射击、第三人称冒险,还是俯视角解谜,你迟早会撞上一个绕不开的核心问题:如何让玩家“看”得舒服?这个“看”,指的就是3D相机系统。它远不止是一个挂在角色身后的“眼睛”,而是连接玩家与虚拟世界的桥梁,直接决定了游戏的操控感、沉浸感和叙事节奏。
我见过太多新手项目,角色移动、场景美术都做得有模有样,但相机一跑起来就让人头晕目眩,或者关键时刻该给的视角给不到,瞬间让所有努力大打折扣。问题的根源往往在于,开发者把相机当成了一个简单的“跟随节点”,而忽略了它背后一整套关于空间变换、插值动画、碰撞规避和输入响应的复杂逻辑。
一个健壮的3D相机系统,需要同时处理好几件事:跟随目标(比如角色)并保持合适的距离和角度;平滑地响应输入(鼠标、摇杆)来旋转视角;在复杂环境中智能地规避遮挡(比如墙壁、柱子);以及根据游戏状态(战斗、对话、过场)动态切换不同的视角模式。这听起来像是一大堆数学和逻辑,但别怕,Godot已经为我们提供了强大而灵活的构建模块,关键在于理解它们并组合使用。
本文将深入拆解Godot Engine 4.x中的3D相机系统。我不会只停留在Camera3D节点的基本属性上,而是会带你从零搭建几个实战中最高频的相机模式:第一人称自由视角、带弹簧臂的第三人称跟随相机,以及可切换的固定视角相机。我们会重点剖析视角控制背后的数学原理(四元数、变换插值),并利用AnimationPlayer和Tween节点实现丝滑的视角切换与运镜动画。无论你是想做一个《生化危机》式的越肩视角,还是《旷野之息》那样的自由探索相机,这里都有你需要的“配方”和“火候”掌握技巧。
2. 核心组件拆解:Camera3D、SpringArm3D与空间变换
在动手搭建之前,我们必须先吃透Godot中与相机相关的几个核心节点。它们就像乐高积木,不同的组合能搭建出完全不同的结构。
2.1 Camera3D节点:不只是“取景框”
Camera3D是视点的载体。它的属性面板里藏着许多影响最终画面的关键参数:
- 投影模式:
Projection。这是第一个重大选择。Perspective(透视投影)模拟人眼,有近大远小的效果,适合绝大多数3D游戏。Orthogonal(正交投影)则消除了透视,物体大小不随距离改变,常用于2.5D游戏或策略游戏的斜45度视角。Frustum(视锥投影)则允许你自定义近/远裁剪平面,用于特殊效果。 - 视野:
Fov。在透视模式下,它决定了相机的“广角”程度。默认的75度是一个比较自然的数值。增大到90度以上能获得更广阔的视野(类似鱼眼),但边缘物体会产生明显变形;减小到45度左右则像望远镜,视野狭窄但能突出远景。第一人称射击游戏通常使用90-110度的视野来增加周边感知,而电影感强的第三人称游戏可能用60-75度来减少变形。 - 近距与远距裁剪平面:
Near和Far。这是性能与效果的平衡点。Near值不能设得太小(如低于0.05),否则靠近相机的几何体可能会因深度缓冲精度问题出现闪烁(Z-fighting)。Far值则决定了你能看多远。盲目设得很大(如10000)会浪费性能并可能让远处物体因精度不足而闪烁。一个经验法则是:Far值设为你的游戏场景最大可视距离的1.2倍左右即可。对于开放世界,可能需要配合雾效或LOD来掩盖远处细节的消失。 - Cull Mask:剔除遮罩。这是一个按位掩码,用于决定相机渲染哪些视觉层(VisualLayer)。你可以把UI、特效、世界场景分到不同的层,然后让某个相机只渲染UI,另一个相机只渲染世界,再通过
Viewport合成。这是实现画中画、小地图、后视镜等高级功能的基础。
实操心得:调试相机时,一定要打开编辑器中的“调试”菜单下的“可见碰撞体”和“可见导航网格”选项。这能让你直观地看到相机的视锥体(一个淡蓝色的金字塔形),对于调整
Far值和发现相机被卡住的原因至关重要。
2.2 SpringArm3D:第三人称相机的“防撞杆”
SpringArm3D是构建智能跟随相机的神器。你可以把它想象成一根可伸缩的、有碰撞检测的“自拍杆”。
工作原理:
SpringArm3D会从自身原点(通常挂在玩家角色身上)向它的-Z轴方向(Godot中-Z是前方)发射一道射线(或形状检测)。如果检测到碰撞,它就会自动缩短自身的长度,让子节点(通常是Camera3D)保持在碰撞点之前,从而避免相机穿墙。没有碰撞时,它会逐渐恢复到设定的Spring Length。关键属性:
Spring Length:弹簧臂的默认(最大)长度。这决定了相机在无障碍时与父节点的距离。Collision Mask:碰撞遮罩。务必将其设置为与场景中墙壁、地面等障碍物相同的物理层,否则碰撞检测会失效。Shape:碰撞检测的形状。默认为Ray(射线),速度快但只能检测单点。如果相机体积较大或场景复杂,可以换成Capsule(胶囊体)或Box(盒子)形状进行更精确的检测。Margin:碰撞余量。即使检测到碰撞,相机也会与碰撞点保持这个距离,防止相机“贴”在墙上。
常见坑点:
SpringArm3D的碰撞检测是在_physics_process中进行的。如果你的相机移动逻辑写在_process里,可能会出现相机位置更新和碰撞检测不同步的问题,导致相机抖动或偶尔穿模。最佳实践是将所有与物理交互相关的相机逻辑(包括SpringArm的更新)都放在_physics_process中。
2.3 理解Node3D的变换:位置、旋转与缩放的基础
相机的一切运动,本质都是对Node3D(Camera3D和SpringArm3D的基类)的transform属性进行操作。一个Transform3D包含两部分:
basis:一个3x3的矩阵,定义了节点的局部坐标系(X, Y, Z轴的方向),蕴含了旋转和缩放信息。origin:一个Vector3,定义了节点在父节点空间中的位置。
绝对不要直接使用rotation属性!这是很多问题的根源。rotation属性存储的是欧拉角(Euler Angles),虽然人类容易理解,但在三维空间中会带来“万向节死锁”和插值路径错误的问题。Godot在内部使用四元数(Quaternion)来表示旋转,basis就是其外在体现。
- 如何旋转相机:应该使用
rotate_object_local(Vector3.AXIS, angle)方法。例如,rotate_object_local(Vector3.UP, delta)是让相机绕自身的Y轴(上方)旋转,这是水平环顾;rotate_object_local(Vector3.RIGHT, delta)是绕自身X轴(右侧)旋转,用于抬头低头。 - 如何让相机看向某点:使用
look_at(target_position, Vector3.UP)方法。第二个参数Vector3.UP非常重要,它定义了世界的“上”方向,用于稳定相机的滚转,避免相机倒置。 - 如何平滑移动/旋转:直接每帧设置
transform会显得生硬。我们需要插值。Godot提供了lerp(线性插值)和slerp(球面线性插值,专用于旋转)方法。对于位置,可以用transform.origin = transform.origin.lerp(target_position, weight);对于旋转,可以先将basis转为四元数再进行slerp。
3. 构建实战:三种经典相机模式详解
理论说得再多,不如动手搭一个。我们接下来就构建三个最常用的相机模式,并附上完整的代码和避坑指南。
3.1 第一人称自由视角相机
第一人称相机通常直接作为玩家角色(一个CharacterBody3D)的子节点,位置在眼睛高度。
场景结构:
CharacterBody3D (玩家根节点) ├── CollisionShape3D ├── MeshInstance3D (身体模型,可选) └── Camera3D (第一人称相机) └── RayCast3D (用于检测交互,如拾取物品)核心脚本逻辑(附加在CharacterBody3D上):
extends CharacterBody3D @export var mouse_sensitivity: float = 0.002 @export var camera_pitch_range: Vector2 = Vector2(-89, 89) # 俯仰角限制(度) var camera: Camera3D var rotation_input: Vector2 = Vector2.ZERO var camera_pitch: float = 0.0 # 当前俯仰角(度) func _ready(): # 获取相机节点 camera = $Camera3D # 捕获鼠标,实现鼠标控制视角 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _input(event): # 鼠标移动事件控制视角旋转 if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: rotation_input = -event.relative * mouse_sensitivity # 按ESC键释放鼠标 if event.is_action_pressed("ui_cancel"): if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) else: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _physics_process(delta): # 1. 处理水平旋转(绕世界Y轴) rotate_y(rotation_input.x) # 2. 处理垂直旋转(绕相机本地X轴),并限制角度 camera_pitch += rotation_input.y camera_pitch = clamp(camera_pitch, deg_to_rad(camera_pitch_range.x), deg_to_rad(camera_pitch_range.y)) camera.rotation.x = camera_pitch # 3. 清空本帧的旋转输入 rotation_input = Vector2.ZERO # 4. 处理基于WASD的移动逻辑(此处略去,非本文重点) # var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back") # var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() # if direction: # velocity.x = direction.x * speed # velocity.z = direction.z * speed # else: # velocity.x = move_toward(velocity.x, 0, speed) # velocity.z = move_toward(velocity.z, 0, speed) # move_and_slide()注意事项:
- 鼠标灵敏度:
mouse_sensitivity的值需要反复测试。0.002是一个常用起点,但应根据目标平台(PC/主机)和玩家偏好进行调整,最好做成可配置的游戏设置。- 俯仰角限制:必须限制垂直视角,通常是在
±89度之间,防止相机翻转180度导致控制颠倒。- 旋转顺序:代码中先绕世界Y轴旋转角色整体(
rotate_y),再绕相机本地X轴旋转相机(camera.rotation.x)。这个顺序很重要,它确保了水平旋转总是基于世界坐标系,而俯仰旋转是基于相机自身的坐标系,符合人类直觉。- 帧率无关:本例中旋转是基于每帧的鼠标位移
event.relative,其值本身与帧率无关。但如果你是基于时间的旋转(例如手柄摇杆),则需要乘以delta。
3.2 带弹簧臂的第三人称跟随相机
这是3D动作游戏、RPG中最常见的相机。它通过SpringArm3D实现智能避障。
场景结构:
CharacterBody3D (玩家根节点) ├── CollisionShape3D ├── MeshInstance3D (角色模型) └── SpringArm3D (弹簧臂) └── Camera3D (第三人称相机)核心脚本逻辑(附加在CharacterBody3D上):
extends CharacterBody3D @export var mouse_sensitivity: float = 0.005 @export var gamepad_sensitivity: float = 2.0 @export var camera_distance: float = 5.0 @export var camera_height: float = 2.0 @export var vertical_angle_limit: Vector2 = Vector2(-30, 60) # 度 @onready var spring_arm: SpringArm3D = $SpringArm3D @onready var camera: Camera3D = $SpringArm3D/Camera3D var rotation_input: Vector2 = Vector2.ZERO var current_rotation: Vector3 = Vector3.ZERO # 存储弹簧臂的欧拉角(仅用于限制) func _ready(): # 初始化弹簧臂长度和高度 spring_arm.spring_length = camera_distance spring_arm.position.y = camera_height # 初始化相机看向角色背部稍上方,更符合第三人称视觉习惯 camera.look_at(global_position + Vector3(0, 1, 0), Vector3.UP) Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _input(event): # 鼠标控制 if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: rotation_input.x -= event.relative.x * mouse_sensitivity # 水平 rotation_input.y -= event.relative.y * mouse_sensitivity # 垂直 # 手柄右摇杆控制 var gamepad_input = Input.get_vector("camera_right", "camera_left", "camera_down", "camera_up") if gamepad_input.length_squared() > 0.1: # 加入死区,避免摇杆微动触发 rotation_input.x += gamepad_input.x * gamepad_sensitivity * get_physics_process_delta_time() rotation_input.y += gamepad_input.y * gamepad_sensitivity * get_physics_process_delta_time() func _physics_process(delta): # 应用旋转输入到弹簧臂 if rotation_input.length_squared() > 0: # 水平旋转:绕世界Y轴旋转整个弹簧臂父节点(即角色) rotate_y(rotation_input.x) # 垂直旋转:绕弹簧臂自身的X轴旋转,并限制角度 current_rotation.x += rotation_input.y current_rotation.x = clamp(current_rotation.x, deg_to_rad(vertical_angle_limit.x), deg_to_rad(vertical_angle_limit.y)) spring_arm.rotation.x = current_rotation.x rotation_input = Vector2.ZERO # 弹簧臂的碰撞检测和长度调整是自动的,我们只需确保它的目标位置正确。 # 通常,我们希望相机始终看向角色中心点或角色上方的一个点。 var look_at_target = global_position + Vector3(0, 1.8, 0) # 看向角色眼睛高度 camera.look_at(look_at_target, Vector3.UP) # 可选:根据角色速度动态调整弹簧臂长度,产生冲刺时镜头拉远的效果 # var target_length = camera_distance + velocity.length() * 0.1 # spring_arm.spring_length = lerp(spring_arm.spring_length, target_length, delta * 5.0)避坑技巧:
- 手柄死区处理:手柄摇杆有物理回中偏差,直接读取会有微小输入导致相机抖动。通过判断
length_squared() > 0.1来设置一个死区是常见做法。- 垂直角度限制:必须限制垂直旋转角度,防止相机钻到角色脚下或飞到头顶正上方,导致视角混乱。
- LookAt目标点:
camera.look_at的目标点不一定是角色的global_position(脚底)。看向global_position + Vector3(0, 身高 * 0.8, 0)(大约胸部或头部高度)通常观感更自然。- 弹簧臂抖动:如果相机在复杂墙角轻微抖动,可以尝试增加
SpringArm3D的Margin值,或者使用Capsule形状并调整其Radius,让检测更“柔和”。也可以对spring_length的变化应用平滑插值(如上面注释的lerp代码)。
3.3 固定视角与动画过渡
在一些解谜、冒险或过场动画中,我们需要将相机切换到某个预设的固定位置,并可能伴随一个平滑的移动动画。
实现思路:
- 在场景中放置多个
Marker3D节点,作为相机的预设位置和朝向(transform)。 - 使用
AnimationPlayer或Tween来插值Camera3D的transform属性,从一个位置平滑过渡到另一个。 - 切换时,可能需要暂时禁用玩家对相机的控制。
使用Tween实现平滑切换:
extends Camera3D @export var transition_duration: float = 1.0 @export var transition_ease_type: Tween.EaseType = Tween.EASE_IN_OUT @export var transition_trans_type: Tween.TransitionType = Tween.TRANS_CUBIC var original_transform: Transform3D var is_transitioning: bool = false func transition_to_marker(marker: Marker3D): if is_transitioning: return is_transitioning = true original_transform = transform var target_transform = marker.global_transform var tween = create_tween() tween.set_ease(transition_ease_type) tween.set_trans(transition_trans_type) # 同时插值位置和旋转(通过四元数slerp) # 注意:直接tween_property(transform)可能不会按预期工作,需要分别处理origin和basis tween.tween_method(_interpolate_transform, 0.0, 1.0, transition_duration) await tween.finished is_transitioning = false func _interpolate_transform(weight: float): # 线性插值位置 var new_origin = original_transform.origin.lerp(target_transform.origin, weight) # 球面线性插值旋转(通过四元数) var original_quat = Quaternion(original_transform.basis) var target_quat = Quaternion(target_transform.basis) var new_quat = original_quat.slerp(target_quat, weight) transform = Transform3D(Basis(new_quat), new_origin)使用AnimationPlayer实现更复杂的运镜: 对于更复杂的相机动画,比如沿着一条贝塞尔曲线Path3D运动,同时伴随着焦距变化,AnimationPlayer是更好的选择。
- 创建一个
AnimationPlayer节点。 - 为其
Camera3D子节点创建一条动画轨道,录制transform关键帧。 - 你可以在动画中混合使用位置、旋转关键帧,甚至调用函数(例如在动画中途触发某个游戏事件)。
- 在代码中通过
animation_player.play("camera_pan_animation")来触发。
经验之谈:
Tween适合简单的、动态生成的两点间过渡。AnimationPlayer适合预先设计好的、复杂的、多段式的镜头运动,并且可以在编辑器中可视化调整。对于固定视角切换,我通常将Marker3D的transform和切换动画都预先在编辑器中摆好,通过信号或直接调用来触发,这样设计和迭代效率更高。
4. 高级技巧与性能优化
掌握了基本模式后,我们来看看如何让相机系统更健壮、更高效。
4.1 环境碰撞与遮挡处理
SpringArm3D解决了相机不穿墙的问题,但有时我们还需要处理环境物体短暂遮挡玩家的情况。例如,角色走到一棵树后面,树不应该完全挡住玩家。
解决方案:射线检测与材质透明
- 从相机位置向玩家角色发射一道射线(
RayCast3D)。 - 如果射线击中了非玩家、非地面的物体(通过碰撞层判断),则获取该物体的
MeshInstance3D。 - 将该物体的材质临时切换为一个半透明的“溶解”材质,或者直接将其
visibility_layer从相机渲染层中移除。 - 当射线不再击中该物体时,恢复其原有状态。
# 挂在Camera3D上 @onready var occlusion_ray: RayCast3D = $OcclusionRayCast3D func _physics_process(delta): occlusion_ray.target_position = to_local(player.global_position) # 射线指向玩家 occlusion_ray.force_raycast_update() if occlusion_ray.is_colliding(): var collider = occlusion_ray.get_collider() if collider is MeshInstance3D and collider != player: # 假设我们有一个让物体变透明的函数 fade_out_object(collider) else: # 恢复之前被淡出的物体 restore_faded_objects()4.2 相机抖动与平滑阻尼
直接每帧将相机位置设置为目标位置会产生生硬的运动。使用阻尼平滑可以极大提升手感。Godot 4.x提供了lerp和smoothstep函数,但对于相机跟随,更常用的是指数平滑,它模拟了弹簧阻尼系统。
var current_camera_offset: Vector3 = Vector3.ZERO var camera_follow_speed: float = 5.0 # 调整这个值控制平滑度 func _physics_process(delta): # 计算理想的目标偏移(例如,在玩家身后5个单位,高2个单位) var target_offset = -player.global_transform.basis.z * 5.0 + Vector3.UP * 2.0 # 使用指数平滑逼近目标值 current_camera_offset = current_camera_offset.lerp(target_offset, camera_follow_speed * delta) # 应用最终位置 global_position = player.global_position + current_camera_offset原理:lerp的第三个参数weight如果基于delta和时间常数,就构成了指数平滑。camera_follow_speed * delta作为权重,值越大,相机跟进越快(越紧),值越小,延迟和惯性感越强。对于快速移动的游戏(如赛车),需要较大的值;对于电影感强的游戏,可以适当减小,让镜头有“滞后感”。
4.3 不同情境下的相机切换与混合
一个成熟的游戏往往需要多种相机模式。例如:探索时用自由第三人称,进入战斗时拉近变成越肩视角,触发剧情时切换到脚本动画相机。
状态机模式管理相机:
enum CameraMode {FREE, LOCKED_TARGET, SCRIPTED} var current_mode: CameraMode = CameraMode.FREE var current_target: Node3D = null func set_camera_mode(mode: CameraMode, target: Node3D = null): if current_mode == mode and current_target == target: return current_mode = mode current_target = target match mode: CameraMode.FREE: # 启用玩家输入,恢复弹簧臂控制 spring_arm.spring_length = 5.0 set_process_input(true) CameraMode.LOCKED_TARGET: # 锁定到某个敌人,相机在玩家和敌人之间 if target: # 计算一个介于玩家和敌人之间的位置 var look_point = (global_position + target.global_position) / 2 camera.look_at(look_point, Vector3.UP) set_process_input(false) # 禁用玩家手动控制 CameraMode.SCRIPTED: # 完全由AnimationPlayer控制 animation_player.play("cutscene_camera") set_process_input(false)平滑混合:当切换相机模式时,如果直接“跳切”,会很突兀。可以在切换的瞬间,记录旧相机的transform,然后启动一个短暂的Tween,从旧transform插值到新模式的起始transform,实现无缝过渡。
5. 常见问题排查与调试实录
即使按照最佳实践搭建,相机系统依然可能出各种诡异问题。这里记录几个我踩过的坑和解决方案。
5.1 相机剧烈抖动或“抽搐”
- 可能原因1:更新循环冲突。检查你的相机逻辑写在
_process还是_physics_process里。SpringArm3D的碰撞检测在_physics_process中。如果你的移动逻辑在_process,而相机跟随逻辑在_physics_process,由于帧率(_process)和物理帧率(_physics_process)不同步,会导致位置计算不一致,产生抖动。- 解决:确保所有与物理世界交互(移动、碰撞检测)的节点逻辑都放在
_physics_process中。相机跟随逻辑也应放在这里。
- 解决:确保所有与物理世界交互(移动、碰撞检测)的节点逻辑都放在
- 可能原因2:父子变换循环依赖。如果你的相机逻辑里直接修改了父节点(如玩家)的
global_transform,而玩家逻辑里又根据相机位置做了一些计算,可能会形成循环依赖,导致每帧计算结果不稳定。- 解决:理清数据流。通常,相机的位置/旋转应完全由玩家状态(位置、输入)单向决定,避免反向影响。
- 可能原因3:浮点数精度误差累积。长时间运行后,特别是进行了大量旋转计算后,相机的
transform.basis可能不再是一个严格正交归一化的矩阵(即三个轴不再完全垂直且长度为1)。- 解决:定期(例如每帧或每几帧)对相机的
transform进行正交归一化:transform = transform.orthonormalized()。注意,这会丢失缩放信息,如果相机有缩放需求需额外处理。
- 解决:定期(例如每帧或每几帧)对相机的
5.2 相机穿墙或SpringArm失效
- 可能原因1:碰撞层设置错误。
SpringArm3D的Collision Mask没有包含场景中墙壁等障碍物所在的物理层。- 解决:在编辑器中,确保障碍物的
Collision Layer至少有一层被勾选,并且这一层在SpringArm3D的Collision Mask中也已被勾选。
- 解决:在编辑器中,确保障碍物的
- 可能原因2:碰撞形状不合适。使用默认的
Ray形状时,如果障碍物边缘很薄,或者相机模型本身有体积,射线可能从缝隙中穿过。- 解决:将
SpringArm3D的Shape改为Capsule或Box,并适当调整Radius/Extents,使其略大于相机模型的视觉范围。
- 解决:将
- 可能原因3:障碍物不是
StaticBody3D或RigidBody3D。SpringArm3D只能检测到具有CollisionShape3D的物理体。- 解决:确保所有需要阻挡相机的物体都添加了正确的碰撞体。
5.3 视角控制不跟手或有延迟
- 可能原因:平滑阻尼过强。如果你使用了很强的平滑插值(
lerp的权重很小),相机响应会有明显的延迟感。- 解决:区分旋转和位置的平滑度。玩家对视角旋转的延迟容忍度远低于对位置跟随的延迟。通常,旋转阻尼应该非常小甚至为0(即立即响应输入),而位置阻尼可以设置得大一些以实现平滑跟随。可以尝试将旋转计算的
lerp权重设为1.0(无插值),或者使用更快的插值函数。
- 解决:区分旋转和位置的平滑度。玩家对视角旋转的延迟容忍度远低于对位置跟随的延迟。通常,旋转阻尼应该非常小甚至为0(即立即响应输入),而位置阻尼可以设置得大一些以实现平滑跟随。可以尝试将旋转计算的
5.4 在斜坡或不平地面上相机高度异常
- 可能原因:相机跟随逻辑只考虑了角色的
global_position.y。当角色上下坡时,如果相机高度是固定的世界坐标,会导致视角切入地面或悬空。- 解决:让相机的高度参考点跟随角色的某个骨骼点(如头部),或者使用从角色位置向下发射射线检测地面的方法,动态计算相机应该处于的高度。
# 在玩家脚本中,计算地面高度 var ground_ray = $GroundRayCast3D ground_ray.target_position = Vector3(0, -10, 0) # 向下发射10米 ground_ray.force_raycast_update() var ground_height = global_position.y if ground_ray.is_colliding(): ground_height = ground_ray.get_collision_point().y # 将地面高度信息传递给相机脚本,相机以此为基础加上一个偏移量
调试相机是一个需要耐心和观察力的过程。多使用Godot编辑器的“远程”运行和调试功能,在游戏运行时检查相机节点的属性变化,结合场景视图的实时预览,能快速定位问题所在。记住,一个好的相机系统是“隐形”的,当玩家完全沉浸在游戏中而感觉不到它的存在时,你的工作就成功了。
