三种实现FPS Controller的区别

  1. Transform Translate 允许移动物理,但无物理碰撞
  2. Rigid body + Capsule Collider 符合物理学 不会鬼穿墙 无法滞空运动 可与Physics Object 交互
  3. Charactor Controller 不会鬼穿墙 提供的API相对多 无法与Physic objects互动 可以滞空运动(太空人) 提供多种运动效果例如 slopes steps 等

Rigid body + Capsule Collider

控制摄像头旋转 FPMouseLook.cs 跟main camera 绑定

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

public class FPMouseLook : MonoBehaviour
{
    private Transform cameraTransform;
    [SerializeField] private Transform characterTransform;
    private Vector3 cameraRotation;

    public float MouseSensitivity;
    public Vector2 MaxMinAngle;
    // Start is called before the first frame update
    void Start()
    {
        cameraTransform = transform;
    }

    // Update is called once per frame
    void Update()
    {
        var tmp_MouseX = Input.GetAxis("Mouse X");
        var tmp_MouseY = Input.GetAxis("Mouse Y");

        cameraRotation.x -= tmp_MouseY * MouseSensitivity;//MouseSensitivity=18
        cameraRotation.y += tmp_MouseX * MouseSensitivity;//MouseSensitivity=18

        cameraRotation.x = Mathf.Clamp(cameraRotation.x, MaxMinAngle.x, MaxMinAngle.y);
        cameraTransform.rotation = Quaternion.Euler(cameraRotation.x, cameraRotation.y, 0);

        characterTransform.rotation = Quaternion.Euler(0, cameraRotation.y, 0);
    }
}

函数解析

Input.GetAxis

一、触屏类 1、Mouse X 鼠标沿屏幕X移动时触发 2、Mouse Y 鼠标沿屏幕Y移动时触发 3、Mouse ScrollWheel 鼠标滚轮滚动是触发 二、键盘类 1、Vertical 键盘按上或下键时触发 2、Horizontal 键盘按左或右键时触发

使用示例:

Input.GetAxis("Mouse Y");

返回值是一个数,正负代表方向

Mathf.Clamp

函数定义:

static function Clamp (value : float, min : float, max : float) : float

解释: 限制value的值在minmax之间, 如果value小于min,返回min。 如果value大于max,返回max,否则返回value

控制降落 + 跳跃

FPMoveMent.cs

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

public class FPMoveMent : MonoBehaviour
{
    //自由落体速度
    public float Speed;
    public float grativty;
    
    //跳跃
    public float JumpHeight;
    private Transform characterTransform;
    private Rigidbody characterRigidbody;
    private bool isGrounded = true;
    // Start is called before the first frame update
    void Start()
    {
        characterTransform = transform;
        characterRigidbody = GetComponent<Rigidbody>();
    }

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

    private void FixedUpdate()
    {
        Debug.Log(isGrounded);
        if (isGrounded)
        {
            var tmp_Horizontal = Input.GetAxis("Horizontal");
            var tmp_Vertical = Input.GetAxis("Vertical");


            var tmp_CurrentDirection = new Vector3(tmp_Horizontal, 0, tmp_Vertical);
            //自身坐标转化为世界坐标
            tmp_CurrentDirection = characterTransform.TransformDirection(tmp_CurrentDirection);
            tmp_CurrentDirection *= Speed;

            var tmp_CurrentVelocity = characterRigidbody.velocity;
            var tmp_VelocityChange = tmp_CurrentDirection - tmp_CurrentVelocity;
            tmp_VelocityChange.y = 0;

            characterRigidbody.AddForce(tmp_VelocityChange, ForceMode.VelocityChange);

Debug.Log("button: " + Input.GetButtonDown("Jump"));
            if (Input.GetButtonDown("Jump"))
            {
                characterRigidbody.velocity = new Vector3(tmp_CurrentVelocity.x, CalculateJumpHeightSpeed(), tmp_CurrentVelocity.z);
            }
        }

        characterRigidbody.AddForce(new Vector3(0, -grativty * characterRigidbody.mass,0));
    }

