如何簡(jiǎn)潔實(shí)現(xiàn)游戲中的AI

端午節(jié)放假總結(jié)了一下好久前寫(xiě)過(guò)的一些游戲引擎雌隅,其中NPC等游戲AI的實(shí)現(xiàn)無(wú)疑是最繁瑣的部分恰起,現(xiàn)在检盼,給大家分享一下:

從一個(gè)簡(jiǎn)單的情景開(kāi)始

怪物吨枉,是游戲中的一個(gè)基本概念哄芜。游戲中的單位分類(lèi)认臊,不外乎玩家、NPC剧腻、怪物這幾種书在。其中胯陋,AI 一定是與三類(lèi)實(shí)體都會(huì)產(chǎn)生交集的游戲模塊之一遏乔。
以我們熟悉的任意一款游戲中的人形怪物為例,假設(shè)有一種怪物的 AI 需求是這樣的:

  •  大部分情況下盟萨,漫無(wú)目的巡邏捻激。
    
  •  玩家進(jìn)入視野,鎖定玩家為目標(biāo)開(kāi)始攻擊垃杖。
    
  •  Hp 低到一定程度调俘,怪會(huì)想法設(shè)法逃跑旺垒,并說(shuō)幾句話先蒋。
    

我們以這個(gè)為模型,進(jìn)行這篇文章之后的所有討論眯搭。為了簡(jiǎn)化問(wèn)題坦仍,以省去一些不必要的討論叨襟,將文章的核心定位到人工智能上糊闽,這里需要注意幾點(diǎn)的是:

  •  不再考慮 entity 之間的消息傳遞機(jī)制,例如判斷玩家進(jìn)入視野提澎,不再通過(guò)事件機(jī)制觸發(fā)盼忌,而是通過(guò)該人形怪的輪詢觸發(fā)。
    
  •  不再考慮 entity 的行為控制機(jī)制看成,簡(jiǎn)化這個(gè) entity 的控制模型跨嘉。不論是底層是基于 SteeringBehaviour 或者是瞬移祠乃,不論是異步驅(qū)的還是主循環(huán)輪詢,都不在本文模型的討論之列琴拧。
    

首先可以很容易抽象出來(lái) 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();
    }

然后蚓胸,我們可以通過(guò)一個(gè)簡(jiǎn)單的有限狀態(tài)機(jī) (FSM) 來(lái)控制這個(gè)單位的行為赢织。不同狀態(tài)下馍盟,單位都具有不同的行為準(zhǔn)則贞岭,以形成智能體。
具體來(lái)說(shuō)话速,我們可以定義這樣幾種狀態(tài):

  •  巡邏狀態(tài): 會(huì)執(zhí)行巡邏泊交,同時(shí)檢查是否有敵對(duì)單位接近柱查,接近的話進(jìn)入戰(zhàn)斗狀態(tài)。
    
  •  戰(zhàn)斗狀態(tài): 會(huì)執(zhí)行戰(zhàn)斗研乒,同時(shí)檢查自己的血量是否達(dá)到逃跑線以下雹熬,達(dá)成檢查了就會(huì)逃跑。
    
  •  逃跑狀態(tài): 會(huì)逃跑铅乡,同時(shí)說(shuō)一次話仰楚。
    

最原始的狀態(tài)機(jī)的代碼:

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

以逃跑狀態(tài)為例:

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

決策邏輯與上下文分離

上述是一個(gè)最簡(jiǎn)單僧界、最常規(guī)的狀態(tài)機(jī)實(shí)現(xiàn)捂襟。估計(jì)只有學(xué)生會(huì)這樣寫(xiě)葬荷,業(yè)界肯定是沒(méi)人這樣寫(xiě) AI 的纽帖,不然游戲怎么死的都不知道懊直。

首先有一個(gè)非常明顯的性能問(wèn)題:狀態(tài)機(jī)本質(zhì)是描述狀態(tài)遷移的,并不需要記錄 entity 的 context雕崩,如果 entity 的 context 記錄在 State上盼铁,那么狀態(tài)機(jī)這個(gè)遷移邏輯就需要每個(gè) entity 都來(lái)一份 instance尝偎,這么一個(gè)簡(jiǎn)單的狀態(tài)遷移就需要消耗大約 X 個(gè)字節(jié)致扯,那么一個(gè)場(chǎng)景 1w 個(gè)怪,這些都屬于白白消耗的內(nèi)存醒陆。就目前的實(shí)現(xiàn)來(lái)看刨摩,具體的一個(gè) State 實(shí)例內(nèi)部 hold 住了 Unit,所以 State 實(shí)例是沒(méi)辦法復(fù)用的澡刹。

針對(duì)這一點(diǎn)罢浇,我們做一下優(yōu)化。對(duì)這個(gè)狀態(tài)機(jī)攒岛,把 Context 完全剝離出來(lái)灾锯。

修改狀態(tài)機(jī)接口定義:

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

還是拿之前實(shí)現(xiàn)好的逃跑狀態(tài)作為例子:

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

這樣顺饮,就區(qū)分了動(dòng)態(tài)與靜態(tài)兼雄。靜態(tài)的是狀態(tài)之間的遷移邏輯赦肋,只要不做熱更新嘲碱,是不會(huì)變的結(jié)構(gòu)。動(dòng)態(tài)的是狀態(tài)遷移過(guò)程中的上下文恕稠,根據(jù)不同的上下文來(lái)決定鹅巍。

分層有限狀態(tài)機(jī)

最原始的狀態(tài)機(jī)方案除了性能存在問(wèn)題料祠,還有一個(gè)比較嚴(yán)重的問(wèn)題髓绽。那就是這種狀態(tài)機(jī)框架無(wú)法描述層級(jí)結(jié)構(gòu)的狀態(tài)。
假設(shè)需要對(duì)一開(kāi)始的需求進(jìn)行這樣的擴(kuò)展:怪在巡邏狀態(tài)下有可能進(jìn)入怠工狀態(tài)枫攀,同時(shí)要求,怠工狀態(tài)下也會(huì)進(jìn)行進(jìn)入戰(zhàn)斗的檢查图焰。

這樣的話技羔,雖然在之前的框架下卧抗,單獨(dú)做一個(gè)新的怠工狀態(tài)也可以,但是仔細(xì)分析一下超陆,我們會(huì)發(fā)現(xiàn)浦马,其實(shí)本質(zhì)上巡邏狀態(tài)只是一個(gè)抽象的父狀態(tài)晶默,其存在的意義就是進(jìn)行戰(zhàn)斗檢查磺陡;而具體的是在按路線巡邏還是怠工漠畜,其實(shí)都是巡邏狀態(tài)的一個(gè)子狀態(tài)。

狀態(tài)之間就有了層級(jí)的概念蝴悉,各自獨(dú)立的狀態(tài)機(jī)系統(tǒng)就無(wú)法滿足需求拍冠,需要一種分層次的狀態(tài)機(jī)簇抵,原先的狀態(tài)機(jī)接口設(shè)計(jì)就需要徹底改掉了碟摆。

在重構(gòu)狀態(tài)框架之前,需要注意兩點(diǎn):

因?yàn)楦笭顟B(tài)需要關(guān)注子狀態(tài)的運(yùn)行結(jié)果断盛,所以狀態(tài)的 Drive 接口需要一個(gè)運(yùn)行結(jié)果的返回值。

子狀態(tài)栖博,比如怠工厢洞,一定是有跨幀的需求在的躺翻,所以這個(gè) Result,我們定義為 Continue踊淳、Sucess迂尝、Failure剪芥。

子狀態(tài)一定是由父狀態(tài)驅(qū)動(dòng)的税肪。

考慮這樣一個(gè)組合狀態(tài)情景:巡邏時(shí),需要依次得先走到一個(gè)點(diǎn)锻梳,然后怠工一會(huì)兒净捅,再走到下一個(gè)點(diǎn)灸叼,然后再怠工一會(huì)兒,循環(huán)往復(fù)屁魏。這樣就需要父狀態(tài)(巡邏狀態(tài))注記當(dāng)前激活的子狀態(tài)氓拼,并且根據(jù)子狀態(tài)執(zhí)行結(jié)果的不同來(lái)修改激活的子狀態(tài)集合。這樣不僅是 Unit 自身有上下文坏匪,連組合狀態(tài)也有了自己的上下文适滓。

為了簡(jiǎn)化討論恋追,我們還是從 non-ContextFree 層次狀態(tài)機(jī)系統(tǒng)設(shè)計(jì)開(kāi)始苦囱。

修改后的狀態(tài)定義:

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

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

組合狀態(tài)的定義:

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;
        }
        // ...
    }

巡邏狀態(tài)現(xiàn)在是一個(gè)組合狀態(tài):

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

看過(guò)《游戲人工智能編程精粹》的同學(xué)可能看到這里就會(huì)發(fā)現(xiàn),這種層次狀態(tài)機(jī)其實(shí)就是這本書(shū)里講的目標(biāo)驅(qū)動(dòng)的狀態(tài)機(jī)羹铅。組合狀態(tài)就是組合目標(biāo),子狀態(tài)就是子目標(biāo)造锅。父目標(biāo) / 狀態(tài)的調(diào)度取決于子目標(biāo) / 狀態(tài)的完成情況。

這種狀態(tài)框架與普通的 trivial 狀態(tài)機(jī)模型的區(qū)別僅僅是增加了對(duì)層次狀態(tài)的支持倒谷,狀態(tài)的遷移還是需要靠顯式的 ChangeState 來(lái)做。

這本書(shū)里面的狀態(tài)框架牵祟,每個(gè)狀態(tài)的執(zhí)行 status 記錄在了實(shí)例內(nèi)部诺苹,不方便后續(xù)的優(yōu)化雹拄,我們這里實(shí)現(xiàn)的時(shí)候首先把這個(gè)做成純驅(qū)動(dòng)式的。但是還不夠∑汉澹現(xiàn)在之前的 ContextFree 優(yōu)化成果已經(jīng)回退掉了翩肌,我們還需要補(bǔ)充回來(lái)。

分層的上下文

我們對(duì)之前重構(gòu)出來(lái)的層次狀態(tài)機(jī)框架再進(jìn)行一次 Context 分離優(yōu)化兑宇。
要優(yōu)化的點(diǎn)有這樣幾個(gè):

首先是繼續(xù)之前的顾孽,unit 不應(yīng)該作為一個(gè) state 自己的內(nèi)部 status比规。

組合狀態(tài)的實(shí)例內(nèi)部不應(yīng)該包括自身執(zhí)行的 status。目前的組合狀態(tài)测秸,可以動(dòng)態(tài)增刪子狀態(tài),也就是根據(jù) status 決定了結(jié)構(gòu)的狀態(tài)钞瀑,理應(yīng)分離靜態(tài)與動(dòng)態(tài)雕什。巡邏狀態(tài)組合了兩個(gè)子狀態(tài)——A 和 B壹士,邏輯中是一個(gè)完成了就添加另一個(gè)偿警,這樣一想的話,其實(shí)巡邏狀態(tài)應(yīng)該重新描述——先進(jìn)行 A,再進(jìn)行 B,循環(huán)往復(fù)抄瑟。
  
  由于有了父狀態(tài)的概念枉疼,其實(shí)狀態(tài)接口的設(shè)計(jì)也可以再迭代骂维,理論上只需要一個(gè) drive 即可贺纲。因?yàn)闋顟B(tài)內(nèi)部的上下文要全部分離出來(lái),所以也沒(méi)必要對(duì)外提供 OnEnter懈叹、OnExit澄成,提供這兩個(gè)接口的意義只是做一層內(nèi)部信息的隱藏,但是現(xiàn)在內(nèi)部的 status 沒(méi)了肾砂,也就沒(méi)必要隱藏了镐确。
  
具體分析一下需要拆出的 status:

  • 一部分是 entity 本身的 status,這里可以簡(jiǎn)單的認(rèn)為是 unit臼氨。
  • 另一部分是 state 本身的 status储矩。
  • 對(duì)于組合狀態(tài),這個(gè) status 描述的是我當(dāng)前執(zhí)行到哪個(gè) substate屡拨。
  • 對(duì)于原子狀態(tài),這個(gè) status 描述的種類(lèi)可能有所區(qū)別哥艇。
  • 例如 MoveTo/Flee十饥,OnEnter 的時(shí)候,修改了 unit 的 status,然后 Drive 的時(shí)候去 check隙赁。
  • 例如 Idle,OnEnter 時(shí)改了自己的 status厚掷,然后 Drive 的時(shí)候去 check田绑。
    經(jīng)過(guò)總結(jié),我們可以發(fā)現(xiàn)冬竟,每個(gè)狀態(tài)的 status 本質(zhì)上都可以通過(guò)一個(gè)變量來(lái)描述。一個(gè) State 作為一個(gè)最小粒度的單元调缨,具有這樣的 Concept: 輸入一個(gè) Context,輸出一個(gè) Result。

