P&D 過河遊戲智能幫助實現
-
實現狀態圖的自動生成
-
講解圖數據在程序中的表示方法
-
利用算法實現下一步的計算
對於過河遊戲,首先需要知道其中各個狀態之間的轉換關係,繪製狀態轉移圖如下:
其中,P代表出發岸上的牧師,D代表出發岸上的惡魔,加號和減號分別標識船停在出發一側的岸上還是終點一側的岸邊。則黃色框代表起始的狀態,此時出發岸上有三個牧師和三個惡魔,船停在出發一側的岸上;紅色框代表遊戲終止時的狀態,此時爲勝利狀態,出發岸上沒有牧師和惡魔,船停在終點一側的岸邊。雙箭頭表示兩個狀態間可以相互轉移。
“sense-think-act” paradigm(範式) 是構造 agent、robot、NPC(Non-Player Character) 的基礎概念。自從上世紀80年代提出以來,我們使用 Sense-Think-Act 範例思考機器人如何工作,並設計它們。 即使機器人最終設計的方式不同,STA 通常仍然是一個有用的開始。 隨着機器人技術的發展,協同技術越來越重要,機器人之間的 “Communicate”通常也納入設計之中。本節主要關注 NPC 如何產生看似理性(Rational)的行爲(模擬人)的設計方法。
根據以上的狀態轉移圖,首先完成“感知”部分的設計:
感知是 agent 接收世界信息的行爲,其獲取的數據將是思考的輸入。在遊戲中,程序是可以獲得遊戲世界任意信息的,設計一個“乾死”玩家的算法通常是比較容易的,因此如何限制信息獲取是設計不同級別 agent 的核心問題。
在遊戲中,定義獲取信息能力通常可以從視覺、聽覺和嗅覺等渠道去考慮:
- 視覺(Vision)
- 識別“敵人”的位置和屬性
- 識別“障礙物”及其範圍
- 聽覺(Sound)
- 識別事件的方向和距離
- 嗅覺(Smell)
- 獲得玩家/事件的痕跡
在這裏,我使用最簡單的方法讓遊戲智能體感知到應該作出下一步決策,就是增加一個按鈕,每當玩家點擊這個按鈕,遊戲智能體就會改變當前的狀態並作出對應的改變。
相關的代碼如下:
void OnGUI() {
if (result == 0 && GUI.Button(new Rect(Screen.width/2-70, Screen.height/2 - 70, 120, 60), "Next", bStyle))
action.nextMove();
if (result != 0 && GUI.Button(new Rect(Screen.width/2-70, Screen.height/2 - 130, 120, 60), "Replay", bStyle)) {
result = 0;
action.replay();
}
if (result == 1)
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-190, 100, 50), "You lose!", style);
else if(result == 2)
GUI.Label(new Rect(Screen.width/2-60, Screen.height/2-190, 100, 50), "You win!", style);
}
以上代碼增加了一個“next”按鈕,當遊戲未結束且用戶點擊此按鈕時,執行nextMove,完成下一步移動。
接着完成“思考”的部分,需要使遊戲智能體作出下一步走向哪一狀態的決策:
Think 就是算法,它的輸入是感知的數據,輸出是行爲(behaviours)。 思考的算法,通常就是我們所說的遊戲規則的一部分,即 agent 能做什麼,該做什麼。 通常這不是程序員的工作,而是遊戲設計師的工作。遊戲 agent 的思考類似人腦的決策過程,這其中的關鍵不是打造最強大腦,而是建立符合遊戲玩家難度曲線,可以控制、且符合社會準則的行爲。另一個相關問題,玩家難度曲線在編程階段是未知的,依賴衆多玩家與 agent 的操作與對抗結果。它在遊戲測試和運維過程中存在巨大不確定性(在數據驅動的設計和集成腳本引擎等章節中討論解決方案)。
過程中,比較重要的點就是表示狀態。
public enum BoatAct { P, D, PP, DD, PD };
public struct Next {
public BoatAct boatAct;
}
因爲在每一步決策時,智能體只需要決定向船上放若干個人物,所以船的狀態就只有五種,用結構體Next來存儲船的狀態。
int from_P, from_D, direction = boat.direction;
int[] fromNum = fromBank.getNum();
from_P = fromNum[0];
from_D = fromNum[1];
以上代碼用來獲取出發岸上的人物數量,可以通過getNum函數即時獲取,因此不需要額外的數組來存儲,並且因爲人物數量恆定,只需一側岸邊的人物數量就可以推出另一邊的;diection變量用來表示船的行駛方向,這裏也可以直接獲取。
船的方向以及岸邊的人物數量構成了每一步的狀態空間,船上的人物數量構成了每一步的決策狀態空間。
爲了符合以上說明,使遊戲智能體的決策具有一定的不確定性,這裏增加一個隨機決策的函數:
private int randomValue() {
float a = Random.Range(0f, 1f);
if (a <= 0.5f) return 1;
else return 2;
}
當可行的狀態空間有兩個時,智能體隨機選擇其中一個狀態轉移。
接下來是“思考”部分最關鍵的步驟,即作出下一步決策:
public void GetNextPassager() {
int from_P, from_D, direction = boat.direction;
int[] fromNum = fromBank.getNum();
from_P = fromNum[0];
from_D = fromNum[1];
if (from_P == 3 && from_D == 3 && direction == 1) {
int turn = randomValue();
if (turn == 1) next.boatAct = BoatAct.PD;
else next.boatAct = BoatAct.DD;
}
else if (direction == -1 && from_P == 2 && from_D == 2) next.boatAct = BoatAct.P;
else if (direction == -1 && from_P == 3 && from_D == 2) next.boatAct = BoatAct.D;
else if (direction == -1 && from_P == 3 && from_D == 1) next.boatAct = BoatAct.D;
else if (direction == 1 && from_P == 3 && from_D == 2) next.boatAct = BoatAct.DD;
else if (direction == -1 && from_P == 3 && from_D == 0) next.boatAct = BoatAct.D;
else if (direction == 1 && from_P == 3 && from_D == 1) next.boatAct = BoatAct.PP;
else if (direction == -1 && from_P == 1 && from_D == 1) next.boatAct = BoatAct.PD;
else if (direction == 1 && from_P == 2 && from_D == 2) next.boatAct = BoatAct.PP;
else if (direction == -1 && from_P == 0 && from_D == 0) next.boatAct = BoatAct.D;
else if (direction == 1 && from_P == 0 && from_D == 3) next.boatAct = BoatAct.DD;
else if (direction == -1 && from_P == 0 && from_D == 1) {
if (randomValue() == 1) next.boatAct = BoatAct.D;
else next.boatAct = BoatAct.P;
}
else if (direction == -1 && from_P == 0 && from_D == 2) next.boatAct = BoatAct.D;
else if (direction == 1 && from_P == 2 && from_D == 1) next.boatAct = BoatAct.P;
else if (direction == 1 && from_P == 0 && from_D == 2) next.boatAct = BoatAct.DD;
else if (direction == 1 && from_P == 1 && from_D == 1) next.boatAct = BoatAct.PD;
}
此函數通過對不同的狀態進行判斷並作出相應的決策。
最後則要完成“行爲”部分。當智能體作出決策時,需要指揮相應的遊戲對象完成狀態的改變。
行動(Act)將思考(Think)的結果作爲輸入,該部分的任務就是使得 agent 行爲更符合物理世界的規律,使得“心想事成”這樣理想的結果變得不確定。
首先增加變量status,用來表示船的狀態改變:
private int status;
status爲0時人物上船,爲1時船從一側岸邊到另一側,爲2時人物下船。這樣可以避免狀態間的轉換太過生硬,保證轉換時仍有人物的動作。
public void nextMove() {
int direction = boat.direction;
ICharacterController p, d;
if (status == 0) {
GetNextPassager();
if (direction == 1 && next.boatAct == BoatAct.PP) {
p = fromBank.findCharacter(0);
moveCharacter(p);
p = fromBank.findCharacter(0);
moveCharacter(p);
}
else if (direction == 1 && next.boatAct == BoatAct.P) {
p = fromBank.findCharacter(0);
moveCharacter(p);
}
else if (direction == 1 && next.boatAct == BoatAct.PD) {
p = fromBank.findCharacter(0);
moveCharacter(p);
d = fromBank.findCharacter(1);
moveCharacter(d);
}
else if (direction == 1 && next.boatAct == BoatAct.D) {
d = fromBank.findCharacter(1);
moveCharacter(d);
}
else if (direction == 1 && next.boatAct == BoatAct.DD) {
d = fromBank.findCharacter(1);
moveCharacter(d);
d = fromBank.findCharacter(1);
moveCharacter(d);
}
else if (direction == -1 && next.boatAct == BoatAct.PP) {
p = toBank.findCharacter(0);
moveCharacter(p);
p = toBank.findCharacter(0);
moveCharacter(p);
}
else if (direction == -1 && next.boatAct == BoatAct.P) {
p = toBank.findCharacter(0);
moveCharacter(p);
}
else if (direction == -1 && next.boatAct == BoatAct.PD) {
p = toBank.findCharacter(0);
moveCharacter(p);
d = toBank.findCharacter(1);
moveCharacter(d);
}
else if (direction == -1 && next.boatAct == BoatAct.D) {
d = toBank.findCharacter(1);
moveCharacter(d);
}
else if (direction == -1 && next.boatAct == BoatAct.DD) {
d = toBank.findCharacter(1);
moveCharacter(d);
d = toBank.findCharacter(1);
moveCharacter(d);
}
if (direction == 1) direction = -1;
else direction = 1;
status = 1;
}
else if (status == 1) {
moveBoat();
status = 2;
}
else if (status == 2) {
ICharacterController[] pass = boat.getRoles();
for (int i = 0; i < pass.Length; i++)
if (pass[i] != null) moveCharacter(pass[i]);
status = 0;
}
}
以上函數完成了狀態轉移相關的動作,通過moveBoat函數和moveCharacter函數來操縱人物和船體的動作。
public void moveBoat() {
int i;
for (i = 0; i < boat.roles.Length; i++)
if (boat.roles[i] != null) break;
if (i == boat.roles.Length) return;
boat.Move();
int[] fromNum = fromBank.getNum();
int[] toNum = toBank.getNum();
int[] boatNum = boat.getNum();
int direction = boat.direction;
GUI.result = gameover.check(fromNum, toNum, boatNum, direction);
}
public void moveCharacter(ICharacterController role) {
BankController bank;
Vector3 t, pos;
if (role.onBoat) {
if (boat.direction == -1) bank = toBank;
else bank = fromBank;
boat.getOff(role.role.name);
t = bank.pos[bank.emptyNum()];
t.x *= bank.direction;
role.moveable.destination(t);
role.getOn(bank);
bank.getOn(role);
}
else {
bank = role.bank;
if (boat.emptyNum() == -1) return;
if (bank.direction != boat.direction) return;
bank.getOff(role.role.name);
int e = boat.emptyNum();
if (boat.direction == -1) pos = boat.to_pos[e];
else pos = boat.from_pos[e];
role. moveable.destination(pos);
role.getOn(boat);
boat.getOn(role);
}
int[] fromNum = fromBank.getNum();
int[] toNum = toBank.getNum();
int[] boatNum = boat.getNum();
int direction = boat.direction;
GUI.result = gameover.check(fromNum, toNum, boatNum, direction);
}
綜上,利用“感知-思考-行爲”模型完成了過河遊戲的智能實現。
遊戲效果如下: