Una implementación de JavaScript de árboles de comportamiento. Son útiles para implementar IA. Si necesita más información sobre los árboles de comportamiento, consulte GameDevAIPro, hay un buen artículo sobre árboles de comportamiento de Alex Champandard y Philip Dunstan.
Si necesita soporte para TypeScript, consulte la rama typescript
que será la próxima versión 3.0 y, si tiene algunas preguntas y comentarios, hágalos saber.
Si usas npm:
npm install behaviortree
o usando hilo:
yarn add behaviortree
Este paquete no tiene dependencias propias.
Primero, debo mencionar que es posible utilizar esta biblioteca también en un entorno common-js como el nodo v8. Para que esto funcione, debe cambiar todas las declaraciones import
con declaraciones require()
.
Así que en lugar de
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } from 'behaviortree'
solo usa
const { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } = require ( 'behaviortree' )
Utilizo la nueva sintaxis de los módulos ES porque creo que es muy legible. Entonces todo el código está escrito así. Para ver ejemplos funcionales de ambas versiones, visite/clone el repositorio de ejemplos.
Una tarea es un Node
simple (para ser precisos, un nodo hoja), que se encarga de todo el trabajo sucio en su método run
, que devuelve true
, false
o "running"
. Para mayor claridad y para ser flexible, utilice las constantes exportadas proporcionadas para esos valores de retorno ( SUCCESS
, FAILURE
, RUNNING
).
Cada método de su tarea recibe la pizarra, que asigna al crear una instancia de BehaviorTree. Una pizarra es básicamente un objeto que contiene datos y métodos que todas las tareas necesitan para realizar su trabajo y comunicarse con el 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
}
} )
Los métodos:
start
: se llama antes de llamar a run. Pero no si la tarea se reanuda después de finalizar con this.running()end
: se llama después de llamar a run. Pero no si la tarea terminó con this.running()run
: contiene las cosas principales que desea que haga la tarea Una Sequence
llamará a cada uno de sus subnodos uno tras otro hasta que un nodo falle (devuelve FAILURE
) o se llame a todos los nodos. Si la llamada de un nodo falla, la Sequence
devolverá FAILURE
; de lo contrario, llamará 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
]
} )
Un Selector
llama a todos los nodos de su lista hasta que un nodo devuelve SUCCESS
y luego regresa como exitoso. Si ninguno de sus subnodos llama SUCCESS
el selector devuelve 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
]
} )
Un selector Random
simplemente llama a uno de sus subnodos al azar, si eso devuelve RUNNING
, se llamará nuevamente en la próxima ejecución.
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
]
} )
Crear una instancia de un árbol de comportamiento es bastante sencillo. Simplemente cree una instancia de la clase BehaviorTree
y especifique la forma del árbol, usando los nodos mencionados anteriormente y la pizarra que los nodos pueden usar.
import { BehaviorTree } from 'behaviortree'
var bTree = new BehaviorTree ( {
tree : mySelector ,
blackboard : { }
} )
La blackboard
que especificó se pasará a cada método start()
, end()
y run()
como primer argumento. Puede usarlo para que el árbol de comportamiento sepa en qué objeto (por ejemplo, un jugador artificial) se está ejecutando, dejar que interactúe con el mundo o mantener bits de estado si es necesario. Para ejecutar el árbol, puedes llamar step()
siempre que tengas tiempo para realizar algunos cálculos de IA en el bucle del juego.
bTree . step ( )
BehaviorTree viene con un registro interno en el que puedes registrar tareas y luego hacer referencia a ellas en tus nodos por sus nombres, que tú elijas. Esto es realmente útil si necesita el mismo comportamiento en varios árboles o si desea separar la definición de tareas y la construcción de los árboles.
// Register a task:
BehaviorTree . register ( 'testtask' , myTask )
// Or register a sequence or priority:
BehaviorTree . register ( 'test sequence' , mySequence )
A los que ahora puedes simplemente hacer referencia en tus nodos, como:
import { Selector } from 'behaviortree'
const mySelector = new Selector ( {
nodes : [ 'my awesome task' , 'another awe# task to do' ]
} )
Usar el registro tiene un beneficio más: para tareas simples con un solo método run
, existe una forma breve de escribirlas:
BehaviorTree . register ( 'testtask' , ( blackboard ) => {
console . log ( 'I am doing stuff' )
return SUCCESS
} )
Y ahora un ejemplo de cómo todos podrían trabajar 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 )
En este ejemplo sucede lo siguiente: cada paso en setInterval
(nuestro bucle de juego), el perro ladra (implementamos esto con un nodo registrado, porque lo hacemos dos veces) luego camina aleatoriamente, luego ladra nuevamente y luego, si se encuentra parado junto a un árbol y orina en el árbol.
Cada nodo también puede ser un Decorator
, que envuelve un nodo normal (u otro decorado) y controla su valor o llamada, agrega algunas condiciones o hace algo con su estado devuelto. En el directorio src/decorators
encontrará algunos decoradores ya implementados para inspiración o uso, como un InvertDecorator
que niega el valor de retorno del nodo decorado o un CooldownDecorator
que garantiza que el nodo solo se llame una vez dentro de un período de inactividad.
const decoratedSequence = new InvertDecorator ( {
node : 'awesome sequence doing stuff'
} )
Crear un decorador propio. Simplemente necesita una clase que extienda la clase Decorator
y anule el método de decoración. Simplemente mire dentro de la subcarpeta src/decorators
para verificar algunas implementaciones de referencia.
Tenga en cuenta que no puede simplemente crear una instancia de la clase Decorator
y pasar los métodos decorate
como un modelo como decorador dinámico, porque así funcionan las cosas en este momento.
Hay varios decoradores "simples" ya creados para su conveniencia. Consulte el directorio src/decorators
para obtener más detalles (y las especificaciones de lo que están haciendo). Usarlos es tan simple como:
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE , decorators } from 'behaviortree'
const { AlwaysSucceedDecorator } = decorators
Hay una clase BehaviorTreeImporter
definida que se puede usar para completar una instancia BehaviorTree
a partir de una definición JSON para un árbol. Una estructura de definición se ve así:
{
"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 " }
]
}
A través de la propiedad type
, el importador busca Decorators
, Selectors
, Sequences
y sus propias clases definidas a partir de una definición de tipo interna, así como tareas del registro BehaviorTree
, y devuelve un objeto que puede usarse como tree
dentro del constructor BehaviorTree
.
Si no le gustan las nuevas declaraciones import
, aún debería poder utilizar las declaraciones require
tradicionales:
const {
BehaviorTree ,
Sequence ,
Task ,
SUCCESS ,
FAILURE ,
decorators : { AlwaysSucceedDecorator }
} = require ( 'behaviortree' )
Puede agregar un parámetro introspector
al método step
que contiene una instancia de la clase Introspector
u otra clase que implemente una interfaz similar. Hacer eso le permite recopilar estadísticas/datos útiles sobre cada ejecución de su árbol de comportamiento y le muestra qué tareas se ejecutaron y arrojaron qué resultados. Útil para comprender la corrección del árbol.
Pero no haga esto en un entorno de producción, porque el trabajo que se realiza allí simplemente no es necesario para una evaluación regular.
const { Introspector } = require ( 'behaviortree' )
const introspector = new Introspector ( )
bTree . step ( { introspector } )
console . log ( introspector . lastResult )
Eso resultaría en algo como:
{
name : 'select' ,
result : Symbol ( running ) ,
children : [
{
name : 'targeting' ,
result : false
} ,
{
name : 'jump' ,
result : Symbol ( running )
}
]
}
¿Quieres contribuir? Si tiene algunas ideas o críticas, simplemente abra un problema aquí en GitHub. Si quieres ensuciarte las manos, puedes bifurcar este repositorio. Pero tenga en cuenta: si escribe código, no olvide escribir pruebas. Y luego haz una solicitud de extracción. Estaré feliz de ver lo que viene.
Las pruebas se realizan con broma y uso hilo para administrar paquetes y bloquear versiones.
yarn
yarn test
RUNNING
Copyright (C) 2013-2020 Georg Tavonio
Por el presente se otorga permiso, sin cargo, a cualquier persona que obtenga una copia de este software y los archivos de documentación asociados (el "Software"), para operar con el Software sin restricciones, incluidos, entre otros, los derechos de uso, copia, modificación, fusión. , publicar, distribuir, sublicenciar y/o vender copias del Software, y permitir que las personas a quienes se les proporciona el Software lo hagan, sujeto a las siguientes condiciones:
El aviso de derechos de autor anterior y este aviso de permiso se incluirán en todas las copias o partes sustanciales del Software.
EL SOFTWARE SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O IMPLÍCITA, INCLUYENDO PERO NO LIMITADO A LAS GARANTÍAS DE COMERCIABILIDAD, IDONEIDAD PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO LOS AUTORES O TITULARES DE DERECHOS DE AUTOR SERÁN RESPONSABLES DE NINGÚN RECLAMO, DAÑO U OTRA RESPONSABILIDAD, YA SEA EN UNA ACCIÓN CONTRACTUAL, AGRAVIO O DE OTRA MANERA, QUE SURJA DE, FUERA DE O EN RELACIÓN CON EL SOFTWARE O EL USO U OTRAS NEGOCIOS EN EL SOFTWARE.