前言
在2d场景中,人物移动是一个基础的功能,当遇到有障碍物时需要进行位置偏移修复。否则就只是单纯的上下左右移动,这样效果看起来会很差。
实现的方法有很多,这里简单介绍一下我的实现方法。如果有更好的方法请推荐给我~
这个实现方式并没有考虑太多的性能问题,只是一个简单的实现功能的算法。基于这个算法上可以进行自己的优化和扩展。
需求
角色遇到障碍物时不可穿越,根据移动方向进行偏移修正。
效果
实现
实现分为几步:1.准备工作 2.输入检测 3.碰撞检测和移动修正
场景中所有的碰撞检测全部基于矩形进行运算。
项目使用UGUI进行开发。
准备
场景碰撞体信息
移动中需要得到场景中所有需要检测的碰撞体的矩形范围。
数据可以写到一个文件中然后进行读取。如果数据量不大的话,直接写入脚本进行读取也可以。我使用的是直接写入脚本。
场景估计会经常改动,所以我是写了一个编辑器去读取场景所有碰撞体信息。
核心:
private List<Rect> ResetData()
{
List<Rect> rects = new List<Rect>();
for (int index = 0; index < m_BoxRoot.childCount; index++)
{
RectTransform childRectTransform = m_BoxRoot.GetChild(index) as RectTransform;
BoxCollider box = childRectTransform.GetComponent<BoxCollider>();
if (box != null)
{
box.center = Vector3.zero;
box.size = childRectTransform.sizeDelta;
Rect rect = childRectTransform.rect;
rect.x += childRectTransform.position.x;
rect.y += childRectTransform.position.y;
rects.Add(rect);
}
}
return rects;
}
得到矩形的RectTransform,可以得到矩形盒。重点是要加上位置,这样才是真正的世界坐标。
Unity的Rect的Y轴和场景的Y轴是不一致的,这一点很重要。获取YMin和YMax进行运算时这一点心中要搞清楚是否和想要的相同。
Rct的轴向:
Rect的官方文档:https://docs.unity3d.com/ScriptReference/Rect.html
摇杆
摇杆的话我是使用EasyTouch。网上有很多信息,下载一个安装到自己项目就可以使用了。不使用的话,自己写一下也可以。
我使用的EasyTouch的版本是5.0.0.
接入后场景有两个对象。对象一被我改了名字,其余的我都没有改,直接创建摇杆即可。
ETCJoystick etcJoystick = ETCInput.GetControlJoystick("Joystick");//通过名字“Joystick”找到摇杆对象。
etcJoystick.onMove.AddListener(JoystickMove);//JoystickMove是我的回调函数
输入
大体有两种:
1.在Update函数中监听Input.GetKeyDown和Input.GetKeyUp,然后监听自己的键盘输入就可以了。
void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
m_InputDir = Vector2.left;
}
if (Input.GetKeyDown(KeyCode.D))
{
m_InputDir = Vector2.right;
}
if (Input.GetKeyDown(KeyCode.W))
{
m_InputDir = Vector2.up;
}
if (Input.GetKeyDown(KeyCode.S))
{
m_InputDir = Vector2.down;
}
if (Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.D) || Input.GetKeyUp(KeyCode.W) ||
Input.GetKeyUp(KeyCode.S))
{
m_InputDir = Vector2.zero;
}
if (m_InputDir != Vector2.zero)
{
Move(m_Speed * m_InputDir);
//Move是移动函数,m_Speed是速度大小。
}
}
2.使用摇杆。
我是使用EasyTouch的。
private void JoystickMove(Vector2 vec)
{
if (vec != Vector2.zero)
{
Move(m_Speed * vec);//解释同上
}
}
碰撞检测
这里就是移动的重点了。
核心思路:碰撞后,判断哪个轴是需要反弹出来的。最终的位移是输入值+偏移值。
设计思路:
using System.Collections.Generic;
using UnityEngine;
public class MoveController : MonoBehaviour
{
private RectTransform m_RectTransform;
private List<Rect> m_BoxRects;
private Rect m_PlayerRect;
public float m_Speed;
private Rect m_OveRect;
private Rect[] m_SideRects;
private class Line
{
public Vector2 m_Start;
public Vector2 m_End;
public Line(Vector2 start, float width,float height)
{
m_Start = start;
m_End = m_Start + new Vector2(width, height);
}
public Line(Vector2 start, Vector2 end)
{
m_Start = start;
m_End = end;
}
}
private Line m_DetectLine;
private Line[] m_SideLines;
void Start()
{
m_RectTransform = this.transform as RectTransform;
m_BoxRects = GameConfig.LoadSceneBoxData();//在准备工作中就记录好场景碰撞盒的信息
//摇杆
ETCJoystick etcJoystick = ETCInput.GetControlJoystick("Joystick");
etcJoystick.onMove.AddListener(JoystickMove);
}
private void JoystickMove(Vector2 vec)
{
if (vec != Vector2.zero)
{
Move(m_Speed * vec);
}
}
private void Move(Vector2 dir)
{
Vector2 absDir = new Vector2(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
//使用归一化的方向值
Vector2 speed = dir.normalized;
speed.x = absDir.x < 1 ? dir.x : speed.x;
speed.y = absDir.y < 1 ? dir.y : speed.y;
Vector2 absSpeed = new Vector2(Mathf.Abs(speed.x), Mathf.Abs(speed.y));
Vector2 curMove = Vector2.zero;
while (curMove.x <= absDir.x && curMove.y <= absDir.y)
{
Go(speed);
curMove += absSpeed;
}
}
private void Go(Vector2 dir)
{
m_PlayerRect = m_RectTransform.rect;
//偏移值
m_PlayerRect.position += dir;
//世界坐标
m_PlayerRect.x += m_RectTransform.position.x;
m_PlayerRect.y += m_RectTransform.position.y;
Vector2 newDir = Vector2.zero;
for (var index = 0; index < m_BoxRects.Count; index++)
{
newDir = CheckBoxLine(m_BoxRects[index], dir);
if (newDir != Vector2.zero)
{
break;
}
}
m_RectTransform.position = m_RectTransform.position + new Vector3(dir.x + newDir.x, dir.y + newDir.y);
//修正位置直到没有发生碰撞
if (newDir != Vector2.zero)
{
Go(dir + newDir);
}
}
private Vector2 CheckBoxLine(Rect boxRect, Vector2 dir)
{
Vector2 newDir = Vector2.zero;
if (!boxRect.Overlaps(m_PlayerRect))
{
return newDir;
}
GetOverRectLine(boxRect);
float maxValue = m_OveRect.width > m_OveRect.height ? m_OveRect.width : m_OveRect.height;
//检测线段
Line[] dirLines = new Line[]
{
new Line(m_OveRect.center-dir*(maxValue/2f), dir.x * maxValue, dir.y * maxValue)
};
bool over = false;
for (int dirIndex = 0; dirIndex < dirLines.Length; dirIndex++)
{
if (over)
{
break;
}
for (int index = 0; index < m_SideLines.Length; index++)
{
if (IsRectCross(m_SideLines[index], dirLines[dirIndex]))
{
//得到相交矩形的非相交轴的长度
if (index < 2)
{
newDir.y = (dir.y > 0 ? -1 : 1) * (m_OveRect.height + 0.1f);
}
else
{
newDir.x = (dir.x > 0 ? -1 : 1) * (m_OveRect.width + 0.1f);
}
over = true;
break;
}
}
}
return newDir;
}
private void GetOverRectLine(Rect boxRect)
{
Vector2 minPos = m_PlayerRect.min;
Vector2 maxPos = m_PlayerRect.max;
if (boxRect.x > minPos.x)
{
minPos.x = boxRect.x;
}
if (boxRect.xMax < maxPos.x)
{
maxPos.x = boxRect.xMax;
}
if (boxRect.y > minPos.y)
{
minPos.y = boxRect.y;
}
if (boxRect.yMax < maxPos.y)
{
maxPos.y = boxRect.yMax;
}
//得到相交矩形
m_OveRect = new Rect(minPos, maxPos - minPos);
if (m_OveRect.width < 0.02f)
{
m_OveRect.width = 0.02f;
}
if (m_OveRect.height < 0.02f)
{
m_OveRect.height = 0.02f;
}
//场景碰撞盒的四条边
m_SideLines = new Line[]
{
new Line(boxRect.min, boxRect.width,0),//top
new Line(new Vector2(boxRect.xMin, boxRect.yMax), boxRect.width, 0),//bottom
new Line(boxRect.min,0, boxRect.height),
new Line(new Vector2(boxRect.xMax, boxRect.yMin),0, boxRect.height),
};
}
//计算线段是否相交
private bool IsRectCross(Line p1, Line p2)
{
bool IsCross = !(Mathf.Max(p1.m_Start.x, p1.m_End.x) < Mathf.Min(p2.m_Start.x, p2.m_End.x) || Mathf.Max(p1.m_Start.y, p1.m_End.y) < Mathf.Min(p2.m_Start.y, p2.m_End.y) ||
Mathf.Max(p2.m_Start.x, p2.m_End.x) < Mathf.Min(p1.m_Start.x, p1.m_End.x) || Mathf.Max(p2.m_Start.y, p2.m_End.y) < Mathf.Min(p1.m_Start.y, p1.m_Start.y));
if (IsCross)
{
if ((((p1.m_Start.x - p2.m_Start.x) * (p2.m_End.y - p2.m_Start.y) - (p1.m_Start.y - p2.m_Start.y) * (p2.m_End.x - p2.m_Start.x)) *
((p1.m_End.x - p2.m_Start.x) * (p2.m_End.y - p2.m_Start.y) - (p1.m_End.y - p2.m_Start.y) * (p2.m_End.x - p2.m_Start.x))) > 0 ||
(((p2.m_Start.x - p1.m_Start.x) * (p1.m_End.y - p1.m_Start.y) - (p2.m_Start.y - p1.m_Start.y) * (p1.m_End.x - p1.m_Start.x)) *
((p2.m_End.x - p1.m_Start.x) * (p1.m_End.y - p1.m_Start.y) - (p2.m_End.y - p1.m_Start.y) * (p1.m_End.x - p1.m_Start.x))) > 0)
{
IsCross = false;
}
}
return IsCross;
}
}
技术点:
- 线段相交的判断原理是线段与另一线段的两点必然是有较小的一方,否则没有交互。具体可以搜索关键字“线段相交 算法”
参考网站: https://segmentfault.com/a/1190000004070478 - 相交矩形的计算是根据轴向得到最大最小点,结合起来就是相交的矩形。
- 修正轴向是根据移动方向和场景矩形的轴边是否相交,相交轴意味着是非相交轴的的方向导致相交的。所以是要减去非相交轴的相交矩形的长度。
- 计算是否碰撞时使用归一化进行循环计算的原因是当速度很大可能会直接穿过场景对象,导致计算时没有发生碰撞。所以使用较小的速度保证不会穿模。