Unity 使用NavMesh实现简易的摇杆功能
引言
在日常的unity项目开发中,经常会遇到角色移动的问题,在这里我们作一个建议的摇杆功能来控制角色的移动。要求如下:
- 摇杆在没有UI遮挡的任何位置点击,出现摇杆,并以此作为原点拖动。
- 人物朝着摇杆方向移动。
- 场景有边界,有障碍物。
实现方案
场景
场景使用navmesh进行烘培,关于场景的烘培方法,网上有很多,这里就不过多赘述了。障碍物挂载NavMeshObstacle,可调节大小。
摇杆
为了方便以后使用,我们需要在摇杆控制脚本中开放几个事件:开始移动事件,移动中事件,移动结束事件。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
/// <summary>
/// 虚拟摇杆
/// </summary>
public class JoyStick : MonoBehaviour
{
[SerializeField]
/// <summary>
/// 摇杆中心点
/// 需要在UI界面指定
/// </summary>
private Transform _PotTran;
#region Unity Events
/// <summary>
/// 移动开始事件
/// </summary>
[System.Serializable]
public class OnMoveStartHandler : UnityEvent<Vector2> { }
/// <summary>
/// 移动中事件
/// </summary>
[System.Serializable]
public class OnMoveHandler : UnityEvent<Vector2> { }
/// <summary>
/// 移动完成事件
/// </summary>
[System.Serializable]
public class OnMoveEndHandler : UnityEvent { }
[SerializeField]
public OnMoveStartHandler OnMoveStart;
[SerializeField]
public OnMoveHandler OnMove;
[SerializeField]
public OnMoveEndHandler OnMoveEnd;
#endregion
/// <summary>
/// 摇杆中心点活动区域
/// </summary>
private Rect _Rect;
/// <summary>
/// 点击原点位置
/// </summary>
private Vector2 _OriginVec;
/// <summary>
/// 是否在拖拽
/// </summary>
private bool _Dragging = false;
void Start()
{
_Rect = transform.GetComponent<RectTransform>().rect;
}
void Update()
{
if (IsPointerOverUI())
{
StopDrag();
return;
}
if (Input.GetMouseButtonDown(0))
{
StartDrag(Input.mousePosition);
}
if (Input.GetMouseButtonUp(0))
{
StopDrag();
}
if (Input.GetMouseButton(0))
{
Drag(Input.mousePosition);
}
}
/// <summary>
/// 中心点返回原处
/// </summary>
private void BackOrigin()
{
_PotTran.localPosition = Vector3.zero;
}
/// <summary>
/// 设置中心点位置
/// </summary>
private void SetOriginPos(Vector3 pos)
{
_PotTran.localPosition = pos;
}
/// <summary>
/// 开始拖动
/// </summary>
/// <param name="pos"></param>
private void StartDrag(Vector2 pos) {
if (!_Dragging)
{
_Dragging = true;
_OriginVec = pos;
OnMoveStart?.Invoke(pos);
}
}
/// <summary>
/// 拖动中
/// </summary>
/// <param name="pos"></param>
private void Drag(Vector2 pos) {
if (!_Dragging) {
return;
}
float oldDis = Vector3.Distance(pos, _OriginVec);
//UI位移要大于一定数值,根据自己要求修改
if (oldDis > 0.1f)
{
float newDis = oldDis;
if (newDis > _Rect.width * 0.5f)
{
newDis = _Rect.width * 0.5f;
}
Vector2 gap = (pos - _OriginVec) / oldDis * newDis;
SetOriginPos(gap);
OnMove?.Invoke((pos - _OriginVec).normalized * Time.deltaTime * 3);
}
}
/// <summary>
/// 停止拖动
/// </summary>
private void StopDrag() {
if (_Dragging)
{
_Dragging = false;
BackOrigin();
OnMoveEnd?.Invoke();
}
}
/// <summary>
/// 判断鼠标点击是否有UI在鼠标下面
/// </summary>
/// <returns></returns>
public bool IsPointerOverUI()
{
PointerEventData eventData = new PointerEventData(EventSystem.current);
eventData.pressPosition = Input.mousePosition;
eventData.position = Input.mousePosition;
List<RaycastResult> list = new List<RaycastResult>();
if (EventSystem.current)
EventSystem.current.RaycastAll(eventData, list);
return list.Count > 0;
}
}
角色控制
这里我们只实现了角色的移动和暂停。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// 角色
/// </summary>
public class CharactorAgent : MonoBehaviour
{
/// <summary>
/// agent
/// </summary>
private NavMeshAgent _Agent;
/// 是否正在移动
/// </summary>
private bool _IsMoving = false;
void Start()
{
_Agent = GetComponent<NavMeshAgent>();
if (!_Agent)
{
_Agent = gameObject.AddComponent<NavMeshAgent>();
}
_Agent.enabled = true;
}
void Update()
{
}
/// <summary>
/// 人物移动
/// </summary>
/// <param name="offset">相对位移</param>
public void Move(Vector3 offset)
{
if (!_Agent.enabled)
{
_Agent.enabled = true;
}
//转向,可根据自己需求添加转向动画等
transform.forward = offset;
ChangeMove(true);
_Agent.Move(offset);
}
/// <summary>
/// 更改移动状态
/// 可在此处更改玩家动作
/// </summary>
/// <param name="isMove"></param>
public void ChangeMove(bool isMove)
{
if (!_IsMoving && isMove)
{
_IsMoving = true;
}
if (_IsMoving && !isMove)
{
_IsMoving = false;
}
}
/// <summary>
/// 停止移动
/// </summary>
public void StopMove()
{
if (_Agent.enabled)
{
_Agent.ResetPath();
_Agent.isStopped = true;
_Agent.enabled = false;
}
ChangeMove(false);
}
}
摄像机的跟随功能没有做,unity本身就有许多摄像机跟随脚本可以使用。
主逻辑
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 主逻辑
/// </summary>
public class TestService : MonoBehaviour
{
[SerializeField]
private JoyStick _JoyStick;
[SerializeField]
private CharactorAgent _CharactorAgent;
void Start()
{
Canvas canvas = _JoyStick.GetComponentInParent<Canvas>();
RectTransform parentRect = _JoyStick.transform.parent.GetComponent<RectTransform>();
Vector3 oldPos = _JoyStick.transform.localPosition;
_JoyStick.OnMoveStart.AddListener(delegate (Vector2 vec)
{
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, vec, canvas.worldCamera, out Vector2 localPos))
{
_JoyStick.transform.localPosition = localPos;
}
});
_JoyStick.OnMove.AddListener(delegate (Vector2 vec)
{
_CharactorAgent.Move(new Vector3(vec.x, 0, vec.y));
});
_JoyStick.OnMoveEnd.AddListener(delegate ()
{
_CharactorAgent.StopMove();
_JoyStick.transform.localPosition = oldPos;
});
}
}