欢迎光临散文网 会员登陆 & 注册

给猫看的游戏AI实战(五)忙碌的搬运工与AI协作

2017-09-15 17:52 作者:皮皮关做游戏  | 我要投稿

上一节我们讲解了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

我们的微信公众号:皮皮关

给猫看的游戏AI实战(五)忙碌的搬运工与AI协作的评论 (共 条)

分享到微博请遵守国家法律