Unity物理游泳系统实现:浮力、阻力与角色控制实战指南
1. 项目概述为什么Unity游泳系统是个“坑”做游戏开发这么多年Unity里实现一个“看起来还行”的游泳系统可能一个下午就能搞定。但如果你想让角色在水下的运动感觉真实、物理反馈合理、操作手感顺滑那恭喜你你即将踏入一个充满细节和陷阱的领域。这绝不仅仅是给角色加个浮力、调个阻力那么简单。很多开发者包括我自己早期都栽在了这里——角色要么像块石头一样沉底要么像火箭一样在水里乱窜要么就是转向时感觉像在太空里飘完全没有水的“阻滞感”和“浮力感”。这个项目标题“Unity游泳系统避坑指南”精准地戳中了痛点。它不是一个从零开始的教学而是一份针对已经尝试过、并可能已经踩过坑的开发者的“排雷手册”。核心目标很明确利用Unity的物理引擎主要是Rigidbody结合C#脚本模拟出符合直觉的水下物理运动。这涉及到对浮力、阻力、扭矩、输入处理、状态管理等一系列概念的深度理解和巧妙应用。网上能找到的很多片段代码往往只解决了单一问题但把它们拼凑在一起时各种奇怪的物理现象和逻辑冲突就出现了。本文将基于我实际项目中的经验拆解这些“坑”并提供一套经过实战检验的、完整的C#脚本实现方案。2. 核心物理原理与设计思路拆解在动手写代码之前我们必须先统一思想我们要模拟的是什么不是真实的、符合流体力学公式的复杂水流而是玩家感知中的“真实”水下运动。这意味着我们需要抓住几个关键物理特性的感觉并用相对简化的模型去实现它。2.1 浮力不仅仅是向上的力浮力的基础公式大家都知道F_buoyancy ρ * g * V。但在游戏里水的密度ρ、重力加速度g和角色排开水的体积V都是我们可以灵活控制的“感觉参数”。第一个大坑浮力作用点的错误。很多新手会把浮力直接施加在角色的质心Rigidbody.centerOfMass。这会导致角色在水中无法自然地“抬头”或“翻身”。一个漂浮物其浮心排开水体积的中心和重心不重合时就会产生旋转的力矩使其姿态稳定在某一角度。对于人形角色我们通常希望胸部肺所在位置受到的浮力更大腿部稍小这样角色在水中会自然呈现微微后仰、头部向上的姿态。我们的实现思路是在角色身上设置多个“浮力点”比如头部、胸部、腹部、左右大腿、左右小腿每个点根据其预设的“浮力系数”独立计算浮力。这样不仅能模拟姿态还能让角色在被水流冲击或玩家操作时产生更自然的身体摆动。2.2 水阻力运动“黏滞感”的来源阻力是营造水下感觉的灵魂。在空气中我们通常只考虑很小的线性阻尼Drag和角阻尼Angular Drag。但在水里阻力要大得多且其特性更复杂。第二个大坑使用单一的Drag/Angular Drag值。Unity自带的Drag是线性阻尼与速度成正比方向与速度方向相反。这在空气中是合理的但在水中阻力与速度的平方成正比高速时并且与物体的迎流面积和形状有关。简单地调高Drag值会导致低速移动时也异常迟钝高速移动时阻力又感觉不够。我们的策略是分轴计算阻力。角色前进、后退、左右平移、上下升降所受到的阻力应该可以分别配置。例如垂直上浮的阻力可以小于水平前进的阻力模拟人体截面形状的影响。同时我们还需要计算旋转阻力当角色尝试转身时施加一个与角速度相反的扭矩这个扭矩的大小也应该可以配置并且可能和当前的前进速度有关高速前进时转身更困难。2.3 输入与控制从陆地思维到水下思维陆地角色的控制如Character Controller或简单的力驱动直接改变速度或位置。在水下玩家的输入应该被理解为“意图”而物理系统负责将这个“意图”转化为受阻力、浮力、惯性影响的最终运动。第三个大坑直接修改Rigidbody.velocity。这是最致命也最常见的错误。直接设置velocity会完全覆盖物理引擎的计算导致浮力和阻力失效运动变得极其诡异。正确的做法是通过AddForce和AddTorque来施加力和力矩。玩家的输入如WASD键被映射为期望的前进方向力和转向力矩。这个力的大小需要精心设计它不是一个固定值而应该可能与当前速度有关例如采用PID控制器来逼近目标速度或者与角色姿态有关仰泳时推力效率降低。2.4 状态管理游泳不是一种状态很多人用一个bool isSwimming来管理。这太粗糙了。角色与水的关系是渐变的、有层次的。第四个大坑二元状态切换。角色可能处于“完全出水”、“部分浸没”、“完全浸没”、“快速上浮”、“快速下潜”、“水面漂浮”等多种状态。每种状态下受力的计算方式、玩家的控制系数、动画机的混合树都可能不同。我们需要一个更精细的状态机即使是简单的枚举来区分这些状态并平滑地过渡参数。例如当角色从水面潜入水下时浮力系数、阻力系数、控制灵敏度都应该有一个插值过渡的过程而不是瞬间切换以避免运动突变。3. 核心组件与脚本架构设计基于以上思路我们不采用一个庞大的、上千行的“God Script”而是将其拆分为职责清晰的组件通过Unity的GetComponent方式通信这样更利于调试和迭代。3.1 Swimmer 核心管理器这是挂载在玩家角色根物体上的主脚本。它不直接处理物理而是作为协调中心。职责管理游泳状态枚举None, SurfaceFloat, Submerged, Diving, Emerging。根据角色浸没程度通过射线检测或物理触发器估算切换状态。引用持有对BuoyancyEffector、WaterDragEffector、SwimController等组件的引用。公共接口提供EnterWater()、ExitWater()等方法供外部如水面触发器调用。参数配置定义不同状态下的参数预设如SurfaceFloat状态下的浮力上浮力强度、Submerged状态下的控制力强度等用于插值过渡。3.2 BuoyancyEffector 浮力模拟器这个组件负责计算并施加浮力。它需要挂在带有Rigidbody的角色上。数据结构定义一个内部类BuoyancyPoint。它是一个System.Serializable的类包含Transform point浮力作用点的变换组件引用。float buoyancyFactor该点的浮力系数0-1。胸部可以设为0.8头部0.6腿部0.3。float depthBeforeSubmerged该点完全浸没所需的深度米。可以模拟物体逐渐浸入水中的过程。float depthAfterSubmerged该点完全浸没后继续下沉的深度米。用于计算浸没部分的比例。工作原理在FixedUpdate中遍历所有BuoyancyPoint。对于每个点将其世界坐标转换为水面高度坐标系下的局部高度假设水面是Y0的平面。根据该点的高度、depthBeforeSubmerged和depthAfterSubmerged计算一个submersion因子0到1表示该点被水浸没的比例。这里需要一个平滑的函数比如Mathf.Clamp01。浮力大小 submersion * buoyancyFactor * globalBuoyancyStrength。globalBuoyancyStrength是一个在Swimmer中控制的全局强度可以根据状态插值。浮力方向 Vector3.up。使用Rigidbody.AddForceAtPosition(force, point.position, ForceMode.Force)施加浮力。这是关键AddForceAtPosition不仅施加力还会在力作用点与质心不重合时自动产生扭矩这正是我们想要的“自动调整姿态”的效果。// BuoyancyEffector 核心计算片段 void FixedUpdate() { if (!isActive) return; foreach (var bp in buoyancyPoints) { if (bp.point null) continue; float waterHeight 0; // 这里应替换为实际的水面高度采样可能是从全局WaterManager获取 float pointHeight bp.point.position.y; float depth waterHeight - pointHeight; // 计算浸没比例 float submersion Mathf.Clamp01((depth - bp.depthBeforeSubmerged) / (bp.depthAfterSubmerged - bp.depthBeforeSubmerged 0.0001f)); if (submersion 0) continue; // 计算浮力 float buoyancyMagnitude submersion * bp.buoyancyFactor * currentGlobalStrength; Vector3 buoyancyForce Vector3.up * buoyancyMagnitude; // 在特定点施加浮力 rigidbody.AddForceAtPosition(buoyancyForce, bp.point.position, ForceMode.Force); } }3.3 WaterDragEffector 水阻力模拟器这个组件负责计算并施加水和旋转阻力。参数Vector3 linearDragFactors分别对应X左右、Y上下、Z前后轴的线性阻力系数。可以分别设置。float angularDragFactor旋转阻力系数。bool useQuadraticDrag是否启用与速度平方成正比的阻力更真实但可能不稳定。工作原理在FixedUpdate中获取刚体当前的世界空间速度 (velocity) 和角速度 (angularVelocity)。分解速度将世界速度转换到角色的局部坐标系。这样localVel.x就是左右平移速度localVel.y是升降速度localVel.z是前进后退速度。计算线性阻力对每个局部轴的速度分量根据linearDragFactors中对应的系数计算阻力。如果启用平方阻力则阻力与localVel * Mathf.Abs(localVel)成正比。然后将计算出的局部阻力转换回世界坐标系使用AddForce施加模式为ForceMode.Force或ForceMode.Acceleration。计算旋转阻力直接对angularVelocity施加一个相反的扭矩大小为-angularVelocity * angularDragFactor。// WaterDragEffector 核心计算片段 void FixedUpdate() { if (!isActive) return; Vector3 worldVel rigidbody.velocity; Vector3 localVel transform.InverseTransformDirection(worldVel); Vector3 angularVel rigidbody.angularVelocity; // 计算各轴线性阻力 Vector3 linearDrag Vector3.zero; linearDrag.x -localVel.x * linearDragFactors.x; linearDrag.y -localVel.y * linearDragFactors.y; linearDrag.z -localVel.z * linearDragFactors.z; if (useQuadraticDrag) { linearDrag.x * Mathf.Abs(localVel.x); linearDrag.y * Mathf.Abs(localVel.y); linearDrag.z * Mathf.Abs(localVel.z); } // 转换回世界坐标系并施加 Vector3 worldDrag transform.TransformDirection(linearDrag); rigidbody.AddForce(worldDrag, ForceMode.Acceleration); // 使用Acceleration忽略质量 // 计算并施加旋转阻力 Vector3 angularDrag -angularVel * angularDragFactor; rigidbody.AddTorque(angularDrag, ForceMode.Acceleration); }注意这里使用了ForceMode.Acceleration。这意味着我们施加的是“加速度”力的大小会自动乘以质量。在阻力计算中这通常更方便因为它直接抵消的是速度与物体质量无关在水里阻力主要和形状、速度有关和质量关系不大。你可以根据手感在Force和Acceleration之间选择。3.4 SwimController 游泳控制器这个组件处理玩家输入并将其转换为对刚体施加的力和扭矩。输入映射在Update中获取标准的Input.GetAxis(“Horizontal/Vertical”)以及鼠标输入。力计算推进力将输入的垂直轴W/S映射为一个目标前进速度。然后计算当前前进速度局部Z轴速度与目标速度的差值。使用一个比例系数或简单的PID的P项将这个差值转换为一个向前或向后的力。thrustForce (targetSpeed - currentForwardSpeed) * thrustResponseSpeed。转向扭矩将输入的水平轴A/D和鼠标横向移动映射为一个目标角速度偏航。同样计算差值并施加扭矩。turnTorque (targetTurnRate - currentYawRate) * turnResponseSpeed。俯仰与横滚控制通常我们允许玩家控制俯仰鼠标上下或E/Q键但不直接控制横滚。横滚应由浮力点和阻力自动稳定。俯仰控制同样通过计算角速度差值施加扭矩来实现。状态影响thrustResponseSpeed和turnResponseSpeed这些控制响应参数应该由Swimmer组件根据当前状态如是在水面还是水下来提供。水下可以更迟钝水面可以更灵敏。3.5 WaterVolume 水体区域触发器这是一个简单的Box Collider或Mesh Collider设置为Trigger挂载在一个空物体或水体模型上并附加WaterVolume脚本。职责当角色进入(OnTriggerEnter)时调用角色身上Swimmer组件的EnterWater()方法。当角色退出(OnTriggerExit)时调用ExitWater()。扩展它可以存储水体的属性如密度、流速用于模拟水流等并传递给进入的Swimmer。4. 完整C#脚本实现与关键参数详解下面将整合以上组件给出关键脚本的完整实现并详细解释每个可调参数的意义和调试技巧。4.1 Swimmer.cs 主管理器脚本using UnityEngine; public enum SwimState { OutOfWater, EnteringWater, SurfaceFloating, Submerged, ExitingWater } public class Swimmer : MonoBehaviour { public Rigidbody rb; // 必须赋值 public BuoyancyEffector buoyancy; public WaterDragEffector waterDrag; public SwimController swimController; [Header(状态参数)] public SwimState currentState SwimState.OutOfWater; public float submersionThreshold 0.7f; // 判定为“浸没”所需的浸没比例 private float currentSubmersion 0f; // 当前估算浸没比例 [Header(参数过渡)] public float stateTransitionTime 0.5f; private float stateTransitionTimer 0f; private SwimState targetState; [Header(状态特定参数)] public float surfaceBuoyancyStrength 1.2f; public float submergedBuoyancyStrength 0.8f; public float surfaceDragFactor 1.5f; public float submergedDragFactor 2.5f; public float surfaceControlStrength 1f; public float submergedControlStrength 0.6f; void Start() { if (rb null) rb GetComponentRigidbody(); // 初始不在水中禁用相关组件 SetSwimmingComponentsActive(false); } void Update() { // 估算浸没比例简化版通过角色底部和顶部的射线检测 EstimateSubmersion(); // 状态逻辑 switch (currentState) { case SwimState.OutOfWater: if (currentSubmersion 0.1f) { TransitionToState(SwimState.EnteringWater); } break; case SwimState.EnteringWater: stateTransitionTimer Time.deltaTime; float t Mathf.Clamp01(stateTransitionTimer / stateTransitionTime); // 插值参数 buoyancy.currentGlobalStrength Mathf.Lerp(0f, surfaceBuoyancyStrength, t); waterDrag.dragMultiplier Mathf.Lerp(1f, surfaceDragFactor, t); swimController.controlMultiplier Mathf.Lerp(0f, surfaceControlStrength, t); if (stateTransitionTimer stateTransitionTime) { if (currentSubmersion submersionThreshold) { TransitionToState(SwimState.Submerged); } else { TransitionToState(SwimState.SurfaceFloating); } } break; case SwimState.SurfaceFloating: if (currentSubmersion submersionThreshold) { TransitionToState(SwimState.Submerged); } else if (currentSubmersion 0.05f) { TransitionToState(SwimState.ExitingWater); } // 水面漂浮时可以施加一个轻微的力让角色保持头部 above water break; case SwimState.Submerged: if (currentSubmersion submersionThreshold) { TransitionToState(SwimState.SurfaceFloating); } break; case SwimState.ExitingWater: stateTransitionTimer Time.deltaTime; float tExit Mathf.Clamp01(stateTransitionTimer / stateTransitionTime); buoyancy.currentGlobalStrength Mathf.Lerp(surfaceBuoyancyStrength, 0f, tExit); waterDrag.dragMultiplier Mathf.Lerp(surfaceDragFactor, 1f, tExit); swimController.controlMultiplier Mathf.Lerp(surfaceControlStrength, 0f, tExit); if (stateTransitionTimer stateTransitionTime) { TransitionToState(SwimState.OutOfWater); SetSwimmingComponentsActive(false); } break; } } void EstimateSubmersion() { // 简化实现从角色底部脚部和顶部头部向水面假设Y0发射射线 // 更复杂的实现可以使用多个采样点取平均 RaycastHit hit; float bottomHeight transform.position.y - 1f; // 假设角色高度2米底部在y-1 float topHeight transform.position.y 1f; // 顶部在y1 float waterHeight 0; // 实际项目中应从WaterManager获取 float bottomSub Mathf.Clamp01((waterHeight - bottomHeight) / 2f); // 底部浸没比例 float topSub Mathf.Clamp01((waterHeight - topHeight) / 2f); // 顶部浸没比例 currentSubmersion (bottomSub topSub) * 0.5f; // 粗略平均 } void TransitionToState(SwimState newState) { if (currentState newState) return; targetState newState; stateTransitionTimer 0f; currentState newState; if (newState SwimState.EnteringWater) { SetSwimmingComponentsActive(true); } } void SetSwimmingComponentsActive(bool active) { if (buoyancy ! null) buoyancy.enabled active; if (waterDrag ! null) waterDrag.enabled active; if (swimController ! null) swimController.enabled active; } // 供WaterVolume调用 public void EnterWater() { if (currentState SwimState.OutOfWater) { TransitionToState(SwimState.EnteringWater); } } public void ExitWater() { if (currentState ! SwimState.OutOfWater currentState ! SwimState.ExitingWater) { TransitionToState(SwimState.ExitingWater); } } }关键参数详解submersionThreshold(0.7)这个值决定了角色从“水面漂浮”切换到“完全浸没”状态的阈值。0.7意味着角色身体70%浸入水中才算潜水。调低这个值会让角色更容易进入潜水状态。stateTransitionTime(0.5f)状态切换时参数插值的时间。太短会感觉突兀太长会感觉响应迟钝。0.3到0.8秒是比较舒适的范围。surfaceBuoyancyStrengthvssubmergedBuoyancyStrength通常水面浮力可以稍大帮助角色稳定漂浮水下浮力可以稍小让玩家更容易下潜。这两个值需要和角色重力、浮力点配置一起反复调试。surfaceControlStrengthvssubmergedControlStrength这是最重要的手感参数之一。水下阻力大所以控制力应该减弱如0.6。如果水下控制力还是1玩家会感觉自己在“开飞船”而不是游泳。4.2 BuoyancyEffector.cs 浮力组件完整版using System.Collections.Generic; using UnityEngine; public class BuoyancyEffector : MonoBehaviour { [System.Serializable] public class BuoyancyPoint { public Transform point; [Range(0, 2)] public float buoyancyFactor 1f; public float depthBeforeSubmerged 0.1f; public float depthAfterSubmerged 0.5f; [HideInInspector] public float currentSubmersion; } public ListBuoyancyPoint buoyancyPoints new ListBuoyancyPoint(); public float waterHeight 0f; // 可以从全局管理器获取 public float globalBuoyancyStrength 1f; [HideInInspector] public float currentGlobalStrength 0f; // 由Swimmer控制 private Rigidbody rb; void Start() { rb GetComponentRigidbody(); if (rb null) { Debug.LogError(BuoyancyEffector requires a Rigidbody on the same GameObject.); enabled false; } } void FixedUpdate() { if (buoyancyPoints.Count 0) return; float totalSubmersion 0f; foreach (var bp in buoyancyPoints) { if (bp.point null) continue; float pointHeight bp.point.position.y; float depth waterHeight - pointHeight; // 核心浸没比例计算 float submersion 0f; if (depth bp.depthBeforeSubmerged) { float depthInterval bp.depthAfterSubmerged - bp.depthBeforeSubmerged; if (depthInterval 0) { submersion Mathf.Clamp01((depth - bp.depthBeforeSubmerged) / depthInterval); } else { // 如果深度区间为0或负则视为完全浸没或完全未浸没 submersion (depth bp.depthBeforeSubmerged) ? 1f : 0f; } } bp.currentSubmersion submersion; totalSubmersion submersion; if (submersion 0) { // 计算浮力与浸没比例和系数成正比 float buoyancyMagnitude submersion * bp.buoyancyFactor * currentGlobalStrength; // 可选加入轻微的随机扰动模拟水流增加真实感强度要非常小 // Vector3 forceDir (Vector3.up Random.insideUnitSphere * 0.02f).normalized; Vector3 buoyancyForce Vector3.up * buoyancyMagnitude; // 在特定点施加力产生扭矩 rb.AddForceAtPosition(buoyancyForce, bp.point.position, ForceMode.Force); } } // 可以将平均浸没比例传递给Swimmer用于状态判断更精确 // Swimmer swimmer GetComponentSwimmer(); // if (swimmer ! null) swimmer.UpdateSubmersion(totalSubmersion / buoyancyPoints.Count); } // 在Editor中可视化浮力点 void OnDrawGizmosSelected() { foreach (var bp in buoyancyPoints) { if (bp.point ! null) { Gizmos.color Color.cyan; Gizmos.DrawSphere(bp.point.position, 0.05f); // 绘制浸没深度范围 Vector3 pos bp.point.position; Gizmos.color Color.blue; Gizmos.DrawLine(pos, pos Vector3.down * bp.depthBeforeSubmerged); Gizmos.color Color.red; Gizmos.DrawLine(pos Vector3.down * bp.depthBeforeSubmerged, pos Vector3.down * bp.depthAfterSubmerged); } } } }关键参数详解与调试技巧buoyancyFactor这是每个浮力点的“贡献度”。对于人形角色一个典型的设置是胸部Chest1.2头部Head0.8腹部Hips1.0左右大腿Thigh0.4左右小腿Calf0.2。总和不一定为1最终的整体浮力由globalBuoyancyStrength统一调节。depthBeforeSubmerged和depthAfterSubmerged这是模拟物体逐渐浸入水中的关键。假设一个浮力点在Y0水面。depthBeforeSubmerged0.1意味着该点需要下沉到水面以下0.1米才开始产生浮力。depthAfterSubmerged0.5意味着该点下沉到水面以下0.5米时达到最大浮力浸没比例100%。在这两个深度之间浮力线性增长。调试时将这两个值可视化用上面的Gizmos代码确保它们符合角色模型的比例。对于脚部的点depthBeforeSubmerged可以设大一些让脚更容易沉下去。浮力点布局技巧浮力点越多姿态模拟越精细但计算量也越大。对于人形角色6-8个点头、胸、腹、左右上臂、左右大腿通常就够了。点的位置要尽量分布在身体外围这样产生的扭矩才有效。不要把点都放在身体中轴线上。4.3 SwimController.cs 游泳控制器完整版using UnityEngine; public class SwimController : MonoBehaviour { public Rigidbody rb; public Transform viewTransform; // 通常是摄像机或角色上身的朝向 [Header(推进控制)] public float maxSwimSpeed 5f; public float acceleration 8f; public float deceleration 10f; [Range(0, 1)] public float controlMultiplier 1f; // 由Swimmer控制 [Header(转向控制)] public float maxTurnSpeed 90f; // 度/秒 public float turnAcceleration 180f; // 度/秒^2 public float mouseSensitivity 2f; [Header(俯仰控制)] public float maxPitchSpeed 60f; public float pitchAcceleration 120f; public bool invertPitch false; private float currentForwardSpeed 0f; private float currentTurnSpeed 0f; private float currentPitchSpeed 0f; private Vector3 swimInput Vector3.zero; // 存储输入 void Start() { if (rb null) rb GetComponentRigidbody(); if (viewTransform null) viewTransform Camera.main.transform; Cursor.lockState CursorLockMode.Locked; // 锁定鼠标用于第一人称 } void Update() { // 获取输入 float vertical Input.GetAxis(Vertical); // W/S float horizontal Input.GetAxis(Horizontal); // A/D float mouseX Input.GetAxis(Mouse X); float mouseY Input.GetAxis(Mouse Y); // 计算目标速度基于输入和朝向 swimInput.z vertical; // 前后 swimInput.x horizontal; // 左右平移可选取决于设计 // 计算目标角速度 float targetTurnSpeed (horizontal * maxTurnSpeed mouseX * mouseSensitivity * maxTurnSpeed) * controlMultiplier; float targetPitchSpeed ((invertPitch ? -mouseY : mouseY) * mouseSensitivity * maxPitchSpeed) * controlMultiplier; // 平滑过渡当前角速度可选也可以直接在FixedUpdate中用力控制 currentTurnSpeed Mathf.MoveTowards(currentTurnSpeed, targetTurnSpeed, turnAcceleration * Time.deltaTime); currentPitchSpeed Mathf.MoveTowards(currentPitchSpeed, targetPitchSpeed, pitchAcceleration * Time.deltaTime); } void FixedUpdate() { if (controlMultiplier 0) return; // 1. 处理前进/后退推进力 Vector3 localVelocity transform.InverseTransformDirection(rb.velocity); currentForwardSpeed localVelocity.z; float targetForwardSpeed swimInput.z * maxSwimSpeed * controlMultiplier; float speedDifference targetForwardSpeed - currentForwardSpeed; // 根据差值方向选择加速或减速 float appliedAcceleration (speedDifference 0) ? acceleration : deceleration; float forwardForce speedDifference * appliedAcceleration; // 将力施加在局部Z轴前向然后转换到世界坐标系 Vector3 worldForwardForce transform.forward * forwardForce; rb.AddForce(worldForwardForce, ForceMode.Force); // 2. 处理左右平移力可选如果设计允许 if (Mathf.Abs(swimInput.x) 0.1f) { float strafeForce swimInput.x * acceleration * controlMultiplier; Vector3 worldStrafeForce transform.right * strafeForce; rb.AddForce(worldStrafeForce, ForceMode.Force); } // 3. 处理转向扭矩偏航 if (Mathf.Abs(currentTurnSpeed) 0.1f) { // 将角速度度/秒转换为扭矩。这里简化处理直接施加一个与角速度成正比的扭矩。 // 更精确的做法是计算角速度差并应用角加速度。 Vector3 turnTorque transform.up * currentTurnSpeed * 0.1f; // 0.1是一个经验系数需要调试 rb.AddTorque(turnTorque, ForceMode.Force); } // 4. 处理俯仰扭矩 if (Mathf.Abs(currentPitchSpeed) 0.1f) { Vector3 pitchTorque transform.right * currentPitchSpeed * 0.1f; rb.AddTorque(pitchTorque, ForceMode.Force); } // 5. 自动回正横滚可选增加稳定性 // 获取当前横滚角局部X轴旋转 float rollAngle transform.localEulerAngles.z; if (rollAngle 180) rollAngle - 360; // 施加一个与横滚角相反的扭矩使其回正 float rollStabilizationTorque -rollAngle * 2f; // 系数需要调试 rb.AddTorque(transform.forward * rollStabilizationTorque, ForceMode.Force); } }关键参数详解与手感调试maxSwimSpeed(5f)水下最大前进速度。陆地奔跑速度可能在8左右水下5感觉已经很快。这个值需要和WaterDragEffector的阻力系数一起调。如果阻力很大即使这个值设到10也达不到。acceleration和deceleration这是控制“油门响应”的关键。acceleration决定从静止加速到最大速度的快慢deceleration决定松开按键后减速的快慢。水下deceleration通常应该比acceleration大一些因为水的阻力会让停止更快。建议从accel8, decel12开始调。turnAcceleration和pitchAcceleration角加速度。值越大转身/抬头越灵敏。水下建议设置在120-200之间。太大容易晕太小感觉迟钝。自动回正横滚代码中第5步是一个简单的稳定器。-rollAngle * 2f这个公式意味着横滚角越大回正的扭矩越大。系数2f是刚度调大它角色会更抗拒横滚感觉更“僵硬”调小则允许更多横滚感觉更“柔软”。这是一个非常重要的手感微调参数。5. 集成、调试与常见问题排查将所有脚本挂载到角色上并正确设置引用后就可以开始漫长的调试了。游泳手感是玄学需要极大的耐心。5.1 集成步骤与初始配置准备角色确保你的角色模型有一个Rigidbody组件。建议设置Mass质量根据角色大小设定60-80kg比较合理Drag和Angular Drag先设为0因为我们用自定义的WaterDragEffectorConstraints约束通常冻结Rigidbody的Rotation X和Rotation Z只允许绕Y轴旋转不对于游泳我们需要角色能在所有轴上自由旋转所以不要冻结任何旋转约束。但可以冻结Position X和Position Z如果是在无限泳池测试。挂载脚本将Swimmer、BuoyancyEffector、WaterDragEffector、SwimController四个脚本都挂到角色根物体上。设置引用在Swimmer脚本的Inspector中将rb、buoyancy、waterDrag、swimController分别拖拽赋值。配置浮力点在BuoyancyEffector中点击buoyancyPoints列表的“”号添加多个浮力点。在场景中将对应的Transform如Chest、Head等骨骼或空物体拖入。调整每个点的buoyancyFactor和深度参数。配置阻力在WaterDragEffector中设置linearDragFactors。初始建议值(2.5, 1.5, 3.0)。意思是左右平移阻力2.5上下升降阻力1.5前后前进阻力3.0。因为人体前后截面大所以前进阻力最大。angularDragFactor可以先设为5.0。配置控制器在SwimController中将viewTransform赋值通常是主摄像机。设置maxSwimSpeed5,acceleration8,deceleration12。maxTurnSpeed90,turnAcceleration150。创建水体创建一个Cube缩放成泳池大小赋予一个蓝色的半透明材质。为其添加Box Collider勾选Is Trigger。挂载WaterVolume脚本简单脚本只有OnTriggerEnter/Exit调用Swimmer.Enter/ExitWater。5.2 调试流程与手感微调调试的核心是隔离问题逐个击破。第一步只开浮力。禁用SwimController和WaterDragEffector。将角色放入水中。观察角色是否能稳定漂浮姿态是否自然头脚是否倾斜。如果像一根棍子一样直上直下说明浮力点太集中或buoyancyFactor设置不合理需要调整胸部浮力大于脚部。如果翻滚不停说明某个浮力点扭矩过大或者重心(Rigidbody.centerOfMass)位置不对。尝试在脚本中微调rb.centerOfMass将其稍微向下骨盆位置移动。第二步加上阻力。启用WaterDragEffector。用手在Scene视图拖动角色给它一个初始速度然后松开。观察角色减速是否自然是否会在水中旋转如果阻力不对称可能会。调整linearDragFactors和angularDragFactor直到感觉“粘稠但顺滑”。第三步加上控制。启用SwimController。尝试前后游动。感觉是否像在糖浆里划动阻力太大或推力太小还是像在空气中飞阻力太小调整顺序先调maxSwimSpeed和acceleration/deceleration让加速/减速感觉合理。再调linearDragFactors.z前后阻力确保达到最大速度后松开按键能在一个合理的距离内停下。最后调转向手感turnAcceleration和angularDragFactor确保转身既有响应又不会太飘。第四步测试状态切换。让角色跑跳入水观察从空中到水中的过渡是否平滑stateTransitionTime。尝试上浮到水面观察是否能稳定漂浮surfaceBuoyancyStrength是否足够抵消重力。5.3 常见问题与解决方案速查表问题现象可能原因解决方案角色入水后像石头一样沉底1. 浮力总和小于重力。2.Swimmer状态未切换浮力组件未启用。3. 浮力点depthBeforeSubmerged设置过大未触发。1. 增大globalBuoyancyStrength或各浮力点的buoyancyFactor。2. 检查WaterVolume触发器是否触发Swimmer.EnterWater是否被调用。3. 减小depthBeforeSubmerged或在Editor中通过Gizmos可视化检查浮力点与水面的相对位置。角色在水中疯狂旋转或翻滚1. 浮力点分布严重不对称产生过大扭矩。2.Rigidbody.centerOfMass位置不对。3.WaterDragEffector的angularDragFactor太小。4.SwimController的转向扭矩系数太大。1. 检查并对称布置浮力点左右。2. 在脚本Start中打印或可视化rb.centerOfMass可能需要通过代码或子物体调整。3. 大幅增加angularDragFactor尝试10或20。4. 减小SwimController中计算扭矩时的系数如代码中的0.1f。游泳手感“飘”像在太空1.WaterDragEffector阻力系数太小。2.Rigidbody的Drag/Angular Drag未归零与自定义阻力冲突。3. 角色质量(Mass)太小。1. 增加linearDragFactors和angularDragFactor。2. 确保Rigidbody的Drag和Angular Drag设为0。3. 适当增加角色Mass如从1调到60。游泳手感“涩”转身困难1.WaterDragEffector阻力系数太大尤其是angularDragFactor。2.SwimController的turnAcceleration或扭矩系数太小。3.maxTurnSpeed太低。1. 减小angularDragFactor和linearDragFactors。2. 增加turnAcceleration和扭矩系数。3. 提高maxTurnSpeed。从水中跃出时角色在空中乱转离开水面后SwimController仍在施加转向或稳定扭矩。确保Swimmer在ExitingWater和OutOfWater状态时将SwimController的controlMultiplier设为0或直接禁用SwimController组件。角色头部无法露出水面或露出后不停抖动水面漂浮状态(SurfaceFloating)的浮力逻辑有问题。浮力与重力在临界点附近震荡。在Swimmer的SurfaceFloating状态中加入一个“水面保持力”。当角色头部接近水面时施加一个额外的、温和的向上力并增加一个小的阻尼使其稳定。也可以使用Mathf.SmoothDamp来平滑目标浮力强度。性能问题特别是多个角色时BuoyancyEffector中每帧对每个浮力点进行大量计算和AddForceAtPosition调用。1. 减少浮力点数量用更少的点模拟大体姿态。2. 将部分计算移到FixedUpdate中并考虑使用ObjectPool管理大量游泳者。3. 对于非主角的NPC可以使用简化的浮力模型如只用一个中心点。5.4 高级优化与扩展思路当基础系统运行稳定后可以考虑以下扩展让游泳体验更上一层楼动态水流让WaterVolume携带一个FlowDirection和FlowStrength。在WaterDragEffector和SwimController计算速度时将水流速度考虑进去。角色顺流时省力逆流时费力。游泳体力系统在SwimController中关联一个耐力值。持续加速或快速转向会消耗耐力耐力低时controlMultiplier下降最大速度降低。不同泳姿通过动画状态机切换不同的游泳动画自由泳、蛙泳。同时在Swimmer中切换不同的控制参数集。例如蛙泳模式下的acceleration更大但maxSwimSpeed较低自由泳则相反。水面涟漪与音效在Swimmer中检测角色身体部位如手、脚与水面的碰撞速度触发相应的粒子效果水花和音效。这需要更精细的碰撞体设置。水下视觉与音效后处理进入Submerged状态时启用一个全局的水下后处理效果颜色偏蓝、模糊、焦散光效并切换音频混响区营造沉浸感。这套游泳系统框架的搭建核心在于理解物理组件之间的相互作用以及通过大量参数调试来逼近那种“感觉对了”的手感。它没有唯一的正确答案取决于你的游戏风格——是偏向拟真还是偏向爽快。最重要的是有了这个可扩展的组件化基础你可以像搭积木一样不断试验和添加新的特性最终打造出独一无二的水下体验。

相关新闻

最新新闻

日新闻

周新闻

月新闻