【Unity 3D學習筆記】P&D 過河遊戲智能實現

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);
    }

綜上,利用“感知-思考-行爲”模型完成了過河遊戲的智能實現。

遊戲效果如下:

遊戲視頻

項目傳送

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章