1. 程式人生 > >如何簡潔實現遊戲中的AI

如何簡潔實現遊戲中的AI

端午節放假總結了一下好久前寫過的一些遊戲引擎,其中NPC等遊戲AI的實現無疑是最繁瑣的部分,現在,給大家分享一下:

從一個簡單的情景開始

怪物,是遊戲中的一個基本概念。遊戲中的單位分類,不外乎玩家、NPC、怪物這幾種。其中,AI 一定是與三類實體都會產生交集的遊戲模組之一。 以我們熟悉的任意一款遊戲中的人形怪物為例,假設有一種怪物的 AI 需求是這樣的:

  • 大部分情況下,漫無目的巡邏。
  • 玩家進入視野,鎖定玩家為目標開始攻擊。
  • Hp 低到一定程度,怪會想法設法逃跑,並說幾句話。

我們以這個為模型,進行這篇文章之後的所有討論。為了簡化問題,以省去一些不必要的討論,將文章的核心定位到人工智慧上,這裡需要注意幾點的是:

  • 不再考慮 entity 之間的訊息傳遞機制,例如判斷玩家進入視野,不再通過事件機制觸發,而是通過該人形怪的輪詢觸發。
  • 不再考慮 entity 的行為控制機制,簡化這個 entity 的控制模型。不論是底層是基於 SteeringBehaviour 或者是瞬移,不論是非同步驅的還是主迴圈輪詢,都不在本文模型的討論之列。

首先可以很容易抽象出來 IUnit:

public interface IUnit
    {
        void ChangeState(UnitStateEnum state);
        void Patrol(); 
        IUnit GetNearestTarget
()
; void LockTarget(IUnit unit); float GetFleeBloodRate(); bool CanMove(); bool HpRateLessThan(float rate); void Flee(); void Speak(); }
public interface IUnit
    {
        void ChangeState(UnitStateEnum state);
        void Patrol(); 
        IUnit GetNearestTarget
()
; void LockTarget(IUnit unit); float GetFleeBloodRate(); bool CanMove(); bool HpRateLessThan(float rate); void Flee(); void Speak(); }

然後,我們可以通過一個簡單的有限狀態機 (FSM) 來控制這個單位的行為。不同狀態下,單位都具有不同的行為準則,以形成智慧體。 具體來說,我們可以定義這樣幾種狀態:

  • 巡邏狀態: 會執行巡邏,同時檢查是否有敵對單位接近,接近的話進入戰鬥狀態。
  • 戰鬥狀態: 會執行戰鬥,同時檢查自己的血量是否達到逃跑線以下,達成檢查了就會逃跑。
  • 逃跑狀態: 會逃跑,同時說一次話。

最原始的狀態機的程式碼:

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        TUnit Self { get; }
        void OnEnter();
        void Drive();
        void OnExit();
    }

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        TUnit Self { get; }
        void OnEnter();
        void Drive();
        void OnExit();
    }

以逃跑狀態為例:

public class FleeState : UnitStateBase
    {
        public FleeState(IUnit self) : base(UnitStateEnum.Flee, self)
        {
        }
        public override void OnEnter()
        {
            Self.Flee();
        }
        public override void Drive()
        {
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }

            Self.ChangeState(UnitStateEnum.Patrol);
        }
    }
public class FleeState : UnitStateBase
    {
        public FleeState(IUnit self) : base(UnitStateEnum.Flee, self)
        {
        }
        public override void OnEnter()
        {
            Self.Flee();
        }
        public override void Drive()
        {
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }
 
            Self.ChangeState(UnitStateEnum.Patrol);
        }
    }

決策邏輯與上下文分離

上述是一個最簡單、最常規的狀態機實現。估計只有學生會這樣寫,業界肯定是沒人這樣寫 AI 的,不然遊戲怎麼死的都不知道。

首先有一個非常明顯的效能問題:狀態機本質是描述狀態遷移的,並不需要記錄 entity 的 context,如果 entity 的 context 記錄在 State上,那麼狀態機這個遷移邏輯就需要每個 entity 都來一份 instance,這麼一個簡單的狀態遷移就需要消耗大約 X 個位元組,那麼一個場景 1w 個怪,這些都屬於白白消耗的記憶體。就目前的實現來看,具體的一個 State 例項內部 hold 住了 Unit,所以 State 例項是沒辦法複用的。

針對這一點,我們做一下優化。對這個狀態機,把 Context 完全剝離出來。

