Skip to content

Commit 0043e2f

Browse files
committed
Clean APIs and document
1 parent 9921d30 commit 0043e2f

28 files changed

+341
-149
lines changed

Documentation/Overview.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# A User Guide *Beyond GOAP* [DRAFT]
2+
3+
## What is GOAP?
4+
5+
GOAP (Goal oriented action planning) refers to a family of planning AIs inspired by [Jeff Orkin's GOAP](http://alumni.media.mit.edu/~jorkin/gdc2006_orkin_jeff_fear.pdf).
6+
7+
In GOAP, an agent are assigned a goal (escape a room, take cover, knock a target down, heal, ...) and utilize actions whose *preconditions*, *cost* and *effects* are known to fulfill the goal condition.
8+
9+
A search algorithm (such as A*) resolves the action sequence which constitutes a path to the goal.
10+
11+
A *heuristic*, estimating the extra cost to reach the goal, is often provided.
12+
13+
## While you GOAP
14+
15+
While reading, also check the [Sentinel demo](https://youtu.be/mbLNALyt5So) and associate [project files](https://github.com/active-logic/xgoap-demos).
16+
17+
## Planning Agent/Model
18+
19+
This library provides a solver and APIs to help you implement your own planning AIs.
20+
21+
The critical part of your AI is the model, aka 'agent'; the model represents an AI's knowledge of the environment they operate in, including itself.
22+
23+
- Your model is a class implementing `Agent` or `Mapped` (to express planning actions, aka 'options').
24+
25+
The solver needs to generate and organize copies of the model object, therefore cloning, hashing and comparing for equality are common operations applied many times over.
26+
27+
- Minimally, tag your model [*Serializable*](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/serialization/) or (much better) implement `Clonable`.
28+
- Override `Equals()` and `GetHashCode()` (sloppy hashcodes decrease performance)
29+
30+
### Planning actions
31+
32+
Specify planning actions (options) via `Agent` and/or `Mapped` (or both!)
33+
34+
Usually, `Agent` suffices - easier to implement, and (currently) faster.
35+
36+
```cs
37+
Option[] Agent.Options() => new Option[]{ JumpForward, Shoot, ... };
38+
39+
public Cost JumpForward{ ... }
40+
```
41+
42+
Use `Mapped` when you need to parameterize planning actions:
43+
44+
```cs
45+
(Option, Action)[] Mapped.Options(){
46+
var n = inventory.Length;
47+
var opt = new (Option, Action)[n];
48+
for(int i = 0; i < n; i++){
49+
var j = i; // don't capture the iterator!
50+
opt[i] = ( () => Use(j),
51+
() => client.Use(inventory[j]) );
52+
}
53+
return opt;
54+
}
55+
```
56+
57+
In the above example:
58+
- An inventory is considered
59+
- An option is generated for each item in the inventory
60+
- Options are mapped to game action via `(Option, System.Action)` tuples
61+
62+
NOTE: *In the above we are careful* not *to use the iterator `i` inside the lambda, otherwise all invocations of the lambda function would end up using a value of `n-1`*.
63+
64+
`Mapped` options are flexible and type safe, giving you complete control over how a planning action maps to a game action; `Agent` is much faster to implement.
65+
66+
### The Clonable interface
67+
68+
Implement `Allocate()` to create a model object instance. The purpose of this function is to perform all memory allocations upfront, not determine state.
69+
70+
Implement `Clone(T storage)` to copy model state. This function **must** assign all fields (to avoid leaking dirty state).
71+
72+
```cs
73+
class MyModel : Clonable<MyModel>{
74+
75+
T byRef; // Assuming T : Clonable<T>; not required but handy
76+
int byValue;
77+
78+
MyModel(){
79+
byRef = new T(); // allocate everything
80+
// byValue = 5; // let's not do extra work
81+
}
82+
83+
public MyModel Allocate() => new MyModel();
84+
85+
public MyModel Clone(MyModel storage){
86+
this.byRef.Clone(storage.byRef); // Don't shallow copy
87+
byValue = 5; // Set all fields
88+
}
89+
90+
}
91+
```
92+
93+
Designed for instance reuse, this API enables optimizations, such as pooling.
94+
95+
Note: *The `Allocate` API is required because newing a `T : class, new()` object resolves to an `AlloceSlow` variant (the name says it all)*
96+
97+
### Test your model
98+
99+
Beyond GOAP cleanly separates your planning model from the game engine and/or actor (the object implementing actual game actions). This allows putting your model under test even before integrating an actual game actor.
100+
101+
## Integration
102+
103+
With a working model handy, you want to plug this into your game/simulation. The library provides a simple integration, mainly intended for (but not tied to) Unity3D.
104+
105+
The integration implements a two step *(planning -> action)* cycle:
106+
107+
1 - A plan is generated
108+
2 - The *first* action in the plan is applied
109+
(Rinse and repeat until the goal is attained)
110+
111+
We might plan once and step through all steps; however since world state changes dynamically, re-planning often keeps our agents on track.
112+
113+
NOTE: *In the future the integration will give you more control over how often replanning is applied*
114+
115+
To use the integration, subclass `GameAI`, as explained below.
116+
117+
### Subclassing `GameAI`
118+
119+
A `Goal` consists in a function, which verifies whether an instance of the model `T` satisfies the goal, and a heuristic function 'h', which measures the distance/cost between a model state designated as 'current' and the goal.
120+
Sometimes you don't have a heuristic, or can't come up with anything just yet. That's okay (still, a heuristic dramatically speeds up planning).
121+
122+
[`GameAI`](../Runtime/GameAI.cs) specifies a handful of functions that you need to implement in order to get your game actors going:
123+
124+
- Supply a goal for the agent to strive towards.
125+
- Link your planning model
126+
- (optionally) implement an `Idle()` mode.
127+
- Implement `IsActing()` to indicate when the actor are available for planning.
128+
129+
The `Goal()` method (assume `x` of type `T`):
130+
131+
```cs
132+
override public Goal<T> Goal() => (
133+
x => cond, // such as `x.someValue == true`
134+
x => heuristic // such as `x.DistTo(x.target)` or null if unavailable
135+
);
136+
```
137+
138+
Your implementation of `T Model()` should a model instance which represents the current state of the agent and their environment, for example:
139+
140+
```cs
141+
// Model definition
142+
class MyModel{ float x, float z; }
143+
144+
// inside MyAI : GameAI<MyModel>
145+
override public MyModel Model(){
146+
return new MyModel(transform.position.x, transform.position.z);
147+
}
148+
```
149+
150+
While `IsActing()` returns false, the planner will be running and evaluating the next action; how you implement this (whether event based, or testing the state of the game actor...) is entirely up to you; likewise the `Idle()` function.
151+
152+
```cs
153+
override public bool IsActing() => SomeCondition() && SomeOther();
154+
```
155+
156+
### Providing counterparts for planning options
157+
158+
Since planning actions aren't 'real' game actions, your `GameAI` implementation must supply these.
159+
160+
- With `Agent`, all planning actions must have same-name, no-arg counterparts in `GameAI`.
161+
- With `Mapped`, one approach consists in defining an interface, which specifies methods to be implemented both as planning actions, and as game actions. The [Baker](`../Tests/Models/Baker.cs`) example illustrates this approach.
162+
163+
## Running your AI (Unity 3D only)
164+
165+
Once you have implemented your `GameAI` subclass, it can be added to any game object (In Unity, `GameAI` derives from `Monobehaviour`).
166+
167+
Additionally, tweaks are available...
168+
169+
- *verbose* - gives you basic information (in the console) about what actions are applied to the game AI
170+
171+
Then, under 'solver params':
172+
173+
- *Frame budget* - max number of planning actions per game frame.
174+
- *Max nodes* - max number of states that should exist within the planner at any given time.
175+
- *Max iter* - the max number of iterations allowed to find a solution; after which the planner just bails out.
176+
- *Tolerance* - represents how closely the heuristic should be followed. For example if you don't care about a $10 difference (if 'cost' represents money) or a 0.5 seconds delta (if 'time cost' is the heuristic), set this to $10 or 0.5 seconds.
177+
Leaving this number to zero forces a full ordering, which significantly slows down the planner; but if you set this too high, you weaken the heuristic (which is also slower!) so there's no point in cranking it up.
178+
- *Safe* - If your actions are cleanly implemented, a failing action won't mutate model state; then, uncheck this and get a small performance bonus. If unsure, leave unchecked.

Documentation/Overview.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Documentation/Semantic sugar.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Semantic sugar in API design
2+
3+
## Problem being solved
4+
5+
In XGOAP, there was both 'planning actions' and 'planning functions'. The intention simply is to support both no-arg and parametric planning moves.
6+
7+
In practice, however, the solver does not deal with parametric functions, it only deals with parameterized invocations. In other words, choosing argument sets is still the client's responsibility.
8+
9+
As a result, how 'planning functions' are implemented is via explicit mappings between the planning action and the actual move performed by the agent, which created a need for naming this mapping.

Documentation/Semantic sugar.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,46 +43,41 @@ A woodcutter has the *GetAxe*, *ChopLog* and *CollectBranches* actions. Here is
4343
```cs
4444
using Activ.GOAP;
4545

46-
[Serializable] // Helps cloning model state (or implement `Clonable`)
47-
public class WoodChopper : Agent{
46+
public class WoodChopper : Agent, Clonable<WoodChopper>{
4847

4948
public bool hasAxe, hasFirewood;
49+
Option[] opt; // Caching reduces array alloc overheads
5050
51-
// In production, cache your action list(s) to avoid GC overheads
52-
public Func<Cost>[] Actions() => new Func<Cost>[]{
53-
ChopLog, GetAxe, CollectBranches
54-
};
51+
public Option[] Options()
52+
=> opt = opt ?? new Option[]{ ChopLog, GetAxe, CollectBranches };
5553

5654
public Cost GetAxe(){
5755
if(hasAxe) return false;
5856
hasAxe = true;
5957
return 2;
6058
}
6159

62-
public bool ChopLog(){
63-
if(!hasAxe) return false;
64-
hasFirewood = true;
65-
return 4;
66-
}
60+
public Cost ChopLog() => hasAxe ? (Cost)(hasFirewood = true, 4f) : (Cost)false;
61+
62+
public Cost CollectBranches() => (hasFirewood = true, 8);
63+
64+
// Clonable<WoodChopper>
6765
68-
// Expression-bodied shorthands are supported
69-
public bool CollectBranches() => (hasFirewood = true, 8);
66+
public WoodChopper Allocate() => new WoodChopper();
7067

71-
// Needed to avoid reentering previously visited states while searching
72-
override public bool Equals(object other){
73-
if(other == null) return false;
74-
if(other is WoodChopper that){
75-
return this.hasAxe == that.hasAxe
76-
&& this.hasFirewood == that.hasFirewood;
77-
} else return false;
68+
public WoodChopper Clone(WoodChopper x){
69+
x.hasAxe = hasAxe;
70+
x.hasFirewood = hasFirewood;
71+
return x;
7872
}
7973

80-
// Helps quickly finding duplicate states
81-
override public int GetHashCode()
82-
=> (hasAxe ? 1 : 0) + (hasFirewood ? 2 : 0);
74+
// Override for correctness (don't compare refs) and faster hashes
8375
84-
override public string ToString()
85-
=> $"WoodChopper[axe:{hasAxe} f.wood:{hasFirewood} ]";
76+
override public bool Equals(object other) => other is WoodChopper that
77+
&& hasAxe == that.hasAxe && hasFirewood == that.hasFirewood;
78+
79+
override public int GetHashCode() => (hasAxe ? 1 : 0)
80+
+ (hasFirewood ? 2 : 0);
8681

8782
}
8883
```
@@ -92,15 +87,15 @@ Run the model and get the next planned action:
9287
```cs
9388
var chopper = new WoodChopper();
9489
var solver = new Solver<WoodChopper>();
95-
var next = solver.Next(chopper, new Goal<WoodChopper>(x => x.hasFirewood));
90+
var next = solver.Next(chopper, goal: (x => x.hasFirewood, null));
9691
```
9792

9893
Parametric actions are supported; they are concise and type safe. Check the [Baker](Tests/Models/Baker.cs) example.
9994

100-
The goal argument (here, `x => x.hasFirewood`) returns a `bool` to indicate whether the goal has been reached.
101-
10295
Quick and simple Unity integration via [GameAI.cs](Runtime/GameAI.cs) - for details, [read here](Documentation/BakerUnity.md).
10396

97+
Ready to GOAP?, [follow the guide](Documentation/Overview.md)
98+
10499
## Getting involved
105100

106101
If you'd like to get involved, consider opening (or fixing) an issue.

Runtime/Agent.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace Activ.GOAP{
44

5+
public delegate Cost Option();
6+
57
public interface Agent{
6-
Func<Cost>[] Actions();
8+
Option[] Options();
79
}
810

9-
public interface Parametric{
10-
Complex[] Functions();
11+
public interface Mapped{
12+
(Option option, Action action)[] Options();
1113
}
1214

1315
public interface Clonable<T>{

Runtime/Complex.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

Runtime/Complex.cs.meta

Lines changed: 0 additions & 11 deletions
This file was deleted.

Runtime/Details/Cost.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
using System;
22
using ArgEx = System.ArgumentException;
3+
using static Activ.GOAP.Strings;
34

45
namespace Activ.GOAP{
56
// Note: in general cost values not strictly positive are unsafe;
67
// however this isn't checked here since it depends on the solver
78
public readonly struct Cost{
89

9-
const string CostRangeErr = "Cost must be strictly positive";
10-
1110
public readonly bool done;
1211
public readonly float cost;
1312

@@ -19,8 +18,7 @@ public static implicit operator Cost(bool flag)
1918
public static implicit operator Cost(float cost)
2019
=> new Cost(true, cost);
2120

22-
public static implicit operator Cost(ValueTuple<object, float> t){
23-
return new Cost(true, t.Item2);
24-
}
21+
public static implicit operator Cost((object, float cost) t)
22+
=> new Cost(true, t.cost);
2523

2624
}}

Runtime/Details/Node.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ namespace Activ.GOAP{
44
public class Node<T> : Base{
55

66
public readonly Node<T> prev;
7-
public readonly Func<Cost> source;
7+
public readonly Option source;
88
public readonly T state;
99
public float value;
1010
public float cost{ get; private set; }
1111
readonly object effect;
1212

13-
public Node(Func<Cost> planningAction, T result,
13+
public Node(Option planningAction, T result,
1414
Node<T> prev = null, float cost = 0f){
1515
this.source = Assert(planningAction, "Action");;
1616
this.state = Assert(result, "Result");

0 commit comments

Comments
 (0)