[Unity] Attempt to implement a third-person controller (fourth, mobile control ②)

[Unity] Attempt to implement a third-person controller (fourth, mobile control ②)

[Statement] This third-person controller is a reproduction of the third-person controller in the unity star asset. If necessary, you can directly download the asset and learn it. I personally don't like the behavior of reinventing the wheel, but the official controller has some unsatisfactory bugs when I use it. Still, the controller looks pretty good to me, and while seamless animation isn't possible, personal projects should figure out how to get it moving first.
[Version] This project is based on Unity [2021.3lts] version

direction correction

Remember our control of the displacement angle in the Move method?
Now that we have obtained the camera, now we only need to calculate the angle of the camera, and our direction and the camera have achieved a linkage effect

_targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg 
		+ _mainCamera.transform.eulerAngles.y;

I added a part of the animation control to my own code, which I will talk about later, it is just the control of a value.

speed expansion

Next we can create speeds like running. We can make it in the following two ways:
Ⅰ. Create bool values ​​for different states. When a state is activated, activate the bool value directly.
Ⅱ. Create an enum for a group of mutually exclusive states, such as idle, walk, and run should be mutually exclusive, and create a group of mutually exclusive states for it.
To be honest, I don't think there is much difference between the two methods. If you are not familiar with enum, the first method is enough. The second type is mainly to see a little bit clearer. Still the same sentence, there is no pros and cons, just whether you are used to it.

public enum PlayerActionState
{
    
    
    Idle,
    Walk,
    Run
}

public PlayerActionState playerActionState = PlayerActionState.Idle;

We now consider where to put the control of this state. In fact, you can put it in the previous input control part. After all, the state of our character is mostly determined by the input value. After we change there, we only need to combine the information of the character’s fall in Move to judge the state of the character. Can. Of course, we have also done external state data in the input control, so we can also directly view it in Move. We first judge the current state of the character at the beginning of each Move execution

private void PlayerStateJudge()
{
    
    
	playerActionState = PlayerActionState.Idle;
	if (_inputsMassage.move!=Vector2.zero)
	{
    
    
		playerActionState = PlayerActionState.Walk;
		if (_inputsMassage.run)
			playerActionState = PlayerActionState.Run;
	}
}

After the judgment is completed, we assign a value to _currentSpeed ​​according to the state

_currentSpeed = playerActionState switch
{
    
    
	PlayerActionState.Idle => 0f,
	PlayerActionState.Walk => walkSpeed,
	PlayerActionState.Run => _runSpeed,
	_ => 0f
};

That's it for running.

new status

We will add a crouching state to the character. Of course, this effect involves animation changes
because it is a relatively independent state, which we control through bool.
We judge at the beginning of Move (please consider the dependency problem when judging all states, and there is no need to prioritize the judgment of dependencies)
and then consider the priority of the run state and the squatting state. The higher the priority, the later the assignment.
We think that when you enter the next state After squatting, you cannot run, so the fixed speed is squatting walking speed. Not performed when exiting the squat state.

_currentSpeed = playerActionState switch
{
    
    
	PlayerActionState.Idle => 0f,
	PlayerActionState.Walk => walkSpeed,
	PlayerActionState.Run => _runSpeed,
	_ => 0f
};
if (_isCrouch) 
	_currentSpeed = _crouchSpeed;

gradual change in speed

Currently, our speed is directly changing. If a friend connects to the animation for testing, we will find that our speed changes directly, and there is a "jump" between animations. We need to gradually change the speed to lubricate this speed
. If you have studied numerical analysis, you will find that this is interpolation. Then when interpolating, we need to get two points to create an interpolation function for them. So what are these two points? It is the current speed and the speed to be changed to.
We first record the current velocity:

//玩家当前水平速度的参考
float currentHorizontalSpeed = new Vector3(_characterController.velocity.x, 
        0.0f, _characterController.velocity.z).magnitude;//偏离度,保证目标速度与目前速度相差大才可以插值,避免小幅度的抽搐
float speedOffset = 0.1f;

The magnitude here is the exact length (we talked about the problem of speed and vector before, only the size is needed here).
Then there will be semantic repetition, so the speed we judge above becomes targetSpeed.
We start to judge the degree of deviation. If there is a big difference between the target value and the current value, we need to have an interpolation change process to make it smooth; when it is less than the deviation degree, we think that no one will be able to see the direct change, so directly its changed.

//判断偏离度
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
	currentHorizontalSpeed > targetSpeed + speedOffset)
{
    
    
	_currentSpeed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed ,Time.deltaTime * SpeedChangeRate);
	//四舍五入到小数点后3位
	_currentSpeed = Mathf.Round(_currentSpeed * 1000f) / 1000f;
}
else
{
    
    
	_currentSpeed = targetSpeed;
}

At this point, our mobile control part is basically completed. After I connect to the animation system, there will be a problem of animation shaking. If you also have this problem or have a way to solve this problem, please leave a message below

the code

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

public class ThirdPlayerMoveController : MonoBehaviour
{
    
    
    CharacterController _characterController;
    PlayerInput _playerInput;
    PlayerInputsMassage _inputsMassage;
    GameObject _mainCamera;
    Animator _anim;

    public PlayerActionState playerActionState = PlayerActionState.Idle;

    [Header("相机设置")]
    public GameObject _cinemachineFollowTarget;
    float _cinemachineTagertX;
    float _cinemachineTagertY;
    [Tooltip("相机仰角")]
    public float TopClamp = 70.0f;
    [Tooltip("相机俯角")]
    public float BottomClamp = -30.0f;
    [Tooltip("额外的度数覆盖摄像头。有用的微调相机位置时,锁定")]
    public float CameraAngleOverride = 0.0f;

