Unity中使用Transform.position实现带碰撞限制的自定义移动方案问询
自定义Transform移动的碰撞限制解决方案
问题背景
通过Transform.position实现了自定义物理移动逻辑,已完成自主物理计算,无需使用Rigidbody(会引入斜坡移动Bug且增加不必要的处理步骤)。现需基于碰撞体检测结果限制移动方向,射线方案需大量发射射线且可靠性不足,寻求可行替代方案。
补充:使用Rigidbody无法达到自定义浮点物理值实现的流畅移动效果。
可行解决方案
1. 利用Physics.ComputePenetration做穿透修正
这是Unity内置的碰撞计算API,可直接获取两个碰撞体的穿透向量与距离,用来修正移动后的位置,精准解决穿墙问题,无需大量射线。
2. 预碰撞检测(Overlap系列API)
在执行移动前,用Physics.OverlapCapsule或Physics.OverlapBox检测目标位置是否存在碰撞体,提前调整移动方向或截断移动距离,避免进入碰撞体。
3. 复用CharacterController的碰撞逻辑
无需使用CharacterController.Move,而是利用其内置的碰撞检测能力,通过CharacterController的stepOffset、slopeLimit等参数处理地形碰撞,再将自定义移动的结果同步到Transform。
代码实现示例(基于Physics.ComputePenetration)
以下是修改后的Movement脚本,加入了碰撞穿透修正逻辑:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Movement : MonoBehaviour { [Header("Player")] public float PlayerHeight = 2f; public CapsuleCollider PlayerCollider; // 给玩家添加胶囊碰撞体并赋值 [Header("Objects")] public Transform Direction; // Input float CurrentMoveX; float CurrentMoveZ; float TargetMoveX; float TargetMoveZ; [Header("Settings")] public float CurrentMoveSpeedMode = 5f; [Header("Movement Physics")] public float AccelSpeed = 4.5f; public float DeaccelSpeed = 4.5f; public float SnapRange = 0.01f; [Header("Gravity Physics")] public float GravityStrength = 1f; [Header("Ground")] public float GroundDetectionRadius = 0.3f; bool IsGrounded; // Units float GForce = 9.80665f; float SpeedConversionValue = 3.6f; // Direction Vector3 MoveDirection; // Vertical float CurrentVerticalVelocity; void Update() { HorizontalMovement(); MoveOutput(); } void HorizontalMovement() { InputDirection(); EditedDirection(); Ground(); } void InputDirection() { // Move X - Horizontal if(Input.GetAxisRaw("Horizontal") < 0f) { TargetMoveX = -CurrentMoveSpeedMode / SpeedConversionValue; if(CurrentMoveX > TargetMoveX) { if(CurrentMoveX > 0f) { CurrentMoveX -= GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveX <= 0f) { CurrentMoveX -= GForce * AccelSpeed * Time.deltaTime; } } else if(CurrentMoveX <= TargetMoveX) { CurrentMoveX = TargetMoveX; } } else if(Input.GetAxisRaw("Horizontal") > 0f) { TargetMoveX = CurrentMoveSpeedMode / SpeedConversionValue; if(CurrentMoveX < TargetMoveX) { if(CurrentMoveX < 0f) { CurrentMoveX += GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveX >= 0f) { CurrentMoveX += GForce * AccelSpeed * Time.deltaTime; } } else if(CurrentMoveX >= TargetMoveX) { CurrentMoveX = TargetMoveX; } } else if(Input.GetAxisRaw("Horizontal") == 0f) { TargetMoveX = 0f; if(CurrentMoveX > 0.1f) { CurrentMoveX -= GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveX < -0.1f) { CurrentMoveX += GForce * AccelSpeed * Time.deltaTime; } else { CurrentMoveX = TargetMoveX; } } // Move Z - Vertical if(Input.GetAxisRaw("Vertical") < 0f) { TargetMoveZ = -CurrentMoveSpeedMode / SpeedConversionValue; if(CurrentMoveZ > TargetMoveZ) { if(CurrentMoveZ > 0f) { CurrentMoveZ -= GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveZ <= 0f) { CurrentMoveZ -= GForce * AccelSpeed * Time.deltaTime; } } else if(CurrentMoveZ <= TargetMoveZ) { CurrentMoveZ = TargetMoveZ; } } else if(Input.GetAxisRaw("Vertical") > 0f) { TargetMoveZ = CurrentMoveSpeedMode / SpeedConversionValue; if(CurrentMoveZ < TargetMoveZ) { if(CurrentMoveZ < 0f) { CurrentMoveZ += GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveZ >= 0f) { CurrentMoveZ += GForce * AccelSpeed * Time.deltaTime; } } else if(CurrentMoveZ >= TargetMoveZ) { CurrentMoveZ = TargetMoveZ; } } else if(Input.GetAxisRaw("Vertical") == 0f) { TargetMoveZ = 0f; if(CurrentMoveZ > SnapRange) { CurrentMoveZ -= GForce * (AccelSpeed + DeaccelSpeed) * Time.deltaTime; } else if(CurrentMoveZ < -SnapRange) { CurrentMoveZ += GForce * AccelSpeed * Time.deltaTime; } else { CurrentMoveZ = TargetMoveZ; } } } void EditedDirection() { // Directionalize And Normalize Movement Input Vector3 DirectionInput = Direction.forward * CurrentMoveZ + Direction.right * CurrentMoveX; Vector3 DirectionNormal = DirectionInput.normalized; DirectionNormal = new Vector3(Mathf.Abs(DirectionNormal.x), 0, Mathf.Abs(DirectionNormal.z)); Vector3 OutputDirection = new Vector3(DirectionInput.x * DirectionNormal.x, 0, DirectionInput.z * DirectionNormal.z); // Slope Detection RaycastHit TerrainHit; if(Physics.SphereCast(transform.position, PlayerHeight / 4f, Vector3.down, out TerrainHit)) { if(TerrainHit.normal != Vector3.up) { OnSlopeFunction(); } else { NotOnSlopeFunction(); } } else { NotOnSlopeFunction(); } // Follow Terrain/Slopes void OnSlopeFunction() { MoveDirection = Vector3.ProjectOnPlane(OutputDirection, TerrainHit.normal); } void NotOnSlopeFunction() { MoveDirection = OutputDirection; } } void Ground() { // Prevent Clipping Through Floor (Especially when moving down slopes) GroundAlign(); void GroundAlign() { RaycastHit GroundAlignHit; if(Physics.Raycast(transform.position, Vector3.down, out GroundAlignHit, PlayerHeight / 2f)) { Vector3 GroundAlignPos = GroundAlignHit.point; transform.position = new Vector3(transform.position.x, GroundAlignPos.y + PlayerHeight / 2f, transform.position.z); } } } void MoveOutput() { // 计算目标位置 Vector3 targetPosition = transform.position + MoveDirection * Time.deltaTime; // 碰撞穿透修正 Collider[] hitColliders = Physics.OverlapCapsule( PlayerCollider.bounds.center, PlayerCollider.bounds.center + Vector3.up * PlayerHeight, PlayerCollider.radius, ~LayerMask.GetMask("Player") // 排除自身层 ); foreach (var collider in hitColliders) { if (collider == PlayerCollider) continue; Vector3 penetrationDirection; float penetrationDistance; if (Physics.ComputePenetration( PlayerCollider, transform.position, transform.rotation, collider, collider.transform.position, collider.transform.rotation, out penetrationDirection, out penetrationDistance)) { // 修正位置,移出穿透区域 targetPosition += penetrationDirection * penetrationDistance; } } // 应用修正后的位置 transform.position = targetPosition; } }
关键说明
- 需要给玩家对象添加
CapsuleCollider并赋值给脚本中的PlayerCollider变量 Physics.ComputePenetration会自动计算碰撞体间的穿透修正向量,确保角色不会穿墙- 预碰撞检测可在计算
targetPosition前执行,进一步避免进入碰撞体
内容的提问来源于stack exchange,提问作者Niek Kleiboer
相关产品推荐
相关产品推荐

