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 }
1 public interface IState<TState, TUnit> where TState : IConvertible2 {3 TState Enum { get; }4 TUnit Self { get; }5 void OnEnter();6 void Drive();7 void OnExit();8 }
以逃跑狀態為例:
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 18 Self.ChangeState(UnitStateEnum.Patrol);19 }20 }
1 public interface IState<TState, TUnit> where TState : IConvertible2 {3 TState Enum { get; }4 void OnEnter(TUnit self);5 void Drive(TUnit self);6 void OnExit(TUnit self);7 }
還是拿之前實現好的逃跑狀態作為例子:
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 15 var unit = self.GetNearestTarget();16 if (unit != null)17 {18 return;19 }20 21 self.ChangeState(UnitStateEnum.Patrol);22 }23 }
考慮這樣一個組合狀態情景:巡邏時,需要依次得先走到一個點,然后怠工一會兒,再走到下一個點,然后再怠工一會兒,循環往復。這樣就需要父狀態(巡邏狀態)注記當前激活的子狀態,并且根據子狀態執行結果的不同來修改激活的子狀態集合。這樣不僅是Unit自身有上下文,連組合狀態也有了自己的上下文。
為了簡化討論,我們還是從non-ContextFree層次狀態機系統設計開始。
1 public interface IState<TState, TCleverUnit, TResult> 2 where TState : IConvertible3 {4 // ...5 TResult Drive();6 // ...7 }
組合狀態的定義:
1 public abstract class UnitCompositeStateBase : UnitStateBase 2 { 3 protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>(); 4 5 // ... 6 protected Result ProcessSubStates() 7 { 8 if (subStates.Count == 0) 9 {10 return Result.Success;11 }12 13 var front = subStates.First;14 var res = front.Value.Drive();15 16 if (res != Result.Continue)17 {18 subStates.RemoveFirst();19 }20 21 return Result.Continue;22 }23 // ...24 }25
1 public class PatrolState : UnitCompositeStateBase 2 { 3 // ... 4 public override void OnEnter() 5 { 6 base.OnEnter(); 7 AddSubState(new MoveToState(Self)); 8 } 9 10 public override Result Drive()11 {12 if (subStates.Count == 0)13 {14 return Result.Success;15 }16 17 var unit = Self.GetNearestTarget();18 if (unit != null)19 {20 Self.LockTarget(unit);21 return Result.Success;22 }23 24 var front = subStates.First;25 var ret = front.Value.Drive();26 27 if (ret != Result.Continue)28 {29 if (front.Value.Enum == CleverUnitStateEnum.MoveTo)30 {31 AddSubState(new IdleState(Self));32 }33 else34 {35 AddSubState(new MoveToState(Self));36 }37 }38 39 return Result.Continue;40 }41 }
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 8 public class Context<T> 9 {10 public Continuation Continuation { get; set; }11 public T Self { get; set; }12 }
1 public interface IState<TCleverUnit, TResult>2 {3 TResult Drive(Context<TCleverUnit> ctx);4 }
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 19 return Result.Success;20 }21 22 var nextStep = 0;23 if (ctx.Continuation != null)24 {25 // Continuation26 var thisContinuation = ctx.Continuation;27 28 ctx.Continuation = thisContinuation.SubContinuation;29 30 var ret = subStates[nextStep].Drive(ctx);31 32 if (ret == Result.Continue)33 {34 thisContinuation.SubContinuation = ctx.Continuation;35 ctx.Continuation = thisContinuation;36 37 return Result.Continue;38 }39 else if (ret == Result.Failure)40 {41 ctx.Continuation = null;42 43 return Result.Failure;44 }45 46 ctx.Continuation = null;47 nextStep = thisContinuation.NextStep + 1;48 }49 50 for (; nextStep < subStates.Count; nextStep++)51 {52 var ret = subStates[nextStep].Drive(ctx);53 if (ret == Result.Continue)54 {55 ctx.Continuation = new Continuation()56 {57 SubContinuation = ctx.Continuation,58 NextStep = nextStep,59 };60 61 return Result.Continue;62 } 63 else if (ret == Result.Failure) 64 {65 ctx.Continuation = null;66 67 return Result.Failure;68 }69 }70 71 ctx.Continuation = null;72 73 return Result.Success;74 }75 }
subStates是readonly的,在組合狀態構造的一開始就確定了值。這樣結構本身就是靜態的,而上下文是動態的。不同的entity instance共用同一個樹的instance。
1 public interface IO<T>2 {3 T Drive(Context ctx);4 }
public class Sequence : IO<Result> { private readonly ICollection<IO<Result>> subTrees; public Sequence(ICollection<IO<Result>> subTrees) { this.subTrees = subTrees; } public Result Drive(Context ctx) { throw new NotImplementedException(); } }
With結點的實現,采用我們之前說的第一種方案:
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 10 if (thisContinuation != null)11 {12 // Continuation13 ctx.Continuation = thisContinuation.SubContinuation;14 15 // 0表示需要繼續ioGet16 // 1表示需要繼續subTree17 if (thisContinuation.NextStep == 1)18 {19 skipIoGet = true;20 value = (T) thisContinuation.Param;21 }22 }23 24 if (!skipIoGet)25 {26 value = ioGet.Drive(ctx);27 28 if (ctx.Continuation != null)29 {30 // ioGet拋出了Continue31 if (thisContinuation == null)32 {33 thisContinuation = new Continuation()34 {35 SubContinuation = ctx.Continuation,36 NextStep = 0,37 };38 }39 else40 {41 thisContinuation.SubContinuation = ctx.Continuation;42 thisContinuation.NextStep = 0;43 }44 45 ctx.Continuation = thisContinuation;46 47 return default(TR);48 }49 }50 51 var oldValue = box.SetVal(value);52 var ret = subTree.Drive(ctx);53 54 box.SetVal(oldValue);55 56 if (ctx.Continuation != null)57 {58 // subTree拋出了Continue59 if (thisContinuation == null)60 {61 thisContinuation = new Continuation()62 {63 SubContinuation = ctx.Continuation,64 };65 }66 67 ctx.Continuation = thisContinuation;68 thisContinuation.Param = value;69 }70 71 return ret;72 }73 }
這樣,我們的層次狀態機就全部組件化了。我們可以用通用的語義結點來組合出任意的子狀態,這些子狀態是不具名的,對構建過程更友好。
具體的代碼例子:
Par( Seq(IsFleeing, ((Box<object> a) => With(a, GetNearestTarget, Check(IsNull(a))))(new Box<object>()), Patrol) ,Seq(IsAttacking, ((Box<float> a) => With(a, GetFleeBloodRate, Check(HpRateLessThan(a))))(new Box<float>())) ,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))))))
看起來似乎是變得復雜了,原來可能只需要一句new XXXState(),現在卻需要自己用代碼拼接出來一個行為邏輯。但是仔細想一下,改成這樣的描述其實對整個工作流是有好處的。之前的形式完全是硬編碼,而現在,似乎讓我們看到了轉數據驅動的可能性。
#region HpRateLessThan private class MessageHpRateLessThan : IO<bool> { public readonly float p0; public MessageHpRateLessThan(float p0) { this.p0 = p0; } public bool Drive(Context ctx) { return ((T)ctx.Self).HpRateLessThan(p0); } } public static IO<bool> HpRateLessThan(float p0) { return new MessageHpRateLessThan(p0); } #endregion
public abstract class Thunk<T> { public abstract T GetUserValue(); }
((Box<IO<Result>> a) => With(a, GetNearestTarget, Negate(a)))(new Box<IO<Result>>())
public abstract class IO<T> : Thunk<IO<T>> { public abstract T Drive(Context ctx); public override IO<T> GetUserValue() { return this; } }
(declare (HpRateLessThan :: (Float -> IO Result)) (GetFleeBloodRate :: Float) (IsNull :: (Object -> Bool)) (Idle :: IO Result))(declare (check :: (Bool -> IO Result)) (loop :: (IO Result -> IO Result)) (par :: (List IO Result -> IO Result)))
(import Prelude)(import BaseAI)(define Root (par [(seq [(check IsFleeing) ((/a (check (IsNull a))) GetNearestTarget)]) (seq [(check IsAttacking) ((/b (HpRateLessThan b)) GetFleeBloodRate)]) (seq [(check IsNormal) (loop (par [((/c (seq [(check (IsNull c)) (LockTarget c)])) GetNearestTarget) (seq [(seq [(check ReachCurrentPatrolPoint) MoveToNextPatrolPoiont]) Idle])]))])]))
可以看到,跟S-Expression沒什么太大的區別,可能lambda的聲明方式變了下。
module Common whereimport qualified Data.Map as Maptype Identifier = Stringtype ValEnv = Map.Map Identifier Valtype TypeEnv = Map.Map Identifier Typetype DecEnv = Map.Map Identifier (String,Dec)data Type = NormalType String | GenericType String Type | APPType [Type]data Dec = DefineDec Pat Exp | ImportDec String | DeclareDec Pat Type | DeclaresDec [Dec] data Exp = ConstExp Val | VarExp Identifier | LambdaExp Pat Exp | AppExp Exp Exp | ADTExp String [Exp] data Val = NilVal | BoolVal Bool | IntVal Integer | FloatVal Float | StringVal String data Pat = VarPat Identifier
我在這里省去了一些跟這篇文章討論的DSL無關的語言特性,比如Pattern的定義我只保留了VarPat;Value的定義我去掉了ClosureVal,雖然語言本身仍然是支持first class function的。
algebraic data type的一個好處就是清晰易懂,定義起來不過區區二十行,但是我們一看就知道之后輸出的AST會是什么樣。
haskell的ParseC用起來其實跟PEG是沒有本質區別的,組合子本身是自底向上描述的,而parser也是通過parse小元素的parser來構建parse大元素的parser。
例如,haskell的ParseC庫就有這樣幾個強大的特性:
我們可以先根據這些基本的,封裝出來一些通用combinator。
比如正則規則中的star:
star :: Parser a -> Parser [a]star p = star_p where star_p = try plus_p <|> (return []) plus_p = (:) <$> p <*> star_p
比如plus:
plus :: Parser a -> Parser [a]plus p = plus_p where star_p = try plus_p <|> (return []) <?> "plus_star_p" plus_p = (:) <$> p <*> star_p <?> "plus_plus_p"
基于這些,我們可以做組裝出來一個parse lambda-exp的parser(p_seperate是對char、plus這些的組裝,表示形如a,b,c這樣的由特定字符分隔的序列):
p_lambda_exp :: Parser Expp_lambda_exp = p_between '(' ')' inner <?> "p_lambda_exp" where inner = make_lambda_exp <$ char '//' <*> p_seperate (p_parse p_pat) "," <*> p_parse p_exp make_lambda_exp [] e = (LambdaExp NilPat e) make_lambda_exp (p:[]) e = (LambdaExp p e) make_lambda_exp (p:ps) e = (LambdaExp p (make_lambda_exp ps e))
有了所有exp的parser,我們就可以組裝出來一個通用的exp parser:
p_exp :: Parser Exp p_exp = listplus [p_var_exp, p_const_exp, p_lambda_exp, p_app_exp, p_adt_exp, p_list_exp] <?> "p_exp"
其中,listplus是一種具有優先級的lookahead:
listplus :: [Parser a] -> Parser alistplus lst = foldr (<|>) mzero (map try lst)
-- Prelude.bh
Right [DeclaresDec [ DeclareDec (VarPat "seq") (AppType [GenericType "List" (GenericType "IO" (NormalType "Result")),GenericType "IO" (NormalType "Result")]),DeclareDec (VarPat "check") (AppType [NormalType "Bool",GenericType "IO" (NormalType "Result")])]]
-- BaseAI.bhRight [DeclaresDec [ DeclareDec (VarPat "HpRateLessThan") (AppType [NormalType "Float",GenericType "IO" (NormalType "Result")]),DeclareDec (VarPat "Idle") (GenericType "IO" (NormalType "Result"))]]
-- AI00001.bhRight [ ImportDec "Prelude",ImportDec "BaseAI",DefineDec (VarPat "Root") (AppExp (VarExp "par") (ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsFleeing") ,ADTExp "Cons" [ AppExp (LambdaExp (VarPat "a")(AppExp (VarExp "check") (AppExp (VarExp "IsNull") (VarExp "a")))) (VarExp "GetNearestTarget") ,ConstExp NilVal]]) ,ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsAttacking") ,ADTExp "Cons" [ AppExp (LambdaExp (VarPat "b") (AppExp (VarExp "HpRateLessThan") (VarExp "b"))) (VarExp "GetFleeBloodRate") ,ConstExp NilVal]]) ,ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsNormal") ,ADTExp "Cons" [ AppExp (VarExp "loop") (AppExp (VarExp "par") (ADTExp "Cons" [ AppExp (LambdaExp (VarPat "c") (AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (AppExp (VarExp"IsNull") (VarExp "c")) ,ADTExp "Cons" [ AppExp (VarExp "LockTarget") (VarExp "c") ,ConstExp NilVal]]))) (VarExp "GetNearestTarget") ,ADTExp "Cons" [ AppExp (VarExp"seq") (ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "ReachCurrentPatrolPoint") ,ADTExp "Cons" [ VarExp "MoveToNextPatrolPoiont" ,ConstExp NilVal]]) ,ADTExp "Cons" [ VarExp "Idle" ,ConstExp NilVal]]) ,ConstExp NilVal]])) ,ConstExp NilVal]]) ,ConstExp NilVal]]]))]
前面兩部分是我把在其他模塊定義的declares,選擇性地拿過來兩條。第三部分是這個人形怪AI的整個的AST。其中嵌套的Cons展開之后就是語言內置的List。
exp_type :: Exp -> TypeEnv -> Maybe Typeexp_type (AppExp lexp aexp) env = (exp_type aexp env) >>= (/at -> case lexp of LambdaExp (VarPat var) exp -> (merge_type_env (Just env) (make_type_env var (Just at))) >>= (/env1 -> exp_type lexp env1) _ -> (exp_type lexp env) >>= (/ltype -> check_type ltype at)) where check_type (AppType (t1:(t2:[]))) at = if t1 == at then (Just t2) else Nothing check_type (AppType (t:ts)) at = if t == at then (Just (AppType ts)) else Nothing
public static IO<Result> Root = Prelude.par(Help.MakeList( Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsFleeing) ,(((Box<Object> a) => Help.With(a, BaseAI.GetNearestTarget, Prelude.check(BaseAI.IsNull())))(new Box<Object>())))) ,Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsAttacking) ,(((Box<Float> b) => Help.With(b, BaseAI.GetFleeBloodRate, BaseAI.HpRateLessThan()))(new Box<Float>())))) ,Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsNormal) ,Prelude.loop(Prelude.par(Help.MakeList( (((Box<Object> c) => Help.With(c, BaseAI.GetNearestTarget, Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsNull()) ,BaseAI.LockTarget()))))(new Box<Object>())) ,Prelude.seq(Help.MakeList( Prelude.seq(Help.MakeList( Prelude.check(BaseAI.ReachCurrentPatrolPoint) ,BaseAI.MoveToNextPatrolPoiont)) ,BaseAI.Idle)))))))))
((Box<float> a) => (Help.With(a, UnitAI.GetFleeBloodRate, Math.Plus(a, 0.1)))(new Box<float>())
public static Thunk<float> Plus(Thunk<float> a, Thunk<float> b) { return Help.MakePureThunk(a.GetUserValue() + b.GetUserValue()); }
如果a和b都是literal value,那就沒問題,但是如果有一個是被box包裹的,那就很顯然是有問題的。
所以需要對Thunk這個概念做一下擴展,使之能區別出動態的值與靜態的值。一般情況下的值,都是pure的;box包裹的值,是impure的。同時,這個pure的性質具有值傳遞性,如果這個值屬于另一個值的一部分,那么這個整體的pure性質與值的局部的pure性質是一致的。這里特指的值,包括List與IO。
整體的概念我們應該拿haskell中的impure monad做類比,比如haskell中的IO。haskell中的IO依賴于OS的輸入,所以任何返回IO monad的函數都具有傳染性,引用到的函數一定還會被包裹在IO monad之中。
所以,對于With這種情況的傳遞,應該具有這樣的特征:
所以With結點構造的時候,計算pure應該特殊處理一下。但是這個特殊處理的代碼污染性比較大,我在本文就不列出了,只是這樣提一下。
有了pure與impure的標記,我們在對函數調用的時候,就需要額外走一層。
本來一個普通的函數調用,比如UnitAI.Func(p0, p1, p2)與Math.Plus(p0, p1)。前者返回一種computing是毫無疑問的,后者就需要根據參數的類型來決定是返回一種計算還是直接的值。
為了避免在這個Plus里面改來改去,我們把Closure這個概念給抽象出來。同時,為了簡化討論,我們只列舉T0 -> TR這一種情況,對應的標準庫函數取Abs。
public class Closure<T0, TR> : Thunk<Closure<T0, TR>> { class UserFuncApply : Thunk<TR> { private Closure<T0, TR> func; private Thunk<T0> p0; public UserFuncApply(Closure<T0, TR> func, Thunk<T0> p0) { this.func = func; this.p0 = p0; this.pure = false; } public override TR GetUserValue() { return func.funcThunk(p0).GetUserValue(); } } private bool isUserFunc = false; private FuncThunk<T0, TR> funcThunk; private Func<T0, TR> userFunc; public Closure(FuncThunk<T0, TR> funcThunk) { this.funcThunk = funcThunk; } public Closure(Func<T0, TR> func) { this.userFunc = func; this.funcThunk = p0 => Help.MakePureThunk(userFunc(p0.GetUserValue())); this.isUserFunc = true; } public override Closure<T0, TR> GetUserValue() { return this; } public Thunk<TR> Apply(Thunk<T0> p0) { if (!isUserFunc || Help.AllPure(p0)) { return funcThunk(p0); } return new UserFuncApply(this, p0); } }
其中,UserFuncApply就是之前所說的一層計算的概念。UserFunc表示的是等效于可以編譯期計算的一種標準庫函數。
這樣定義:
public static class Math { public static readonly Thunk<Closure<float, float>> Abs = Help.MakeUserFuncThunk<float,float>(System.Math.Abs); }
Message類型的Closure構造,都走FuncThunk構造函數;普通函數類型的構造,走Func構造函數,并且包裝一層。
Help.Apply是為了方便做代碼生成,描述一種declarative的application。其實就是直接調用Closure的Apply。
考慮以下幾種case:
public void Test() { var box1 = new Box<float>(); // Math.Abs(box1) -> UserFuncApply // 在GetUserValue的時候才會求值 var ret1 = Help.Apply(Math.Abs, box1); // Math.Abs(0.2f) -> Thunk<float> // 直接構造出來了一個Thunk<float>(0.2f) var ret2 = Help.Apply(Math.Abs, Help.MakePureThunk(0.2f)); // UnitAISets<IUnit>.HpRateLessThan(box1) -> Message var ret3 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, box1); // UnitAISets<IUnit>.HpRateLessThan(0.2f) -> Message var ret4 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, Help.MakePureThunk(0.2f)); }
與之前的runtime版本唯一表現上有區別的地方在于,對于純pure參數的userFunc,在Apply完之后會直接計算出來值,并重新包裝成一個Thunk;而對于參數中有impure的情況,返回一個UserFuncApply,在GetUserValue的時候才會求值。
新聞熱點
疑難解答