Context 暫時(shí)只需要包括這個(gè) Unit,和之前所說(shuō)的 status默责。同時(shí),考慮這樣一個(gè)問(wèn)題:

  • 父狀態(tài) A,子狀態(tài) B芦鳍。
  • 子狀態(tài) B 向上返回 Continue 的同時(shí),status 記錄下來(lái)為 b菲宴。
  • 父狀態(tài) ADrive 子狀態(tài)的結(jié)果為 Continue势誊,自身也需要向上拋出 Continue,同時(shí)自己也有 status 為 a勋颖。
    這樣侥祭,再還原現(xiàn)場(chǎng)時(shí),就需要即給 A 一個(gè) a,還需要讓 A 有能力從 Context 中拿到需要給 B 的 b窑滞。因此上下文的結(jié)構(gòu)理應(yīng)是遞歸定義的,是一個(gè)層級(jí)結(jié)構(gòu)撬槽。

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 的接口定義為:

public interface IState<TCleverUnit, TResult>
     {
         TResult Drive(Context<TCleverUnit> ctx);
    }

public interface IState<TCleverUnit, TResult>
     {
         TResult Drive(Context<TCleverUnit> ctx);
    }

已經(jīng)相當(dāng)簡(jiǎn)潔了暂题。

這樣纵苛,我們對(duì)之前的巡邏狀態(tài)也做下修改,達(dá)到一個(gè) ContextFree 的效果贝椿。利用 Context 中的 Continuation 來(lái)確定當(dāng)前結(jié)點(diǎn)應(yīng)該從什么狀態(tài)繼續(xù):

public class PatrolState : IState<ICleverUnit, Result>
    {
        private readonly List<IState<ICleverUnit, Result>> subStates;
        public PatrolState()
        {
            subStates = new List<IState<ICleverUnit, Result>>()
            {
                new MoveToState(),
                new IdleState(),
            };
        }
        public Result Drive(Context<ICleverUnit> ctx)
        {
            var unit = ctx.Self.GetNearestTarget();
            if (unit != null)
            {
                ctx.Self.LockTarget(unit);

                return Result.Success;
            }

            var nextStep = 0;
            if (ctx.Continuation != null)
            {
                // Continuation
                var thisContinuation = ctx.Continuation;

                ctx.Continuation = thisContinuation.SubContinuation;

                var ret = subStates[nextStep].Drive(ctx);

                if (ret == Result.Continue)
                {
                    thisContinuation.SubContinuation = ctx.Continuation;
                    ctx.Continuation = thisContinuation;

                    return Result.Continue;
                }
                else if (ret == Result.Failure)
                {
                    ctx.Continuation = null;

                    return Result.Failure;
                }

                ctx.Continuation = null;
                nextStep = thisContinuation.NextStep + 1;
            }

            for (; nextStep < subStates.Count; nextStep++)
            {
                var ret = subStates[nextStep].Drive(ctx);
                if (ret == Result.Continue)
                {
                    ctx.Continuation = new Continuation()
                    {
                        SubContinuation = ctx.Continuation,
                        NextStep = nextStep,
                    };

                    return Result.Continue;
                } 
                else if (ret == Result.Failure) 
                {
                    ctx.Continuation = null;

                    return Result.Failure;
                }
            }

            ctx.Continuation = null;

            return Result.Success;
        }
    }
public class PatrolState : IState<ICleverUnit, Result>
    {
        private readonly List<IState<ICleverUnit, Result>> subStates;
        public PatrolState()
        {
            subStates = new List<IState<ICleverUnit, Result>>()
            {
                new MoveToState(),
                new IdleState(),
            };
        }
        public Result Drive(Context<ICleverUnit> ctx)
        {
            var unit = ctx.Self.GetNearestTarget();
            if (unit != null)
            {
                ctx.Self.LockTarget(unit);
 
                return Result.Success;
            }
 
            var nextStep = 0;
            if (ctx.Continuation != null)
            {
                // Continuation
                var thisContinuation = ctx.Continuation;
 
                ctx.Continuation = thisContinuation.SubContinuation;
 
                var ret = subStates[nextStep].Drive(ctx);
 
                if (ret == Result.Continue)
                {
                    thisContinuation.SubContinuation = ctx.Continuation;
                    ctx.Continuation = thisContinuation;
 
                    return Result.Continue;
                }
                else if (ret == Result.Failure)
                {
                    ctx.Continuation = null;
 
                    return Result.Failure;
                }
 
                ctx.Continuation = null;
                nextStep = thisContinuation.NextStep + 1;
            }
 
            for (; nextStep < subStates.Count; nextStep++)
            {
                var ret = subStates[nextStep].Drive(ctx);
                if (ret == Result.Continue)
                {
                    ctx.Continuation = new Continuation()
                    {
                        SubContinuation = ctx.Continuation,
                        NextStep = nextStep,
                    };
 
                    return Result.Continue;
                } 
                else if (ret == Result.Failure) 
                {
                    ctx.Continuation = null;
 
                    return Result.Failure;
                }
            }
 
            ctx.Continuation = null;
 
            return Result.Success;
        }
    }

