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

Unity角色移动穿模问题求解(不使用NavMeshAgent)

角色斜坡/下坡移动穿透地形问题解决

角色带有Rigidbody和Capsule Collider组件,地形配备Terrain Collider,通过鼠标或触摸点击目标点移动,下坡或行走斜坡时会出现穿透地形的情况,如何在不使用NavMeshAgent的前提下解决这个问题?

原实现代码

PlayerController.cs

using UnityEngine;

public class PlayerController : BaseController
{
    protected override void Init()
    {
        base.Init();
        WorldObjectType = Define.WorldObject.Player;
        Managers.Input.MouseAction -= OnMouseEvent;
        Managers.Input.MouseAction += OnMouseEvent;
    }

    protected override void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position;

        if (dir.magnitude < 0.1f)
        {
            State = Define.State.Idle;
        }
        else
        {
            float moveDist = Mathf.Clamp(5.0f * Time.deltaTime, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;
            transform.rotation = Quaternion.Slerp(transform.rotation,
                Quaternion.LookRotation(dir), 20 * Time.deltaTime);
        }
    }

    private void OnMouseEvent(Define.MouseEvent evt)
    {
        OnMouseEvent_IdleRun(evt);
    }

    private void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 70.0f);
        
        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                if (raycastHit)
                {
                    int layer = hit.transform.gameObject.layer;

                    if (layer == (int)Define.Layer.Ground)
                    {
                        _destPos = hit.point;
                        State = Define.State.Moving;
                    }
                    else
                    {
                        DeliverGameObject dgo = gameObject.GetComponent<DeliverGameObject>();
                        dgo.Selected = hit.transform.gameObject;
                    }
                }
                
                break;
            
            case Define.MouseEvent.Press:
                if (raycastHit)
                    _destPos = hit.point;
                break;
        }
    }
}

BaseController.cs

using System.Collections;
using UnityEngine;

public abstract class BaseController : MonoBehaviour
{
    [SerializeField] protected Vector3 _destPos;
    [SerializeField] protected Define.State _state = Define.State.Idle;
    [SerializeField] public GameObject _lockTarget;
    protected Rigidbody _rigidbody;

    public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unknown;
    
    
    public virtual Define.State State
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case Define.State.Die:
                    anim.CrossFade("DIE", 0.1f);
                    break;
                case Define.State.Idle:
                    anim.CrossFade("IDLE", 0.1f);
                    break;
                case Define.State.Moving:
                    anim.CrossFade("RUN", 0.1f);
                    break;
                case Define.State.Rush:
                    anim.CrossFade("RUSH", 0.1f);
                    break;
                case Define.State.Attack:
                    anim.CrossFade("ATTACK", 0.1f, -1, 0.0f);
                    break;
                case Define.State.Skill:
                    anim.CrossFade("SKILL", 0.1f, -1, 0.0f);
                    break;
                case Define.State.Skill2:
                    anim.CrossFade("SKILL2", 0.1f, -1, 0.0f);
                    break;
                case Define.State.Jump:
                    anim.CrossFade("JUMP", 0.1f);
                    break;
                case Define.State.KnockBackCreeper:
                    anim.CrossFade("RUSH", 0.1f);
                    break;
            }
        }
    }

    private void Start()
    {
        Init();
    }

    private void Update()
    {
        switch (State)
        {
            case Define.State.Die:
                UpdateDie();
                break;
            case Define.State.Moving:
                UpdateMoving();
                break;
            case Define.State.Idle:
                UpdateIdle();
                break;
            case Define.State.Rush:
                UpdateRush();
                break;
            case Define.State.Attack:
                UpdateAttack();
                break;
            case Define.State.Skill:
                UpdateSkill();
                break;
            case Define.State.Skill2:
                UpdateSkill2();
                break;
            case Define.State.KnockBackCreeper:
                UpdateKnockBackCreeper();
                break;
        }
    }

    protected virtual void Init()
    {
        _rigidbody = gameObject.GetComponent<Rigidbody>();
        _rigidbody.constraints = RigidbodyConstraints.FreezeAll;
    }
    
    protected virtual void UpdateDie() { }
    protected virtual void UpdateMoving() { }
    protected virtual void UpdateIdle() { }
    protected virtual void UpdateRush() { }
    protected virtual void UpdateAttack() { }
    protected virtual void UpdateSkill() { }
    protected virtual void UpdateSkill2() { }
    protected virtual void UpdateKnockBackCreeper() { }

    protected IEnumerator Despawn(GameObject gameObject, float animPlayTime)
    {
        yield return new WaitForSeconds(animPlayTime);
        Managers.Game.Despawn(gameObject);
    }
    
}

问题分析与解决方案

原代码直接修改transform.position移动角色,忽略了地形高度变化,导致下坡时角色位置穿透地形。可通过射线检测修正移动后的位置,确保角色始终贴合地形表面。

修改后的PlayerController中UpdateMoving函数如下:

protected override void UpdateMoving()
{
    Vector3 dir = _destPos - transform.position;

    if (dir.magnitude < 0.1f)
    {
        State = Define.State.Idle;
    }
    else
    {
        RaycastHit hit;
        float moveDist = Mathf.Clamp(5.0f * Time.deltaTime, 0, dir.magnitude);
        Vector3 rayStart = transform.position + Vector3.up;
        Vector3 rayDir = (transform.position + dir.normalized * moveDist) - rayStart; 
        bool raycastHit = Physics.Raycast(rayStart, rayDir, out hit, 70f, _mask);
        Debug.DrawRay(rayStart, rayDir * 5, Color.blue, 1.0f);

        if (raycastHit)
        {
            transform.position = hit.point;
            transform.rotation = Quaternion.Slerp(transform.rotation,
                Quaternion.LookRotation(dir), 20 * Time.deltaTime);
        }
    }
}

方案说明

  • 从角色位置上方发射射线,检测移动路径终点的地形位置
  • 将角色位置设置为射线命中的地形点,确保角色始终贴合地形表面
  • 保留原有的朝向平滑过渡逻辑,保证移动时的视觉流畅性

内容的提问来源于stack exchange,提问作者Adam_WY_Jeong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 11:35:31