-
Notifications
You must be signed in to change notification settings - Fork 113
Expand file tree
/
Copy pathBehaviorTree.js
More file actions
366 lines (327 loc) · 10.9 KB
/
BehaviorTree.js
File metadata and controls
366 lines (327 loc) · 10.9 KB
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import { createUUID } from '../b3.functions';
import { COMPOSITE, DECORATOR } from '../constants';
import * as Decorators from '../decorators';
import * as Composites from '../composites';
import * as Actions from '../actions';
import Tick from './Tick';
/**
* The BehaviorTree class, as the name implies, represents the Behavior Tree
* structure.
*
* There are two ways to construct a Behavior Tree: by manually setting the
* root node, or by loading it from a data structure (which can be loaded
* from a JSON). Both methods are shown in the examples below and better
* explained in the user guide.
*
* The tick method must be called periodically, in order to send the tick
* signal to all nodes in the tree, starting from the root. The method
* `BehaviorTree.tick` receives a target object and a blackboard as
* parameters. The target object can be anything: a game agent, a system, a
* DOM object, etc. This target is not used by any piece of Behavior3JS,
* i.e., the target object will only be used by custom nodes.
*
* The blackboard is obligatory and must be an instance of `Blackboard`. This
* requirement is necessary due to the fact that neither `BehaviorTree` or
* any node will store the execution variables in its own object (e.g., the
* BT does not store the target, information about opened nodes or number of
* times the tree was called). But because of this, you only need a single
* tree instance to control multiple (maybe hundreds) objects.
*
* Manual construction of a Behavior Tree
* --------------------------------------
*
* var tree = new b3.BehaviorTree();
*
* tree.root = new b3.Sequence({children:[
* new b3.Priority({children:[
* new MyCustomNode(),
* new MyCustomNode()
* ]}),
* ...
* ]});
*
*
* Loading a Behavior Tree from data structure
* -------------------------------------------
*
* var tree = new b3.BehaviorTree();
*
* tree.load({
* 'title' : 'Behavior Tree title'
* 'description' : 'My description'
* 'root' : 'node-id-1'
* 'nodes' : {
* 'node-id-1' : {
* 'name' : 'Priority', // this is the node type
* 'title' : 'Root Node',
* 'description' : 'Description',
* 'children' : ['node-id-2', 'node-id-3'],
* },
* ...
* }
* })
*
*
* @module b3
* @class BehaviorTree
**/
export default class BehaviorTree {
/**
* Initialization method.
* @method initialize
* @constructor
**/
constructor() {
/**
* The tree id, must be unique. By default, created with `createUUID`.
* @property {String} id
* @readOnly
**/
this.id = createUUID();
/**
* The tree title.
* @property {String} title
* @readonly
**/
this.title = 'The behavior tree';
/**
* Description of the tree.
* @property {String} description
* @readonly
**/
this.description = 'Default description';
/**
* A dictionary with (key-value) properties. Useful to define custom
* variables in the visual editor.
*
* @property {Object} properties
* @readonly
**/
this.properties = {};
/**
* The reference to the root node. Must be an instance of `BaseNode`.
* @property {BaseNode} root
**/
this.root = null;
/**
* The reference to the debug instance.
* @property {Object} debug
**/
this.debug = null;
}
/**
* This method loads a Behavior Tree from a data structure, populating this
* object with the provided data. Notice that, the data structure must
* follow the format specified by Behavior3JS. Consult the guide to know
* more about this format.
*
* You probably want to use custom nodes in your BTs, thus, you need to
* provide the `names` object, in which this method can find the nodes by
* `names[NODE_NAME]`. This variable can be a namespace or a dictionary,
* as long as this method can find the node by its name, for example:
*
* //json
* ...
* 'node1': {
* 'name': MyCustomNode,
* 'title': ...
* }
* ...
*
* //code
* var bt = new b3.BehaviorTree();
* bt.load(data, {'MyCustomNode':MyCustomNode})
*
*
* @method load
* @param {Object} data The data structure representing a Behavior Tree.
* @param {Object} [names] A namespace or dict containing custom nodes.
**/
load(data, names) {
names = names || {};
this.title = data.title || this.title;
this.description = data.description || this.description;
this.properties = data.properties || this.properties;
var nodes = {};
var id, spec, node;
// Create the node list (without connection between them)
for (id in data.nodes) {
spec = data.nodes[id];
var Cls;
if (spec.name in names) {
// Look for the name in custom nodes
Cls = names[spec.name];
} else if (spec.name in Decorators) {
// Look for the name in default nodes
Cls = Decorators[spec.name];
} else if (spec.name in Composites) {
Cls = Composites[spec.name];
} else if (spec.name in Actions) {
Cls = Actions[spec.name];
} else {
// Invalid node name
throw new EvalError('BehaviorTree.load: Invalid node name + "' +
spec.name + '".');
}
node = new Cls(spec.properties);
node.id = spec.id || node.id;
node.title = spec.title || node.title;
node.description = spec.description || node.description;
node.properties = spec.properties || node.properties;
nodes[id] = node;
}
// Connect the nodes
for (id in data.nodes) {
spec = data.nodes[id];
node = nodes[id];
if (node.category === COMPOSITE && spec.children) {
for (var i = 0; i < spec.children.length; i++) {
var cid = spec.children[i];
node.children.push(nodes[cid]);
}
} else if (node.category === DECORATOR && spec.child) {
node.child = nodes[spec.child];
}
}
this.root = nodes[data.root];
}
/**
* This method dump the current BT into a data structure.
*
* Note: This method does not record the current node parameters. Thus,
* it may not be compatible with load for now.
*
* @method dump
* @return {Object} A data object representing this tree.
**/
dump() {
var data = {};
var customNames = [];
data.title = this.title;
data.description = this.description;
data.root = (this.root) ? this.root.id : null;
data.properties = this.properties;
data.nodes = {};
data.custom_nodes = [];
if (!this.root) return data;
var stack = [this.root];
while (stack.length > 0) {
var node = stack.pop();
var spec = {};
spec.id = node.id;
spec.name = node.name;
spec.title = node.title;
spec.description = node.description;
spec.properties = node.properties;
spec.parameters = node.parameters;
// verify custom node
var proto = (node.constructor && node.constructor.prototype);
var nodeName = (proto && proto.name) || node.name;
if (!Decorators[nodeName] && !Composites[nodeName] && !Actions[nodeName] && customNames.indexOf(nodeName) < 0) {
var subdata = {};
subdata.name = nodeName;
subdata.title = (proto && proto.title) || node.title;
subdata.category = node.category;
customNames.push(nodeName);
data.custom_nodes.push(subdata);
}
// store children/child
if (node.category === COMPOSITE && node.children) {
var children = [];
for (var i = node.children.length - 1; i >= 0; i--) {
children.push(node.children[i].id);
stack.push(node.children[i]);
}
spec.children = children;
} else if (node.category === DECORATOR && node.child) {
stack.push(node.child);
spec.child = node.child.id;
}
data.nodes[node.id] = spec;
}
return data;
}
/**
* Propagates the tick signal through the tree, starting from the root.
*
* This method receives a target object of any type (Object, Array,
* DOMElement, whatever) and a `Blackboard` instance. The target object has
* no use at all for all Behavior3JS components, but surely is important
* for custom nodes. The blackboard instance is used by the tree and nodes
* to store execution variables (e.g., last node running) and is obligatory
* to be a `Blackboard` instance (or an object with the same interface).
*
* Internally, this method creates a Tick object, which will store the
* target and the blackboard objects.
*
* Note: BehaviorTree stores a list of open nodes from last tick, if these
* nodes weren't called after the current tick, this method will close them
* automatically.
*
* @method tick
* @param {Object} target A target object.
* @param {Blackboard} blackboard An instance of blackboard object.
* @return {Constant} The tick signal state.
**/
tick(target, blackboard) {
if (!blackboard) {
throw 'The blackboard parameter is obligatory and must be an ' +
'instance of b3.Blackboard';
}
/* CREATE A TICK OBJECT */
var tick = new Tick();
tick.debug = this.debug;
tick.target = target;
tick.blackboard = blackboard;
tick.tree = this;
/* TICK NODE */
var state = this.root._execute(tick);
/* CLOSE NODES FROM LAST TICK, IF NEEDED */
var lastOpenNodes = blackboard.get('openNodes', this.id);
var currOpenNodes = tick._openNodes.slice(0);
// does not close if it is still open in this tick
var start = 0;
var i;
for (i = 0; i < Math.min(lastOpenNodes.length, currOpenNodes.length); i++) {
start = i + 1;
if (lastOpenNodes[i] !== currOpenNodes[i]) {
break;
}
}
// close the nodes
for (i = lastOpenNodes.length - 1; i >= start; i--) {
lastOpenNodes[i]._close(tick);
}
/* POPULATE BLACKBOARD */
blackboard.set('openNodes', currOpenNodes, this.id);
blackboard.set('nodeCount', tick._nodeCount, this.id);
return state;
}
/**
* Close all open nodes to ensure close function in nodes be called.
* If stop running tree tick via external, call this function.
* @param target
* @param blackboard
* @returns {*}
*/
close(target, blackboard) {
if (!blackboard) {
throw 'The blackboard parameter is obligatory and must be an ' +
'instance of b3.Blackboard';
}
/* CREATE A TICK OBJECT */
var tick = new Tick();
tick.debug = this.debug;
tick.target = target;
tick.blackboard = blackboard;
tick.tree = this;
/* CLOSE ALL OPEN NODES */
var lastOpenNodes = blackboard.get('openNodes', this.id);
for (var i = 0; i < lastOpenNodes.length; i++) {
lastOpenNodes[i]._close(tick);
}
/* POPULATE BLACKBOARD */
blackboard.set('openNodes', tick._openNodes.slice(0), this.id);
blackboard.set('nodeCount', tick._nodeCount, this.id);
}
};