subStates 是 readonly 的,在組合狀態(tài)構(gòu)造的一開(kāi)始就確定了值访雪。這樣結(jié)構(gòu)本身就是靜態(tài)的坝橡,而上下文是動(dòng)態(tài)的计寇。不同的 entity instance 共用同一個(gè)樹(shù)的 instance元莫。

優(yōu)化到這個(gè)版本柒竞,至少在性能上已經(jīng)符合要求了,所有實(shí)例共享一個(gè)靜態(tài)的狀態(tài)遷移邏輯稼虎。面對(duì)之前提出的需求霎俩,也能夠解決。至少算是一個(gè)經(jīng)過(guò)對(duì)《游戲人工智能編程精粹》中提出的目標(biāo)驅(qū)動(dòng)狀態(tài)機(jī)模型優(yōu)化后的一個(gè)符合工業(yè)應(yīng)用標(biāo)準(zhǔn)的 AI 框架。拿來(lái)做小游戲或者是一些 AI 很簡(jiǎn)單的游戲已經(jīng)綽綽有余了捌肴。

心動(dòng)了嗎?還不趕緊動(dòng)起來(lái)孽查,打造屬于自己的游戲世界起宽!頓時(shí)滿滿的自豪感绿映,真的很想知道大家的想法叉弦,還請(qǐng)持續(xù)關(guān)注更新库车,更多干貨和資料請(qǐng)直接聯(lián)系我柠衍,也可以加群710520381,邀請(qǐng)碼:柳貓,歡迎大家共同討論

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末履怯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帜篇,老刑警劉巖笙隙,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掏呼,死亡現(xiàn)場(chǎng)離奇詭異憎夷,居然都是意外死亡兔沃,警方通過(guò)查閱死者的電腦和手機(jī)乒疏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)械哟,“玉大人,你說(shuō)我怎么就攤上這事爸业。” “怎么了钧忽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么蛋叼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮顽爹,結(jié)果婚禮上镜粤,老公的妹妹穿的比我還像新娘肉渴。我一直安慰自己,他們只是感情好绪钥,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布儒拂。 她就那樣靜靜地躺著见转,像睡著了一般池户。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赊抖,一...
    開(kāi)封第一講書(shū)人閱讀 52,736評(píng)論 1 312
  • 那天寨典,我揣著相機(jī)與錄音耸成,去河邊找鬼。 笑死弦追,一個(gè)胖子當(dāng)著我的面吹牛花竞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播零远,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼厌蔽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奴饮!你這毒婦竟也來(lái)了罢猪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后拼坎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡余舶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年赂摆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹤树,死狀恐怖曲伊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情单雾,我是刑警寧澤屿储,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布祖屏,位于F島的核電站雹食,受9級(jí)特大地震影響钝荡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜梁剔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砾省。 院中可真熱鬧声登,春花似錦件舵、人聲如沸坑质。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至垮耳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芯咧,已是汗流浹背芬位。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工揽惹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓撼泛,卻偏偏與公主長(zhǎng)得像损俭,于是被迫代替她去往敵國(guó)和親仔夺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子日裙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器朝墩,智...
    卡卡羅2017閱讀 134,716評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,867評(píng)論 6 342
  • 羨慕 月光的手 憑兩只袖管 奏起黑夜 點(diǎn)點(diǎn)音符 是輕彈的淚珠 還是逃離的心結(jié) 沒(méi)有清脆的答案 只有節(jié)拍 和跟著節(jié)拍...
    何事亂翻書(shū)閱讀 203評(píng)論 0 3
  • 缺愛(ài):對(duì)于愛(ài)情,缺愛(ài)的孩子比較“慢熱”扣泊,心里有恐懼感阱飘,害怕如果我投入了結(jié)果會(huì)怎樣…… 但是一旦投入了蔗喂,就會(huì)比較偏激...
    嬌之語(yǔ)閱讀 15,317評(píng)論 0 4
  • 【讀書(shū)10分鐘】 工作生活更輕松 秦剛老師在《創(chuàng)業(yè)僅半年,如何融資超1.5億》中分享了: 一位互聯(lián)網(wǎng)業(yè)內(nèi)的傳奇人物...
    璇轉(zhuǎn)閱讀 134評(píng)論 0 0