修改狀態機介面定義:

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        void OnEnter(TUnit self);
        void Drive(TUnit self);
        void OnExit(TUnit self);
    }

public interface IState<TState, TUnit> where TState : IConvertible
    {
        TState Enum { get; }
        void OnEnter(TUnit self);
        void Drive(TUnit self);
        void OnExit(TUnit self);
    }

還是拿之前實現好的逃跑狀態作為例子:

public class FleeState : UnitStateBase
    {
        public FleeState() : base(UnitStateEnum.Flee)
        {
        }
        public override void OnEnter(IUnit self)
        {
            base.OnEnter(self);
            self.Flee();
        }
        public override void Drive(IUnit self)
        {
            base.Drive(self);

            var unit = self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }

            self.ChangeState(UnitStateEnum.Patrol);
        }
    }
public class FleeState : UnitStateBase
    {
        public FleeState() : base(UnitStateEnum.Flee)
        {
        }
        public override void OnEnter(IUnit self)
        {
            base.OnEnter(self);
            self.Flee();
        }
        public override void Drive(IUnit self)
        {
            base.Drive(self);
 
            var unit = self.GetNearestTarget();
            if (unit != null)
            {
                return;
            }
 
            self.ChangeState(UnitStateEnum.Patrol);
        }
    }

這樣,就區分了動態與靜態。靜態的是狀態之間的遷移邏輯,只要不做熱更新,是不會變的結構。動態的是狀態遷移過程中的上下文,根據不同的上下文來決定。

分層有限狀態機

最原始的狀態機方案除了效能存在問題,還有一個比較嚴重的問題。那就是這種狀態機框架無法描述層級結構的狀態。 假設需要對一開始的需求進行這樣的擴充套件:怪在巡邏狀態下有可能進入怠工狀態,同時要求,怠工狀態下也會進行進入戰鬥的檢查。

這樣的話,雖然在之前的框架下,單獨做一個新的怠工狀態也可以,但是仔細分析一下,我們會發現,其實本質上巡邏狀態只是一個抽象的父狀態,其存在的意義就是進行戰鬥檢查;而具體的是在按路線巡邏還是怠工,其實都是巡邏狀態的一個子狀態。

狀態之間就有了層級的概念,各自獨立的狀態機系統就無法滿足需求,需要一種分層次的狀態機,原先的狀態機介面設計就需要徹底改掉了。

在重構狀態框架之前,需要注意兩點:

因為父狀態需要關注子狀態的執行結果,所以狀態的 Drive 介面需要一個執行結果的返回值。

子狀態,比如怠工,一定是有跨幀的需求在的,所以這個 Result,我們定義為 Continue、Sucess、Failure。

子狀態一定是由父狀態驅動的。

考慮這樣一個組合狀態情景:巡邏時,需要依次得先走到一個點,然後怠工一會兒,再走到下一個點,然後再怠工一會兒,迴圈往復。這樣就需要父狀態(巡邏狀態)註記當前啟用的子狀態,並且根據子狀態執行結果的不同來修改啟用的子狀態集合。這樣不僅是 Unit 自身有上下文,連組合狀態也有了自己的上下文。

為了簡化討論,我們還是從 non-ContextFree 層次狀態機系統設計開始。

修改後的狀態定義:

public interface IState<TState, TCleverUnit, TResult> 
        where TState : IConvertible
    {
        // ...
        TResult Drive();
        // ...
    }

public interface IState<TState, TCleverUnit, TResult> 
        where TState : IConvertible
    {
        // ...
        TResult Drive();
        // ...
    }

組合狀態的定義:

public abstract class UnitCompositeStateBase : UnitStateBase
    {
        protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>();

        // ...
        protected Result ProcessSubStates()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }

            var front = subStates.First;
            var res = front.Value.Drive();

            if (res != Result.Continue)
            {
                subStates.RemoveFirst();
            }

            return Result.Continue;
        }
        // ...
    }
public abstract class UnitCompositeStateBase : UnitStateBase
    {
        protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>();
 
        // ...
        protected Result ProcessSubStates()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }
 
            var front = subStates.First;
            var res = front.Value.Drive();
 
            if (res != Result.Continue)
            {
                subStates.RemoveFirst();
            }
 
            return Result.Continue;
        }
        // ...
    }

巡邏狀態現在是一個組合狀態:

public class PatrolState : UnitCompositeStateBase
    {
        // ...
        public override void OnEnter()
        {
            base.OnEnter();
            AddSubState(new MoveToState(Self));
        }

        public override Result Drive()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }

            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                Self.LockTarget(unit);
                return Result.Success;
            }

            var front = subStates.First;
            var ret = front.Value.Drive();

            if (ret != Result.Continue)
            {
                if (front.Value.Enum == CleverUnitStateEnum.MoveTo)
                {
                    AddSubState(new IdleState(Self));
                }
                else
                {
                    AddSubState(new MoveToState(Self));
                }
            }
            
            return Result.Continue;
        }
    }
public class PatrolState : UnitCompositeStateBase
    {
        // ...
        public override void OnEnter()
        {
            base.OnEnter();
            AddSubState(new MoveToState(Self));
        }
 
        public override Result Drive()
        {
            if (subStates.Count == 0)
            {
                return Result.Success;
            }
 
            var unit = Self.GetNearestTarget();
            if (unit != null)
            {
                Self.LockTarget(unit);
                return Result.Success;
            }
 
            var front = subStates.First;
            var ret = front.Value.Drive();
 
            if (ret != Result.Continue)
            {
                if (front.Value.Enum == CleverUnitStateEnum.MoveTo)
                {
                    AddSubState(new IdleState(Self));
                }
                else
                {
                    AddSubState(new MoveToState(Self));
                }
            }
            
            return Result.Continue;
        }
    }

看過《遊戲人工智慧程式設計精粹》的同學可能看到這裡就會發現,這種層次狀態機其實就是這本書裡講的目標驅動的狀態機。組合狀態就是組合目標,子狀態就是子目標。父目標 / 狀態的排程取決於子目標 / 狀態的完成情況。

這種狀態框架與普通的 trivial 狀態機模型的區別僅僅是增加了對層次狀態的支援,狀態的遷移還是需要靠顯式的 ChangeState 來做。

這本書裡面的狀態框架,每個狀態的執行 status 記錄在了例項內部,不方便後續的優化,我們這裡實現的時候首先把這個做成純驅動式的。但是還不夠。現在之前的 ContextFree 優化成果已經回退掉了,我們還需要補充回來。

分層的上下文

我們對之前重構出來的層次狀態機框架再進行一次 Context 分離優化。 要優化的點有這樣幾個:

首先是繼續之前的,unit 不應該作為一個 state 自己的內部 status。

組合狀態的例項內部不應該包括自身執行的 status。目前的組合狀態,可以動態增刪子狀態,也就是根據 status 決定了結構的狀態,理應分離靜態與動態。巡邏狀態組合了兩個子狀態——A 和 B,邏輯中是一個完成了就新增另一個,這樣一想的話,其實巡邏狀態應該重新描述——先進行 A,再進行 B,迴圈往復。      由於有了父狀態的概念,其實狀態介面的設計也可以再迭代,理論上只需要一個 drive 即可。因為狀態內部的上下文要全部分離出來,所以也沒必要對外提供 OnEnter、OnExit,提供這兩個介面的意義只是做一層內部資訊的隱藏,但是現在內部的 status 沒了,也就沒必要隱藏了。    具體分析一下需要拆出的 status:

  • 一部分是 entity 本身的 status,這裡可以簡單的認為是 unit。
  • 另一部分是 state 本身的 status。
  • 對於組合狀態,這個 status 描述的是我當前執行到哪個 substate。
  • 對於原子狀態,這個 status 描述的種類可能有所區別。
  • 例如 MoveTo/Flee,OnEnter 的時候,修改了 unit 的 status,然後 Drive 的時候去 check。
  • 例如 Idle,OnEnter 時改了自己的 status,然後 Drive 的時候去 check。 經過總結,我們可以發現,每個狀態的 status 本質上都可以通過一個變數來描述。一個 State 作為一個最小粒度的單元,具有這樣的 Concept: 輸入一個 Context,輸出一個 Result。

Context 暫時只需要包括這個 Unit,和之前所說的 status。同時,考慮這樣一個問題:

  • 父狀態 A,子狀態 B。
  • 子狀態 B 向上返回 Continue 的同時,status 記錄下來為 b。
  • 父狀態 ADrive 子狀態的結果為 Continue,自身也需要向上丟擲 Continue,同時自己也有 status 為 a。 這樣,再還原現場時,就需要即給 A 一個 a,還需要讓 A 有能力從 Context 中拿到需要給 B 的 b。因此上下文的結構理應是遞迴定義的,是一個層級結構。

