行为树的 JavaScript 实现。它们对于实施人工智能很有用。如果您需要有关行为树的更多信息,请查看 GameDevAIPro,其中有一篇来自 Alex Champandard 和 Philip Dunstan 的关于行为树的好文章。
如果您需要 TypeScript 支持,请查看即将发布的 3.0 版本的typescript
分支,如果您有任何问题和意见,请告知。
如果您使用 npm:
npm install behaviortree
或使用纱线:
yarn add behaviortree
该包没有自己的依赖项。
首先,我应该提到的是,也可以在像 Node v8 这样的 common-js 环境中使用这个库。为此,您应该将所有import
语句切换为require()
语句。
所以而不是
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } from 'behaviortree'
只需使用
const { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } = require ( 'behaviortree' )
我使用新的 ES 模块语法,因为我认为它非常可读。所以所有的代码都是这样写的。要查看两个版本的工作示例,请访问/克隆示例的存储库。
任务是一个简单的Node
(准确地说是叶节点),它负责处理其run
方法中的所有脏工作,该方法返回true
、 false
或"running"
。为了清楚起见并保持灵活性,请使用提供的导出常量作为这些返回值( SUCCESS
、 FAILURE
、 RUNNING
)。
任务的每个方法都会接收黑板,该黑板是您在实例化行为树时分配的。黑板基本上是一个对象,它保存所有任务执行其工作以及与世界通信所需的数据和方法。
import { Task , SUCCESS } from 'behaviortree'
const myTask = new Task ( {
// (optional) this function is called directly before the run method
// is called. It allows you to setup things before starting to run
start : function ( blackboard ) {
blackboard . isStarted = true
} ,
// (optional) this function is called directly after the run method
// is completed with either this.success() or this.fail(). It allows you to clean up
// things, after you run the task.
end : function ( blackboard ) {
blackboard . isStarted = false
} ,
// This is the meat of your task. The run method does everything you want it to do.
run : function ( blackboard ) {
return SUCCESS
}
} )
方法:
start
- 在调用 run 之前调用。但如果任务在以 this.running() 结束后恢复,则不会end
- 在调用 run 后调用。但如果任务通过 this.running() 完成则不会run
- 包含您希望任务执行的主要操作Sequence
将依次调用其每个子节点,直到一个节点失败(返回FAILURE
)或调用所有节点。如果一个节点调用失败,则Sequence
将返回FAILURE
本身,否则它将调用SUCCESS
。
import { Sequence } from 'behaviortree'
const mySequence = new Sequence ( {
nodes : [
// here comes in a list of nodes (Tasks, Sequences or Priorities)
// as objects or as registered strings
]
} )
Selector
调用其列表中的每个节点,直到一个节点返回SUCCESS
,然后自身返回成功。如果它的子节点都没有调用SUCCESS
则选择器返回FAILURE
。
import { Selector } from 'behaviortree'
const mySelector = new Selector ( {
nodes : [
// here comes in a list of nodes (Tasks, Sequences or Priorities)
// as objects or as registered strings
]
} )
Random
选择器只是随机调用其子节点之一,如果返回RUNNING
,它将在下次运行时再次调用。
import { Random } from 'behaviortree'
const mySelector = new Random ( {
nodes : [
// here comes in a list of nodes (Tasks, Sequences or Priorities)
// as objects or as registered strings
]
} )
创建行为树的实例相当简单。只需实例化BehaviorTree
类并使用上面提到的节点和节点可以使用的黑板来指定树的形状。
import { BehaviorTree } from 'behaviortree'
var bTree = new BehaviorTree ( {
tree : mySelector ,
blackboard : { }
} )
您指定的blackboard
将作为第一个参数传递到每个start()
、 end()
和run()
方法中。您可以使用它,让行为树知道它正在哪个对象(例如人工玩家)上运行,让它与世界交互或根据需要保存状态位。要运行树,只要您有时间在游戏循环中进行一些 AI 计算,您就可以调用step()
。
bTree . step ( )
BehaviourTree 附带一个内部注册表,您可以在其中注册任务,然后按您选择的名称在节点中引用它们。如果您需要在多个树中执行相同的行为,或者想要将任务的定义和树的构造分开,这非常方便。
// Register a task:
BehaviorTree . register ( 'testtask' , myTask )
// Or register a sequence or priority:
BehaviorTree . register ( 'test sequence' , mySequence )
您现在可以在节点中简单地引用它,例如:
import { Selector } from 'behaviortree'
const mySelector = new Selector ( {
nodes : [ 'my awesome task' , 'another awe# task to do' ]
} )
使用注册表还有一个好处,对于只有一种run
方法的简单任务,有一种简短的方法可以编写它们:
BehaviorTree . register ( 'testtask' , ( blackboard ) => {
console . log ( 'I am doing stuff' )
return SUCCESS
} )
现在是一个如何协同工作的例子。
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } from 'behaviortree'
BehaviorTree . register (
'bark' ,
new Task ( {
run : function ( dog ) {
dog . bark ( )
return SUCCESS
}
} )
)
const tree = new Sequence ( {
nodes : [
'bark' ,
new Task ( {
run : function ( dog ) {
dog . randomlyWalk ( )
return SUCCESS
}
} ) ,
'bark' ,
new Task ( {
run : function ( dog ) {
if ( dog . standBesideATree ( ) ) {
dog . liftALeg ( )
dog . pee ( )
return SUCCESS
} else {
return FAILURE
}
}
} )
]
} )
const dog = new Dog ( /*...*/ ) // the nasty details of a dog are omitted
const bTree = new BehaviorTree ( {
tree : tree ,
blackboard : dog
} )
// The "game" loop:
setInterval ( function ( ) {
bTree . step ( )
} , 1000 / 60 )
在这个例子中,会发生以下情况:每次经过setInterval
(我们的游戏循环)时,狗都会吠叫——我们用注册节点实现了这一点,因为我们这样做了两次——然后它随机走动,然后再次吠叫,然后如果它发现自己站在一棵树旁边,它在树上撒尿。
每个节点也可以是Decorator
,它包装一个常规(或另一个装饰)节点,并控制它们的值或调用、添加一些条件或对其返回的状态执行某些操作。在src/decorators
目录中,您会发现一些已经实现的装饰器以供灵感或使用,例如InvertDecorator
可以否定装饰节点的返回值,或者CooldownDecorator
可以确保节点在冷却停机时间内仅被调用一次。
const decoratedSequence = new InvertDecorator ( {
node : 'awesome sequence doing stuff'
} )
创建一个自己的装饰器。您只需要一个扩展Decorator
类并重写装饰方法的类。只需查看src/decorators
子文件夹即可检查一些参考实现。
请注意,您不能简单地实例化Decorator
类并将decorate
方法作为蓝图作为动态装饰器传递,因为现在的工作方式。
为了您的方便,已经构建了几个“简单”装饰器。检查src/decorators
目录以获取更多详细信息(以及它们正在执行的操作的规范)。使用它们非常简单:
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE , decorators } from 'behaviortree'
const { AlwaysSucceedDecorator } = decorators
定义了一个BehaviorTreeImporter
类,可用于从树的JSON 定义中填充BehaviorTree
实例。定义结构如下所示:
{
"type" : " selector " ,
"name" : " the root " ,
"nodes" : [
{
"type" : " ifEnemyInSight " ,
"name" : " handling enemies " ,
"node" : { "type" : " walk " , "name" : " go to enemy " }
},
{
"type" : " cooldown " ,
"name" : " jumping around " ,
"cooldown" : 1 ,
"node" : { "type" : " jump " , "name" : " jump up " }
},
{ "type" : " idle " , "name" : " doing nothing " }
]
}
通过type
属性,导入器从内部类型定义中查找Decorators
、 Selectors
、 Sequences
和您自己定义的类,以及从BehaviorTree
注册表中查找任务,并返回一个对象,该对象可以在BehaviorTree
构造函数中用作tree
。
如果您不喜欢新的import
语句,您仍然应该能够使用传统的require
语句:
const {
BehaviorTree ,
Sequence ,
Task ,
SUCCESS ,
FAILURE ,
decorators : { AlwaysSucceedDecorator }
} = require ( 'behaviortree' )
您可以将introspector
参数添加到包含Introspector
类或实现类似接口的另一个类的实例的step
。这样做可以让您收集有关行为树每次运行的有用统计数据/数据,并向您显示哪些任务确实运行并返回了哪些结果。有助于了解树的正确性。
但不要在生产环境中执行此操作,因为在那里完成的工作根本不需要进行定期评估。
const { Introspector } = require ( 'behaviortree' )
const introspector = new Introspector ( )
bTree . step ( { introspector } )
console . log ( introspector . lastResult )
这会导致类似的结果:
{
name : 'select' ,
result : Symbol ( running ) ,
children : [
{
name : 'targeting' ,
result : false
} ,
{
name : 'jump' ,
result : Symbol ( running )
}
]
}
你想贡献吗?如果您有任何想法或批评,请在 GitHub 上提出问题。如果你想亲自动手,可以分叉这个存储库。但请注意:如果您编写代码,请不要忘记编写测试。然后提出拉取请求。我很高兴看到接下来会发生什么。
测试是用jest完成的,我使用yarn来管理包和锁定版本。
yarn
yarn test
RUNNING
分支节点中开始和结束调用的问题版权所有 (C) 2013-2020 乔治·塔沃尼斯
特此免费授予获得本软件和相关文档文件(“软件”)副本的任何人不受限制地使用本软件,包括但不限于使用、复制、修改、合并的权利、发布、分发、再许可和/或销售软件的副本,并允许向其提供软件的人员这样做,但须满足以下条件:
上述版权声明和本许可声明应包含在本软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有者均不对因本软件或本软件中的使用或其他交易而产生或与之相关的任何索赔、损害或其他责任负责,无论是合同、侵权行为还是其他行为。软件。