1. 程式人生 > >AI與遊戲——吃豆人(4)方法綜述

AI與遊戲——吃豆人(4)方法綜述

這一部分先提一下一些基本的目前廣泛用於遊戲中的AI演算法。這裡最好現有點機器學習相關知識,不然可能會不知所云。後面提到具體演算法我會盡量列出一些參考文獻,例子也繼續在吃豆人上面舉例。

目前主流方法主要有,專門的行為表達方法例如有限狀態機,行為樹,和一些基於單元的AI。
有限狀態機FSM不用多說,就是一個狀態在某種條件下為遷移至另一種狀態,這些狀態和條件都是之前預設好的,下圖是FSM在吃豆人的一個例子,FSM後面就不再說了,其他的演算法在後面會繼續提到。樹查詢演算法指的是搜尋行為函式的空間,然後見了可能動作順序的樹,常見的有Alpha-Beta和蒙特卡洛搜尋樹。演化計算指的是基於群體的全域性最優求解,例如遺傳演算法,演化策略,粒子群最佳化演演算法。監督學習指的是學習已有的資料集模型,資料集是有一些資料例項與相應的目標值組成,最常見的是神經網路。增強學習則主要是解決動作序列與獎勵與懲罰的關係,不同於監督學習的值。無監督學習也是需要資料集,只不過資料集不再需要目標值,常見的有k均值,層次聚類等。還有一些其他的典型演算法如TD-learning(時間差學習法)及其一個註明特例Q-learning。目前,神經演化演算法和帶有人工神經網路的TD學習是最受歡迎的AI演算法。
有限狀態機

那麼如何在遊戲中使用這些演算法呢,首先要做的就是表述。沒錯,就是要將當前的遊戲狀態以及要執行的動作表達成數字才能用這些演算法進行計算,例如FSM就是用圖表示,行為樹是用樹來表示。這也是遊戲AI第一步也是最難得一步之一。好的表述也可以帶來很好的實驗與實踐效果,遊戲AI特徵的質量的問題也是需要注重解決的問題。

這裡在介紹一個東西,效用(Unity),Unity是一個度量,通常可以被看做是一個函式,用於幫助演算法去決定好的路線。為了達到這個目的,效用函式採集查詢並收集空間中“有好處”的資訊,對當時的情景進行大致的評估。例如,在象棋對局中,如果走的這一步會是“將”面臨危險那麼這一步的unity函式就會為0,而如果下一步可以讓對面的“將”處於危險,那麼這時的unity函式就會很大。所以unity函式的作用就是要最大化利益,最小化風險與錯誤。這些類似於增強學習的獎勵與懲罰。所謂的學習過程,就是最大化Unity的過程。

下面介紹一下特定行為程式設計,擺闊之前提到的FSM,以及行為樹BT(如下圖所示),行為樹跟FSM很相似,都是在有限的狀態轉換下執行任務,只是行為樹的模組行更佳:如果設計的好的話,行為樹可以通過簡單任務的組合來執行復雜的行為。其最大的不同是,BT是行為的組合而不是狀態,BT也更容易設計,測試與debug。BT很成功的運用在光環2,生化奇兵等遊戲。

行為樹例子

行為樹包括以下幾部分:
1、序列(藍色矩形):一個孩子節點成功執行,那麼將按照箭頭的順序繼續執行,當所有孩子節點執行完後,父節點才算執行成功,否則序列執行失敗。
2、選擇(紅色矩陣及其子節點):選擇包括兩種,基於概率的與基於優先順序的,圖中可以看做是基於概率的,每個動作都有概率執行,在AI學習的過程便是通過調節各個分支概率的大小(類似蒙特卡洛樹)。也可以看做基於優先順序的,數字越大優先順序越高,當高優先順序的無法執行時,執行次優先順序的。
3、裝飾(紫色六邊形):裝飾的作用是增強某個子節點的行為。例子中的裝飾器就是一個迴圈,使任務一直射擊直至對方血量為零。

對於FSM,吃豆人原始碼中給出的一個控制器startpacman就是一個FSM的例子。那麼如何將BT應用於我們之前提到的吃豆人遊戲呢?下圖就是一個很好的應用。這個行為樹用於控制吃豆人的尋找豆子的行為,其中的選擇部分是基於優先順序的,優先沒有魔鬼的,然後是有豆子的,最後才是,沒有豆子的,找到豆子之後如果沒有魔鬼則一直吃下一個豆子。

吃豆人的行為樹

那麼這個方法會比之前的Startman方法好嗎,好多少呢?首先這裡的的尋找方法看起來比之前更細粒度一些,那麼我們就來實現一下看看效果怎麼樣。

public MOVE findPath(Game game, int current)
    {
        boolean withPill;
        ArrayList GhostIndexes = new ArrayList();
        ArrayList<JunctionData> roundFromJunctions=nodes[current].closestJunctions;

        ArrayList<Integer> targets=new ArrayList<Integer>();

        int[] pills=game.getPillIndices();
        int[] powerPills=game.getPowerPillIndices();

        for(int w=0;w<pills.length;w++)                 //check which pills are available
            if(game.isPillStillAvailable(w))
                targets.add(pills[w]);

        for(int w=0;w<powerPills.length;w++)            //check with power pills are available
            if(game.isPowerPillStillAvailable(w))
                targets.add(powerPills[w]);

        for(Constants.GHOST ghost : Constants.GHOST.values())
            GhostIndexes.add(game.getGhostCurrentNodeIndex(ghost));

        for(int i = 0; i<roundFromJunctions.size(); i++)
        {
            withPill = false;

            for (int j = 0; j < roundFromJunctions.get(i).path.length; j++)
            {
                if (GhostIndexes.contains(roundFromJunctions.get(i).path[j]))
                    return game.getNextMoveAwayFromTarget(game.getPacmanCurrentNodeIndex(), roundFromJunctions.get(i).path[0], Constants.DM.PATH);
                if (targets.contains(roundFromJunctions.get(i).path[j]))
                    withPill = true;
            }

            if(withPill)
                return game.getNextMoveTowardsTarget(game.getPacmanCurrentNodeIndex(), roundFromJunctions.get(i).path[0], Constants.DM.PATH);
        }

        int[] targetsArray=new int[targets.size()];     //convert from ArrayList to array

        for(int i=0;i<targetsArray.length;i++)
            targetsArray[i]=targets.get(i);

        //return the next direction once the closest target has been identified
        return game.getNextMoveTowardsTarget(current,game.getClosestNodeIndexFromNodeIndex(current,targetsArray, Constants.DM.PATH), Constants.DM.PATH);

    }

程式碼如上所示,這裡是基於之前startman控制器以及找周圍junction的方法做,只是將原本startman中的第三部之前新增上圖中的步驟,先找pacman的周圍的岔路,然後看到各個岔路的路徑上有沒有魔鬼,有的話就原理這個junction,有豆子的話就靠近這個junction,都沒有就向地圖中最近的豆子靠近。那麼結果如何呢,這裡我分別讓兩種方法進行100次,看兩種方法的平均得分。

第一種方法(原startman):4014.9
第二種方法(用行為樹):3326.7

看來方法並沒有什麼優化,原因可能是這個地圖的“走廊”太短,所以行為樹方法並沒有起到什麼作用。不過這只是一個小嚐試,後面還有更多更好的演算法值得去嘗試。