本节摘要:3D 物理服务器与 2D 同源——固定步长离散推进,四类碰撞体的谱系原样平移,新增的是三维空间的复杂度与三类新兵器:导航网格寻路、关节约束、布娃娃系统。本节讲三维运动范式的选择、导航的配置与代价、关节布娃娃的使用边界,以及 3D 性能的独有注意点。
从 2D 挪到 3D,物理的第一课不是学新东西而是确认旧知识平移有效:固定步长、运动学与动力学之分、层与掩码——全部原样成立。真正的新东西是三维带来的空间复杂度:平面里的"前方"只有左右,立体里的"前方"是球面上的任意方向。于是需要新工具来回答新问题:敌人怎么绕过障碍走向玩家(导航)、电梯轿厢怎么约束在井道里(关节)、角色死亡怎么倒得自然(布娃娃)。
3D 角色控制仍是运动学身体的天下——2D 章"手感归代码、碰撞归引擎"的哲学不变,三维版的移动模板结构一致,只是速度向量多了一根轴。需要重新审视的是两类边界情形。
**其一,斜坡与台阶。**三维地形起伏后,移动方法对斜坡的处理(爬升、下滑)与台阶高度成为手感参数的一部分。引擎提供斜坡处理选项(是否把斜坡当墙、最大爬坡角),调校原则与 2D 跳跃手感相同:参数上面板,跑起来调。
**其二,旋转的表示。**2D 角色只有一个朝向角,3D 角色的朝向是完整旋转。引擎内部用变换与四元数表示旋转(避免万向节锁),日常代码则常用"朝向目标平滑转身"的辅助方法,不必手写四元数数学:
# 3D 敌人朝玩家平滑转身:四元数藏在辅助方法里 func _physics_process(delta: float) -> void: var to_player := (%Player.global_position - global_position) to_player.y = 0.0 # 只绕竖直轴转身 if to_player.length() > 0.1: var target := Basis.looking_at(to_player.normalized()) %Body.global_transform.basis = \ %Body.global_transform.basis.lerp(target, 6.0 * delta).orthonormalized()
动力学刚体在 3D 的典型岗位同样是道具:掉落的碎片、可推动的箱子、滚落的巨石。三维刚体多了角运动的调校空间(角阻尼、惯性),推箱子手感的好坏多半取决于阻尼参数而非碰撞逻辑。
敌人要绕开石堆走向玩家,逐帧手写避障是灾难。正解是导航系统:在场景里烘焙一张导航网格(标记哪些表面可走、坡度上限多少),之后任何角色声明"我要去某点",导航代理自动给出路径并沿路移动。
配置流程三步:场景中加导航区域节点、把地形与障碍的网格喂给它、烘焙生成导航网格。角色侧挂导航代理,代码只剩目标设定:
# 敌人巡逻与追击:导航代理全程包办路径 func _physics_process(delta: float) -> void: match _state: StateName.PATROL: if navigation_agent.is_navigation_finished(): _patrol_index = (_patrol_index + 1) % _patrol_points.size() navigation_agent.target_position = _patrol_points[_patrol_index] StateName.CHASE: navigation_agent.target_position = %Player.global_position var next := navigation_agent.get_next_path_position() velocity = (next - global_position).normalized() * SPEED move_and_slide()
导航的两笔账要提前算清。烘焙账:导航网格静态生成,场景布局变了要重烘焙;运行时动态障碍(比如玩家推来的箱子)不自动反映,需要另配动态障碍对象。性能账:寻路计算有开销,几十个代理同时频繁重设目标要降频(每几百毫秒更新一次目标而非每帧),或者只给视野内的代理开寻路。
⚠️ 常见坑:烘焙了导航网格但代理不动。九成是代理半径与高度没配——烘焙时按角色尺寸留通道,通道比角色窄就"无路可走"。烘焙参数按最大的角色体型设定,是团队项目的统一做法。
关节是物理世界的铰链:门轴、链条、电梯缆绳都是关节约束下的刚体组合。引擎提供多种关节类型(铰链、滑动、六自由度),每种对应一类受限运动。用法心法是"约束出机构":想清楚这个机构在现实里靠什么连接(门靠合页、吊桥靠枢轴),选对应关节,调好约束限位(最大角度、最大行程)。
布娃娃是关节的戏剧性应用:角色死亡时,骨架的各骨骼临时变成一串关节连接的刚体,身体在物理世界里瘫软倒下。它带来无与伦比的真实感,但两笔代价明确:其一,布娃娃状态与动画系统互斥,切换时机管理不当会出现"诈尸"或"定格";其二,多具布娃娃同屏的性能与稳定性都是考验。工程惯例是限量使用——主角与剧情角色享受布娃娃,杂兵用预制倒地动画。
# 死亡时切换到布娃娃:骨架切换物理模式 func die() -> void: died.emit() for bone in %Skeleton.get_bones(): %Skeleton.physical_bones_add_collision_exception(%Skeleton.get_bone(bone)) %Skeleton.animate_physical_bones = true # 骨骼转为物理驱动
三维物理的开销结构与 2D 不同,三条经验值得入行前记住。其一,碰撞形状用简单几何:胶囊与长方体的碰撞计算远快于凸包与三角网格,视觉模型再精致,物理轮廓也永远用简单形状拼。其二,物理频率按需降档:每秒六十步是默认,射击游戏的手感可能需要,慢节奏解谜完全可以降到三十步,物理开销直接减半。其三,睡眠机制是白捡的优化:静止的刚体自动进入睡眠不再计算,别用外力每帧唤醒它们(比如每帧微调位置),否则等于放弃了这项免费午餐。

3D 世界能动能打能倒下了。第 7 章收束到玩家最近的一层:界面与输入。