上一節我們講解了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