[声明] このサードパーソン コントローラーは unity star アセットのサードパーソン コントローラーを複製したもので、必要に応じてアセットを直接ダウンロードして学習することができます。個人的には車輪の再発明の振る舞いが好きではありませんが、公式のコントローラーは私が使用すると満足できないバグがいくつかあります。それでも、コントローラーは私にはかなり良さそうに見えます。シームレスなアニメーションは不可能ですが、個人的なプロジェクトでは、最初にそれを動かす方法を見つけ出す必要があります。
[バージョン] このプロジェクトはUnity [2021.3lts]バージョンに基づいています
方向修正
Move メソッドでの変位角度の制御を覚えていますか?
カメラを取得したので、カメラの角度を計算するだけでよく、方向とカメラがリンク効果を達成しました。
_targetRotation = Mathf.Atan2(currentInput.x, currentInput.z) * Mathf.Rad2Deg
+ _mainCamera.transform.eulerAngles.y;
アニメーション コントロールの一部を独自のコードに追加しました。これについては後で説明しますが、これは単なる値のコントロールです。
速度拡張
次に、ランニングのような速度を作成できます。次の 2 つの方法で作成できます:
Ⅰ. さまざまな状態の bool 値を作成します。状態がアクティブ化されると、bool 値を直接アクティブ化します。
Ⅱ. アイドル、ウォーク、ランなどの相互に排他的な状態のグループの列挙型を作成し、それに対して相互に排他的な状態のグループを作成します。
正直なところ、両者に大きな違いはないと思いますが、enum に慣れていない場合は、前者の方法で十分です。2 番目のタイプは主に、もう少しはっきりと見えるようにするためのものです。それでも同じ文章、慣れているかどうかだけで、賛否両論はありません。
public enum PlayerActionState
{
Idle,
Walk,
Run
}
public PlayerActionState playerActionState = PlayerActionState.Idle;
ここで、この状態の制御をどこに置くかを検討します。実は先ほどの入力制御部分に入れることができます. 結局, キャラクターの状態はほとんど入力値によって決定されます. そこを変更した後は, キャラクターの転倒の情報を組み合わせて判断するだけです.キャラクターの状態。もちろん、外部状態データも入力コントロールで実行したので、Move で直接表示することもできます。各 Move 実行の開始時に、まずキャラクターの現在の状態を判断します
private void PlayerStateJudge()
{
playerActionState = PlayerActionState.Idle;
if (_inputsMassage.move!=Vector2.zero)
{
playerActionState = PlayerActionState.Walk;
if (_inputsMassage.run)
playerActionState = PlayerActionState.Run;
}
}
判定終了後、状態に応じて_currentSpeedに値を代入します
_currentSpeed = playerActionState switch
{
PlayerActionState.Idle => 0f,
PlayerActionState.Walk => walkSpeed,
PlayerActionState.Run => _runSpeed,
_ => 0f
};
走るのはそれだけです。
新しいステータス
キャラクターにしゃがみ状態を追加します.もちろん、この効果にはアニメーションの変更が含まれます.
これは、比較的独立した状態であり、bool を介して制御するためです.
Move の最初に判断し (すべての状態を判断する際に依存関係の問題を考慮してください。依存関係の判断を優先する必要はありません)、次に
実行状態とスクワッティング状態の優先度を考慮します。優先度が高いほど、しゃがんだ後
、次の状態に入ると、走れないと思いますので、固定速度はしゃがむ歩行速度です。しゃがみ状態からの脱出時は行わない。
_currentSpeed = playerActionState switch
{
PlayerActionState.Idle => 0f,
PlayerActionState.Walk => walkSpeed,
PlayerActionState.Run => _runSpeed,
_ => 0f
};
if (_isCrouch)
_currentSpeed = _crouchSpeed;
速度の緩やかな変化
現在、私たちの速度は直接変化しています。友人がテストのためにアニメーションに接続すると、速度が直接変化し、アニメーション間に「ジャンプ」があることがわかります。この速度を滑らかにするために速度を徐々に変化させる必要があります
. 数値解析を勉強したことがあれば、これは補間であることがわかります. 次に、補間するときに、補間関数を作成するために 2 つのポイントを取得する必要があります。では、この2点とは?現在の速度と変更後の速度です。
最初に現在の速度を記録します。
//玩家当前水平速度的参考
float currentHorizontalSpeed = new Vector3(_characterController.velocity.x,
0.0f, _characterController.velocity.z).magnitude;、
//偏离度,保证目标速度与目前速度相差大才可以插值,避免小幅度的抽搐
float speedOffset = 0.1f;
ここでの大きさは正確な長さです (前に速度とベクトルの問題について話しましたが、ここではサイズだけが必要です).
その後、意味的な繰り返しがあるため、上記で判断した速度が targetSpeed になります。
乖離度の判定を開始し、目標値と現在値の差が大きい場合は補間変更処理をして滑らかにする必要があり、乖離度以下の場合はノーと判断します。直接の変化を見ることができるので、直接変化します。
//判断偏离度
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;
}
この時点で、モバイル制御部分は基本的に完成しています。アニメーション システムに接続した後、アニメーションが揺れる問題が発生します。この問題が発生した場合、またはこの問題の解決策がある場合は、以下にメッセージを残してください。
コード
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;
}
}
}