Une implémentation JavaScript des arbres de comportement. Ils sont utiles pour mettre en œuvre des IA. Si vous avez besoin de plus d'informations sur les arbres de comportement, regardez sur GameDevAIPro, il y a un bel article sur les arbres de comportement d'Alex Champandard et Philip Dunstan.
Si vous avez besoin de la prise en charge de TypeScript, veuillez consulter la branche typescript
qui sera la prochaine version 3.0 et si vous avez des questions et des commentaires, veuillez les faire savoir.
Si vous utilisez npm :
npm install behaviortree
ou en utilisant du fil :
yarn add behaviortree
Ce package n'a pas de dépendances propres.
Tout d'abord, je dois mentionner qu'il est également possible d'utiliser cette bibliothèque dans un environnement common-js comme le nœud v8. Pour que cela fonctionne, vous devez remplacer toutes les instructions import
par des instructions require()
.
Alors au lieu de
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } from 'behaviortree'
il suffit d'utiliser
const { BehaviorTree , Sequence , Task , SUCCESS , FAILURE } = require ( 'behaviortree' )
J'utilise la nouvelle syntaxe des modules ES, car je pense qu'elle est très lisible. Donc tout le code est écrit comme ça. Pour voir des exemples fonctionnels des deux versions, visitez/clonez le dépôt des exemples.
Une tâche est un simple Node
(pour être précis, un nœud feuille), qui s'occupe de tout le sale boulot dans sa méthode run
, qui renvoie soit true
, false
ou "running"
. Pour plus de clarté et pour être flexible, veuillez utiliser les constantes exportées fournies pour ces valeurs de retour ( SUCCESS
, FAILURE
, RUNNING
).
Chaque méthode de votre tâche reçoit le tableau noir que vous attribuez lors de l'instanciation du BehaviorTree. Un tableau noir est fondamentalement un objet contenant les données et les méthodes dont tout le monde a besoin pour effectuer son travail et communiquer avec le monde.
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
}
} )
Les méthodes :
start
- Appelé avant l'appel de run. Mais pas si la tâche reprend après avoir terminé avec this.running()end
- Appelé après l'appel de run. Mais pas si la tâche s'est terminée avec this.running()run
- Contient les principales choses que vous souhaitez que la tâche fasse Une Sequence
appellera chacun de ses sous-nœuds les uns après les autres jusqu'à ce qu'un nœud échoue (renvoie FAILURE
) ou que tous les nœuds soient appelés. Si un nœud appelle échoue, la Sequence
renverra FAILURE
elle-même, sinon elle appellera 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
appelle chaque nœud de sa liste jusqu'à ce qu'un nœud renvoie SUCCESS
, puis renvoie lui-même comme succès. Si aucun de ses sous-nœuds n'appelle SUCCESS
le sélecteur renvoie 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 sélecteur Random
appelle simplement l'un de ses sous-nœuds de manière aléatoire, si cela renvoie RUNNING
, il sera rappelé lors de la prochaine exécution.
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
]
} )
Créer une instance d'un arbre de comportement est assez simple. Instanciez simplement la classe BehaviorTree
et spécifiez la forme de l'arbre, en utilisant les nœuds mentionnés ci-dessus et le tableau noir que les nœuds peuvent utiliser.
import { BehaviorTree } from 'behaviortree'
var bTree = new BehaviorTree ( {
tree : mySelector ,
blackboard : { }
} )
Le blackboard
que vous avez spécifié sera transmis à chaque méthode start()
, end()
et run()
comme premier argument. Vous pouvez l'utiliser pour indiquer à l'arbre de comportement sur quel objet (par exemple un joueur artificiel) il s'exécute, le laisser interagir avec le monde ou conserver des bits d'état si vous en avez besoin. Pour exécuter l'arborescence, vous pouvez appeler step()
chaque fois que vous avez le temps d'effectuer certains calculs d'IA dans votre boucle de jeu.
bTree . step ( )
BehaviorTree est livré avec un registre interne dans lequel vous pouvez enregistrer des tâches et les référencer ultérieurement dans vos nœuds par leurs noms, que vous choisissez. C'est vraiment pratique si vous avez besoin du même comportement dans plusieurs arbres, ou si vous souhaitez séparer la définition des tâches et la construction des arbres.
// Register a task:
BehaviorTree . register ( 'testtask' , myTask )
// Or register a sequence or priority:
BehaviorTree . register ( 'test sequence' , mySequence )
Auxquels vous pouvez désormais simplement faire référence dans vos nœuds, comme :
import { Selector } from 'behaviortree'
const mySelector = new Selector ( {
nodes : [ 'my awesome task' , 'another awe# task to do' ]
} )
L'utilisation du registre présente un avantage supplémentaire : pour les tâches simples avec une seule méthode run
, il existe un moyen court de les écrire :
BehaviorTree . register ( 'testtask' , ( blackboard ) => {
console . log ( 'I am doing stuff' )
return SUCCESS
} )
Et maintenant un exemple de la façon dont tous pourraient travailler ensemble.
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 )
Dans cet exemple, ce qui suit se produit : à chaque passage sur setInterval
(notre boucle de jeu), le chien aboie – nous avons implémenté cela avec un nœud enregistré, car nous le faisons deux fois – puis il se promène au hasard, puis il aboie à nouveau et puis s'il se retrouve debout à côté d'un arbre et fait pipi dessus.
Chaque nœud peut également être un Decorator
, qui encapsule un nœud régulier (ou un autre décoré) et contrôle sa valeur ou son appel, ajoute des conditions ou fait quelque chose avec son état renvoyé. Dans le répertoire src/decorators
, vous trouverez des décorateurs déjà implémentés pour vous inspirer ou utiliser, comme un InvertDecorator
qui annule la valeur de retour du nœud décoré ou un CooldownDecorator
qui garantit que le nœud n'est appelé qu'une seule fois pendant une période d'arrêt de refroidissement.
const decoratedSequence = new InvertDecorator ( {
node : 'awesome sequence doing stuff'
} )
Créer son propre décorateur. Vous avez simplement besoin d'une classe qui étend la classe Decorator
et remplace la méthode decor. Regardez simplement dans le sous-dossier src/decorators
pour vérifier certaines implémentations de référence.
Attention, vous ne pouvez pas simplement instancier la classe Decorator
et transmettre les méthodes decorate
comme modèle en tant que décorateur dynamique, à cause de la façon dont les choses fonctionnent actuellement.
Il existe plusieurs décorateurs « simples » déjà construits pour votre commodité. Consultez le répertoire src/decorators
pour plus de détails (et les spécifications de ce qu'ils font). Leur utilisation est aussi simple que :
import { BehaviorTree , Sequence , Task , SUCCESS , FAILURE , decorators } from 'behaviortree'
const { AlwaysSucceedDecorator } = decorators
Il existe une classe BehaviorTreeImporter
définie qui peut être utilisée pour remplir une instance BehaviorTree
à partir d'une définition JSON pour un arbre. Une structure de définition ressemble à ceci :
{
"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 " }
]
}
Grâce à la propriété type
, l'importateur recherche Decorators
, Selectors
, Sequences
et vos propres classes définies à partir d'une définition de type interne ainsi que des tâches du registre BehaviorTree
, et renvoie un objet qui peut être utilisé comme tree
dans le constructeur BehaviorTree
.
Si vous n'aimez pas les nouvelles instructions import
, vous devriez toujours pouvoir utiliser les instructions require
traditionnelles :
const {
BehaviorTree ,
Sequence ,
Task ,
SUCCESS ,
FAILURE ,
decorators : { AlwaysSucceedDecorator }
} = require ( 'behaviortree' )
Vous pouvez ajouter un paramètre introspector
à la méthode step
contenant une instance de la classe Introspector
ou une autre classe implémentant une interface similaire. Cela vous permet de rassembler des statistiques/données utiles sur chaque exécution de votre arbre de comportement et vous montre quelles tâches ont été exécutées et ont renvoyé quels résultats. Utile pour comprendre l’exactitude de l’arbre.
Mais ne faites pas cela dans un environnement de production, car le travail qui y est effectué n'est tout simplement pas nécessaire pour une évaluation régulière.
const { Introspector } = require ( 'behaviortree' )
const introspector = new Introspector ( )
bTree . step ( { introspector } )
console . log ( introspector . lastResult )
Cela donnerait quelque chose comme :
{
name : 'select' ,
result : Symbol ( running ) ,
children : [
{
name : 'targeting' ,
result : false
} ,
{
name : 'jump' ,
result : Symbol ( running )
}
]
}
Vous souhaitez contribuer ? Si vous avez des idées ou des critiques, ouvrez simplement un ticket, ici sur GitHub. Si vous voulez vous salir les mains, vous pouvez créer ce référentiel. Mais attention : si vous écrivez du code, n'oubliez pas d'écrire des tests. Et puis faites une pull request. Je serai heureux de voir ce qui s'en vient.
Les tests sont effectués avec Jest et j'utilise Yarn pour gérer les packages et verrouiller les versions.
yarn
yarn test
RUNNING
Copyright (C) 2013-2020 Georg Tavonius
L'autorisation est accordée par la présente, gratuitement, à toute personne obtenant une copie de ce logiciel et des fichiers de documentation associés (le « Logiciel »), d'utiliser le Logiciel sans restriction, y compris, sans limitation, les droits d'utilisation, de copie, de modification, de fusion. , publier, distribuer, accorder des sous-licences et/ou vendre des copies du Logiciel, et permettre aux personnes à qui le Logiciel est fourni de le faire, sous réserve des conditions suivantes :
L'avis de droit d'auteur ci-dessus et cet avis d'autorisation doivent être inclus dans toutes les copies ou parties substantielles du logiciel.
LE LOGICIEL EST FOURNI « EN L'ÉTAT », SANS GARANTIE D'AUCUNE SORTE, EXPRESSE OU IMPLICITE, Y COMPRIS MAIS SANS LIMITATION LES GARANTIES DE QUALITÉ MARCHANDE, D'ADAPTATION À UN USAGE PARTICULIER ET DE NON-VIOLATION. EN AUCUN CAS LES AUTEURS OU LES TITULAIRES DES DROITS D'AUTEUR NE SERONT RESPONSABLES DE TOUTE RÉCLAMATION, DOMMAGES OU AUTRE RESPONSABILITÉ, QUE CE SOIT DANS UNE ACTION CONTRACTUELLE, DÉLIT OU AUTRE, DÉCOULANT DE, DE OU EN RELATION AVEC LE LOGICIEL OU L'UTILISATION OU D'AUTRES TRANSACTIONS DANS LE LOGICIEL.