Home > Blockchain >  My character passes through the terrain when it moves
My character passes through the terrain when it moves

Time:01-20

https://youtube.com/shorts/NYgzV5Bzd5E

As you can see from this link, my character passes through the terrain when it moves on a slope or downhill. Character has Rigidbody and Capsule collider, terrain has Terrain collider. It moves by mouse or touch input, not keyboard input. How to solve this problem without using NavMeshAgent?

Here is my code. If you need other parts of code, please reply me.

    // State pattern is applied, so this function is excuted instead of Update() when character moves.
    protected override void UpdateMoving()
    {
        // _destPos is the point pressed by the mouse on terrain.
        Vector3 dir = _destPos - transform.position;

        // Idle State
        if (dir.magnitude < 0.1f)
        {
            State = Define.State.Idle;
        }
        // Moving State
        // Moving logic, _stat.moveSpeed is Character's move speed.
        else
        {
            float moveDist = Mathf.Clamp(_stat.moveSpeed * Time.deltaTime, 0, dir.magnitude);
            transform.position  = dir.normalized * moveDist;
            transform.rotation = Quaternion.Slerp(transform.rotation,
                Quaternion.LookRotation(dir), 20 * Time.deltaTime);
        }
    }

    // This is mouse event function, not related this question directly.
    private void onm ouseEvent_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;
                    }
                }
                break;
            
            case Define.MouseEvent.Press:
                if (raycastHit)
                    _destPos = hit.point;
                break;
        }
    }

And this is full code.

using UnityEngine;

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

    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 onm ouseEvent(Define.MouseEvent evt)
    {
        onm ouseEvent_IdleRun(evt);
    }

    private void onm ouseEvent_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;
        }
    }
}
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);
    }
    
}

This is my solution of my question, I modified UpdateMoving() in PlayerController

    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);
            }
        }
    }

CodePudding user response:

The code that actually moves the character does not do anything with the terrains collider or your characters rigid body. You are just continuously moving the transform towards the target. If you look at the character controllers from the standard assets you will find that they are continuously raycasting from the character to the floor to determine if they are grounded or not.

I am on mobile now and can edit my answer later with a code example.

CodePudding user response:

In your code you are setting the position of the player each frame. To ensure that it doesn't clip through the terrain, you can also change the y position based on the terrain height where the player currently is.

  Vector3 pos = transform.position;
  pos.y = Terrain.activeTerrain.SampleHeight(transform.position);
  transform.position = pos;

I got this code from the documentation

You can also add an offset to the y position if your character isn't placed on the ground perfectly. 화이팅!ㅋㅋ

  • Related