    private float CalculateJumpHeightSpeed()
    {
        return Mathf.Sqrt(2 * grativty * JumpHeight);
    }

    private void OnCollisionStay(Collision _other)
    {
        isGrounded = true;
    }

    private void OnCollisionExit(Collision _other)
    {
        isGrounded = false;
    }
}

函数解析

OnCollisionStay()

OnCollisionStay与OnCollisionEnter的区别 OnCollisionStay();

  1. 两者之间处在碰撞状态下
  2. 两者之间有相对移动的情况下才触发。

FixedUpdate

  1. MonoBehaviour.Update 更新 当MonoBehaviour启用时,其Update在每一帧被调用。

  2. MonoBehaviour.FixedUpdate 固定更新

  • 当MonoBehaviour启用时,其 FixedUpdate在每一帧被调用。
  • 处理Rigidbody时,需要用FixedUpdate代替Update。例如:给刚体加一个作用力时,你必须应用作用力在FixedUpdate里的固定帧,而不是Update中的帧。(两者帧长不同)
  1. MonoBehaviour.LateUpdate 晚于更新
  • 当Behaviour启用时,其LateUpdate在每一帧被调用。
  • LateUpdate是在所有Update函数调用后被调用。这可用于调整脚本执行顺序。例如:当物体在Update里移动时,跟随物体的相机可以在LateUpdate里实现。
  1. Update和FixedUpdate的区别:
  • update跟当前平台的帧数有关,而FixedUpdate是真实时间,所以处理物理逻辑的时候要把代码放在FixedUpdate而不是Update.
  • Update是在每次渲染新的一帧的时候才会调用,也就是说,这个函数的更新频率和设备的性能有关以及被渲染的物体(可以认为是三角形的数量)。在性能好的机器上可能fps 30,差的可能小些。这会导致同一个游戏在不同的机器上效果不一致,有的快有的慢。因为Update的执行间隔不一样了。
  • FixedUpdate,是在固定的时间间隔执行,不受游戏帧率的影响。有点想Tick。所以处理Rigidbody的时候最好用FixedUpdate。

PS:FixedUpdate的时间间隔可以在项目设置中更改,Edit->ProjectSetting->time 找到Fixedtimestep。就可以修改了。

  1. Update和LateUpdate的区别
  • 在圣典里LateUpdate被解释成一句话:LateUpdate是在所有Update函数调用后被调用。这可用于调整脚本执行顺序。例如:当物体在Update里移动时,跟随物体的相机可以在LateUpdate里实现。这句我看了云里雾里的,后来看了别人的解释才明白过来。
  • LateUpdate是晚于所有Update执行的。例如:游戏中有2个脚步,脚步1含有Update和LateUpdate,脚步2含有Update,那么当游戏执行时,每一帧都是把2个脚步中的Update执行完后才执行LateUpdate 。虽然是在同一帧中执行的,但是Update会先执行,LateUpdate会晚执行。
  • 现在假设有2个不同的脚本同时在Update中控制一个物体,那么当其中一个脚本改变物体方位、旋转或者其他参数时,另一个脚步也在改变这些东西,那么这个物体的方位、旋转就会出现一定的反复。如果还有个物体在Update中跟随这个物体移动、旋转的话,那跟随的物体就会出现抖动。 如果是在LateUpdate中跟随的话就会只跟随所有Update执行完后的最后位置、旋转,这样就防止了抖动。

参考: physicalSystyme.md

移动 方案2

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

public class FPCharacterControllerMovement : MonoBehaviour
{
    private CharacterController characterController;
    private Transform characterTransForm;
    private Vector3 movementDirection;
    public float MovementSpeed;

    public float Gravity = 9.8f;
    public float JumpHeight;

