Skip to content

Commit 14df3cc

Browse files
committed
Encapsulate model state reuse
1 parent 45f85e7 commit 14df3cc

17 files changed

+218
-51
lines changed

Runtime/Details/Dish.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Ex = System.Exception;
2+
using System;
3+
4+
// Containers for reusable planning states
5+
namespace Activ.GOAP{
6+
internal abstract class Dish<T> where T : class{
7+
8+
protected T proto, state;
9+
10+
public abstract void Init(T prototype);
11+
public abstract T Avail();
12+
13+
public virtual void Invalidate (){}
14+
public void Consume () => state = null;
15+
16+
}
17+
18+
// Dirty dish assumes failing actions do not mutate model state
19+
// Faster because no 'cleaning the dish' after a failed action.
20+
internal class DirtyDish<T> : Dish<T> where T : class{
21+
22+
bool dirty; Clonable<T> clonable;
23+
24+
override public void Init(T prototype){
25+
clonable = (Clonable<T>) prototype;
26+
proto = prototype; dirty = true;
27+
}
28+
29+
override public T Avail()
30+
{ if(dirty) state = Clone(); dirty = false; return state; }
31+
32+
override public void Invalidate() => dirty = true;
33+
34+
T Clone() => clonable.Clone(state ?? clonable.Allocate());
35+
36+
}
37+
38+
// Cleans after every action; supports serial clones
39+
internal class PolyDish<T> : Dish<T> where T : class{
40+
41+
override public void Init(T prototype) { proto = prototype; }
42+
43+
override public T Avail() { state = Clone(); return state; }
44+
45+
T Clone() => (proto is Clonable<T> src)
46+
? src.Clone(state ?? src.Allocate())
47+
: CloneUtil.DeepClone(proto);
48+
49+
}}

Runtime/Details/Dish.cs.meta

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

