-
兼容任意摄像机角度,手感平滑,良好的物理特性,摇杆跟随
一、摇杆的UI搭建
Panel里主要是一个摇杆的背景(圆环)和摇杆头
二、摇杆在背景的圆圈范围内移动
2.1. 先实现摇杆跟随手指移动
摇杆是使用UGUI实现的,所以这里的实现思路主要是将手指按下时候的屏幕坐标转换为对应尺寸的UGUI的画布坐标就行了。
因为屏幕坐标的原点在左下角,先将其转换为以屏幕中心为原点的坐标,然后根据当前的画布尺寸进行缩放即可得到适配所有分辨率的UI坐标。
public static Vector2 ScreenToAnchorPos(Vector3 screenPos, Vector2 canvasSize)
{
Vector2 screenPos2;
screenPos2.x = screenPos.x - (Screen.width / 2f);
screenPos2.y = screenPos.y - (Screen.height / 2f);
Vector2 anchorPos;
anchorPos.x = (screenPos2.x / Screen.width) * canvasSize.x;
anchorPos.y = (screenPos2.y / Screen.height) * canvasSize.y;
return anchorPos;
}
然后在每一帧将手指按下的位置转换为UGUI的坐标,再赋值给摇杆
if (_down)
_stickRect.anchoredPosition = PufferToolkit.ScreenToAnchorPos(Input.mousePosition,CanvasSize);
2.2. 将摇杆限制在圆环内
首先定义一个圆环的半径,计算现在摇杆的位置到圆环中心的距离,如果大于半径就将它重置到刚好到半径的位置,将摇杆的位置看作一个向量,刚好到圆环边缘的位置就可以通过把向量归一化之后乘以半径得到。
-
代码如下
if (dis > Radius)
{
_stickRect.anchoredPosition = _stickRect.anchoredPosition.normalized * Radius;
}
2.3.将摇杆的输入的方向输出
使用委托的方式实现,当其他脚本注册了这里的委托就会收到摇杆的输入事件
-
定义一个委托,在Update中调用,手指按下时输出方向,抬起时输出0
private Action<Vector2> _onJoyStickInput;
if (_down)
{
_stickRect.anchoredPosition = PufferToolkit.ScreenToAnchorPos(Input.mousePosition,CanvasSize);
Vector2 dir = _stickRect.anchoredPosition - Vector2.zero;
float dis = dir.magnitude;
if (dis > Radius)
{
_stickRect.anchoredPosition = _stickRect.anchoredPosition.normalized * Radius;
}
_onJoyStickInput?.Invoke(dir);
}
else
{
_onJoyStickInput?.Invoke(Vector2.zero);
}
三. 角色的移动
3.1. 角色的设置
使用刚体加胶囊碰撞体来模拟角色的物理效果
人物移动这里我使用了一个由Speed参数控制的动画混合树,将Idle动画和奔跑的动画融合起来,Speed为1的时候人物全力奔跑,为0的时候人物不动,这样做可以将人物移动的速度,播放的动画以及摇杆拖拽的距离都关联起来获得更好的移动效果。
3.2. 角色的移动
首先让角色获取到摇杆的引用,然后注册委托。
由于角色运动时可能需要与其他物体发生物理碰撞等,所以、不在OnJoyStickInput方法中写移动逻辑而仅仅获取一个方向和改变朝向,之后根据需要在Update或者FixedUpdate中写移动逻辑。
JoyStickPanel.GetJoyStickPanel().OnJoyStickInput(OnJoyStickInput);
private void OnJoyStickInput(Vector2 inputDir)
{
_moveDir =new Vector3(inputDir.x, 0, inputDir.y);
if (_moveDir.magnitude > 0.01f) //如果有方向输入,人物就转向那个方向
transform.rotation =Quaternion.Lerp(transform.rotation,Quaternion.LookRotation(_moveDir),Time.deltaTime*10);
}
FixedUpdate中写得到更好的物理交互效果。因为tranfrom.Translate()方法需要传入的方向需要是相对于该物体坐标轴的子向量,如果角色的坐标轴与世界坐标轴重合那么是没有问题的,但是我们移动的时候会让角色朝向不同的方向因此就需要将_moveDir转换成相对于角色当前坐标轴朝向的向量才能得到正确的效果
private void FixedUpdate()
{
transform.Translate( transform.InverseTransformDirection(_moveDir)*Time.deltaTime*MoveSpeed);
}
这段代码完成之后就可以在通过摇杆来控制角色的移动了,但是有一个限制,因为没有考虑到摄像机朝向的问题,当摄像机的朝向和世界坐标系保持一致也就是摄像机的角度y为0时,这段代码可以得到很好的效果,比如从屏幕上右滑获的方向就是世界坐标系中右边的方向都是(1,0,0)。
但如果摄像机角度的y不为0了,屏幕上右滑获取的(1,0,0)按照刚才那样就拿不到摄像机画面中右边的世界方向了,则人物移动就会出现与我们预期不符的状况,所以需要将输入的向量根据摄像机的角度做一定的旋转来适配不同的摄像机角度。
这里用一个四元数乘向量来将其旋转
private void OnJoyStickInput(Vector2 inputDir)
{
_moveDir= Quaternion.Euler(0,Camera.main.transform.localEulerAngles.y, 0) * new Vector3(inputDir.x, 0, inputDir.y);
}
这之后不管摄像机是什么角度,我们都能得到正确的移动效果
3.3 移动的优化
3.3.1 增加动画
这里选择在获取到输入的时候就改变动画的状态,然后在移动代码中判断动画的速度,确保跑的动画播放了人物再移动
private void OnJoyStickInput(Vector2 inputDir)
{
_moveDir= Quaternion.Euler(0,Camera.main.transform.localEulerAngles.y, 0) * new Vector3(inputDir.x, 0, inputDir.y);
}
//控制动画状态机混合树的值,0是Idle,1是全力奔跑
_animSpeed = _moveDir.magnitude / 95f;//摇杆的半径为95
_animator.SetFloat(_animSpeedHash, _animSpeed);
if (_moveDir.magnitude > 0.01f) //如果有方向输入,人物就转向那个方向
transform.rotation =Quaternion.Lerp(transform.rotation,Quaternion.LookRotation(_moveDir),Time.deltaTime*10);
}
private void FixedUpdate()
{
if (_animSpeed > 0.05f) //先判断动画是否切换到跑步状态,确保人物没迈腿的时候不移动
{
transform.Translate( transform.InverseTransformDirection(_moveDir)*Time.deltaTime*MoveSpeed);
}
}
3.3.2 移动更加丝滑
由于之前写的摇杆逻辑,抬手之后输出的方向马上就变为了(0,0,0),这会导致人物突然停下,为了得到更加丝滑的效果,这里的_moveDir改用插值的方式获取,
private void OnJoyStickInput(Vector2 inputDir)
{
//将输入方向转换为XZ平面移动的方向, 需要根據摄像机的朝向旋转方向
var targetDir = Quaternion.Euler(0,Camera.main.transform.localEulerAngles.y, 0) * new Vector3(inputDir.x, 0, inputDir.y);
//用插值是为了不让抬手的时候人物马上停下,有一个减速的过程
_moveDir =Vector3.Lerp(_moveDir,targetDir,10*Time.deltaTime);
//控制动画状态机混合树的值,0是Idle,1是全力奔跑
_animSpeed = _moveDir.magnitude / 95f;//摇杆的半径为95
_animator.SetFloat(_animSpeedHash, _animSpeed);
if (_moveDir.magnitude > 0.01f) //如果有方向输入,人物就转向那个方向
transform.rotation =Quaternion.Lerp(transform.rotation,Quaternion.LookRotation(_moveDir),Time.deltaTime*10);
if (_moveDir.magnitude > 95f)//限制移动速度
_moveDir = _moveDir.normalized * 95f;
}
四、摇杆的跟随功能
当滑动超出了摇杆的半径之后,整个摇杆摇杆会向那边偏移,思路是clong一个摇杆头出来不受到半径的约束,然后每一帧整个摇杆往那边插值,参考下面的代码
public void Awake()
{
_joyStickBackRect = transform.Find("JoyStickBack").GetComponent<RectTransform>();
_stickRect = _joyStickBackRect.transform.Find("Stick").GetComponent<RectTransform>();
_stickRectClone = Instantiate(_stickRect, transform);
_stickRectClone.GetComponent<Image>().color = new Color(1, 1, 1, 0);
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
_down = true;
_downPos = Input.mousePosition;
_joyStickBackRect.anchoredPosition = PufferToolkit.ScreenToAnchorPos(_downPos,CanvasSize);
}
if (Input.GetMouseButtonUp(0))
{
_down = false;
}
if (_down)
{
_joyStickBackRect.anchoredPosition = PufferToolkit.ScreenToAnchorPos(_downPos,CanvasSize);
_stickRectClone.anchoredPosition = PufferToolkit.ScreenToAnchorPos(Input.mousePosition,CanvasSize);
_stickRect.transform.position = _stickRectClone.transform.position;
Vector2 dir = _stickRect.anchoredPosition - Vector2.zero;
float dis = dir.magnitude;
if (dis > Radius)
{
_stickRect.anchoredPosition = _stickRect.anchoredPosition.normalized * Radius;
_downPos = Vector3.Lerp(_downPos, Input.mousePosition, Time.deltaTime * FollowSpeed);
}
_onJoyStickInput?.Invoke(dir);
}
else
{
_onJoyStickInput?.Invoke(Vector2.zero);
}
}