【Unity动作系统】急停实现思路

很早前,我在b站看到网友的Unity制作视频,看到丝滑的跑步、冲刺、急停、待机切换,很是羡慕,自己操作起来总是困难重重。

意识到自身不足后,我又回去补唐老狮的Unity小框架了,我现在的进度是这样的(供参考):唐老狮C#四部曲、唐老狮Unity入门基础核心、Unity进阶之InputSystem、唐老狮Unity基础小框架。

流程

在实现这个简单的动作系统之前,我先进行了三个工作,一是写了单例模式基类脚本,二是把相机逻辑、InputSystem配置和书写好了,三是写(抄袭)了个角色控制器基类脚本。之后才开始实现这个简单的动作系统。

动画系统思路

最先我想用有限状态机FSM+blendtree实现的,但最后还是采用以下的方式(先从比较常规的方法做起来比较好) 大体思路是,将跑步动画、冲刺动画装到1D混合树,设置bool变量HasInput、速度变量Speed、bool变量Spring。如果HasInput为false,且Speed小于0.65,从混合树过渡到跑步急停;如果HasInput为true,且Speed大于0.65,从混合树过渡到冲刺急停;至于Spring,按下的时候,会将speed从当前速度往1平滑过渡,松开时再返回到原来速度。 欢迎大佬们分享自己的思路和方法。

代码展示

首先写个单例模式,之前写过单例模式的文章,可以去参考一下。
然后写角色控制器基类,角色控制器基类需要包含以下内容:地面检测、坡度检测、重力系统、角色移动方向。这段我直接拿的第三方代码魔改的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

namespace GUIGUIGUI
{
[RequireComponent(typeof(CharacterController))]
public abstract class CharacterMovementControlBase : MonoBehaviour
{
//主要拆分一下,
//1.首先我们创建个球型碰撞范围检测,需要中心点、偏移量、半径(就是检测范围)、检测层级
//2.其次我们考虑重力,重力加速度(上正下负)、y轴速度、y轴移动方向、y轴速度最大值(防止速度太快了)、下落时间、判断是否下落的最小时间
protected CharacterController _controller;
protected Animator _animator;

//地面检测
protected bool _characterIsOnGround;//角色是否在地面
[SerializeField, Header("地面检测")] protected float _characterOffset;//检测偏移量
[SerializeField] protected float _detectionRange;//检测范围
[SerializeField] protected LayerMask _whatisGround;//检测层级

//重力
protected readonly float CharacterGravity = -9.8f;//比如角色被击飞,上正下负
protected float _characterVerticalVelocity;//用来更新角色Y轴速度
protected float _fallOutDeltaTime;
protected float _fallOutTime = 0.15f;//用来防止角色下楼梯鬼畜,这个值用于重置下落时间,小于这个值也不播放下落动画
protected readonly float _characterVerticalMaxVelocity = 54f;//角色低于这个值才应用重力
protected Vector3 _characterVerticalDirection;//角色的Y轴移动方向。我们用CC的Move函数
protected Vector3 movedirection;
//初始化组件
protected virtual void Awake()
{
_controller = GetComponent<CharacterController>();
_animator = GetComponent<Animator>();
}

//初始化数据
protected virtual void Start()
{
_fallOutDeltaTime = _fallOutTime;
}

private void Update()
{
SetCharacterGracity();
UpdateCharacterGravity();

}
//地面检测
private bool GroundDetection()
{
//检测中心点
Vector3 detectionPosition = new Vector3(transform.position.x, transform.position.y - _characterOffset, transform.position.z);
//返回检测范围
return Physics.CheckSphere(detectionPosition, _detectionRange, (int)_whatisGround, QueryTriggerInteraction.Ignore);
}

//重力
private void SetCharacterGracity()
{
_characterIsOnGround = GroundDetection();//检测是否在地面上
//如果角色在地面需要重置FallOutTime和垂直速度
if (_characterIsOnGround)
{

_fallOutDeltaTime = _fallOutTime;
if (_characterVerticalVelocity < 0f)
{
_characterVerticalVelocity = -2f;//_characterVerticalVelocity是个累积的东西,我们需要重置一下,就重置成-2吧
}
}
//角色在空中,需要判断是走楼梯还是下落
else
{
if (_fallOutDeltaTime > 0f)
{
_fallOutDeltaTime -= Time.deltaTime;//0.15秒是帮助角色从较低高度下落,不想播放下跌动画,可以自定义时间的
}

else
{
//此刻我们处于空中状态,_fallOutDeltaTime的0.15秒被挥霍完变成0了,在空中0.15秒无法解决下落
//说明角色还没有落地,应该不是下楼梯,有必要播下落动画
}

if (_characterVerticalVelocity < _characterVerticalMaxVelocity)
{
_characterVerticalVelocity += CharacterGravity * Time.deltaTime;
}
}
}

protected virtual void OnAnimatorMove()
{
_animator.ApplyBuiltinRootMotion();
UpdateCharacterMoveDirection(_animator.deltaPosition);

}

//重力移动模拟
private void UpdateCharacterGravity()
{
_characterVerticalDirection.Set(0, _characterVerticalVelocity, 0);//我天还能用Set方法
_controller.Move(_characterVerticalDirection * Time.deltaTime);
}

//坡道检测,返回移动方向
private Vector3 StopResetDirection(Vector3 moveDirection)
{
if (Physics.Raycast(transform.position + (transform.up * 5f), Vector3.down, out RaycastHit hit, _controller.height * .85f,
(int)_whatisGround, QueryTriggerInteraction.Ignore))
{
if (Vector3.Dot(Vector3.up, hit.normal) != 0)//说明在坡上,hit,normal是发现向量,其实我觉得这步检查没必要
{
return moveDirection = Vector3.ProjectOnPlane(moveDirection, hit.normal);
}
}
return moveDirection;
}

protected void UpdateCharacterMoveDirection(Vector3 Direction)
{
movedirection = StopResetDirection(Direction);
_controller.Move(movedirection * Time.deltaTime);
}
//这步没必要,只是方便我们检测。
private void OnDrawGizmos()
{
Vector3 detectionPosition = new Vector3(transform.position.x, transform.position.y - _characterOffset, transform.position.z);
Gizmos.DrawWireSphere(detectionPosition, _detectionRange);
}

}
}

