unity3D項目 移動方式整理和完整角色控制器的實現


unity給我們提供了多種角色移動的思路 先簡單整理下前三種

 

一、直接修改組件位置

最基礎的方式,是改變物體位置的最直接的方式 

適用於既沒有物理系統,也對移動沒有特殊要求的情況

    public float speed;

    void Update()
    {
        Move();
    }

    void Move()
    {
        float MoveX = Input.GetAxisRaw("Horizontal");
        float MoveZ = Input.GetAxisRaw("Vertical");
        Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;

        //1、Translate
        transform.Translate(dir * Time.deltaTime * speed);

        //2、修改positon
        //transform.position += dir * Time.deltaTime * speed;

        //3、TransformPoint
        //transform.TransformPoint(dir * Time.deltaTime * speed);
    }

 

二、物理方法

物理方式通過對物體施加力,改變物體運動狀態的方式讓物體移動

其中最核心的組件就是Rigibody

簡單介紹Rigibody

參數 含義 功能
Mass 質量 物體的質量(任意單位)。建議一個物體的質量不要與其他物體 相差100倍
Drag 阻力 當受力移動時物體受到的空氣阻力。0表示沒有空氣阻力,極 大時使物體立即停止運動
Angular Drag 角阻力 當受扭力旋轉時物體受到的空氣阻力。0表示沒有空氣阻力, 極大時使物體立即停止旋轉
Use Gravity 使用重力 該物體是否受重力影響,若激活,則物體受重力影響
Is Kinematic 是否是運動學 游戲對象是否遵循運動學物理定律,若激活,該物體不再受物理引擎驅動,而只能通過變換來操作。適用於模擬運動的平台或者模擬由鉸鏈關節連接的剛體
Interpolate 插值 物體運動插值模式。當發現剛體運動時抖動,可以嘗試下面的 選項:None(無),不應用插值;Interpolate(內插值),基於上一幀變換來平滑本幀變換;Extrapolate(外插值),基於下一幀變換來 平滑本幀變換
Collision Detection 碰撞檢測 碰撞檢測模式。用於避免高速物體穿過其他物體卻未觸發碰 撞。碰撞模式包括Discrete (不連續)、Continuous (連續)、 Continuous Dynamic (動態連續〉3種。其中,Discrete模式用來 檢測與場景中其他碰撞器或其他物體的碰撞;Continuous模式 用來檢測與動態碰撞器(剛體)的碰撞;Continuous Dynamic模 式用來檢測與連續模式和連續動態模式的物體的碰撞,適用於 高速物體
Constraints 約束 對剛體運動的約束。其中,Freeze Position(凍結位置)表示剛體 在世界中沿所選軸的移動將無效,Freeze Rotation(凍結旋轉)表示剛體在世界中沿所選的X、Y、Z軸的旋轉將無效

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class RigibodyMove : MonoBehaviour
{
    Rigidbody rb;
    public float speed;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {
        Move();
    }

    void Move()
    {
        float MoveX = Input.GetAxisRaw("Horizontal");
        float MoveZ = Input.GetAxisRaw("Vertical");
        Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;

        //1、AddForce
        //rb.AddForce(dir * speed * Time.deltaTime);
        //2、修改velocity
        //rb.velocity += dir * speed * Time.deltaTime;
        //3、MovePosition
        rb.MovePosition(transform.position + dir * speed * Time.deltaTime);
    }
}

 

三、角色控制器

unity提供了一個Character Controller組件,可以自動幫助我們構建碰撞和重力效果

但其中重力 只有調用SimpleMove方法移動會自動使用重力,使用Move方法移動則沒有重力,需要自己添加

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    public float speed;
    public float gravity;

    CharacterController cc;

    bool isGrounded = false;
    float fallSpeed = 0; //掉落速度 會隨着滯空時間不斷增加

    // Start is called before the first frame update
    void Start()
    {
        cc = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update()
    {
        Movement();
    }

    void Movement()
    {
        //cc提供了isGrounded接口用於檢測是否落地
        isGrounded = cc.isGrounded;

        //不在地面時,下墜速度不斷增加
        if (!isGrounded)
        {
            fallSpeed -= gravity;
        }
        else
        {
            if (fallSpeed < 0)
            {
                fallSpeed = 0;
            }
        }

        //平移
        float MoveX = Input.GetAxisRaw("Horizontal");
        float MoveZ = Input.GetAxisRaw("Vertical");
        //Vector3 dir = new Vector3(MoveX, 0, MoveZ).normalized;

        //平移加上掉落 
        Vector3 v = (transform.forward * MoveZ + transform.right * MoveX).normalized * speed + transform.up * fallSpeed;

        //1、simpleMove 帶重力的移動
        //cc.SimpleMove(dir * speed * Time.deltaTime);
        //2、Move 不帶重力 需要自己設置重力
        cc.Move(v * Time.deltaTime);
        
    }
}

 

四、拓展

4.1

事實上,一個完整的角色控制器需要考慮的問題有很多。像重力、跳躍、阻力、空中移動、 處理斜坡、處理台階、沖刺、蹲伏等等

我們需要考慮制作的游戲類型來選擇合適的方案

對比常用的兩種控制Character和RIgidBody,以下列出了其自帶的功能

CharacterController

  • 處理斜坡
  • 處理台階
  • 不會卡牆

RigidBody

  • 自帶重力
  • 提供阻力
  • 可以和物理對象交互

下面提供Brackeys大佬提供的較為完整的角色控制器代碼

 

首先是一個移動腳本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerController_Brackeys : MonoBehaviour
{
    CharacterController cc;
    public float speed = 12f;
    public float gravity = -9.81f;
    public float jumpHeight = 3f; //跳躍高度

    public Transform groundCheck;
    public float groundDistance = 0.4f;
    public LayerMask groundMask;

    Vector3 velocity;
    bool isGrounded; //檢測是否在地面


    
    void Start()
    {
        cc = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update()
    {
        MoveMent();
    }

    void MoveMent()
    {
        isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);//檢測是否在地面

        if(isGrounded && velocity.y < 0)
        {
            //防止在真正掉落到地面前 停止掉落
            velocity.y = -2;
        }

        float MoveX = Input.GetAxisRaw("Horizontal");
        float MoveZ = Input.GetAxisRaw("Vertical");

        Vector3 dir = (transform.right * MoveX + transform.forward * MoveZ).normalized;

        //平移
        cc.Move(dir * speed * Time.deltaTime);

        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            //物理公式 v=sqrt(v*-2*g)
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
        }

        velocity.y += gravity * Time.deltaTime;
        //再乘一個Time.deltaTime是由物理決定的 1/2gt^2
        cc.Move(velocity * Time.deltaTime);
    }
}

