上一节我们讲解了AI行为中寻路的算法,比较特别的是我们是融合了算法可视化的理念,将寻路做出了有趣的动态效果。
这一节我们再次转向另一个问题——多个AI协作的问题。为了讲清楚这个问题,我特意做了这个例子:
上图中,3个红色的是物流机器人,绿色的是货物。将货物随意地扔给他们,他们就能自发地将货物依次摆放。如果觉得有趣的话,我们来试着实现一下。┌( ಠ_ಠ)┘
1、实现一个单独的物流机器人
对有一定基础的读者来说,这个例子已经不需要细讲了。
1、搭建场景。
如上图,非常简单,场景包含地面和机器人,墙可要可不要。(为了开发方便,一开始可以把墙隐藏起来)。
机器人自身非常精简,就是一个不要碰撞体Collider、也不要Rigidbody的最普通的胶囊体即可。
另外做一个绿色方块box代表货物,box要有Rigidbody刚体组件。将box拖入工程目录变成prefab以后用到,然后删除方块即可。
2、下面概览一下用到的脚本:
1、摄像机挂载脚本PlayerInput.cs,功能:鼠标点击地面时生成货物。
2、机器人挂载脚本RobotController.cs,功能:AI的所有逻辑。
可以猜到,其实箱子并不是由机器人通过物理推动的,那样实现会非常困难,因为很难瞄准推动的角度,箱子会发生偏移和旋转。
3、实现点击地面,生成箱子。
这个功能对于看了本文好几节的同学来说应该很简单了。代码如下:
public class PlayerInput : MonoBehaviour { public GameObject box_prefeb; void OnClickGround() { Camera cam = Camera.main; // 主摄像机,这样获取很方便 // 老规矩,从鼠标点击的地方,向屏幕内打射线 Ray ray = cam.ScreenPointToRay(Input.mousePosition); // 处理这条射线打到的那个GameObject RaycastHit hitt = new RaycastHit(); Physics.Raycast(ray, out hitt, 100); Debug.DrawLine(cam.transform.position, ray.direction, Color.red); // 如果打到地面,就生成box(也就是货物) if (hitt.transform!=null && hitt.transform.name=="Ground") { Vector3 p = new Vector3(hitt.point.x, 5, hitt.point.z); Instantiate(box_prefeb, p, Quaternion.Euler(0, 0, 0)); } } void Update() { // 每帧检测鼠标点击 if (Input.GetMouseButtonDown(0)) { OnClickGround(); } }}
4、实现货物管理器。
由于我们要将散乱的货物按顺序码好,这就需要给每个货物编号。参考代码如下:
public class RobotController : MonoBehaviour{ Dictionary boxes = new Dictionary(); int id_counter = 1; // 保存货物到boxes容器中,会给货物分配ID void SaveNewBox(GameObject box) { if (box.transform.position.y > 0.251f) { return; } if (boxes.ContainsKey(box)) { return; } boxes[box] = id_counter; id_counter++; } void Update() { GameObject[] all = GameObject.FindGameObjectsWithTag("Box"); foreach (GameObject box in all) { // 保存货物到boxes中,这里会给货物分配ID SaveNewBox(box); } }}
管理货物的方法很简单,每一帧都遍历所有货物,将没有加入boxes字典的货物加入字典,ID增加1。
5、实现机器人移动和整理逻辑。
简单来说,机器人从boxes中找一个需要整理的货物,然后将其设置为当前工作的货物,然后移动它即可。
注意机器人搬运时,有两种状态:1、正在跑向货物。2、正在搬运货物。也就是说,要先跑到货物旁边才能搬运它。用一个bool变量来标识状态。
// 当前正在搬运的货物 GameObject working_box; // going_back表示了机器人的两种状态: // true代表当前箱子已处理完毕,可以去取下一个箱子 // false代表正在推当前的箱子 bool going_back = true;
我一开始做的例子也没有going_back区分状态,机器人会瞬移到货物旁边直接开始搬运。我的例子代码也是慢慢完善才得到的。
逻辑完善以后,代码如下图,加了几个函数,Update函数也要添加一些逻辑:
// 整理货物,即搬运货物到目标位置 bool CleanBox(GameObject box) { Vector3 clean_pos = BoxCleanPos(boxes[box]); if (Vector3.Distance(box.transform.position, clean_pos) > 0.05f) { //MoveTowards(current: Vector3, target: Vector3, maxDistanceDelta: float) : Vector3 Vector3 to = clean_pos - box.transform.position; box.transform.position += to.normalized * Mathf.Min(0.1f, Vector3.Distance(box.transform.position, clean_pos)); transform.position = box.transform.position + to.normalized * -0.5f; return false; } return true; } // 根据ID计算货物对应的位置 Vector3 BoxCleanPos(int id) { int n = (id - 1) % 5; int row = (id - 1) / 5; Vector3 v = new Vector3(-5f + n * 1.0f, 0.25f, -5f + row * 1.0f); return v; } // Update is called once per frame void Update() { GameObject[] all = GameObject.FindGameObjectsWithTag("Box"); foreach (GameObject box in all) { // 保存货物到boxes中,这里会给货物分配ID SaveNewBox(box); } // 如果当前没有正在搬运的货物,则从boxes中查找需要搬运的货物 if (working_box == null) { foreach (var pair in boxes) { Vector3 clean_pos = BoxCleanPos(boxes[pair.Key]); if (Vector3.Distance(pair.Key.transform.position, clean_pos) > 0.05f) { // 找到一个需要搬运的货物,设置为当前正在搬的 working_box = pair.Key; break; } } } // 如果当前正在搬运货物 if (working_box != null) { // 情况一:正在搬运的状态 if (going_back == false) { if (CleanBox(working_box)) { working_box = null; going_back = true; } } else { // 情况二:正在跑向货物的状态 if (Vector3.Distance(working_box.transform.position, transform.position) > 0.05f) { //MoveTowards(current: Vector3, target: Vector3, maxDistanceDelta: float) : Vector3 Vector3 to = working_box.transform.position - transform.position; float f = to.magnitude / 0.1f; to /= f; transform.position += to; } else { going_back = false; } } } }
到此为止,我们已经实现了一个单独的物流机器人了,试试看吧。效果见本段开头只有一个机器人的那个动图。
回想一下前几节介绍的状态机AI的例子,会发现AI逻辑基本都是这样的形式,只要写过一个复杂一点的状态机,再写大部分小游戏AI都会比较有信心了 (ง •̀_•́)ง ~~
2、多机器人协作
可以试验一下,在场景里多复制几个机器人,也不会报错哦~~机器人可以正常搬运,只不过多人同时搬运一个货物,移动会加快。
这是因为多个机器人的逻辑是相同的,他们会同时奔向同一个货物,然后一起搬运。他们的这种行为就好像不知道队友的存在一样,毫无计划性,纯粹的个人主义 ・ω・ ・ω・ ・ω・ ・ω・
要想让多人协作起来,他们之间就必须通过某种方式做信息的交流。
A:我要搬1号货物哦,不要和我抢。
B:那我搬2号货物。
过了一阵:
A:1号货物已搬运完毕。
这里,我们通过在货物上面做标记的方法实现消息通信,为货物创建一个脚本BoxData.cs,并挂在货物的prefab上面:
// BoxData.cspublic class BoxData : MonoBehaviour { public GameObject working_robot = null;}
我这里直接用机器人变量本身作为标记,比较方便。
机器人打算搬某个货物时,要在货物上面标记好自己。别的机器人看到这个货物已经被人占用了,就不会处理这个货物了。
// 修改RobotController.cs // 给货物加锁,也就是打上自己的标记 bool LockBox(GameObject box) { BoxData d = box.GetComponent(); if (d == null) { return false; } if (d.working_robot == null) { d.working_robot = gameObject; } if (d.working_robot != gameObject) { return false; } return true; } // 释放锁,也就是删除货物的标记 bool FreeBoxLock(GameObject box) { BoxData d = box.GetComponent(); if (d == null) { return false; } if (d.working_robot == null) { return true; } if (d.working_robot != gameObject) { return false; } d.working_robot = null; return true; }
在机器人处理货物时做一点改动,用到了面两个函数。下面的代码关键看LockBox和FreeBoxLock两处:
void Update () { GameObject[] all = GameObject.FindGameObjectsWithTag("Box"); foreach (GameObject box in all) { SaveNewBox(box); } if (working_box == null) { foreach (var pair in boxes) { // 如果锁定失败,就代表货物已经被别人占用了 if (!LockBox(pair.Key)) { continue; } Vector3 clean_pos = BoxCleanPos(boxes[pair.Key]); if (Vector3.Distance(pair.Key.transform.position, clean_pos) > 0.05f) { working_box = pair.Key; break; } } } if (working_box != null) { if (going_back == false) { if(CleanBox(working_box)) { // 运送到位后即可释放锁 FreeBoxLock(working_box); working_box = null; going_back = true; } }
这样就OK了。
什么!?这么简单!?是的,无论多少机器人,都能井井有条的协作!ヽ(•̀ω•́ )ゝ
复制10个试一试!
如蚂蚁一样一拥而上的效果,你也可以实现。ヽ(•̀ω•́ )ゝ。看起来炫酷的效果却是用一个非常简单的方法做到的,这就是算法的魅力啊~~~
注意,有一种特殊情况,也已经被解决了,不需要更多考虑,可以想想是为什么:
A:1号货物已搬运完毕。
过了一会儿
C:1号货物被挤到了其他位置,需要再搬运一下
过了一会儿
C:1号货物搬运完毕
代码就不贴了,工程地址会放在文末。下载即可。
3、总结
本节我们介绍了一种模拟整理箱子的Demo,有很大篇幅在制作这个Demo本身,但是重点是第2段。在第2段我们用一种非常简单的方法实现了一种自发性的任务规划。
这有点像公司制度,在制度合理的情况下,每个人只要按制度干活,就能实现良好的协作,事情就能自动处理好。可是天底下不都是这么简单的事,比如现在IT、金融等知识密集型的领域,制度的作用就不像在工厂、车间里那么有效了。这时候需要更复杂的协作机制,将计划和管理的工作独立出来,而且同时让工作者们保持一定自主性,才能达到良好效果。
在很多重视AI的游戏中,上面说的这些也都是可以做到的。比如一些MOBA或者RTS游戏里的高智能电脑,就既懂得自己发展,又懂得和友军协作。
作为AI设计的入门级专栏,本文没有把问题讲得很深入。但是只要引起读者的兴趣,就已经达到本文的目的了。
工程地址:
https://github.com/mayao11/PracticalGameAI/tree/master/AIBlock