编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Unity A星寻路实现之2D角色移动实现

wxchong 2024-06-21 14:25:48 开源技术 12 ℃ 0 评论

背景

前两周工作教忙,学习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提供订阅和反订阅功能,这样当我们点击鼠标后我们就订阅事件,当移动完毕我们就反订阅事件,这样就很轻易地解耦了。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表