Source: Active Logic/Decorators - Last Updated: 2019.7.30
The API ships with several built-in decorators.
After/Delay (Only available in Unity)
NOTE: considered for deprecation; use Wait(float)
instead
Consume the specified delay before evaluating the subtask; after the subtask has completed or failed, reset.
status s = After(5f)?[ Idle() ];
// or ..
Delay delay = 5f;
status s = delay.pass?[ Idle() ];
NOTE: a delay is consumed on a frame basis, whenever control traverses the decorator.
Cooldown
Prevents re-iterating a subtask for a specified duration after the subtask has executed or failed;
status s = Cooldown(5f)?[ Strike() ];
// or...
Cooldown c = 0.1f;
status s = c.pass?[ Strike() ];
Every/Interval
Evaluates periodically; use as a scheduler.
status s = Every(2.5f)?[ Beep() ];
// or...
Interval i = 2.5f;
status s = i.pass?[ Beep() ];
Other constructors
Interval()
- Create an interval with period 1 (seconds); fire on start.
Interval(bool fireOnStart)
- Create an interval with period 1.
Interval(float period, float offset=0f, bool fireOnStart=true)
- Create an interval
Parameters
bool catchup
- When the time gap is large, apply the subtask repeatedly to catchup (default: disabled)
float offset
- Time offset to effect the subtask (default: 0)
period
- The period (default: 1)
InOut
Uses paired conditions to determine when to allow or block a subtask. When the first condition is true, InOut 'enters' the subtask and will then continue evaluating said subtask (even if the first condition subsequently fails), until the second condition becomes false. InOut resets when the subtask succeeds or fails.
status s = InOut(d < 5, d > 10)?[ Chase() ];
// or ..
InOut cond = new InOut();
status s = cond[d < 5, d > 10]?[ Beep() ];
NOTE: with InOut, both conditions evaluate at every iteration, disregarding the status of the target task.
Latch
Blocks the subtask until its condition clears; subsequently the subtask evaluates (disregarding the latch condition) until the subtask completes or fails.
status s = Latch(rage >= 100)?[ Berserk() ];
// or ..
Latch l = new Latch();
status s = l[ rage >= 100 ]?[ Berserk() ];
NOTE: with Latch, the condition evaluates at every iteration, even if the latch is 'open'.
Once
Evaluate the subtask until it completes or fails. Thereafter, skip evaluation and return the final status (stateful, RoR, thread unsafe).
status s = Once()?[ animator.SetTrigger("Strike") ];
// or ..
Once once = new Once();
status s = once.pass?[ animator.SetTrigger("Strike") ];
Use the Once decorator when re-iterating a task after it has completed is not desired. For example, in order to give an item, an agent first should be holding the item. However this action must not reiterate (or the agent won't ever let go of said item).
Where you have a mostly stateless composite, and wish only a couple of tasks to not re-iterate, prefer this decorator to using an ordered composite; as an example, this:
public status Throw(Transform item, Vector3 dir) => Sequence()[
and ? Hold(item) :
and ? Face(dir) && Play("Throw")
* (Wait(0.5) && Impel(that, dir)) : end];
May be written as:
public status Throw(Transform that, Vector3 dir)
=> Once()?[Hold(that)]
&& Face(dir) && Play("Throw")
* (Wait(0.5) && Impel(that, dir));
Timeout
Evaluates the subtask until it completes or fails. If, however, the subtask doesn't complete or fail within the specified time frame, subsequent evaluations are blocked until Reset()
.
status s = Timeout(5f)?[ Idle() ];
// or ..
Timeout t = 5f;
status s = t.pass?[ Idle() ];
Wait
Wait delays execution.
Initially, and until the delay has expired, return cont
; thereafter, return done
until reset (RoR).
Wait(0.5f); // wait 0.5 seconds
// or...
Wait wait = 0.5f;
wait(0.5f);
While and Tie (Drive)
While drives a subtask while a control task is running. With While
, the subtask is treated as a side effect, and its return state is ignored.
While( x )?[ y ]
// or ...
Drive @while = new Drive();
status s = @while[ x, crit: false ]?[ y ];
Similar to While, Tie also drives a subtask while a control task is running; however, while the driving task is running, Tie
returns the value of the subtask.
Tie( x )?[ y ]
// or ...
Drive @tie = new Drive();
status s = @tie[ x, crit: true ]?[ y ];
An example using both:
// Inside "Vehicle class"
status Navigate => Tie( engine.Run() )?[ Move() ];
// Inside "Engine class"
status Run => Drive( Burn().never )?[ Play("Noise") ];
impending Burn(){
if(oil > 0){
oil--; return cont();
}else{
return fail(log && "Out of oil");
}
}
In this case:
- While the engine is running, moving is enabled;
Tie
is used because moving may fail whether the engine is running or not. - Since the engine noise is a side effect,
Drive
is used. Burn
is impending because it never returns the "done" status.
While
and Tie
allow a boolean argument as left hand. Often the left hand represents consuming a resource, or refers to a binary state. In the above example, the Burn()
function may be written as:
bool Burn(){
if(oil > 0){
oil--; return true;
}else{
return false;
}
}
Notes:
- The Tie decorator (eval rh while running) naturally complements the && (eval rh while succeeding) and || (eval rh while failing) operators. Since C# does not allow yet another short-circuiting operator, tie is implemented as a decorator.
- Although
Drive
is stateless, decorator semantics require an object. Reusing this decorator should be safe and (unlike stateful decorators), placing severalTie
/Drive
invocations on the same line of code should be safe.
With
Use With
when data needs to be initialized, then re-initialized whenever an associate subtask has completed or failed.
var s = With()?[ EXP ] % TASK;
var s = With()?[ EXP ] + TASK;
var s = With()?[ EXP ] - TASK;
// or...
Init cond = new Init();
status s = cond.pass?[ EXP ] % TASK; // or replace '%' with '+/-'
Initially, EXP
is evaluated; subsequently, do not evaluate EXP, unless:
%
is used and TASK is complete or failing (re-init on end)+
is used and TASK is complete (re-init on complete)-
is used and TASK is failing (re-init on fail)
TASK
is always evaluated.