Context 如下定義:

public class Continuation
    {
        public Continuation SubContinuation { get; set; }
        public int NextStep { get; set; }
        public object Param { get; set; }
    }

    public class Context<T>
    {
        public Continuation Continuation { get; set; }
        public T Self { get; set; }
    }

public class Continuation
    {
        public Continuation SubContinuation { get; set; }
        public int NextStep { get; set; }
        public object Param { get; set; }
    }
 
    public class Context<T>
    {
        public Continuation Continuation { get; set; }
        public T Self { get; set; }
    }

修改 State 的介面定義為:

            
           

相關推薦

如何簡潔實現遊戲AI

端午節放假總結了一下好久前寫過的一些遊戲引擎,其中NPC等遊戲AI的實現無疑是最繁瑣的部分,現在,給大家分享一下:從一個簡單的情景開始怪物,是遊戲中的一個基本概念。遊戲中的單位分類,不外乎玩家、NPC、怪物這幾種。其中,AI 一定是與三類實體都會產生交集的遊戲模組之一。 以我

一文帶你實現遊戲的音樂、音效設定

在遊戲開發過程中,背景音樂和音效的設定總是繞不過的,今天就來帶大家實現一個簡單的背景音樂和音效的設定邏輯。   1.首先將音樂資源和圖片資源都匯入到工程中(公眾號後臺回覆「AudioTest」可獲得完整工程,圖片和音樂資源來自關東昇老師《Cocos2d-x實戰》,侵刪。):  

ARPG遊戲怪物AI實現

目前專案組正在做的是一款ARPG於MMO結合的遊戲,下面遊戲中AI實現的方式。 一 AI配置 1 配置說明 AI配置使用python指令碼,實現方式上使用偽行為樹的結構,實現約定好關鍵字的意思,結構如下: # ai配置說明文件 data =

使用行為樹(Behavior Tree)實現遊戲AI

方便 不同 sequence 理解 and while 記錄 策略 積累 談到遊戲AI,很明顯智能體擁有的知識條目越多,便顯得更智能,但維護龐大數量的知識條目是個噩夢:使用有限狀態機(FSM),分層有限狀態機(HFSM),決策樹(Decision Tree)來實現遊戲AI總

Unity3d遊戲實現阿拉伯語文字正常顯示

return dex p s != 漢語 div 發現 3d遊戲 let Unity3d遊戲中實現阿拉伯語文字正常顯示 由於項目需求要把遊戲文字顯示為維語版本(維語屬於阿拉伯語系),我先把維語替換進去,之後發現文字是錯的(每個字符都分開了,而且顯示方向也不對)後來在網上查了

實現記憶的經典遊戲-掃雷

game ini 遍歷 github play lob 判斷 mini src 摘要:《掃雷》是款風靡學校機房的智益遊戲。遊戲目標是在最短的時間內根據點擊格子出現的數字找出所有非雷格子。本文使用JS語言簡單完成了掃雷遊戲的核心功能。 一,遊戲規則   1)點擊有雷區,提示遊

【Unity3d遊戲開發】遊戲的貝塞爾曲線以及其在Unity實現

轉載收藏:原文連結https://www.cnblogs.com/msxh/p/6270468.html 閱讀目錄 一、簡介 二、公式 三、實現與應用   RT,馬三最近在參與一款足球遊戲的開發,其中涉及到足球的各種運動軌跡和路徑,比如射門的軌跡,高吊球

[原始碼分享]基於Python的Pygame庫實現的仿微信遊戲的飛機大戰小遊戲

不知大家是否還記得當時微信上風靡一時的打飛機小遊戲,通過控制我方飛機的上下左右移動,發射子彈來擊毀敵機,增加得分。這是一款簡單操作易上手又很有趣味性的遊戲,我使用python作為基本語言,利用pygame仿照微信版本完成了這款低配版飛機大戰遊戲。   我方飛機會按時的不斷髮射子彈,玩家通過上下左

[原始碼和文件分享]基於Python的Pygame庫實現的仿微信遊戲的飛機大戰小遊戲

不知大家是否還記得當時微信上風靡一時的打飛機小遊戲,通過控制我方飛機的上下左右移動,發射子彈來擊毀敵機,增加得分。這是一款簡單操作易上手又很有趣味性的遊戲,我使用python作為基本語言,利用pygame仿照微信版本完成了這款低配版飛機大戰遊戲。 我方飛機會按時的不斷髮射子彈,玩家通過上下左右的方向鍵來躲避