然后是一個視角控制器腳本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MouseLook : MonoBehaviour
{
    public float mouseSensitivity = 100f;

    public Transform playerBody;

    float xRotation = 0f;
    // Start is called before the first frame update
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
    }

    // Update is called once per frame
    void Update()
    {
        FreeLook();
    }

    void FreeLook()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;

        xRotation -= mouseY;
        xRotation = Mathf.Clamp(xRotation, -90f, 90f);

        //攝像頭旋轉x軸
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);

        //角色旋轉y軸
        playerBody.Rotate(Vector3.up * mouseX);
    }
}

參數設置如下:

 

4.2

再提供一個更加全面的角色控制器,考慮了滑坡、高處掉下受傷、下落判斷、奔跑、兔子跳等(沒有包含視角控制)

// From: https://wiki.unity3d.com/index.php/FPSWalkerEnhanced
// Modified:
// 1. Namespace to prevent conflicts.
// 2. Only checks Horizontal and Vertical inputs.

namespace PixelCrushers.SceneStreamer.Example
{
    using UnityEngine;

    [RequireComponent(typeof(CharacterController))]
    public class FPSWalkerEnhanced : MonoBehaviour
    {
        [Tooltip("How fast the player moves when walking (default move speed).")]
        [SerializeField]
        private float m_WalkSpeed = 6.0f;

        [Tooltip("How fast the player moves when running.")]
        [SerializeField]
        private float m_RunSpeed = 11.0f;

        [Tooltip("If true, diagonal speed (when strafing + moving forward or back) can't exceed normal move speed; otherwise it's about 1.4 times faster.")]
        [SerializeField]
        public bool m_LimitDiagonalSpeed = true;

        [Tooltip("If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down.")]
        [SerializeField]
        private bool m_ToggleRun = false;

        //[Tooltip("How high the player jumps when hitting the jump button.")]
        //[SerializeField]
        //private float m_JumpSpeed = 8.0f;

        [Tooltip("How fast the player falls when not standing on anything.")]
        [SerializeField]
        private float m_Gravity = 20.0f;

        [Tooltip("Units that player can fall before a falling function is run. To disable, type \"infinity\" in the inspector.")]
        [SerializeField]
        private float m_FallingThreshold = 10.0f;

        [Tooltip("If the player ends up on a slope which is at least the Slope Limit as set on the character controller, then he will slide down.")]
        [SerializeField]
        private bool m_SlideWhenOverSlopeLimit = false;

        [Tooltip("If checked and the player is on an object tagged \"Slide\", he will slide down it regardless of the slope limit.")]
        [SerializeField]
        private bool m_SlideOnTaggedObjects = false;

        [Tooltip("How fast the player slides when on slopes as defined above.")]
        [SerializeField]
        private float m_SlideSpeed = 12.0f;

        [Tooltip("If checked, then the player can change direction while in the air.")]
        [SerializeField]
        private bool m_AirControl = false;

        [Tooltip("Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast.")]
        [SerializeField]
        private float m_AntiBumpFactor = .75f;

        //[Tooltip("Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping.")]
        //[SerializeField]
        //private int m_AntiBunnyHopFactor = 1;

        private Vector3 m_MoveDirection = Vector3.zero;
        private bool m_Grounded = false;
        private CharacterController m_Controller;
        private Transform m_Transform;
        private float m_Speed;
        private RaycastHit m_Hit;
        private float m_FallStartLevel;
        private bool m_Falling;
        private float m_SlideLimit;
        private float m_RayDistance;
        private Vector3 m_ContactPoint;
        private bool m_PlayerControl = false;
        //private int m_JumpTimer;