    private void Start()
    {
        characterController = GetComponent<CharacterController>();
        // characterForm = GetComponent<Transform>();
        characterTransForm = transform;
    }

    void Update()
    {

Debug.Log(characterController.isGrounded);
        //如果在地面 则返回 true
        if (characterController.isGrounded)
        {
            var tmp_Horizontal = Input.GetAxis("Horizontal");
            var tmp_Vertical = Input.GetAxis("Vertical");

            movementDirection =  characterTransForm.TransformDirection(new Vector3(tmp_Horizontal, 0, tmp_Vertical));
            // characterTransForm.LookAt(characterTransForm.position + tmp_MovementDirection);
            // characterController.SimpleMove(movementDirection * Time.deltaTime * MovementSpeed);

            if (Input.GetButtonDown("Jump"))
            {
               movementDirection.y = JumpHeight; 
            }
        }
        movementDirection.y -=  Gravity * Time.deltaTime;
        characterController.Move(movementDirection * Time.deltaTime * MovementSpeed);//不具备重力算法
    }
}

shift 奔跑 + 下蹲

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

public class FPCharacterControllerMovement : MonoBehaviour
{
    private CharacterController characterController;
    private Transform characterTransForm;
    private Vector3 movementDirection;
    public float MovementSpeed; //移动速度

    public float Gravity = 9.8f;
    public float JumpHeight; //跳跃高度
    public float SprintingSpeed = 8;
    public float WalkSpeed = 4;
    public float SprintingSpeedWhenCrouch = 4;//当下蹲时
    public float WalkSpeedWhenCrouch = 2;//当下蹲时
    public float CrouchHeight = 1f; //下蹲高度
    private bool isCrouched; //是否下蹲
    private float originHeight; //原来的高度

    private void Start()
    {
        characterController = GetComponent<CharacterController>();
        // characterForm = GetComponent<Transform>();
        characterTransForm = transform;
        originHeight = characterController.height;
        isCrouched = false;
    }

    void Update()
    {

Debug.Log(characterController.isGrounded);
        float currentSpeed = WalkSpeed;
        //如果在地面 则返回 true
        if (characterController.isGrounded)
        {
            var tmp_Horizontal = Input.GetAxis("Horizontal");
            var tmp_Vertical = Input.GetAxis("Vertical");

            movementDirection =  characterTransForm.TransformDirection(new Vector3(tmp_Horizontal, 0, tmp_Vertical));
            // characterTransForm.LookAt(characterTransForm.position + tmp_MovementDirection);
            // characterController.SimpleMove(movementDirection * Time.deltaTime * MovementSpeed);

            if (Input.GetButtonDown("Jump"))
            {
               movementDirection.y = JumpHeight; 
            }

            if (Input.GetKeyDown(KeyCode.C))
            {
                var tmpCrouchHeight = isCrouched?originHeight:CrouchHeight ;
                StartCoroutine(DoCrouch(tmpCrouchHeight));
                isCrouched = !isCrouched;
            }
            //shift奔跑
            if (isCrouched)
            {
                currentSpeed = Input.GetKey(KeyCode.LeftShift) ? SprintingSpeedWhenCrouch : WalkSpeedWhenCrouch;
            }
            else

            {
                currentSpeed = Input.GetKey(KeyCode.LeftShift) ? SprintingSpeed : WalkSpeed;
    
            }
        }
        movementDirection.y -=  Gravity * Time.deltaTime;
        characterController.Move(movementDirection * Time.deltaTime * currentSpeed);//不具备重力算法
    }

    private IEnumerator DoCrouch(float target)
    {
        float tmp_CurrentHeight = 0;
        while(Mathf.Abs(characterController.height - target) > 0.1f)
        {
            yield return null;
            characterController.height = Mathf.SmoothDamp(characterController.height, target, ref tmp_CurrentHeight, Time.deltaTime * 5 );
        }
    }
}

--完--