-
Notifications
You must be signed in to change notification settings - Fork 2
/
Transition.cs
214 lines (185 loc) · 6.3 KB
/
Transition.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
using System;
using System.Collections.Generic;
namespace Game.FSM
{
/// <summary>
/// Represents transition between the active state branches.
/// </summary>
public abstract class TransitionBase
{
/// <summary>
/// A list of conditions that needs to be met for the Transition to Invoke.
/// </summary>
public List<Func<bool>> Conditions { get; } = new List<Func<bool>>();
/// <summary>
/// Root context for the transition.
/// </summary>
public IStateContext Context { get; }
private readonly List<IState> targetStates = new List<IState>();
protected TransitionBase(IStateContext ctx)
{
this.Context = ctx ?? throw new ArgumentNullException(nameof(ctx));
}
/// <summary>
/// Adds a target state for transition with no data.
/// </summary>
/// <param name="target"></param>
public void AddTarget(IState target)
{
targetStates.Add(target);
}
/// <summary>
/// Immediately executes transition by switching context subtree to a list of target states.
/// </summary>
public void Execute()
{
if (targetStates.Count == 0)
{
throw new InvalidOperationException("Attempt to execute an empty transition.");
}
var stateContext = Context;
foreach (var state in targetStates)
{
if (stateContext == null) break;
BeforeStateSwitch(state);
stateContext.SwitchState(state);
stateContext = state;
}
}
protected abstract void BeforeStateSwitch(IState state);
protected bool TryInvoke()
{
if (CanInvoke())
{
Context.NextTransition = this;
return true;
}
return false;
}
private bool CanInvoke()
{
return Conditions.TrueForAll(ConditionPredicate);
}
private bool ConditionPredicate(Func<bool> c)
{
return c == null || c.Invoke();
}
}
/// <summary>
/// Transition with a BeforeSwitch action delegate associated with each target state.
/// </summary>
/// <typeparam name="TStateAction"></typeparam>
public abstract class TransitionBase<TStateAction> : TransitionBase
{
private readonly Dictionary<IState, TStateAction> executeActions = new Dictionary<IState, TStateAction>();
protected TransitionBase(IStateContext ctx) : base(ctx)
{
}
protected void AddTarget(IState state, TStateAction action)
{
base.AddTarget(state);
executeActions.Add(state, action);
}
protected override void BeforeStateSwitch(IState state)
{
if (state != null && executeActions.TryGetValue(state, out var action))
{
ApplyStateAction(state, action);
}
}
protected abstract void ApplyStateAction(IState state, TStateAction action);
}
/// <summary>
/// Transition without input data.
/// </summary>
public class Transition : TransitionBase<Action>, ITrigger
{
public Transition(IStateContext ctx) : base(ctx)
{
}
/// <summary>
/// Tries to invoke the transition if all conditions are met.
/// Transition will not be executed immediately, Execute has to be called separetely.
/// </summary>
public void Invoke()
{
TryInvoke();
}
/// <summary>
/// Adds target state with a transition data source function.
/// </summary>
/// <param name="state">The state to transition.</param>
/// <param name="dataSource">Where to get the output data for the target state.</param>
/// <returns></returns>
public void AddTarget<TOut>(IStateWithInput<TOut> state, Func<TOut> dataSource)
{
AddTarget(state, () =>
{
state.SetTransitionData(dataSource());
});
}
protected override void ApplyStateAction(IState state, Action action)
{
action();
}
}
/// <summary>
/// Transition with one input trigger parameter.
/// </summary>
/// <typeparam name="TIn"></typeparam>
public class Transition<TIn> : TransitionBase<Action<TIn>>, ITrigger<TIn>
{
/// <summary>
/// Data for the next execution of the transition.
/// </summary>
private TIn inputData;
public Transition(IStateContext ctx) : base(ctx)
{
}
/// <summary>
/// Tries to invoke the transition with given argument, if all conditions are met.
/// Transition will not be executed immediately, Execute has to be called separetely.
/// </summary>
public void Invoke(TIn arg)
{
if (TryInvoke())
{
inputData = arg;
}
}
/// <summary>
/// Immediately executes the transition with given input data.
/// </summary>
/// <param name="arg"></param>
public void Execute(TIn arg)
{
inputData = arg;
Execute();
}
/// <summary>
/// Adds a target state with transition data of the same type as the trigger data.
/// </summary>
/// <param name="state"></param>
public void AddTarget(IStateWithInput<TIn> state)
{
AddTarget(state, state.SetTransitionData);
}
/// <summary>
/// Adds a target state with transition data of type different from the trigger data.
/// </summary>
/// <param name="state"></param>
/// <param name="convert">Convertion between the trigger data and state transition data.</param>
/// <typeparam name="TOut"></typeparam>
public void AddTarget<TOut>(IStateWithInput<TOut> state, Func<TIn, TOut> convert)
{
AddTarget(state, arg =>
{
state.SetTransitionData(convert(arg));
});
}
protected override void ApplyStateAction(IState state, Action<TIn> action)
{
action(inputData);
}
}
}