        private void Start()
        {
            // Saving component references to improve performance.
            m_Transform = GetComponent<Transform>();
            m_Controller = GetComponent<CharacterController>();

            // Setting initial values.
            m_Speed = m_WalkSpeed;
            m_RayDistance = m_Controller.height * .5f + m_Controller.radius;
            m_SlideLimit = m_Controller.slopeLimit - .1f;
            //m_JumpTimer = m_AntiBunnyHopFactor;
        }


        //private void Update()
        //{
        //    // If the run button is set to toggle, then switch between walk/run speed. (We use Update for this...
        //    // FixedUpdate is a poor place to use GetButtonDown, since it doesn't necessarily run every frame and can miss the event)
        //    if (m_ToggleRun && m_Grounded && Input.GetButtonDown("Run"))
        //    {
        //        m_Speed = (m_Speed == m_WalkSpeed ? m_RunSpeed : m_WalkSpeed);
        //    }
        //}


        private void FixedUpdate()
        {
            float inputX = Input.GetAxis("Horizontal");
            float inputY = Input.GetAxis("Vertical");

            // If both horizontal and vertical are used simultaneously, limit speed (if allowed), so the total doesn't exceed normal move speed
            float inputModifyFactor = (inputX != 0.0f && inputY != 0.0f && m_LimitDiagonalSpeed) ? .7071f : 1.0f;

            if (m_Grounded)
            {
                bool sliding = false;
                // See if surface immediately below should be slid down. We use this normally rather than a ControllerColliderHit point,
                // because that interferes with step climbing amongst other annoyances
                if (Physics.Raycast(m_Transform.position, -Vector3.up, out m_Hit, m_RayDistance))
                {
                    if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
                    {
                        sliding = true;
                    }
                }
                // However, just raycasting straight down from the center can fail when on steep slopes
                // So if the above raycast didn't catch anything, raycast down from the stored ControllerColliderHit point instead
                else
                {
                    Physics.Raycast(m_ContactPoint + Vector3.up, -Vector3.up, out m_Hit);
                    if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
                    {
                        sliding = true;
                    }
                }

                // If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
                if (m_Falling)
                {
                    m_Falling = false;
                    if (m_Transform.position.y < m_FallStartLevel - m_FallingThreshold)
                    {
                        OnFell(m_FallStartLevel - m_Transform.position.y);
                    }
                }

                // If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
                if (!m_ToggleRun)
                {
                    m_Speed = Input.GetKey(KeyCode.LeftShift) ? m_RunSpeed : m_WalkSpeed;
                }

                // If sliding (and it's allowed), or if we're on an object tagged "Slide", get a vector pointing down the slope we're on
                if ((sliding && m_SlideWhenOverSlopeLimit) || (m_SlideOnTaggedObjects && m_Hit.collider.tag == "Slide"))
                {
                    Vector3 hitNormal = m_Hit.normal;
                    m_MoveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
                    Vector3.OrthoNormalize(ref hitNormal, ref m_MoveDirection);
                    m_MoveDirection *= m_SlideSpeed;
                    m_PlayerControl = false;
                }
                // Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
                else
                {
                    m_MoveDirection = new Vector3(inputX * inputModifyFactor, -m_AntiBumpFactor, inputY * inputModifyFactor);
                    m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection) * m_Speed;
                    m_PlayerControl = true;
                }

                // Jump! But only if the jump button has been released and player has been grounded for a given number of frames
                //if (!Input.GetButton("Jump"))
                //{
                //    m_JumpTimer++;
                //}
                //else if (m_JumpTimer >= m_AntiBunnyHopFactor)
                //{
                //    m_MoveDirection.y = m_JumpSpeed;
                //    m_JumpTimer = 0;
                //}
            }
            else
            {
                // If we stepped over a cliff or something, set the height at which we started falling
                if (!m_Falling)
                {
                    m_Falling = true;
                    m_FallStartLevel = m_Transform.position.y;
                }

                // If air control is allowed, check movement but don't touch the y component
                if (m_AirControl && m_PlayerControl)
                {
                    m_MoveDirection.x = inputX * m_Speed * inputModifyFactor;
                    m_MoveDirection.z = inputY * m_Speed * inputModifyFactor;
                    m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection);
                }
            }

            // Apply gravity
            m_MoveDirection.y -= m_Gravity * Time.deltaTime;

            // Move the controller, and set grounded true or false depending on whether we're standing on something
            m_Grounded = (m_Controller.Move(m_MoveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
        }


        // Store point that we're in contact with for use in FixedUpdate if needed
        private void OnControllerColliderHit(ControllerColliderHit hit)
        {
            m_ContactPoint = hit.point;
        }


        // This is the place to apply things like fall damage. You can give the player hitpoints and remove some
        // of them based on the distance fallen, play sound effects, etc.
        private void OnFell(float fallDistance)
        {
            print("Ouch! Fell " + fallDistance + " units!");
        }
    }
}

 

此外unity資源商店給我們提供了角色控制器的資源,可以進行使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM