遊戲開發教育:給貓看的遊戲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


分享到:


相關文章: