Реализация деревьев поведения на языке JavaScript. Они полезны для реализации ИИ. Если вам нужна дополнительная информация о деревьях поведения, загляните на GameDevAIPro, там есть хорошая статья о деревьях поведения от Алекса Шампандара и Филипа Данстана.
Если вам нужна поддержка TypeScript, пожалуйста, посетите ветку typescript
, которая станет предстоящим выпуском 3.0, и если у вас есть вопросы и комментарии, сообщите о них.
Если вы используете npm:
npm install behaviortree
или используя пряжу:
yarn add behaviortree
Этот пакет не имеет собственных зависимостей.
Во-первых, я должен упомянуть, что эту библиотеку можно использовать и в среде common-js, например в узле v8. Чтобы это работало, вам следует переключить все операторы 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
).
Каждый метод вашей задачи получает доску, которую вы назначаете при создании экземпляра BehaviorTree. Доска — это, по сути, объект, который содержит данные и методы для всех задач, необходимых для выполнения работы и общения с миром.
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()
в качестве первого аргумента. Вы можете использовать его, чтобы сообщить дереву поведения, на каком объекте (например, искусственном игроке) оно работает, позволить ему взаимодействовать с миром или хранить биты состояния, если вам нужно. Чтобы запустить дерево, вы можете вызывать функцию step()
всякий раз, когда у вас есть время для вычислений ИИ в игровом цикле.
bTree . step ( )
BehaviorTree поставляется с внутренним реестром, в котором вы можете регистрировать задачи, а затем ссылаться на них в своих узлах по выбранным вами именам. Это действительно удобно, если вам нужна одна и та же часть поведения в нескольких деревьях или вы хотите разделить определение задач и построение деревьев.
// 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
, который можно использовать для заполнения экземпляра BehaviorTree
из определения JSON для дерева. Структура определения выглядит следующим образом:
{
"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
, и возвращает объект, который можно использовать в качестве tree
в конструкторе BehaviorTree
.
Если вам не нравятся новые операторы import
, вы все равно сможете использовать традиционные операторы require
:
const {
BehaviorTree ,
Sequence ,
Task ,
SUCCESS ,
FAILURE ,
decorators : { AlwaysSucceedDecorator }
} = require ( 'behaviortree' )
Вы можете добавить параметр introspector
в метод step
, содержащий экземпляр класса Introspector
или другого класса, реализующего аналогичный интерфейс. Это позволит вам собрать полезную статистику/данные о каждом запуске вашего дерева поведения и покажет вам, какие задачи выполнялись и какие результаты возвращали. Полезно для понимания правильности дерева.
Но не делайте этого в производственной среде, поскольку выполняемая там работа просто не нужна для регулярной оценки.
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
узлах ветвления.Copyright (C) 2013-2020 Георг Тавониус
Настоящим бесплатно любому лицу, получившему копию этого программного обеспечения и связанных с ним файлов документации («Программное обеспечение»), предоставляется разрешение на работу с Программным обеспечением без ограничений, включая, помимо прочего, права на использование, копирование, изменение, объединение. публиковать, распространять, сублицензировать и/или продавать копии Программного обеспечения, а также разрешать лицам, которым предоставлено Программное обеспечение, делать это при соблюдении следующих условий:
Вышеупомянутое уведомление об авторских правах и данное уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ГАРАНТИЯМИ ТОВАРНОЙ ЦЕННОСТИ, ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ОБЛАДАТЕЛИ АВТОРСКИХ ПРАВ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПРЕТЕНЗИИ, УБЫТКИ ИЛИ ДРУГУЮ ОТВЕТСТВЕННОСТЬ, БУДЬ В ДЕЙСТВИЯХ ПО КОНТРАКТУ, ПРАВОНАРУШЕНИЮ ИЛИ ДРУГИМ ОБРАЗОМ, ВОЗНИКАЮЩИЕ ОТ, ИЗ ИЛИ В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ СДЕЛКАМИ, ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ.