Uma implementação JavaScript de árvores de comportamento. Eles são úteis para implementar IAs. Se precisar de mais informações sobre Árvores de Comportamento, dê uma olhada no GameDevAIPro, há um bom artigo sobre Árvores de Comportamento de Alex Champandard e Philip Dunstan.
Se você precisar de suporte TypeScript, verifique o branch typescript
que será a próxima versão 3.0 e se você tiver alguma dúvida e comentário, divulgue-os.
Se você usar npm:
npm install behaviortree
ou usando fio:
yarn add behaviortree
Este pacote não possui dependências próprias.
Primeiramente, devo mencionar que é possível utilizar esta biblioteca também em ambiente common-js como o node v8. Para que isso funcione, você deve trocar todas as instruções import
por instruções require()
.
Então, em vez de
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } from 'behaviortree'
basta usar
const { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } = require ( 'behaviortree' )
Eu uso a nova sintaxe dos módulos ES, porque acho que é muito legível. Então todo o código está escrito assim. Para ver exemplos funcionais de ambas as versões, visite/clone o repositório de exemplos.
Uma tarefa é um Node
simples (para ser mais preciso, um nó folha), que cuida de todo o trabalho sujo em seu método run
, que retorna true
, false
ou "running"
. Para maior clareza e flexibilidade, use as constantes exportadas fornecidas para esses valores de retorno ( SUCCESS
, FAILURE
, RUNNING
).
Cada método da sua tarefa recebe o quadro negro, que você atribui ao instanciar o BehaviorTree. Um quadro negro é basicamente um objeto que contém dados e métodos que todas as tarefas precisam para realizar seu trabalho e se comunicar com o mundo.
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
}
} )
Os métodos:
start
- Chamado antes de run ser chamado. Mas não se a tarefa for retomada após terminar com this.running()end
- Chamado após run ser chamado. Mas não se a tarefa terminar com this.running()run
- Contém as principais coisas que você deseja que a tarefa faça Uma Sequence
chamará cada um de seus subnós, um após o outro, até que um nó falhe (retorne FAILURE
) ou todos os nós sejam chamados. Se a chamada de um nó falhar, a Sequence
retornará FAILURE
, caso contrário, chamará 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
]
} )
Um Selector
chama cada nó em sua lista até que um nó retorne SUCCESS
e então ele mesmo retorne como sucesso. Se nenhum subnó chamar SUCCESS
o seletor retornará 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
]
} )
Um seletor Random
apenas chama um de seus subnós aleatoriamente; se retornar RUNNING
, ele será chamado novamente na próxima execução.
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
]
} )
Criar uma instância de uma árvore de comportamento é bastante simples. Basta instanciar a classe BehaviorTree
e especificar o formato da árvore, usando os nós mencionados acima e o quadro negro que os nós podem usar.
import { BehaviorTree } from 'behaviortree'
var bTree = new BehaviorTree ( {
tree : mySelector ,
blackboard : { }
} )
O blackboard
que você especificou será passado para cada método start()
, end()
e run()
como primeiro argumento. Você pode usá-lo para informar à árvore de comportamento em qual objeto (por exemplo, jogador artificial) ela está sendo executada, deixá-la interagir com o mundo ou manter bits de estado, se necessário. Para executar a árvore, você pode chamar step()
sempre que tiver tempo para alguns cálculos de IA no loop do jogo.
bTree . step ( )
O BehaviorTree vem com um registro interno no qual você pode registrar tarefas e posteriormente referenciá-las em seus nós pelos nomes que você escolher. Isso é realmente útil se você precisar do mesmo comportamento em várias árvores ou quiser separar a definição de tarefas e a construção das árvores.
// Register a task:
BehaviorTree . register ( 'testtask' , myTask )
// Or register a sequence or priority:
BehaviorTree . register ( 'test sequence' , mySequence )
Ao qual agora você pode simplesmente se referir em seus nós, como:
import { Selector } from 'behaviortree'
const mySelector = new Selector ( {
nodes : [ 'my awesome task' , 'another awe# task to do' ]
} )
Usar o registro tem mais um benefício: para tarefas simples com apenas um método run
, há uma maneira curta de escrevê-las:
BehaviorTree . register ( 'testtask' , ( blackboard ) => {
console . log ( 'I am doing stuff' )
return SUCCESS
} )
E agora um exemplo de como todos poderiam trabalhar juntos.
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 )
Neste exemplo acontece o seguinte: a cada passagem no setInterval
(nosso loop de jogo), o cachorro late – implementamos isso com um nó registrado, porque fazemos isso duas vezes – depois ele anda aleatoriamente, depois late novamente e então se encontra-se ao lado de uma árvore e faz xixi na árvore.
Cada nó também pode ser um Decorator
, que envolve um nó regular (ou outro decorado) e controla seu valor ou chamada, adiciona algumas condições ou faz algo com seu estado retornado. No diretório src/decorators
você encontrará alguns decoradores já implementados para inspiração ou uso, como um InvertDecorator
que nega o valor de retorno do nó decorado ou um CooldownDecorator
que garante que o nó seja chamado apenas uma vez dentro de um período de inatividade.
const decoratedSequence = new InvertDecorator ( {
node : 'awesome sequence doing stuff'
} )
Para criar um decorador próprio. Você simplesmente precisa de uma classe que estenda a classe Decorator
e substitua o método Decorator. Basta olhar na subpasta src/decorators
para verificar algumas implementações de referência.
Cuidado, você não pode simplesmente instanciar a classe Decorator
e passar os métodos decorate
como um blueprint como um decorador dinâmico, porque é assim que as coisas funcionam agora.
Existem vários decoradores “simples” já construídos para sua comodidade. Verifique o diretório src/decorators
para mais detalhes (e as especificações do que eles estão fazendo). Usá-los é tão simples quanto:
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE , decorators } from 'behaviortree'
const { AlwaysSucceedDecorator } = decorators
Há uma classe BehaviorTreeImporter
definida que pode ser usada para preencher uma instância BehaviorTree
de uma definição JSON para uma árvore. Uma estrutura de definição é semelhante a esta:
{
"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 " }
]
}
Através da propriedade type
, o importador procura Decorators
, Selectors
, Sequences
e suas próprias classes definidas a partir de uma definição de tipo interna, bem como tarefas do registro BehaviorTree
, e retorna um objeto, que pode ser usado como tree
dentro do construtor BehaviorTree
.
Se você não gostar das novas instruções import
, ainda poderá usar as instruções require
tradicionais:
const {
BehaviorTree ,
Sequence ,
Task ,
SUCCESS ,
FAILURE ,
decorators : { AlwaysSucceedDecorator }
} = require ( 'behaviortree' )
Você pode adicionar um parâmetro introspector
ao método step
que contém uma instância da classe Introspector
ou outra classe que implementa uma interface semelhante. Isso permite que você reúna estatísticas/dados úteis sobre cada execução de sua árvore de comportamento e mostra quais tarefas foram executadas e retornaram quais resultados. Útil para compreender a correção da árvore.
Mas não faça isso em um ambiente de produção, porque o trabalho realizado lá simplesmente não é necessário para avaliação regular.
const { Introspector } = require ( 'behaviortree' )
const introspector = new Introspector ( )
bTree . step ( { introspector } )
console . log ( introspector . lastResult )
Isso resultaria em algo como:
{
name : 'select' ,
result : Symbol ( running ) ,
children : [
{
name : 'targeting' ,
result : false
} ,
{
name : 'jump' ,
result : Symbol ( running )
}
]
}
Você quer contribuir? Se você tiver alguma ideia ou crítica, basta abrir uma issue, aqui no GitHub. Se quiser sujar as mãos, você pode fazer um fork deste repositório. Mas observe: se você escreve código, não se esqueça de escrever testes. E então faça uma solicitação pull. Ficarei feliz em ver o que está por vir.
Os testes são feitos com jest e eu uso o fio para gerenciar pacotes e bloquear versões.
yarn
yarn test
RUNNING
Direitos autorais (C) 2013-2020 Georg Tavonius
É concedida permissão, gratuitamente, a qualquer pessoa que obtenha uma cópia deste software e dos arquivos de documentação associados (o "Software"), para negociar o Software sem restrições, incluindo, sem limitação, os direitos de usar, copiar, modificar, mesclar , publicar, distribuir, sublicenciar e/ou vender cópias do Software e permitir que as pessoas a quem o Software seja fornecido o façam, sujeito às seguintes condições:
O aviso de direitos autorais acima e este aviso de permissão serão incluídos em todas as cópias ou partes substanciais do Software.
O SOFTWARE É FORNECIDO "NO ESTADO EM QUE SE ENCONTRA", SEM GARANTIA DE QUALQUER TIPO, EXPRESSA OU IMPLÍCITA, INCLUINDO, MAS NÃO SE LIMITANDO ÀS GARANTIAS DE COMERCIALIZAÇÃO, ADEQUAÇÃO A UM DETERMINADO FIM E NÃO VIOLAÇÃO. EM HIPÓTESE ALGUMA OS AUTORES OU DETENTORES DE DIREITOS AUTORAIS SERÃO RESPONSÁVEIS POR QUALQUER RECLAMAÇÃO, DANOS OU OUTRA RESPONSABILIDADE, SEJA EM UMA AÇÃO DE CONTRATO, ATO ILÍCITO OU DE OUTRA FORMA, DECORRENTE DE, OU EM CONEXÃO COM O SOFTWARE OU O USO OU OUTRAS NEGOCIAÇÕES NO SOFTWARE.