背景
前两周工作教忙,学习Unity的进度放缓了些,年前稍微有些时间,继续前篇的研究,前篇基本已经实现了A*算法中最重要的一步就是获得了A*路径坐标点数组以及接合OnDrawGizmos实现了可视化的调试,具体参考以下链接
Unity 2d A星(Astar)寻路实现
Unity3D 绘制矩形(DrawRect)及可视化调试
虽然丑了点(也可以看出Unity在方面还有很大空间的),但是表达的比较清楚了
既然原型已经有了,本篇的主要任务是将A*寻路加入的工程中,实现2D角色的A*寻路移动。
目标明确挡在我们面前的还有一个技术的坎,就是A*寻路是一基于网格的,也就是按照上图所示,红色的路径是一系列坐标点,如何让角色按照坐标点集逐个移动呢?呵呵,其实我在前面已经储备好了,可以参考以下两篇
时光煮雨 Unity3d 序列目标点的移动①
时光煮雨 Unity3d 序列目标点的移动② 总结篇
实际有三种选择各种A、tween控件;B、基于时间线动画;C、基于Vector3.MoveTowards。
这里按照我自己一贯的风格选择了C方法,无论是在移动和终止判断上都很简单,不需要太多的临时变量做控制。具体实现后的效果如下
其中绿色框是障碍物,红色是A*寻路路径也是角色移动的路径,黄色线是起点到终点直线移动的路径。
实现
如何实现又不是那么轻松愉快了,首先按照人类的思维,我们应该有这样的逻辑
1、在地图上点击鼠标左键,获得角色移动的起点和终点;
2、根据起点和终点(当然也包含障碍物)使用A*算法计算得到移动路径;
3、逐帧进行角色的路径序列点移动;
4、启动移动后,启动角色自身移动动画(跑动动画),角色到达终点后终止动画。
看到这个需求,估计你会恨“蛋疼”,为什么呢?主要的问题就是实际上如果用Unity来实现,鼠标事件、平移和移动动画都是在Update中实现的,这样就要求有很清晰的思路,在一个函数入口中实现3个功能,而且必然需要大量的控制变量进行控制。实际上我个人觉得这是违反“单一原则”的。
虽然读者可能对于设计模式什么的嗤之以鼻,但以下是我借助UniRx,对于Update进行了解耦,也算提供一种思路吧,希望你能喜欢。下面是我的具体实现
第一步,注册鼠标事件
void Awake()
{
//定义障碍物
ResetMatrix();
//鼠标点击控制
Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0)).Subscribe(LeftMouseClick2);
}
第二步,实现鼠标事件回调函数
private void LeftMouseClick2(long l)
{
//角色 帧动画
PlayerAnimation();
playerTargetPositon = Camera.main.ScreenToWorldPoint(Input.mousePosition);
playerTargetPositon.z = 0;
playerStartPositon = transform.position;
playerStartPositon.z = 0;
Direction = Super.GetDirectionByTan(playerTargetPositon, playerStartPositon);
//假如点击的地点不是障碍物
int x1 = ((int)(playerTargetPositon.x * 100)) / ((int)(GridSize * 100));
int y1 = ((int)(playerTargetPositon.y * 100)) / ((int)(GridSize * 100));
if (Matrix[x1, y1] != 0)
{
DebugDrawLines(transform.position, playerTargetPositon);
IsAStarMoving = false; //非寻路模式
//MixMoveTo(playerTargetPositon); //改进型A*算法移动
//LineMoveTo(playerTargetPositon); //两点间建立直线移动
AStarMoveTo(playerTargetPositon); //A*算法移动
}
}
A、获得的起点和终点;
playerTargetPositon = Camera.main.ScreenToWorldPoint(Input.mousePosition);
playerTargetPositon.z = 0;
playerStartPositon = transform.position;
playerStartPositon.z = 0;
B、播放自身移动动画
//角色 帧动画
PlayerAnimation();
C、启动A*移动
AStarMoveTo(playerTargetPositon); //A*算法移动
第三步,A*算法的移动
/// <summary>
/// A*算法移动
/// </summary>
/// <param name="\"targetPoint\"">
private void AStarMoveTo(Vector3 targetPoint)
{
List<vector3> astarTargets = new List<vector3>();
int index = 0;
//targetPoint.z = 0;
//如果不是A*寻路,且下一格子是障碍,则启动A*移动
if (!IsAStarMoving) //false
{
IsAStarMoving = true;
//A*路径计算
astarTargets = AStarCalc(targetPoint);
DebugAStarRunRectInit(astarTargets);
}
Observable.EveryUpdate().Subscribe(_ =>
{
//1、获得当前位置
Vector3 curenPosition = this.transform.position;
if (Vector3.Distance(curenPosition, targetPoint) < 0.01f)
{
transform.position = targetPoint;
index = 0;
IsAStarMoving = false;
// .Clear() => Dispose is called for all inner disposables, and the list is cleared.
// .Dispose() => Dispose is called for all inner disposables, and Dispose is called immediately after additional Adds.
disposables.Clear();
}
else
{
//距离就等于 间隔时间乘以速度即可
float maxDistanceDelta = Time.deltaTime * speed;
//如果是A*寻路
if (IsAStarMoving)
{
//在寻路移动模式中,主角100%会饶过障碍物的,
//因此只有在非寻路模式中才需要时时判断主角是否将要碰撞障碍物
//基于MoveTowards的平移
if (astarTargets.Count > 0)
{
transform.position = Vector3.MoveTowards(transform.position, astarTargets[index], maxDistanceDelta);
if (Vector3.Distance(transform.position, astarTargets[index]) < 0.01f)
{
if (index == astarTargets.Count - 1)
{
transform.position = targetPoint;
index = 0;
IsAStarMoving = false;
}
else
{
index++;
}
}
}
}
}
}).AddTo(disposables);
}
</vector3></vector3>
A、根据起点和终点获得A*路径
//A*路径计算
astarTargets = AStarCalc(targetPoint);
B、使用Vector3.MoveTowards 实现序列点移动
其它函数展开
A、角色动画
/// <summary>
/// 角色 帧动画控制
/// </summary>
private void PlayerAnimation()
{
SpriteRenderer spriteRenderer = GetComponent<spriterenderer>();
//定时器每隔5帧
Observable.Interval(TimeSpan.FromMilliseconds(150)).Subscribe(_ =>
{
spriteRenderer.sprite = textureArray[currentTexture];
currentTexture = currentTexture == textureArray.Length - 1 ? 0 : currentTexture + 1;
//Debug.Log(\"length:\" + textureArray.Length + \" curent:\" + currentTexture);
}).AddTo(disposables);
}
</spriterenderer>
这里也比较简单的就是定时动画,每150毫秒更换下sprite的Texture
B、A*算法路径获取
/// <summary>
/// 计算A*算法需要的网格
/// </summary>
/// <param name="\"p\"">世界坐标
/// <returns></returns>
private List<vector3> AStarCalc(Vector3 p)
{
List<vector3> aStarTargets = new List<vector3>();
Vector3 playPositon = this.transform.position;
Point2D Start = new Point2D(((int)(playPositon.x * 100) / (int)(GridSize * 100)),
((int)(playPositon.y * 100) / (int)(GridSize * 100)));
//获得屏幕坐标
int x = ((int)(p.x * 100) / (int)(GridSize * 100));
int y = ((int)(p.y * 100) / (int)(GridSize * 100));
Point2D End = new Point2D(x, y); //计算终点坐标
IPathFinder PathFinder = new PathFinderFast(Matrix);
PathFinder.Formula = HeuristicFormula.Manhattan; //使用我个人觉得最快的曼哈顿A*算法
PathFinder.SearchLimit = 2000; //即移动经过方块(20*20)不大于2000个(简单理解就是步数)
List<pathfindernode> path = PathFinder.FindPath(Start, End); //开始寻径
if (path == null)
{
Debug.Log(\"路径不存在!\");
}
else
{
aStarTargets.Clear();
for (int i = path.Count - 1; i >= 0; i--)
{
aStarTargets.Add(new Vector3(path[i].X * GridSize, path[i].Y * GridSize, 0));
}
}
return aStarTargets;
}
</pathfindernode></vector3></vector3></vector3>
这个前篇也已经说过,轮子的用法,无须多解释了。
总结
实际上这里有一个问题本人没有展开,就是关于,角色如何停止下来的问题,以及UniRx是如何解耦Update的,这需要读者对UniRx有所了解,几句话是解释不清楚的。其实原理还是比较简单的,就是流的概念,把Update当作事件流或者叫消息泵,然后UniRx提供订阅和反订阅功能,这样当我们点击鼠标后我们就订阅事件,当移动完毕我们就反订阅事件,这样就很轻易地解耦了。
本文暂时没有评论,来添加一个吧(●'◡'●)