You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 02:07:02