Runtime/GameAI.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ public virtual void Update(){
4848
solver.maxNodes = config.maxNodes;
4949
solver.maxIter = config.maxIter;
5050
solver.tolerance = config.tolerance;
51+
solver.cleanActions = config.safeActions;
5152
if(handler is ActionMap m) m.verbose = verbose;
5253
handler.Effect( solver.isRunning
5354
? solver.Iterate(config.frameBudget)?.Head()
5455
: solver.Next(model, Goal(), config.frameBudget)?.Head(),
5556
this);
56-
if(policies.OnResult(solver.state, ObjectName())){
57+
if(policies.OnResult(solver.status, ObjectName())){
5758
//ebug.Log($"Clear solver - {solver.state}");
5859
solver = null;
5960
}

Runtime/Solver.cs

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,48 @@ public class Solver<T> : SolverStats where T : class{
1616
public float tolerance = 0f;
1717
public bool brfs = false;
1818
public bool cleanActions = true;
19-
public PlanningState state { get; private set; }
19+
public PlanningState status { get; private set; }
2020
public int fxMaxNodes { get; private set; }
2121
public int I { get; private set; }
2222
//
23-
T storage;
24-
T initialState;
25-
Goal<T> goal;
26-
NodeSet<T> avail = null;
23+
T initialState;
24+
Dish<T> dish;
25+
Goal<T> goal;
26+
NodeSet<T> avail = null;
2727

28-
public bool isRunning => state == S.Running;
28+
public bool isRunning => status == S.Running;
2929

3030
public Node<T> Next(T s, in Goal<T> g, int iter=-1){
3131
if(s == null) throw new NullRef(NO_INIT);
32+
if(dish == null) dish = (s is Clonable<T> && cleanActions)
33+
? (Dish<T>)new DirtyDish<T>() : new PolyDish<T>();
3234
initialState = s;
3335
goal = g;
34-
avail = new NodeSet<T>(s, g.h, !brfs, maxNodes, tolerance);
3536
I = 0;
37+
avail = new NodeSet<T>(s, g.h, !brfs, maxNodes,
38+
tolerance);
3639
return Iterate(iter);
3740
}
3841

3942
public Node<T> Iterate(int iter=-1){
4043
if(initialState == null) throw new InvOp(NO_INIT);
41-
if(state == S.MaxIterExceeded) return null;
44+
if(status == S.MaxIterExceeded) return null;
4245
if(iter == -1) iter = maxIter;
4346
int i = 0;
4447
while(avail && i++ < iter && I++ < maxIter){
45-
//rint($"# {I} (avail: {avail.count}) --------------- ");
4648
var current = avail.Pop();
4749
if(goal.match(current.state)){
48-
state = S.Done;
50+
status = S.Done;
4951
return current;
5052
}
5153
ExpandActions(current, avail);
5254
ExpandMethods(current, avail);
5355
if(avail.count > fxMaxNodes) fxMaxNodes = avail.count;
54-
//rint("Avail: " + avail.count);
5556
}
5657
if(avail.capacityExceeded){
57-
state = S.CapacityExceeded;
58+
status = S.CapacityExceeded;
5859
}else{
59-
state = avail
60+
status = avail
6061
? (I < maxIter ? S.Running : S.MaxIterExceeded)
6162
: S.Failed;
6263
}
@@ -67,53 +68,42 @@ void ExpandActions(Node<T> x, NodeSet<T> @out){
6768
if(!(x.state is Agent p)) return;
6869
var count = p.Actions()?.Length ?? 0;
6970
if(count == 0) return;
70-
// Initially assume a dirty state, which just menans that
71-
// the canvas needs to be init'd from x.state.
72-
bool dirty = true;
71+
dish.Init(x.state);
7372
T y = null;
7473
for(int i = 0; i < count; i++){
75-
//y = Clone(x.state);
76-
if(dirty){ y = Clone(x.state); dirty = false; }
74+
y = dish.Avail();
7775
var q = y as Agent;
7876
var r = q.Actions()[i]();
7977
if(r.done){
80-
// Since the action succeeded, state is now dirty.
81-
dirty = true;
78+
dish.Invalidate();
8279
if(!brfs && (r.cost <= 0))
8380
throw new Ex(ZERO_COST_ERR);
8481
var name = p.Actions()[i].Method.Name;
8582
if(@out.Insert(new Node<T>(name, y, x, r.cost)))
86-
storage = null;
83+
dish.Consume();
8784
}
8885
}
8986
}
9087

9188
void ExpandMethods(Node<T> x, NodeSet<T> @out){
9289
if(!(x.state is Parametric p)) return;
93-
if(p.Functions() == null) return;
94-
for(int i = 0; i < p.Functions().Length; i++){
95-
var y = Clone(x.state);
90+
var count = p.Functions()?.Length ?? 0;
91+
if(count == 0) return;
92+
dish.Init(x.state);
93+
T y = null;
94+
for(int i = 0; i < count; i++){
95+
y = dish.Avail();
9696
var q = y as Parametric;
9797
var r = q.Functions()[i].action();
9898
if(r.done){
99+
dish.Invalidate();
99100
if(!brfs && r.cost <= 0)
100101
throw new Ex(ZERO_COST_ERR);
101102
var effect = p.Functions()[i].effect;
102103
if(@out.Insert(new Node<T>(effect, y, x, r.cost)))
103-
storage = null;
104+
dish.Consume();
104105
}
105106
}
106107
}
107108

108-
// TODO: strictly call to Clone() is not needed if an action has
109-
// failed and did not touch the matching state. So, as an unsafe
110-
// optimization, can skip that.
111-
internal T Clone(T x){
112-
if(x is Clonable<T> c){
113-
return c.Clone(storage = storage ?? c.Allocate());
114-
}else{
115-
return CloneUtil.DeepClone(x);
116-
}
117-
}
118-
119109
}}

Runtime/SolverParams.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ [System.Serializable] public class SolverParams{
55
public int maxNodes = 1000;
66
public int maxIter = 1000;
77
public float tolerance = 0;
8+
public bool safeActions = false;
89

910
}}

Runtime/SolverStats.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace Activ.GOAP{
55

66
public interface SolverStats{
77

8-
PlanningState state { get; }
9-
int fxMaxNodes { get; }
10-
int I { get; }
8+
PlanningState status { get; }
9+
int fxMaxNodes { get; }
10+
int I { get; }
1111

1212
}
1313

Tests/Editor/Bench_Dish.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using NUnit.Framework;
2+
3+
namespace Activ.GOAP{
4+
public class Bench_Dish{
5+
6+
T proto = new T();
7+
PolyDish<T> x;
8+
9+
[SetUp] public void Setup() => x = new PolyDish<T>();
10+
11+
[Test] public void Perf_XDish()
12+
=> RunTest(new PolyDish<T>());
13+
14+
[Test] public void Perf_DirtyDish()
15+
=> RunTest(new DirtyDish<T>());
16+
17+
void RunTest(Dish<T> x){
18+
x.Init(proto);
19+
for(int i = 0; i < 1e6; i++){
20+
x.Avail();
21+
x.Invalidate();
22+
x.Avail();
23+
x.Consume();
24+
x.Avail();
25+
}
26+
}
27+
28+
class T : Clonable<T>{
29+
public T Allocate() => new T();
30+
public T Clone(T t) => t;
31+
}
32+
33+
}}

Tests/Editor/Bench_Dish.cs.meta

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

Tests/Editor/DirtyDishTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using NUnit.Framework;
2+
using Activ.GOAP;
3+
4+
namespace Activ.GOAP{
5+
public class DirtyDishTest{
6+
7+
T proto = new T();
8+
DirtyDish<T> x;
9+
10+
[SetUp] public void Setup() => x = new DirtyDish<T>();
11+
[Test] public void Init() => x.Init(proto);
12+
[Test] public void Avail() => x.Avail();
13+
[Test] public void Invalidate() => x.Invalidate();
14+
[Test] public void Consume() => x.Consume();
15+
16+
class T : Clonable<T>{
17+
public T Allocate() => new T();
18+
public T Clone(T t) => t;
19+
}
20+
21+
}}

Tests/Editor/DirtyDishTest.cs.meta

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

0 commit comments

Comments
 (0)