【Unity遊戲開發】用C#和Lua實現Unity的事件分發機制EventDispatcher

一、簡介   最近馬三換了一家大公司工作,公司制度規範了一些,因此平時的業餘時間多了不少。但是人卻懶了下來,最近這一個月都沒怎麼研究新技術,部落格寫得也是拖拖拉拉,週六周天就躺屍在家看帖子、看小說,要麼就是吃雞,唉!真是罪過罪過。希望能從這篇部落格開始有些改善吧,儘量少玩耍

[原始碼和文件分享]基於C#實現的支援AI人機博弈的國際象棋遊戲程式

1 背景和意義 1.1 專案意義 該專案的成功推進和完成將達到 AI 比賽過程自動化的目的,有助於比賽的順暢、成功開展以及比賽時間的有效節約 該專案的成果將有助於《人工智慧原理》課程的學生對於自己編寫的 AI 程式的測試 該專案的成果將有助於國際象棋 AI 的後續

在微信小遊戲實現語音互動

之前在unity裡嘗試用過語音控制,當時的想法是實時控制遊戲角色的移動與攻擊,這在通過線上api解析語義的方式下體驗一般,不過也想到在實時性要求不那麼高的互動場景應該可以用起來。這裡就在微信小遊戲中嘗試一下。 語音互動自然需要一個物件,像我這種手殘人士最適合的

遊戲AI演算法總結與改進

        圖論演算法將地圖結點和結點之間的通路資訊用有向或者無向鄰接矩陣來進行儲存,鄰接矩陣中相連的兩點對應值為1,不相連的對應值為0,同一個圖中可以有多種深度優先序列和廣度優先序列。在路徑搜尋中可以將相鄰兩點的對應值設定為兩者的距離權重值,在演算法搜尋結束後選擇距離最短的路徑分支,從而確定一條演算法上

利用tensorflow.js實現JSAI

非常感謝( Seth Juarez)[twitter.com/sethjuarez]提供的這篇文章。 這不是一篇關於數學的文章,也不是一篇闡述邪惡的有知覺的人工智慧最終會殺死我們所有人文章(我是不會訂閱這類的文章)。親愛的讀者們,這篇文章的目的是帶你走上一條別人未走過的路,那便是講述關於軟體工程的過程以及A

(Tensorflow1.0)強化學習實現遊戲AI(Demo_1)

http://blog.topspeedsnail.com/archives/10459在學習完這篇文章好,打算循序漸進的實現俄羅斯方塊AI和鬥地主AI,並且突破DQN,使用對抗神經網路來實現更強大的AI下面程式碼實現的是上面部落格的程式,發現了tensorflow1.0後

AI說人“畫” | Heart Broken, 遊戲AI碾壓的我們都了哪些套路?

大資料文摘力薦!原創小視訊【AI說人“畫”】系列。用輕鬆的手繪方式,講清楚一些有趣的AI概念。我

[原創]遊戲的實時排行榜實現

取值 ons 雙精度 this log cti 個人 動態 方案 目錄 1. 前言 2. 排行榜分類 3. 思路 4. 實現 復合排序 4.1 等級排行榜 4.2 通天塔排行榜 4.3 坦克排行榜 5. 排名數據的動態更新 6. 取排行榜 7. Show The Cod

即時戰略遊戲AI是怎樣實現的?

導讀:作為實際開發過AI的人,拿一份五年前的程式碼,以最難的體育競技類遊戲為例,來科普一下,什麼叫做遊戲團隊策略,什麼叫做分層狀態機?具體該如何落地到程式碼?如果你能實現體育競技的AI,那即時戰略只是小事一樁。   國內真正做過遊戲AI的很少,說概念的人很多,

遊戲角色ai實現方式討論

很久之前我曾經介紹過不少遊戲角色尋路方面實現的方法,但作為完整角色ai行為,我覺得比較難以介紹,首先這涉及到比較多的知識面,然後實現的方式也很多,比如有限狀態機、決策樹、神經網路等,我認為各有各的優缺點。最後,能實現這個完整過程的手段和框架設計也很多。所以一

利用觀察者模式實現Cocos2DX-lua遊戲的訊息管理系統

http://blog.csdn.net/operhero1990/article/details/48575487                遊戲中某些物件往往需要及時獲知其他特定物件狀體的改變。為降低類之間的耦合度,可以建立訊息管理系統,實現訊息的集中與分發。觀察者