游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

上一节我们讲解了AI行为中寻路的算法,比较特别的是我们是融合了算法可视化的理念,将寻路做出了有趣的动态效果。

这一节我们再次转向另一个问题——多个AI协作的问题。为了讲清楚这个问题,我特意做了这个例子:

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

上图中,3个红色的是物流机器人,绿色的是货物。将货物随意地扔给他们,他们就能自发地将货物依次摆放。如果觉得有趣的话,我们来试着实现一下。┌( ಠ_ಠ)┘

1、实现一个单独的物流机器人

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

对有一定基础的读者来说,这个例子已经不需要细讲了。

1、搭建场景。

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

如上图,非常简单,场景包含地面和机器人,墙可要可不要。(为了开发方便,一开始可以把墙隐藏起来)。

机器人自身非常精简,就是一个不要碰撞体Collider、也不要Rigidbody的最普通的胶囊体即可。

另外做一个绿色方块box代表货物,box要有Rigidbody刚体组件。将box拖入工程目录变成prefab以后用到,然后删除方块即可。

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

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个试一试!

游戏开发教育:给猫看的游戏AI实战5——忙碌的搬运工与AI协作

如蚂蚁一样一拥而上的效果,你也可以实现。ヽ(•̀ω•́ )ゝ。看起来炫酷的效果却是用一个非常简单的方法做到的,这就是算法的魅力啊~~~

注意,有一种特殊情况,也已经被解决了,不需要更多考虑,可以想想是为什么:

  • A:1号货物已搬运完毕。

  • 过了一会儿

  • C:1号货物被挤到了其他位置,需要再搬运一下

  • 过了一会儿

  • C:1号货物搬运完毕

代码就不贴了,工程地址会放在文末。下载即可。

3、总结

本节我们介绍了一种模拟整理箱子的Demo,有很大篇幅在制作这个Demo本身,但是重点是第2段。在第2段我们用一种非常简单的方法实现了一种自发性的任务规划。

这有点像公司制度,在制度合理的情况下,每个人只要按制度干活,就能实现良好的协作,事情就能自动处理好。可是天底下不都是这么简单的事,比如现在IT、金融等知识密集型的领域,制度的作用就不像在工厂、车间里那么有效了。这时候需要更复杂的协作机制,将计划和管理的工作独立出来,而且同时让工作者们保持一定自主性,才能达到良好效果。

在很多重视AI的游戏中,上面说的这些也都是可以做到的。比如一些MOBA或者RTS游戏里的高智能电脑,就既懂得自己发展,又懂得和友军协作。

作为AI设计的入门级专栏,本文没有把问题讲得很深入。但是只要引起读者的兴趣,就已经达到本文的目的了。

工程地址:

https://github.com/mayao11/PracticalGameAI/tree/master/AIBlock


分享到:


相關文章: