上一节我们讲解了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<GameObject, int> boxes = new Dictionary<GameObject, int>();
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.cs
public class BoxData : MonoBehaviour {
public GameObject working_robot = null;
}
我这里直接用机器人变量本身作为标记,比较方便。
机器人打算搬某个货物时,要在货物上面标记好自己。别的机器人看到这个货物已经被人占用了,就不会处理这个货物了。
// 修改RobotController.cs
// 给货物加锁,也就是打上自己的标记
bool LockBox(GameObject box)
{
BoxData d = box.GetComponent<BoxData>();
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<BoxData>();
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
对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。
我们的官网地址:http://levelpp.com/
我们的游戏开发技术交流群:610475807
我们的微信公众号:皮皮关
暂无关于此日志的评论。