久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

漫談?dòng)螒蛑械娜斯ぶ悄?/span>

 勤奮不止 2016-01-29
 本帖最后由 一元 于 2015-11-18 10:47 編輯

漫談?dòng)螒蛑械娜斯ぶ悄? title=


  GameRes游資網(wǎng)授權(quán)發(fā)布 文 / 王迅

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

  怪物,是游戲中的一個(gè)基本概念,。游戲中的單位分類,,不外乎玩家、NPC,、怪物這幾種,。其中,AI一定是與三類實(shí)體都會(huì)產(chǎn)生交集的游戲模塊之一,。

  以我們熟悉的任意一款游戲中的人形怪物為例,,假設(shè)有一種怪物的AI需求是這樣的:

  • 大部分情況下,,漫無目的巡邏,。
  • 玩家進(jìn)入視野,鎖定玩家為目標(biāo)開始攻擊,。
  • Hp低到一定程度,,怪會(huì)想法設(shè)法逃跑,并說幾句話,。

  我們以這個(gè)為模型,,進(jìn)行這篇文章之后的所有討論。為了簡(jiǎn)化問題,,以省去一些不必要的討論,,將文章的核心定位到人工智能上,這里需要注意幾點(diǎn)的是:
  • 不再考慮entity之間的消息傳遞機(jī)制,,例如判斷玩家進(jìn)入視野,,不再通過事件機(jī)制觸發(fā),而是通過該人形怪的輪詢觸發(fā),。
  • 不再考慮entity的行為控制機(jī)制,,簡(jiǎn)化這個(gè)entity的控制模型。不論是底層是基于SteeringBehaviour或者是瞬移,,不論是異步驅(qū)的還是主循環(huán)輪詢,,都不在本文模型的討論之列,。

  首先可以很容易抽象出來IUnit:
  1.      public interface IUnit
  2.      {
  3.          void ChangeState(UnitStateEnum state);
  4.          void Patrol();
  5.          IUnit GetNearestTarget();
  6.         void LockTarget(IUnit unit);
  7.         float GetFleeBloodRate();
  8.          bool CanMove();
  9.          bool HpRateLessThan(float rate);
  10.         void Flee();
  11.          void Speak();
  12.      }
復(fù)制代碼

  然后,我們可以通過一個(gè)簡(jiǎn)單的有限狀態(tài)機(jī)(FSM)來控制這個(gè)單位的行為,。不同狀態(tài)下,,單位都具有不同的行為準(zhǔn)則,以形成智能體,。

  具體來說,,我們可以定義這樣幾種狀態(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í)說一次話,。

  最原始的狀態(tài)機(jī)的代碼:
  1. public interface IUnit
  2. {
  3. void ChangeState(UnitStateEnum state);
  4. void Patrol();
  5. IUnit GetNearestTarget();
復(fù)制代碼

  以逃跑狀態(tài)為例:
  1.      public class FleeState : UnitStateBase
  2.      {
  3.          public FleeState(IUnit self) : base(UnitStateEnum.Flee, self)
  4.          {
  5.          }
  6.          public override void OnEnter()
  7.          {
  8.              Self.Flee();
  9.         }
  10.          public override void Drive()
  11.          {
  12.              var unit = Self.GetNearestTarget();
  13.              if (unit != null)
  14.             {
  15.                 return;
  16.              }

  17.              Self.ChangeState(UnitStateEnum.Patrol);
  18.          }
  19.      }
復(fù)制代碼

  決策邏輯與上下文分離

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

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

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

  修改狀態(tài)機(jī)接口定義:
  1. public interface IState<TState, TUnit> where TState : IConvertible
  2. {
  3. TState Enum { get; }
  4. void OnEnter(TUnit self);
  5. void Drive(TUnit self);
  6. void OnExit(TUnit self);
  7. }
復(fù)制代碼

  還是拿之前實(shí)現(xiàn)好的逃跑狀態(tài)作為例子:
  1. public class FleeState : UnitStateBase
  2. {
  3. public FleeState() : base(UnitStateEnum.Flee)
  4. {
  5. }
  6. public override void OnEnter(IUnit self)
  7. {
  8. base.OnEnter(self);
  9. self.Flee();
  10. }
  11. public override void Drive(IUnit self)
  12. {
  13. base.Drive(self);

  14. var unit = self.GetNearestTarget();
  15. if (unit != null)
  16. {
  17. return;
  18. }

  19. self.ChangeState(UnitStateEnum.Patrol);
  20. }
  21. }
復(fù)制代碼

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

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

  最原始的狀態(tài)機(jī)方案除了性能存在問題,還有一個(gè)比較嚴(yán)重的問題,。那就是這種狀態(tài)機(jī)框架無法描述層級(jí)結(jié)構(gòu)的狀態(tài),。

  假設(shè)需要對(duì)一開始的需求進(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)就無法滿足需求,,需要一種分層次的狀態(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é)果的不同來修改激活的子狀態(tài)集合,。這樣不僅是Unit自身有上下文,,連組合狀態(tài)也有了自己的上下文。

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

  修改后的狀態(tài)定義:
  1. public interface IState<TState, TCleverUnit, TResult>
  2. where TState : IConvertible
  3. {
  4. // ...
  5. TResult Drive();
  6. // ...
  7. }  
復(fù)制代碼

  組合狀態(tài)的定義:
  1. public abstract class UnitCompositeStateBase : UnitStateBase
  2. {
  3. protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>();

  4. // ...
  5. protected Result ProcessSubStates()
  6. {
  7. if (subStates.Count == 0)
  8. {
  9. return Result.Success;
  10. }

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

  13. if (res != Result.Continue)
  14. {
  15. subStates.RemoveFirst();
  16. }

  17. return Result.Continue;
  18. }
  19. // ...
  20. }
復(fù)制代碼

  巡邏狀態(tài)現(xiàn)在是一個(gè)組合狀態(tài):
  1. public class PatrolState : UnitCompositeStateBase
  2. {
  3. // ...
  4. public override void OnEnter()
  5. {
  6. base.OnEnter();
  7. AddSubState(new MoveToState(Self));
  8. }

  9. public override Result Drive()
  10. {
  11. if (subStates.Count == 0)
  12. {
  13. return Result.Success;
  14. }

  15. var unit = Self.GetNearestTarget();
  16. if (unit != null)
  17. {
  18. Self.LockTarget(unit);
  19. return Result.Success;
  20. }

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

  23. if (ret != Result.Continue)
  24. {
  25. if (front.Value.Enum == CleverUnitStateEnum.MoveTo)
  26. {
  27. AddSubState(new IdleState(Self));
  28. }
  29. else
  30. {
  31. AddSubState(new MoveToState(Self));
  32. }
  33. }
  34.       
  35. return Result.Continue;
  36. }
  37. }
復(fù)制代碼

  看過《游戲人工智能編程精粹》的同學(xué)可能看到這里就會(huì)發(fā)現(xiàn),這種層次狀態(tài)機(jī)其實(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來做。

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

  分層的上下文

  我們對(duì)之前重構(gòu)出來的層次狀態(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)部的上下文要全部分離出來,,所以也沒必要對(duì)外提供OnEnter,、OnExit,提供這兩個(gè)接口的意義只是做一層內(nèi)部信息的隱藏,,但是現(xiàn)在內(nèi)部的status沒了,,也就沒必要隱藏了,。

  具體分析一下需要拆出的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描述的種類可能有所區(qū)別。
    例如MoveTo/Flee,,OnEnter的時(shí)候,,修改了unit的status,然后Drive的時(shí)候去check,。
    例如Idle,,OnEnter時(shí)改了自己的status,然后Drive的時(shí)候去check,。
  經(jīng)過總結(jié),,我們可以發(fā)現(xiàn),每個(gè)狀態(tài)的status本質(zhì)上都可以通過一個(gè)變量來描述,。一個(gè)State作為一個(gè)最小粒度的單元,,具有這樣的Concept: 輸入一個(gè)Context,輸出一個(gè)Result,。

  Context暫時(shí)只需要包括這個(gè)Unit,,和之前所說的status。同時(shí),,考慮這樣一個(gè)問題:
  • 父狀態(tài)A,,子狀態(tài)B。
  • 子狀態(tài)B向上返回Continue的同時(shí),,status記錄下來為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如下定義:
  1. public class Continuation
  2. {
  3. public Continuation SubContinuation { get; set; }
  4. public int NextStep { get; set; }
  5. public object Param { get; set; }
  6. }

  7. public class Context<T>
  8. {
  9. public Continuation Continuation { get; set; }
  10. public T Self { get; set; }
  11. }
復(fù)制代碼

  修改State的接口定義為:
  1. public interface IState<TCleverUnit, TResult>
  2. {
  3. TResult Drive(Context<TCleverUnit> ctx);
  4. }
復(fù)制代碼

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

  這樣,,我們對(duì)之前的巡邏狀態(tài)也做下修改,,達(dá)到一個(gè)ContextFree的效果。利用Context中的Continuation來確定當(dāng)前結(jié)點(diǎn)應(yīng)該從什么狀態(tài)繼續(xù):
  1. public class PatrolState : IState<ICleverUnit, Result>
  2. {
  3. private readonly List<IState<ICleverUnit, Result>> subStates;
  4. public PatrolState()
  5. {
  6. subStates = new List<IState<ICleverUnit, Result>>()
  7. {
  8. new MoveToState(),
  9. new IdleState(),
  10. };
  11. }
  12. public Result Drive(Context<ICleverUnit> ctx)
  13. {
  14. var unit = ctx.Self.GetNearestTarget();
  15. if (unit != null)
  16. {
  17. ctx.Self.LockTarget(unit);

  18. return Result.Success;
  19. }

  20. var nextStep = 0;
  21. if (ctx.Continuation != null)
  22. {
  23. // Continuation
  24. var thisContinuation = ctx.Continuation;

  25. ctx.Continuation = thisContinuation.SubContinuation;

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

  27. if (ret == Result.Continue)
  28. {
  29. thisContinuation.SubContinuation = ctx.Continuation;
  30. ctx.Continuation = thisContinuation;

  31. return Result.Continue;
  32. }
  33. else if (ret == Result.Failure)
  34. {
  35. ctx.Continuation = null;

  36.                     return Result.Failure;
  37.                }

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

  41.              for (; nextStep < subStates.Count; nextStep++)
  42.             {
  43.                 var ret = subStates[nextStep].Drive(ctx);
  44.                 if (ret == Result.Continue)
  45.                 {
  46.                     ctx.Continuation = new Continuation()
  47.                     {
  48.                         SubContinuation = ctx.Continuation,
  49.                         NextStep = nextStep,
  50.                     };

  51.                     return Result.Continue;
  52.                  }
  53.                  else if (ret == Result.Failure)
  54.                 {
  55.                     ctx.Continuation = null;

  56.                      return Result.Failure;
  57.                 }
  58.             }

  59.              ctx.Continuation = null;

  60.              return Result.Success;
  61.         }
  62.      }
復(fù)制代碼

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

  語義結(jié)點(diǎn)的抽象

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

  不過我們?cè)谶@篇博客的討論中是不能僅停留在能解決需求的層面上,。目前的方案至少還存在一個(gè)比較嚴(yán)重的問題,,那就是邏輯復(fù)用性太差。組合狀態(tài)需要coding的邏輯太多了,,具體的狀態(tài)內(nèi)部邏輯需要人肉維護(hù),,更可怕的是需要程序員來人肉維護(hù),再多幾個(gè)組合狀態(tài)簡(jiǎn)直不敢想象,。程序員真的沒這么多時(shí)間維護(hù)這些東西好么,。所以我們應(yīng)該嘗試抽象一下組合狀態(tài)是否有一些通用的設(shè)計(jì)pattern。

  為了解決這個(gè)問題,,我們?cè)賹?duì)這幾個(gè)狀態(tài)的分析一下,,可以對(duì)結(jié)點(diǎn)類型進(jìn)行一下歸納。

  結(jié)點(diǎn)基本上是分為兩個(gè)類型:組合結(jié)點(diǎn),、原子結(jié)點(diǎn),。

  如果把這個(gè)狀態(tài)遷移邏輯體看做一個(gè)樹結(jié)構(gòu),那其中組合結(jié)點(diǎn)就是非葉子結(jié)點(diǎn),,原子結(jié)點(diǎn)就是葉子結(jié)點(diǎn),。

  對(duì)于組合結(jié)點(diǎn)來說,其行為是可以歸納的,。
  • 巡邏結(jié)點(diǎn),,不考慮觸發(fā)進(jìn)入戰(zhàn)斗的邏輯,可以歸納為一種具有這樣的行為的組合結(jié)點(diǎn):依次執(zhí)行每個(gè)子結(jié)點(diǎn)(移動(dòng)到某個(gè)點(diǎn),、休息一會(huì)兒),,某個(gè)子結(jié)點(diǎn)返回Success則執(zhí)行下一個(gè),返回Failure則直接向上返回,返回Continue就把Continuation拋出去,。命名具有這樣語義的結(jié)點(diǎn)為Sequence,。
  • 設(shè)想攻擊狀態(tài)下,單位需要同時(shí)進(jìn)行兩種子結(jié)點(diǎn)的嘗試,,一個(gè)是釋放技能,,一個(gè)是說話。兩個(gè)需要同時(shí)執(zhí)行,,并且結(jié)果獨(dú)立,。有一個(gè)返回Success則向上返回Success,全部Failure則返回Failure,,否則返回Continue,。命名具有如此語義的結(jié)點(diǎn)為Parallel。
  • 在Parallel的語義基礎(chǔ)上,,如果要體現(xiàn)一個(gè)優(yōu)先級(jí)/順序性質(zhì),,那么就需要一個(gè)具有依次執(zhí)行子結(jié)點(diǎn)語義的組合結(jié)點(diǎn),命名為Select,。

  Sequence與Select組合起來,,就能完整的描述一”趟“巡邏,Select(ReactAttack, Sequence(MoveTo, Idle)),,可以直接干掉之前寫的Patrol組合狀態(tài),,組合狀態(tài)直接拿現(xiàn)成的實(shí)現(xiàn)好的語義結(jié)點(diǎn)復(fù)用即可。

  組合結(jié)點(diǎn)的抽象問題解決了,,現(xiàn)在我們來看葉子結(jié)點(diǎn),。

  葉子結(jié)點(diǎn)也可以歸納一下pattern,能歸納出三種:
  • Flee,、Idle,、MoveTo三個(gè)狀態(tài),狀態(tài)進(jìn)入的時(shí)候調(diào)一下宿主的某個(gè)函數(shù),,申請(qǐng)開始一個(gè)持續(xù)性的動(dòng)作,。
  • 四個(gè)原子狀態(tài)都有的一個(gè)pattern,就是在Drive中輪詢,,直到某個(gè)條件達(dá)成了才返回,。
  • Attack狀態(tài)內(nèi)部,每次都輪詢都會(huì)向宿主請(qǐng)求一個(gè)數(shù)據(jù),,然后再判斷這個(gè)“外部”數(shù)據(jù)是否滿足一定條件,。

  pattern確實(shí)是有這么三種,但是葉子結(jié)點(diǎn)自身其實(shí)是兩種,,一種是控制單位做某種行為,,一種是向單位查詢一些信息,,其實(shí)本質(zhì)上是沒區(qū)別的,,只是描述問題的方式不一樣,。

  既然我們的最終目標(biāo)是消除掉四個(gè)具體狀態(tài)的定義,轉(zhuǎn)而通過一些通用的語義結(jié)點(diǎn)來描述,,那我們就首先需要想辦法提出一種方案來描述上述的三個(gè)pattern,。

  前兩個(gè)pattern其實(shí)是同一個(gè)問題,區(qū)別就在于那些邏輯應(yīng)該放在宿主提供的接口里面做實(shí)現(xiàn),,哪些邏輯應(yīng)該在AI模塊里做實(shí)現(xiàn),。調(diào)用宿主的某個(gè)函數(shù),調(diào)用是一個(gè)瞬間的操作,,直接改變了宿主的status,,但是截止點(diǎn)的判斷就有不同的實(shí)現(xiàn)方式了。
  • 一種實(shí)現(xiàn)是宿主的API本身就是一個(gè)返回Result的函數(shù),,第一次調(diào)用的時(shí)候,,宿主會(huì)改變自己的狀態(tài),比如設(shè)置單位開始移動(dòng),,之后每幀都會(huì)驅(qū)動(dòng)這個(gè)單位移動(dòng),,而AI模塊再去調(diào)用MoveTo就會(huì)拿到一個(gè)Continue,直到宿主這邊內(nèi)部驅(qū)動(dòng)單位移動(dòng)到目的地,,即向上返回Success,;發(fā)生無法讓單位移動(dòng)完成的情況,就返回Failure,。
  • 另一種實(shí)現(xiàn)是宿主提供一些基本的查詢API,,比如移動(dòng)到某一點(diǎn)、是否到達(dá)某個(gè)點(diǎn),、獲得下一個(gè)巡邏點(diǎn),,這樣的話就相當(dāng)于是把輪詢判斷寫在了AI模塊里。這樣就需要有一個(gè)Check結(jié)點(diǎn),,來包裹這個(gè)查詢到的值,,向上返回一個(gè)IO類型的值。

  而針對(duì)第三種pattern,,可以抽象出這樣一種需求情景,,就是:

  AI模塊與游戲世界的數(shù)據(jù)互操作

  假設(shè)宿主提供了接受參數(shù)的api,提供了查詢接口,,ai模塊需要通過調(diào)用宿主的查詢接口拿到數(shù)據(jù),,再把數(shù)據(jù)傳給宿主來執(zhí)行某種行為。

  我們稱這種語義為With,,With用來求出一個(gè)結(jié)點(diǎn)的值,,并合并在當(dāng)前的env中傳遞給子樹,子樹中可以resolve到這個(gè)symbol。

  有了With語義,,我們就可以方便的在AI模塊中對(duì)游戲世界的數(shù)據(jù)進(jìn)行操作,,請(qǐng)求一個(gè)數(shù)據(jù) => 處理一下 => 返回一個(gè)數(shù)據(jù),更具擴(kuò)展性,。

  With語義的具體需求明確一下就是這樣的:由兩個(gè)子樹來構(gòu)造,,一個(gè)是IOGet,一個(gè)是SubTree,。With會(huì)首先求值IOGet,,然后binding到一個(gè)symbol上,SubTree 可以直接引用這個(gè)symbol,,來當(dāng)做一個(gè)普通的值用,。

     然后考慮下實(shí)現(xiàn)方式。

     C#中,,子樹要想引用這個(gè)symbol,,有兩個(gè)方法:
  • ioget與subtree共同hold住一個(gè)變量,ioget求得的值賦給這個(gè)變量,,subtree構(gòu)造的時(shí)候直接把值傳進(jìn)來,。
  • ioget與subtree共同hold住一個(gè)env,雙方約定統(tǒng)一的key,,ioget求完就把這個(gè)key設(shè)置一下,,subtree構(gòu)造的時(shí)候直接從env里根據(jù)key取值。

  考慮第一種方法,,hold住的不應(yīng)該是值本身,,因?yàn)闃浔旧硎遣煌瑢?shí)例共享的,而這個(gè)值會(huì)直接影響到子樹的結(jié)構(gòu),。所以應(yīng)該用一個(gè)class instance object對(duì)值包裹一下,。

  這樣經(jīng)過改進(jìn)后的第一種方法理論上速度應(yīng)該比env的方式快很多,也方便做一些優(yōu)化,,比如說如果子樹沒有continue就不需要把這個(gè)值存在env中,,比如說由于樹本身的驅(qū)動(dòng)一定是單線程的,不同的實(shí)例可以共用一個(gè)包裹,,執(zhí)行子樹的時(shí)候設(shè)置下包裹中的值,,執(zhí)行完子樹再把包裹中的值還原。

  加入了with語義,,就需要重新審視一下IState的定義了,。既然一個(gè)結(jié)點(diǎn)既有可能返回一個(gè)Result,又有可能返回一個(gè)值,,那么就需要這樣一種抽象:

  有這樣一種泛化的concept,,他只需要提供一個(gè)drive接口,,接口需要提供一個(gè)環(huán)境env,drive一下,,就可以輸出一個(gè)值,。這個(gè)concept的instance,需要是pure的,,也就是結(jié)果唯一取決于輸入的環(huán)境,。不同次輸入,只要環(huán)境相同,,輸出一定相同。

  因?yàn)槊枋龅氖且环N與外部世界的通信,,所以就命名為IO吧:
  1. public interface IO<T>
  2.     {
  3.        T Drive(Context ctx);
  4.    }
復(fù)制代碼

  這樣,,我們之前的所有結(jié)點(diǎn)都應(yīng)該有IO的concept。

  之前提出了Parallel,、Sequence,、Select、Check這樣幾個(gè)語義結(jié)點(diǎn),。具體的實(shí)現(xiàn)細(xì)節(jié)就不再細(xì)說了,,簡(jiǎn)單列一下代碼結(jié)構(gòu):
  1. public class Sequence : IO<Result>
  2.     {
  3.         private readonly ICollection<IO<Result>> subTrees;
  4.         public Sequence(ICollection<IO<Result>> subTrees)
  5.         {
  6.             this.subTrees = subTrees;
  7.         }
  8.         public Result Drive(Context ctx)
  9.         {
  10.             throw new NotImplementedException();
  11.         }
  12.     }
復(fù)制代碼

  With結(jié)點(diǎn)的實(shí)現(xiàn),采用我們之前說的第一種方案:
  1. public class With<T, TR> : IO<TR>
  2.      {
  3.          // ...
  4.          public TR Drive(Context ctx)
  5.          {
  6.             var thisContinuation = ctx.Continuation;
  7.              var value = default(T);
  8.              var skipIoGet = false;

  9.             if (thisContinuation != null)
  10.              {
  11.                  // Continuation
  12.                 ctx.Continuation = thisContinuation.SubContinuation;

  13.                // 0表示需要繼續(xù)ioGet
  14.                  // 1表示需要繼續(xù)subTree
  15.                  if (thisContinuation.NextStep == 1)
  16.                 {
  17.                     skipIoGet = true;
  18.                      value = (T) thisContinuation.Param;
  19.                 }
  20.              }

  21.              if (!skipIoGet)
  22.              {
  23.                 value = ioGet.Drive(ctx);

  24.                 if (ctx.Continuation != null)
  25.                  {
  26.                      // ioGet拋出了Continue
  27.                     if (thisContinuation == null)
  28.                     {
  29.                          thisContinuation = new Continuation()
  30.                         {
  31.                              SubContinuation = ctx.Continuation,
  32.                              NextStep = 0,
  33.                          };
  34.                      }
  35.                      else
  36.                     {
  37.                          thisContinuation.SubContinuation = ctx.Continuation;
  38.                         thisContinuation.NextStep = 0;
  39.                      }

  40.                     ctx.Continuation = thisContinuation;

  41.                     return default(TR);
  42.                 }
  43.              }
  44.             
  45.              var oldValue = box.SetVal(value);
  46.              var ret = subTree.Drive(ctx);

  47.              box.SetVal(oldValue);

  48.              if (ctx.Continuation != null)
  49.              {
  50.                 // subTree拋出了Continue
  51.                  if (thisContinuation == null)
  52.                  {
  53.                      thisContinuation = new Continuation()
  54.                      {
  55.                         SubContinuation = ctx.Continuation,
  56.                      };
  57.                  }

  58.                  ctx.Continuation = thisContinuation;
  59.                 thisContinuation.Param = value;
  60.              }

  61.              return ret;
  62.         }
  63.      }
復(fù)制代碼

  這樣,,我們的層次狀態(tài)機(jī)就全部組件化了,。我們可以用通用的語義結(jié)點(diǎn)來組合出任意的子狀態(tài),這些子狀態(tài)是不具名的,,對(duì)構(gòu)建過程更友好,。

  具體的代碼例子:
  1. Par(
  2.      Seq(IsFleeing, ((Box<object> a) => With(a, GetNearestTarget, Check(IsNull(a))))(new Box<object>()), Patrol)
  3.     ,Seq(IsAttacking, ((Box<float> a) => With(a, GetFleeBloodRate, Check(HpRateLessThan(a))))(new Box<float>()))
  4.     ,Seq(IsNormal, Loop(Par(((Box<object> a) => With(a, GetNearestTarget, Seq(Check(IsNull(a)), LockTarget(a)))(new Box<object>()), Seq(Seq(Check(ReachCurrentPatrolPoint), MoveToNextPatrolPoiont), Idle))))))
復(fù)制代碼

  看起來似乎是變得復(fù)雜了,原來可能只需要一句new XXXState(),,現(xiàn)在卻需要自己用代碼拼接出來一個(gè)行為邏輯,。但是仔細(xì)想一下,改成這樣的描述其實(shí)對(duì)整個(gè)工作流是有好處的,。之前的形式完全是硬編碼,,而現(xiàn)在,似乎讓我們看到了轉(zhuǎn)數(shù)據(jù)驅(qū)動(dòng)的可能性,。

  對(duì)行為結(jié)點(diǎn)做包裝

  當(dāng)然這個(gè)示例還少解釋了一部分,,就是葉子結(jié)點(diǎn),或者說是行為結(jié)點(diǎn)的定義,。

  我們之前對(duì)行為的定義都是在IUnit中,,但是這里顯然不像是之前定義的IUnit。

  如果把每個(gè)行為都看做是樹上的一個(gè)與Select,、Sequence等結(jié)點(diǎn)無異的普通結(jié)點(diǎn)的話,,就需要實(shí)現(xiàn)IO的接口,。抽象出一個(gè)計(jì)算的概念,構(gòu)造的時(shí)候可以構(gòu)造出這個(gè)計(jì)算,,然后通過Drive,,來求得計(jì)算中的值。

  包裝后的一個(gè)行為的代碼:
  1. #region HpRateLessThan
  2.         private class MessageHpRateLessThan : IO<bool>
  3.         {
  4.             public readonly float p0;

  5.             public MessageHpRateLessThan(float p0)
  6.             {
  7.                 this.p0 = p0;
  8.             }

  9.             public bool Drive(Context ctx)
  10.             {
  11.                 return ((T)ctx.Self).HpRateLessThan(p0);
  12.             }
  13.         }

  14.         public static IO<bool> HpRateLessThan(float p0)
  15.         {
  16.             return new MessageHpRateLessThan(p0);
  17.         }
  18.         #endregion