之后配置InputAction,把InputAction封装成一个单例,做我们的输入系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using Unity.VisualScripting;
using UnityEngine;

public class GameInputMgr : SingleMono<GameInputMgr>
{
private GameInoutAction action;
public Vector2 Movement => action.GameInput.Movement.ReadValue<Vector2>();
public Vector2 CameraLook => action.GameInput.CameraLook.ReadValue<Vector2>();
private bool isSpringPressed = false;
public bool Spring
{
get
{
if (action.GameInput.Spring.triggered)
{
isSpringPressed = true;
return true;
}
if (isSpringPressed && !action.GameInput.Spring.IsPressed()) // 检查是否松开
{
isSpringPressed = false;
return false;
}
return isSpringPressed; // 返回当前的按下状态
}
}
private void Awake()
{
action = new GameInoutAction();
}
private void OnEnable()
{
action.Enable();
}
private void OnDisable()
{
action.Disable();
}
}

然后写相机系统。我没有做相机碰撞检测(因为暂时用不到)

一个合格的相机,我们如何去设计呢?可以将相机分为位置更新和旋转更新,旋转更新通过鼠标移动、来改变相机xy旋转量。我们玩游戏时鼠标向上、相机反而往下看,这点需要注意。位置更新,设定好相机看向的物体、通过鼠标滚轮缩放改变距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using UnityEngine;

public class CameraController : MonoBehaviour
{
[SerializeField, Header("相机旋转速度")] private float rotateSpeed;
[SerializeField, Header("相机平滑速度")] private float transformSpeed;
[SerializeField, Header("相机限制距离")] private Vector2 cameraMaxDistace;
[SerializeField, Header("相机上下角")] private Vector2 MaxAngle;
[SerializeField, Header("相机平滑旋转时间")] private float smoothTime;
[SerializeField, Header("看向物体")] private Transform LookObject;
//相机输入
private Vector2 _input;
private Vector3 currentVelocity = Vector3.zero;
private Vector3 rotatePosition;
private float cameraDistace;

private void Update()
{
CameraInput();
SetDistance();
}
private void LateUpdate()
{
UpdateRotate();
UpdatePosition();
}
private void CameraInput()
{
_input.x -= GameInputMgr.GetInstance().CameraLook.y * rotateSpeed;
_input.y += GameInputMgr.GetInstance().CameraLook.x * rotateSpeed;
_input.x = Mathf.Clamp(_input.x,MaxAngle.x,MaxAngle.y);
}

private void UpdateRotate()
{
rotatePosition = Vector3.SmoothDamp(rotatePosition, new Vector3(_input.x, _input.y, 0f), ref currentVelocity,smoothTime);
this.transform.eulerAngles = rotatePosition;
}

private void SetDistance()
{
cameraDistace -= Input.GetAxis("Mouse ScrollWheel");
cameraDistace = Mathf.Clamp(cameraDistace, cameraMaxDistace.x, cameraMaxDistace.y);
}

private void UpdatePosition()
{
Vector3 position = (LookObject.position + (-transform.forward * cameraDistace));
this.transform.position = Vector3.Lerp(this.transform.position , position, Time.deltaTime * transformSpeed);
}
}


最后到我们写角色控制器的时候,按照思路如下写: 图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using UnityEngine;

public class CameraController : MonoBehaviour
{
[SerializeField, Header("相机旋转速度")] private float rotateSpeed;
[SerializeField, Header("相机平滑速度")] private float transformSpeed;
[SerializeField, Header("相机限制距离")] private Vector2 cameraMaxDistace;
[SerializeField, Header("相机上下角")] private Vector2 MaxAngle;
[SerializeField, Header("相机平滑旋转时间")] private float smoothTime;
[SerializeField, Header("看向物体")] private Transform LookObject;
//相机输入
private Vector2 _input;
private Vector3 currentVelocity = Vector3.zero;
private Vector3 rotatePosition;
private float cameraDistace;

private void Update()
{
CameraInput();
SetDistance();
}
private void LateUpdate()
{
UpdateRotate();
UpdatePosition();
}
private void CameraInput()
{
_input.x -= GameInputMgr.GetInstance().CameraLook.y * rotateSpeed;
_input.y += GameInputMgr.GetInstance().CameraLook.x * rotateSpeed;
_input.x = Mathf.Clamp(_input.x,MaxAngle.x,MaxAngle.y);
}

private void UpdateRotate()
{
rotatePosition = Vector3.SmoothDamp(rotatePosition, new Vector3(_input.x, _input.y, 0f), ref currentVelocity,smoothTime);
this.transform.eulerAngles = rotatePosition;
}

private void SetDistance()
{
cameraDistace -= Input.GetAxis("Mouse ScrollWheel");
cameraDistace = Mathf.Clamp(cameraDistace, cameraMaxDistace.x, cameraMaxDistace.y);
}

private void UpdatePosition()
{
Vector3 position = (LookObject.position + (-transform.forward * cameraDistace));
this.transform.position = Vector3.Lerp(this.transform.position , position, Time.deltaTime * transformSpeed);
}
}