    [Header("玩家设置")]
    [Tooltip("这将决定普通行走时的速度")]
    public float walkSpeed = 1.5f;
    [Tooltip("这将决定跑步时的速度")]
    public float _runSpeed = 5.0f;
    private bool _isCrouch = false;
    [Tooltip("这将决定蹲下行走的速度")]
    public float _crouchSpeed = 1.0f;
    [Tooltip("这将决定不同状态速度间切换的速度")]
    public float SpeedChangeRate = 10.0f;

    private float _currentSpeed;
    private float _targetRotation = 0.0f;
    [Tooltip("角色光滑旋转时间")]
    private float RotationSmoothTime = 0.12f;
    [Tooltip("在角色光滑旋转过程中的速度")]
    private float _rotationVelocity;

    private float _threshold = 0.01f;

    private bool IsCurrentDeviceMouse
    {
    
    
        get {
    
     return _playerInput.currentControlScheme == "KeyboardMouse"; }
    }

    private void Awake()
    {
    
    
        // get a reference to our main camera
        if (_mainCamera == null)
        {
    
    
            _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
        }
    }

    // Start is called before the first frame update
    void Start()
    {
    
    
        _characterController = GetComponent<CharacterController>();
        _inputsMassage = GetComponent<PlayerInputsMassage>();
        _playerInput = GetComponent<PlayerInput>();
        _anim = GetComponentInChildren<Animator>();
    }

    private void FixedUpdate()
    {
    
    
        Move();
    }

    private void LateUpdate()
    {
    
    
        CameraRotation();
    }

    /// <summary>
    /// 相机追踪点的控制
    /// </summary>
    private void CameraRotation()
    {
    
    
        if(_inputsMassage.look.sqrMagnitude>_threshold)//look值大于误差代表有输入
        {
    
    
            float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1f : Time.deltaTime;

            _cinemachineTagertX += _inputsMassage.look.x * deltaTimeMultiplier;
            _cinemachineTagertY += _inputsMassage.look.y * deltaTimeMultiplier;
        }
        _cinemachineTagertX = ClampAngle(_cinemachineTagertX, float.MinValue, float.MaxValue);
        _cinemachineTagertY = ClampAngle(_cinemachineTagertY, BottomClamp, TopClamp);

        _cinemachineFollowTarget.transform.rotation = Quaternion.Euler((-_cinemachineTagertY - CameraAngleOverride) * Settings.mouseYmoveTimes,
                _cinemachineTagertX * Settings.mouseXmoveTimes, 0.0f);
    }

    private void Move()
    {
    
    
        _isCrouch = _inputsMassage.crouch;
        //在这里进行状态的判断
        PlayerStateJudge();

        //首先将移动速度赋予临时变量,考虑到有可能在其他地方使用,我们将其存储起来
        //_currentSpeed = walkSpeed;(转换为更加完善的速度控制)
        float targetSpeed = playerActionState switch
        {
    
    
            PlayerActionState.Idle => 0f,
            PlayerActionState.Walk => walkSpeed,
            PlayerActionState.Run => _runSpeed,
            _ => 0f
        };
        if (_isCrouch) targetSpeed = _crouchSpeed;

        //玩家当前水平速度的参考
        float currentHorizontalSpeed = new Vector3(_characterController.velocity.x, 0.0f, _characterController.velocity.z).magnitude;
        //偏离度,保证目标速度与目前速度相差大才可以插值,避免小幅度的抽搐
        float speedOffset = 0.1f;
        //判断偏离度
        if (currentHorizontalSpeed < targetSpeed - speedOffset ||
                currentHorizontalSpeed > targetSpeed + speedOffset)
        {
    
    
            _currentSpeed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed ,Time.deltaTime * SpeedChangeRate);
            //四舍五入到小数点后3位
            _currentSpeed = Mathf.Round(_currentSpeed * 1000f) / 1000f;
        }
        else
        {
    
    
            _currentSpeed = targetSpeed;
        }

        //判断是否进行移动输入
        if (_inputsMassage.move == Vector2.zero) _currentSpeed = 0;

        var currentInput = new Vector3(_inputsMassage.move.x, 0, _inputsMassage.move.y).normalized;

        //单位向量的方向,或者说位移方向
        if (_inputsMassage.move!=Vector2.zero)
        {
    
    
            _targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;


            #region 在位移过程中的转向
            float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                RotationSmoothTime);
            transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
            #endregion

        }

        Vector3 targetDir = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

        _characterController.Move(targetDir.normalized * _currentSpeed * Time.deltaTime);
        //TODO:这里的Move可以执行垂直方向的速度,直接加上垂直的Vector就可以

        _anim.SetFloat("Speed", _currentSpeed);
        _anim.SetBool("Crouch", _isCrouch);
    }

    /// <summary>
    /// 限制角度
    /// </summary>
    /// <param name="lfAngle"></param>
    /// <param name="lfMin"></param>
    /// <param name="lfMax"></param>
    /// <returns></returns>
    private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
    {
    
    
        if (lfAngle < -360f) lfAngle += 360f;
        if (lfAngle > 360f) lfAngle -= 360f;
        return Mathf.Clamp(lfAngle, lfMin, lfMax);
    }

    /// <summary>
    /// 对玩家状态进行判断
    /// </summary>
    private void PlayerStateJudge()
    {
    
    
        playerActionState = PlayerActionState.Idle;
        if (_inputsMassage.move!=Vector2.zero)
        {
    
    
            playerActionState = PlayerActionState.Walk;
            if (_inputsMassage.run)
                playerActionState = PlayerActionState.Run;
        }

    }

}

Guess you like

Origin blog.csdn.net/weixin_52540105/article/details/127714469