復(fù)制代碼

  經(jīng)過包裝的行為結(jié)點(diǎn)的代碼都是有規(guī)律可循的,,所以我們可以比較容易的通過一些代碼生成的機(jī)制來做,。比如通過反射拿到IUnit定義的接口信息,然后直接在這基礎(chǔ)之上做一下包裝,,做出來個(gè)行為結(jié)點(diǎn)的定義,。

  現(xiàn)在我們?cè)倩貞浵掠懻撨^的With,構(gòu)造一個(gè)葉子結(jié)點(diǎn)的時(shí)候,,參數(shù)不一定是literal value,,也有可能是經(jīng)過Box包裹過的。所以就需要對(duì)Boax和literal value抽象出來一個(gè)公共的概念,,葉子結(jié)點(diǎn)/行為結(jié)點(diǎn)可以從這個(gè)概念中拿到值,,而行為結(jié)點(diǎn)計(jì)算本身的構(gòu)造也只需要依賴于這個(gè)概念。

  我們把這個(gè)概念命名為Thunk,。Thunk包裹一個(gè)值或者一個(gè)box,,而就目前來看,這個(gè)Thunk,,僅需要提供一個(gè)我們可以通過其拿到里面的值的接口就夠了,。
  1. public abstract class Thunk<T>
  2.     {
  3.         public abstract T GetUserValue();
  4.     }
復(fù)制代碼

  對(duì)于常量,我們可以構(gòu)造一個(gè)包裹了常量的thunk,;而對(duì)于box,,其天然就屬于Thunk的concept。

  這樣,,我們就通過一個(gè)Thunk的概念,,硬生生把樹中的結(jié)點(diǎn)與值分割成了兩個(gè)概念。這樣做究竟正確不正確呢,?

  如果一個(gè)行為結(jié)點(diǎn)的參數(shù)可能有的類型本來就是一些primitive type,,或者是外部世界(相對(duì)于AI世界)的類型,那肯定是沒問題的,。但如果需要支持這樣一種特性:外部世界的函數(shù),,返回值是AI世界的某個(gè)概念,比如一個(gè)樹結(jié)點(diǎn),;而我的AI世界,,希望的是通過這個(gè)外部世界的函數(shù),動(dòng)態(tài)的拿到一個(gè)結(jié)點(diǎn),,再動(dòng)態(tài)的加到我的樹中,,或者再動(dòng)態(tài)的傳給不通的外部世界的函數(shù),,應(yīng)該怎么做?

  對(duì)于一顆With子樹(Negate表示對(duì)子樹結(jié)果取反,,Continue仍取Continue):
  1. ((Box<IO<Result>> a) =>
  2.      With(a, GetNearestTarget, Negate(a)))(new Box<IO<Result>>())
復(fù)制代碼

  語義需要保證,,這顆子樹執(zhí)行到任意時(shí)刻,都需要是ContextFree的,。

  假設(shè)IOGet返回的是一個(gè)普通的值,,確實(shí)是沒問題的。

  但是因?yàn)锽ox包裹的可能是任意值,,例如,,假設(shè)IOGet返回的是一個(gè)IO,
  • instance a,,執(zhí)行完IOGet之后,,結(jié)構(gòu)變?yōu)镹egate(A)。
  • instance b,,再執(zhí)行IOGet,拿到一個(gè)B,,設(shè)置box里的值為B,,并且拿出來A,這時(shí)候再run subtree,,其實(shí)就是按Negate(B)來跑的,。

  我們只有把IO本身,做到其就是Thunk這個(gè)Concept,。這樣所有的Message對(duì)象,,都是一個(gè)Thunk。不僅如此,,所以在這個(gè)樹中出現(xiàn)的數(shù)據(jù)結(jié)構(gòu),,理應(yīng)都是一個(gè)Thunk,比如List,。

  再次改造IO:
  1.     public abstract class IO<T> : Thunk<IO<T>>
  2.     {
  3.         public abstract T Drive(Context ctx);
  4.         public override IO<T> GetUserValue()
  5.         {
  6.             return this;
  7.         }
  8.     }
復(fù)制代碼

  BehaviourTree

  對(duì)AI有了解的同學(xué)可能已經(jīng)清楚了,,目前我們實(shí)現(xiàn)的就是一個(gè)行為樹的引擎,并且已經(jīng)基本成型,。到目前為止,,我們接觸過的行為樹語義有:

  Sequence、Select,、Parallel,、Check、Negate,。

  其中Sequence與Select是兩個(gè)比較基本的語義,,一個(gè)相當(dāng)于邏輯And,,一個(gè)相當(dāng)于邏輯Or。在組合子設(shè)計(jì)中這兩類組合子也比較常見,。

  不同的行為樹方案,,對(duì)語義結(jié)點(diǎn)的選擇也不一樣。

  比如以前在行為樹這塊比較權(quán)威的一篇halo2的行為樹方案的paper,,里面提到的幾個(gè)常用的組合結(jié)點(diǎn)有這樣幾種:
  • prioritized-list : 每次執(zhí)行優(yōu)先級(jí)最高的結(jié)點(diǎn),,高優(yōu)先級(jí)的始終搶占低優(yōu)先級(jí)的。
  • sequential : 按順序執(zhí)行每個(gè)子結(jié)點(diǎn),,執(zhí)行完最后一個(gè)子結(jié)點(diǎn)后,,父結(jié)點(diǎn)就finished。
  • sequential-looping : 同上,,但是會(huì)loop,。
  • probabilistic : 從子結(jié)點(diǎn)中隨機(jī)選擇一個(gè)執(zhí)行。
  • one-off : 從子結(jié)點(diǎn)中隨機(jī)選擇或按優(yōu)先級(jí)選擇,,選擇一個(gè)排除一個(gè),,直到執(zhí)行完為止。

  而騰訊的behaviac對(duì)組合結(jié)點(diǎn)的選擇除了傳統(tǒng)的Select和Seqence,,halo里面提到的隨機(jī)選擇,,還自己擴(kuò)展了SelectorProbability(雖然看起來像是一個(gè)select,但其實(shí)每次只會(huì)根據(jù)概率選擇一個(gè),,更傾向于halo中的Probabilistic),,SequenceStochastic(隨機(jī)地決定執(zhí)行順序,然后表現(xiàn)起來確實(shí)像是一個(gè)Sequence),。

  其他還有各種常用的修飾結(jié)點(diǎn),,比如前文實(shí)現(xiàn)的Check,還有一些比較常用的:
  • Wait :子樹返回Success的時(shí)候向上Success,,否則向上Continue,。
  • Forever : 永遠(yuǎn)返回Continue。
  • If-Else,、Switch-Cond : 對(duì)于有編程功底的我想就不需要再多做解釋了,。
  • forcedXX : 對(duì)子樹結(jié)果強(qiáng)制取值。

  還有一類屬于特色結(jié)點(diǎn),,雖然通過其他各種方式也都能實(shí)現(xiàn),,但是在行為樹這個(gè)層面實(shí)現(xiàn)的話肯定擴(kuò)展性更強(qiáng)一些,畢竟可以分離一部分程序的職責(zé),。一個(gè)比較典型的應(yīng)用情景是事件驅(qū)動(dòng),,halo的paper中提到了Behaviour Impulse,但是我在在behaviac中并沒有找到類似的概念,。

  halo的paper里面還提到了一些比較細(xì)節(jié)的hack技巧,,比如同一顆行為樹可以應(yīng)用不同的Style,,Parameter Creep等等,有興趣的同學(xué)也可以自行研究,。

  至此,,行為樹的runtime話題需要告一段落了,畢竟是一項(xiàng)成熟了十幾年的技術(shù),。雖然這是目前游戲AI的標(biāo)配,,但是,只有行為樹的話,,離一個(gè)完整的AI工作流還很遠(yuǎn),。到目前為止,行為樹還都是程序?qū)懗鰜淼?,但是正確來說AI應(yīng)該是由策劃或者AI腳本配出來的,。因此,這篇文章的話題還需要繼續(xù),,我們接下來就討論一下這個(gè)程序與策劃之間的中間層,。

  之前的優(yōu)化思路也好,從其他語言借鑒的設(shè)計(jì)pattern也好,,行為樹這種理念本身也好,,本質(zhì)上都是術(shù)。術(shù)很重要,,但是無助于優(yōu)化工作流。這時(shí)候,,我們更需要一種略,。那么,

  略是什么

  這里我們先擴(kuò)展下游戲AI開發(fā)中的一種比較經(jīng)典的工作流,。策劃輸出AI配置,,直接在游戲內(nèi)調(diào)試效果。如果現(xiàn)有接口不滿足需求,,就向程序提開發(fā)需求,,程序加上新接口之后,策劃可以在AI配置里面應(yīng)用新的接口,。這個(gè)AI配置是個(gè)比較廣義的概念,,既可以像很多從立項(xiàng)之初并沒有規(guī)劃AI模塊的游戲那樣,逐漸地,、自發(fā)地形成了一套基于配表做的決策樹,;也可以是像騰訊的behaviac那樣的,用XML文件來描述,。XML天生就是描述數(shù)據(jù)的,,騰訊系的組件普遍特別鐘愛,,tdr這種配表轉(zhuǎn)數(shù)據(jù)的工具是xml,tapp tcplus什么的配置文件全是XML,,倒不是說XML,,而是很多問題解決起來并不直觀。

  配表也好,,XML也好,,json也好,這種描述數(shù)據(jù)的形式本身并沒有錯(cuò),。配表幫很多團(tuán)隊(duì)跨過了從硬編碼到數(shù)據(jù)驅(qū)動(dòng)的開發(fā)模式的轉(zhuǎn)變,,現(xiàn)在國內(nèi)小到創(chuàng)業(yè)手游團(tuán)隊(duì),大到天諭這種幾百人的MMO,,策劃的工作量除了配關(guān)卡就是配表,。

  但是,配表無法自我進(jìn)化【鏈接】,,配表無法自己描述流程是什么樣,,而是流程在描述配表是什么樣。

  針對(duì)策劃配置AI這個(gè)需求,,我們希望抽象出來一個(gè)中間層,,這樣,基于這個(gè)中間層,,開發(fā)相應(yīng)的編輯器也好,,直接利用這個(gè)中間層來配AI也好,都能夠靈活地做到調(diào)試AI這個(gè)最終需求,。如何解決,?我們不妨設(shè)計(jì)一種DSL。

  DSL   

  Domain-specific Language,,領(lǐng)域特定語言,,顧名思義,專門為特定領(lǐng)域設(shè)計(jì)的語言,。設(shè)計(jì)一門DSL遠(yuǎn)容易于設(shè)計(jì)一門通用計(jì)算語言,,我們不用考慮一些特別復(fù)雜的特性,不用加一些增加復(fù)雜度的模塊,,不需要care跟領(lǐng)域無關(guān)的一些流程,。Less is more。

  游戲AI需要怎樣一種DSL

  痛點(diǎn):
  • 對(duì)于游戲AI來說,,需要一種語言可以描述特定類型entity的行為邏輯,。
  • 而對(duì)于程序員來說,只需要提供runtime即可。比如組合結(jié)點(diǎn)的類型,、表現(xiàn)等等,。而具體的行為決策邏輯,由其他層次的協(xié)作者來定義,。
  • 核心需求是做另一種/幾種高級(jí)語言的目標(biāo)代碼生成,,對(duì)于當(dāng)前以及未來幾年來說,對(duì)C#的支持一定是不能少的,,對(duì)python/lua等服務(wù)端腳本的支持也可以考慮,。
  • 對(duì)語言本身的要求是足夠簡(jiǎn)單易懂,declarative,,這樣既可以方便上層編輯器的開發(fā),,也可以在沒編輯器的時(shí)候快速上手。

  分析需求:
  • 因?yàn)樾枰瞿繕?biāo)代碼生成,,而且最主要的目標(biāo)代碼應(yīng)該是C#這種強(qiáng)類型的,,所以需要有簡(jiǎn)單的類型系統(tǒng),以及編譯期簡(jiǎn)單的類型檢查,??梢源_保語言的源文件可以最終codegen成不會(huì)導(dǎo)致編譯出錯(cuò)的C#代碼。
  • 決定行為樹框架好壞的一個(gè)比較致命的因素就是對(duì)With語義的實(shí)現(xiàn),。根據(jù)我們之前對(duì)With語義的討論,,可以看到,這個(gè)With語義的描述其實(shí)是天然的可以轉(zhuǎn)化為一個(gè)lambda的,,所以這門DSL同樣需要對(duì)lambda進(jìn)行支持,。
  • 關(guān)于類型系統(tǒng),需要支持一些內(nèi)建的復(fù)雜類型,,目前來看僅需要List,,只有在seq、select等結(jié)點(diǎn)的構(gòu)造時(shí)會(huì)用到,。還是由于需要支持lambda的原因,我們需要支持Applicative Type,,也就是形如A -> B應(yīng)該是first class type,,而一個(gè)lambda也應(yīng)該是first class function。根據(jù)之前對(duì)runtime的實(shí)現(xiàn)討論,,我們的DSL還需要支持Generic Type,,來支持IO<Result>這樣的類型,以及List<IO<Result>>這樣的類型,。對(duì)內(nèi)建primitive類型的支持只要有String,、Bool、Int、Float即可,。需要支持簡(jiǎn)單的類型推導(dǎo),,實(shí)現(xiàn)hindley-milner的真子集即可,這樣至少我們就不需要在聲明lambda的時(shí)候?qū)懙奶珡?fù)雜,。
  • 需要支持模塊化定義,,也就是最基本的import語義。這樣的話可以方便地模塊化構(gòu)建AI接口,,也可以比較方便地定義一些預(yù)制件,。
  模塊分為兩類:
    一類是抽象的聲明,只有declare,。比如Prelude,,seq、select等一些結(jié)點(diǎn)的具體實(shí)現(xiàn)邏輯一定是在runtime中做的,,所以沒必要在DSL這個(gè)層面填充這類邏輯,。具體的代碼轉(zhuǎn)換則由一些特設(shè)的模塊來做。只需要類型檢查通過,,目標(biāo)語言的CodeGenerator生成了對(duì)應(yīng)的目標(biāo)代碼,,具體的邏輯就在runtime中直接實(shí)現(xiàn)了。
    一類是具體的定義,,只有define,。比如定義某個(gè)具體的AIXXX中的root結(jié)點(diǎn),或者定義某個(gè)通用行為結(jié)點(diǎn),。具體的定義就需要對(duì)外部模塊的define以及declare進(jìn)行組合,。import語義就需要支持從外部模塊導(dǎo)入符號(hào)。

  一種non-trivial的DSL實(shí)現(xiàn)方案

  由于原則是簡(jiǎn)單為主,,所以我在語言的設(shè)計(jì)上主要借鑒的是Scheme,。S表達(dá)式的好處就是代碼本身即數(shù)據(jù),也可以是我們需要的AST,。同時(shí),,由于需要引入簡(jiǎn)單類型系統(tǒng),需要混入一些其他語言的描述風(fēng)格,。我在declare類型時(shí)的語言風(fēng)格借鑒了haskell,,import語句也借鑒了haskell。

  具體來說,,declare語句可能類似于這樣:
  1. (declare
  2.     (HpRateLessThan :: (Float -> IO Result))
  3.     (GetFleeBloodRate :: Float)
  4.     (IsNull :: (Object -> Bool))
  5.     (Idle :: IO Result))

  6. (declare
  7.     (check :: (Bool -> IO Result))
  8.     (loop :: (IO Result -> IO Result))
  9.     (par :: (List IO Result -> IO Result)))
復(fù)制代碼

  因?yàn)槭且許cheme為主要借鑒對(duì)象,,所以內(nèi)建的復(fù)雜類型實(shí)現(xiàn)上本質(zhì)是一個(gè)ADT,當(dāng)然,,有針對(duì)list構(gòu)造專用的語法糖,,但是其parse出來拿到的AST中一個(gè)list終究還是一個(gè)ADT,。

  直接拿例子來說比較直觀:
  1. (import Prelude)
  2. (import BaseAI)

  3. (define Root
  4.     (par [(seq [(check IsFleeing)
  5.                ((\a (check (IsNull a))) GetNearestTarget)])
  6.           (seq [(check IsAttacking)
  7.                ((\b (HpRateLessThan b)) GetFleeBloodRate)])
  8.           (seq [(check IsNormal)
  9.                (loop
  10.                     (par [((\c (seq [(check (IsNull c))
  11.                                      (LockTarget c)])) GetNearestTarget)
  12.                           (seq [(seq [(check ReachCurrentPatrolPoint)
  13.                                      MoveToNextPatrolPoiont])
  14.                                Idle])]))])]))
復(fù)制代碼

  可以看到,跟S-Expression沒什么太大的區(qū)別,,可能lambda的聲明方式變了下,。

  然后是詞法分析和語法分析,這里我選擇的是Haskell的ParseC,。一些更傳統(tǒng)的選擇可能是lex+yacc/flex+bison,。但是這種兩個(gè)工具一起混用學(xué)習(xí)成本就不用說了,也違背了simple is better的初衷,。ParseC使用起來就跟PEG是一樣的,,PEG這種形式,是天然的結(jié)合了正則與top-down parser,。haskell支持的algebraic data types,,天然就是用來定義AST結(jié)構(gòu)的,簡(jiǎn)單直觀,。haskell實(shí)現(xiàn)的hindly-miner類型系統(tǒng),,又是讓你寫代碼基本編譯通過就能直接run出正確結(jié)果,從一定程度上彌補(bǔ)了PEG天生不適合調(diào)試的缺陷,。一個(gè)haskell的庫就能解決lexical&grammar,,實(shí)在方便。

  先是一些AST結(jié)構(gòu)的預(yù)定義:
  1. module Common where

  2. import qualified Data.Map as Map

  3. type Identifier = String
  4. type ValEnv = Map.Map Identifier Val
  5. type TypeEnv = Map.Map Identifier Type
  6. type DecEnv = Map.Map Identifier (String,Dec)

  7. data Type =
  8.     NormalType String
  9.     | GenericType String Type
  10.     | AppType [Type]

  11. data Dec =
  12.     DefineDec Pat Exp
  13.     | ImportDec String
  14.     | DeclareDec Pat Type
  15.     | DeclaresDec [Dec]
  16.         
  17. data Exp =
  18.     ConstExp Val
  19.     | VarExp Identifier
  20.     | LambdaExp Pat Exp
  21.     | AppExp Exp Exp
  22.     | ADTExp String [Exp]
  23.         
  24. data Val =
  25.     NilVal
  26.     | BoolVal Bool
  27.     | IntVal Integer
  28.     | FloatVal Float
  29.     | StringVal String
  30.    
  31. data Pat =
  32.     VarPat Identifier
復(fù)制代碼

  我在這里省去了一些跟這篇文章討論的DSL無關(guān)的語言特性,,比如Pattern的定義我只保留了VarPat,;Value的定義我去掉了ClosureVal,雖然語言本身仍然是支持first class function的,。

  algebraic data type的一個(gè)好處就是清晰易懂,,定義起來不過區(qū)區(qū)二十行,但是我們一看就知道之后輸出的AST會(huì)是什么樣,。

  haskell的ParseC用起來其實(shí)跟PEG是沒有本質(zhì)區(qū)別的,,組合子本身是自底向上描述的,而parser也是通過parse小元素的parser來構(gòu)建parse大元素的parser,。

  例如,,haskell的ParseC庫就有這樣幾個(gè)強(qiáng)大的特性:
  • 提供了char、string,,基元的parse單個(gè)字符或字符串的parser,。
  • 提供了sat,傳一個(gè)predicate,,就可以parse到符合predicate的結(jié)果的parser。
  • 提供了try,,支持parse過程中的lookahead語義,。
  • 提供了chainl,、chainr,這樣就省的我們?cè)跇?gòu)造parser的時(shí)候就無需考慮左遞歸了,。不過這個(gè)我也是寫完了parser才了解到的,,所以基本沒用上,更何況對(duì)于S-expression來說,,需要我來處理左遞歸的情況還是比較少的,。

  我們可以先根據(jù)這些基本的,封裝出來一些通用combinator,。

  比如正則規(guī)則中的star:
  1. star   :: Parser a -> Parser [a]
  2. star p = star_p
  3.     where
  4.         star_p = try plus_p <|> (return [])
  5.         plus_p = (:) <[        DISCUZ_CODE_1689        ]gt; p <*> star_p
復(fù)制代碼

  比如plus:
  1. plus   :: Parser a -> Parser [a]
  2. plus p = plus_p
  3.     where
  4.         star_p = try plus_p <|> (return []) <?> "plus_star_p"
  5.         plus_p = (:) <[        DISCUZ_CODE_1690        ]gt; p <*> star_p  <?> "plus_plus_p"
復(fù)制代碼

  基于這些,,我們可以做組裝出來一個(gè)parse lambda-exp的parser(p_seperate是對(duì)char、plus這些的組裝,,表示形如a,b,c這樣的由特定字符分隔的序列):
  1. p_lambda_exp :: Parser Exp
  2. p_lambda_exp =  p_between '(' ')' inner
  3.               <?> "p_lambda_exp"
  4.     where
  5.         inner = make_lambda_exp
  6.                 <[        DISCUZ_CODE_601        ]nbsp; char '\\'
  7.                 <*> p_seperate (p_parse p_pat) ","
  8.                 <*> p_parse p_exp
  9.         make_lambda_exp []     e = (LambdaExp NilPat e)
  10.         make_lambda_exp (p:[]) e = (LambdaExp p e)
  11.         make_lambda_exp (p:ps) e = (LambdaExp p (make_lambda_exp ps e))
復(fù)制代碼

  有了所有exp的parser,,我們就可以組裝出來一個(gè)通用的exp parser:
  1. p_exp :: Parser Exp   
  2. p_exp =  listplus [p_var_exp, p_const_exp, p_lambda_exp, p_app_exp, p_adt_exp, p_list_exp]
  3.          <?> "p_exp"
復(fù)制代碼

  其中,listplus是一種具有優(yōu)先級(jí)的lookahead:
  1. listplus :: [Parser a] -> Parser a
  2. listplus lst = foldr (<|>) mzero (map try lst)
復(fù)制代碼

  對(duì)于parser來說,,其輸入是源文件其輸出是AST,。具體來說,其實(shí)就是parse出一個(gè)Dec數(shù)組,,拿到AST,,供后續(xù)的pipeline消費(fèi)。

  我們之前舉的AI的例子,,parse出來的AST大概是這副模樣:
  1. -- Prelude.bh
  2. Right [DeclaresDec [
  3. DeclareDec (VarPat "seq") (AppType [GenericType "List" (GenericType "IO" (NormalType "Result")),GenericType "IO" (NormalType "Result")])
  4. ,DeclareDec (VarPat "check") (AppType [NormalType "Bool",GenericType "IO" (NormalType "Result")])]]

  5. -- BaseAI.bh
  6. Right [DeclaresDec [
  7. DeclareDec (VarPat "HpRateLessThan") (AppType [NormalType "Float",GenericType "IO" (NormalType "Result")])
  8. ,DeclareDec (VarPat "Idle") (GenericType "IO" (NormalType "Result"))]]

  9. -- AI00001.bh
  10. Right [
  11. ImportDec "Prelude"
  12. ,ImportDec "BaseAI"
  13. ,DefineDec (VarPat "Root") (AppExp (VarExp "par") (ADTExp "Cons" [
  14.      AppExp (VarExp "seq") (ADTExp "Cons" [
  15.          AppExp (VarExp "check") (VarExp "IsFleeing")
  16.         ,ADTExp "Cons" [
  17.              AppExp (LambdaExp (VarPat "a")(AppExp (VarExp "check") (AppExp (VarExp "IsNull") (VarExp "a")))) (VarExp "GetNearestTarget")
  18.             ,ConstExp NilVal]])
  19.     ,ADTExp "Cons" [
  20.          AppExp (VarExp "seq") (ADTExp "Cons" [
  21.              AppExp (VarExp "check") (VarExp "IsAttacking")
  22.             ,ADTExp "Cons" [
  23.                  AppExp (LambdaExp (VarPat "b") (AppExp (VarExp "HpRateLessThan") (VarExp "b"))) (VarExp "GetFleeBloodRate")
  24.                 ,ConstExp NilVal]])
  25.         ,ADTExp "Cons" [
  26.              AppExp (VarExp "seq") (ADTExp "Cons" [
  27.                  AppExp (VarExp "check") (VarExp "IsNormal")
  28.                 ,ADTExp "Cons" [
  29.                      AppExp (VarExp "loop") (AppExp (VarExp "par") (ADTExp "Cons" [
  30.                          AppExp (LambdaExp (VarPat "c") (AppExp (VarExp "seq") (ADTExp "Cons" [
  31.                              AppExp (VarExp "check") (AppExp (VarExp"IsNull") (VarExp "c"))
  32.                             ,ADTExp "Cons" [
  33.                                  AppExp (VarExp "LockTarget") (VarExp "c")
  34.                                 ,ConstExp NilVal]]))) (VarExp "GetNearestTarget")
  35.                         ,ADTExp "Cons" [
  36.                              AppExp (VarExp"seq") (ADTExp "Cons" [
  37.                                  AppExp (VarExp "seq") (ADTExp "Cons" [
  38.                                      AppExp (VarExp "check") (VarExp "ReachCurrentPatrolPoint")
  39.                                     ,ADTExp "Cons" [
  40.                                          VarExp "MoveToNextPatrolPoiont"
  41.                                         ,ConstExp NilVal]])
  42.                                 ,ADTExp "Cons" [
  43.                                      VarExp "Idle"
  44.                                     ,ConstExp NilVal]])
  45.                             ,ConstExp NilVal]]))
  46.                     ,ConstExp NilVal]])
  47.             ,ConstExp NilVal]]]))]
復(fù)制代碼

  前面兩部分是我把在其他模塊定義的declares,,選擇性地拿過來兩條。第三部分是這個(gè)人形怪AI的整個(gè)的AST,。其中嵌套的Cons展開之后就是語言內(nèi)置的List,。

  正如我們之前所說,做代碼生成之前需要進(jìn)行一步類型檢查的工作,。類型檢查工具其輸入是AST其輸出是一個(gè)檢查結(jié)果,,同時(shí)還可以提供AST中的一些輔助信息,包括各標(biāo)識(shí)符的類型信息等等,。

  類型檢查其實(shí)主要的邏輯在于處理Appliacative Type,,這中間還有個(gè)類型推導(dǎo)的邏輯。形如(\a (Func a)) 10,,AST中并不記錄a的type,,我們的DSL也不需要支持concept、typeclass等有關(guān)type,、subtype的復(fù)雜機(jī)制,,推導(dǎo)的時(shí)候只需要著重處理AppExp,把右邊表達(dá)式的類型求出,,合并一下env傳給左邊表達(dá)式遞歸檢查即可,。

  這部分的代碼:
  1. exp_type :: Exp -> TypeEnv -> Maybe Type
  2. exp_type (AppExp lexp aexp) env =
  3.     (exp_type aexp env) >>= (\at ->
  4.         case lexp of
  5.             LambdaExp (VarPat var) exp -> (merge_type_env (Just env) (make_type_env var (Just at))) >>= (\env1 -> exp_type lexp env1)  
  6.             _ -> (exp_type lexp env) >>= (\ltype -> check_type ltype at))
  7.     where
  8.         check_type (AppType (t1:(t2:[]))) at =
  9.             if t1 == at then (Just t2) else Nothing
  10.         check_type (AppType (t:ts)) at =
  11.             if t == at then (Just (AppType ts)) else Nothing
復(fù)制代碼

  此外,,還需要有一個(gè)通用的CodeGenerator模塊,其輸入也是AST,,其輸出是另一些AST中的輔助信息,,主要是注記下各標(biāo)識(shí)符的import源以及具體的define內(nèi)容,用來方便各目標(biāo)語言CodeGenerator直接復(fù)用邏輯,。

  目標(biāo)語言的CodeGenerator目前只做了C#的,。

  目標(biāo)代碼生成的邏輯就比較簡(jiǎn)單了,畢竟該有的信息前面的各模塊都提供了,,這里根據(jù)之前一個(gè)版本的runtime,,代碼生成的大致樣子:
  1. public static IO<Result> Root =
  2.     Prelude.par(Help.MakeList(
  3.          Prelude.seq(Help.MakeList(
  4.              Prelude.check(BaseAI.IsFleeing)
  5.             ,(((Box<Object> a) => Help.With(a, BaseAI.GetNearestTarget, Prelude.check(BaseAI.IsNull())))(new Box<Object>()))))
  6.         ,Prelude.seq(Help.MakeList(
  7.              Prelude.check(BaseAI.IsAttacking)
  8.             ,(((Box<Float> b) => Help.With(b, BaseAI.GetFleeBloodRate, BaseAI.HpRateLessThan()))(new Box<Float>()))))
  9.         ,Prelude.seq(Help.MakeList(
  10.              Prelude.check(BaseAI.IsNormal)
  11.             ,Prelude.loop(Prelude.par(Help.MakeList(
  12.                  (((Box<Object> c) => Help.With(c, BaseAI.GetNearestTarget, Prelude.seq(Help.MakeList(
  13.                      Prelude.check(BaseAI.IsNull())
  14.                     ,BaseAI.LockTarget()))))(new Box<Object>()))
  15.                 ,Prelude.seq(Help.MakeList(
  16.                      Prelude.seq(Help.MakeList(
  17.                          Prelude.check(BaseAI.ReachCurrentPatrolPoint)
  18.                         ,BaseAI.MoveToNextPatrolPoiont))
  19.                     ,BaseAI.Idle)))))))))
復(fù)制代碼

  總的來說,大致分為這幾個(gè)模塊:Parser,、TypeChecker,、CodeGenerator、目標(biāo)語言的CodeGenerator,。再加上目標(biāo)語言的runtime,,基本上就可以組成這個(gè)DSL的全部了。

  上面列出來的代碼風(fēng)格比較混搭,,畢竟是前后差的時(shí)間比較久了,。。parser部分大概是7月左右完成的,,那時(shí)候喜歡applicative的風(fēng)格,,大量用了<$> <*>;后面的TypeChecker和CodeGenerator都是最近寫的,,寫monad expression的時(shí)候,,Maybe Monad我比較傾向于寫原生的>>=調(diào)用,IO Monad如果這樣寫就煩了,,所以比較多的用了do-notaion,。優(yōu)化什么的由于時(shí)間原因還沒看RWH的后面幾章,而且DSL的compiler對(duì)性能需求的優(yōu)先級(jí)其實(shí)很低了,,所以暫時(shí)沒有考慮過,,各位看官將就一下。

  再擴(kuò)展runtime     

  對(duì)比DSL,,我們可以發(fā)現(xiàn),,DSL支持的特性要比之前實(shí)現(xiàn)的runtime版本多。比如:
  • runtime中壓根就沒有Closure的概念,,但是DSL中我們是完全可以把一個(gè)lambda作為一個(gè)ClosureVal傳給某個(gè)函數(shù)的,。
  • 缺少對(duì)標(biāo)準(zhǔn)庫的支持。比如常用的math函數(shù),。
  • 基于上面這點(diǎn),,還會(huì)引入一個(gè)With結(jié)點(diǎn)的性能問題,,在只有runtime的時(shí)候我們也許不會(huì)With a <- 1+1。但是DSL中是有可能這樣的,,而且生成出來的代碼會(huì)每次run這棵樹的時(shí)候都會(huì)重新計(jì)算一次1+1。

  針對(duì)第一個(gè)問題,,我們要做的工作就多了,。首先我們要記錄下這個(gè)閉包hold住的自由變量,要傳給runtime,,runtime也要記錄,,也要做各種各種,想想都麻煩,,而且完全偏離了游戲AI的話題,,不再討論。

  針對(duì)第二個(gè)問題,,我們可以通過解決第三個(gè)問題來順便解決這個(gè)問題,。

  針對(duì)第三個(gè)問題,我們重新審視一下With語義,。

  With語義所要表達(dá)的其實(shí)是這樣一個(gè)概念:

  把一個(gè)可能會(huì)Continue/Lazy Evaluation的計(jì)算結(jié)果,,綁定到一個(gè)variable上,對(duì)于With下面的子表達(dá)式來說,,這個(gè)variable的值具有l(wèi)exical scope,。

  但是在runtime中,我們按照之前的寫法,,subtree中直接就進(jìn)行了函數(shù)調(diào)用,,很顯然是存在問題的。

  With結(jié)點(diǎn)本身的返回值不一定只是一個(gè)IO<Result>,,有可能是一個(gè)IO<float>,。

  舉例:
  1. ((Box<float> a) => (Help.With(a, UnitAI.GetFleeBloodRate, Math.Plus(a, 0.1)))(new Box<float>())
復(fù)制代碼

  這里Math.Plus屬于這門DSL標(biāo)準(zhǔn)庫的一部分,實(shí)現(xiàn)上我們就對(duì)底層數(shù)學(xué)函數(shù)做一層簡(jiǎn)單的wrapper,。但是這樣由于C#語言是pass-by-value,,我們?cè)跇?gòu)造這顆With的時(shí)候,Math.Plus(a, 0.1)已經(jīng)求值,。但是這個(gè)時(shí)候Box的值還沒有被填充,,求出來肯定是有問題的。

  所以我們需要對(duì)這樣一種計(jì)算再進(jìn)行一次抽象,。希望可以得到的效果是,,對(duì)于Math.Plus(0.1, 0.2),可以在構(gòu)造樹的時(shí)候直接求值,;對(duì)于Math.Plus(0.1, a),,可以得到某種計(jì)算,,在我們需要的時(shí)候再求值。

  先明確下函數(shù)調(diào)用有哪幾種情況:
  • 對(duì)UnitAI,,也就是外部世界的定義的接口的調(diào)用,。這種調(diào)用,對(duì)于AI模塊來說,,本質(zhì)上是pure的,,所以不需要考慮這個(gè)延遲計(jì)算的問題
  • 對(duì)標(biāo)準(zhǔn)庫的調(diào)用

  按我們之前的runtime設(shè)計(jì)思路,Math.Plus這個(gè)標(biāo)準(zhǔn)庫API也許會(huì)被設(shè)計(jì)成這樣:
  1. public static Thunk<float> Plus(Thunk<float> a, Thunk<float> b)
  2.         {
  3.             return Help.MakePureThunk(a.GetUserValue() + b.GetUserValue());
  4.         }
復(fù)制代碼

  如果a和b都是literal value,,那就沒問題,,但是如果有一個(gè)是被box包裹的,那就很顯然是有問題的,。

  所以需要對(duì)Thunk這個(gè)概念做一下擴(kuò)展,,使之能區(qū)別出動(dòng)態(tài)的值與靜態(tài)的值。一般情況下的值,,都是pure的,;box包裹的值,是impure的,。同時(shí),,這個(gè)pure的性質(zhì)具有值傳遞性,如果這個(gè)值屬于另一個(gè)值的一部分,,那么這個(gè)整體的pure性質(zhì)與值的局部的pure性質(zhì)是一致的,。這里特指的值,包括List與IO,。

  整體的概念我們應(yīng)該拿haskell中的impure monad做類比,,比如haskell中的IO。haskell中的IO依賴于OS的輸入,,所以任何返回IO monad的函數(shù)都具有傳染性,,引用到的函數(shù)一定還會(huì)被包裹在IO monad之中。

  所以,,對(duì)于With這種情況的傳遞,,應(yīng)該具有這樣的特征:
  • With內(nèi)部引用到了With外部的symbol,那么這個(gè)With本身應(yīng)該是impure的,。
  • With內(nèi)部只引用了自己的IOGet,,那么這個(gè)With本身是pure的,但是其SubTree是impure的,。

  所以With結(jié)點(diǎn)構(gòu)造的時(shí)候,,計(jì)算pure應(yīng)該特殊處理一下。但是這個(gè)特殊處理的代碼污染性比較大,我在本文就不列出了,,只是這樣提一下,。

  有了pure與impure的標(biāo)記,我們?cè)趯?duì)函數(shù)調(diào)用的時(shí)候,,就需要額外走一層,。

  本來一個(gè)普通的函數(shù)調(diào)用,比如UnitAI.Func(p0, p1, p2)與Math.Plus(p0, p1),。前者返回一種computing是毫無疑問的,,后者就需要根據(jù)參數(shù)的類型來決定是返回一種計(jì)算還是直接的值。

  為了避免在這個(gè)Plus里面改來改去,,我們把Closure這個(gè)概念給抽象出來。同時(shí),,為了簡(jiǎn)化討論,,我們只列舉T0 -> TR這一種情況,對(duì)應(yīng)的標(biāo)準(zhǔn)庫函數(shù)取Abs,。
  1. public class Closure<T0, TR> : Thunk<Closure<T0, TR>>
  2.     {
  3.         class UserFuncApply : Thunk<TR>
  4.         {
  5.             private Closure<T0, TR> func;
  6.             private Thunk<T0> p0;

  7.             public UserFuncApply(Closure<T0, TR> func, Thunk<T0> p0)
  8.             {
  9.                 this.func = func;
  10.                 this.p0 = p0;
  11.                 this.pure = false;
  12.             }

  13.             public override TR GetUserValue()
  14.             {
  15.                 return func.funcThunk(p0).GetUserValue();
  16.             }
  17.         }

  18.         private bool isUserFunc = false;
  19.         private FuncThunk<T0, TR> funcThunk;
  20.         private Func<T0, TR> userFunc;

  21.         public Closure(FuncThunk<T0, TR> funcThunk)
  22.         {
  23.             this.funcThunk = funcThunk;
  24.         }

  25.         public Closure(Func<T0, TR> func)
  26.         {
  27.             this.userFunc = func;
  28.             this.funcThunk = p0 => Help.MakePureThunk(userFunc(p0.GetUserValue()));
  29.             this.isUserFunc = true;
  30.         }

  31.         public override Closure<T0, TR> GetUserValue()
  32.         {
  33.             return this;
  34.         }

  35.         public Thunk<TR> Apply(Thunk<T0> p0)
  36.         {
  37.             if (!isUserFunc || Help.AllPure(p0))
  38.             {
  39.                 return funcThunk(p0);
  40.             }

  41.             return new UserFuncApply(this, p0);
  42.         }
  43.     }
復(fù)制代碼

  其中,,UserFuncApply就是之前所說的一層計(jì)算的概念。UserFunc表示的是等效于可以編譯期計(jì)算的一種標(biāo)準(zhǔn)庫函數(shù),。

  這樣定義:
  1. public static class Math
  2.     {
  3.         public static readonly Thunk<Closure<float, float>> Abs = Help.MakeUserFuncThunk<float,float>(System.Math.Abs);
  4.     }
復(fù)制代碼

  Message類型的Closure構(gòu)造,,都走FuncThunk構(gòu)造函數(shù);普通函數(shù)類型的構(gòu)造,,走Func構(gòu)造函數(shù),,并且包裝一層。

  Help.Apply是為了方便做代碼生成,,描述一種declarative的Application,。其實(shí)就是直接調(diào)用Closure的Apply。

  考慮以下幾種case:
  1. public void Test()
  2.         {
  3.             var box1 = new Box<float>();

  4.             // Math.Abs(box1) -> UserFuncApply
  5.             // 在GetUserValue的時(shí)候才會(huì)求值
  6.             var ret1 = Help.Apply(Math.Abs, box1);

  7.             // Math.Abs(0.2f) -> Thunk<float>
  8.             // 直接構(gòu)造出來了一個(gè)Thunk<float>(0.2f)
  9.             var ret2 = Help.Apply(Math.Abs, Help.MakePureThunk(0.2f));

  10.             // UnitAISets<IUnit>.HpRateLessThan(box1) -> Message
  11.             var ret3 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, box1);

  12.             // UnitAISets<IUnit>.HpRateLessThan(0.2f) -> Message
  13.             var ret4 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, Help.MakePureThunk(0.2f));
  14.         }
復(fù)制代碼

  與之前的runtime版本唯一表現(xiàn)上有區(qū)別的地方在于,,對(duì)于純pure參數(shù)的userFunc,,在Apply完之后會(huì)直接計(jì)算出來值,并重新包裝成一個(gè)Thunk,;而對(duì)于參數(shù)中有impure的情況,,返回一個(gè)UserFuncApply,在GetUserValue的時(shí)候才會(huì)求值,。

  TODO

  到目前為止,,已經(jīng)形成了一套基本的、non-trivial的游戲AI方案,,當(dāng)然后續(xù)還有很多要做的工作,,比如:

  更多的語言特性:
  • DSL中支持注釋、函數(shù)作為普通的value傳遞等等。
  • parser,、typechecker支持更完善的錯(cuò)誤處理,,我之前單獨(dú)寫一個(gè)用例的時(shí)候,就因?yàn)橐恍┘?xì)節(jié)問題,,調(diào)試了老半天,。
  • 標(biāo)準(zhǔn)庫支持更多,比如Y-Combinator

  編輯器化:

  國內(nèi)游戲工業(yè)落后國外的一個(gè)比較重要的因素就是工作流太落后,,要不是因?yàn)閡nity的興起帶動(dòng)了國內(nèi)編輯器化風(fēng)潮,,可能現(xiàn)在還有大部分團(tuán)隊(duì)配技能配戰(zhàn)斗效果都還會(huì)對(duì)著excel盲配。

  AI的配置也需要有編輯器,,這個(gè)編輯器至少能實(shí)現(xiàn)的需求有這樣幾個(gè):
  • 與自己定義的中間層對(duì)接良好(配置文件也好,、DSL也好),具有codegen功能
  • 支持工作空間,、支持模塊化定義,,制作一些prefab什么的
  • 支持可視化調(diào)試

  我們工作室自己做的編輯器是基于java的某個(gè)開源庫做的,看起來比較炫,,但是性能不行,。behaviac的編輯器就是純C#,性能應(yīng)該不錯(cuò),,沒有用過不了解,。這方面的具體話題就不再展開了。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多