Estoy tratando de entender los árboles de comportamiento, así que estoy agregando un código de prueba. Una cosa con la que estoy luchando es cómo evitar un nodo actualmente en ejecución cuando aparece algo de mayor prioridad.
Considere el siguiente árbol de comportamiento simple y ficticio para un soldado:
Supongamos que ha pasado un número de garrapatas y que no había ningún enemigo cerca, el soldado estaba de pie sobre la hierba, por lo que se selecciona el nodo Sentarse para la ejecución:
Ahora la acción de sentarse toma tiempo para ejecutarse porque hay una animación para reproducir, por lo que regresa Running
como su estado. Pasa una o dos marcas, la animación aún se está ejecutando, pero ¿el enemigo está cerca? desencadenantes de nodo de condición. Ahora tenemos que adelantarnos al nodo Sit down lo antes posible para que podamos ejecutar el nodo Attack . Idealmente, el soldado ni siquiera terminaría de sentarse; en cambio, podría invertir su dirección de animación si solo comenzara a sentarse. Para mayor realismo, si ha pasado algún punto de inflexión en la animación, podríamos optar por dejar que termine de sentarse y luego ponerse de pie nuevamente, o tal vez hacer que se apresure a reaccionar ante la amenaza.
Por más que lo intenté, no he podido encontrar orientación sobre cómo manejar este tipo de situación. Toda la literatura y videos que he consumido en los últimos días (y ha sido mucho) parecen eludir este problema. Lo más cercano que he podido encontrar ha sido este concepto de restablecer los nodos en ejecución, pero eso no le da a los nodos como Sit sentarse la oportunidad de decir "¡hey, aún no he terminado!"
Pensé en quizás definir un método Preempt()
o Interrupt()
en mi Node
clase base . Diferentes nodos pueden manejarlo como mejor les parezca, pero en este caso intentaremos que el soldado vuelva a ponerse de pie lo antes posible y luego regrese Success
. Creo que este enfoque también requeriría que mi base Node
tenga el concepto de condiciones por separado de otras acciones. De esa forma, el motor solo puede verificar las condiciones y, si pasan, evitar cualquier nodo que se esté ejecutando actualmente antes de comenzar la ejecución de las acciones. Si no se estableciera esta diferenciación, el motor necesitaría ejecutar nodos indiscriminadamente y, por lo tanto, podría desencadenar una nueva acción antes de adelantarse a la ejecución.
Como referencia, a continuación están mis clases base actuales. Nuevamente, esto es un pico, así que he intentado mantener las cosas lo más simples posible y solo agrego complejidad cuando lo necesito, y cuando lo entiendo, que es con lo que estoy luchando en este momento.
public enum ExecuteResult
{
// node needs more time to run on next tick
Running,
// node completed successfully
Succeeded,
// node failed to complete
Failed
}
public abstract class Node<TAgent>
{
public abstract ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard);
}
public abstract class DecoratorNode<TAgent> : Node<TAgent>
{
private readonly Node<TAgent> child;
protected DecoratorNode(Node<TAgent> child)
{
this.child = child;
}
protected Node<TAgent> Child
{
get { return this.child; }
}
}
public abstract class CompositeNode<TAgent> : Node<TAgent>
{
private readonly Node<TAgent>[] children;
protected CompositeNode(IEnumerable<Node<TAgent>> children)
{
this.children = children.ToArray();
}
protected Node<TAgent>[] Children
{
get { return this.children; }
}
}
public abstract class ConditionNode<TAgent> : Node<TAgent>
{
private readonly bool invert;
protected ConditionNode()
: this(false)
{
}
protected ConditionNode(bool invert)
{
this.invert = invert;
}
public sealed override ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard)
{
var result = this.CheckCondition(agent, blackboard);
if (this.invert)
{
result = !result;
}
return result ? ExecuteResult.Succeeded : ExecuteResult.Failed;
}
protected abstract bool CheckCondition(TAgent agent, Blackboard blackboard);
}
public abstract class ActionNode<TAgent> : Node<TAgent>
{
}
¿Alguien tiene alguna idea que pueda guiarme en la dirección correcta? ¿Mi pensamiento está en la línea correcta, o es tan ingenuo como temo?
fuente
Stop()
devolución de llamada antes de salir de los nodos activos)Respuestas:
Me encontré haciendo la misma pregunta que tú y tuve una gran conversación corta en la sección de comentarios de esta página de blog donde se me proporcionó otra solución al problema.
Lo primero es usar el nodo concurrente. El nodo concurrente es un tipo especial de nodo compuesto. Consiste en una secuencia de comprobaciones previas seguidas de un único nodo de acción. Actualiza todos los nodos secundarios, incluso si su nodo de acción está en estado 'en ejecución'. (A diferencia del nodo de secuencia que debe iniciar su actualización desde el nodo secundario actual en ejecución).
La idea principal es crear dos estados de retorno más para los nodos de acción: "cancelar" y "cancelar".
La falla de la verificación de precondición en el nodo concurrente es un mecanismo que desencadena la cancelación de su nodo de acción en ejecución. Si el nodo de acción no requiere una lógica de cancelación de larga duración, devolverá 'cancelado' inmediatamente. De lo contrario, cambia al estado de 'cancelación' donde puede poner toda la lógica necesaria para la interrupción correcta de la acción.
fuente
Creo que tu soldado puede descomponerse en mente y cuerpo (y cualquier otra cosa). Posteriormente, el cuerpo puede descomponerse en piernas y manos. Luego, cada parte necesita su propio árbol de comportamiento y también una interfaz pública, para solicitudes de partes de nivel superior o inferior.
Por lo tanto, en lugar de administrar micro cada acción, solo envía mensajes instantáneos como "cuerpo, siéntate por un tiempo" o "cuerpo, corre allí", y el cuerpo administrará animaciones, transiciones de estado, retrasos y otras cosas para tú.
Alternativamente, el cuerpo puede manejar comportamientos como este por sí mismo. Si no tiene órdenes, puede preguntar "¿podemos sentarnos aquí?". Más interesante aún, debido a la encapsulación, puede modelar fácilmente características como cansancio o aturdimiento.
Incluso puede intercambiar partes: hacer elefante con intelecto de zombie, agregar alas a humanos (él ni siquiera lo notará), o cualquier otra cosa.
Sin una descomposición como esta, apuesto a que corre el riesgo de encontrarse con una explosión combinatoria, tarde o temprano.
También: http://www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf
fuente
Anoche, acostada en la cama, tuve una especie de epifanía sobre cómo podría hacer esto sin introducir la complejidad a la que me inclinaba en mi pregunta. Implica el uso del compuesto "paralelo" (mal llamado, en mi humilde opinión). Esto es lo que estoy pensando:
Esperemos que todavía sea bastante legible. Los puntos importantes son:
Creo que esto funcionará (lo intentaré en mi pico pronto), a pesar de ser un poco más desordenado de lo que había imaginado. Lo bueno es que eventualmente podría encapsular subárboles como piezas lógicas reutilizables y referirme a ellos desde múltiples puntos. Eso aliviará la mayor parte de mi preocupación allí, así que creo que esta es una solución viable.
Por supuesto, me encantaría saber si alguien tiene alguna idea sobre esto.
ACTUALIZAR : aunque este enfoque técnicamente funciona, he decidido que sux. Esto se debe a que los subárboles no relacionados deben "conocer" las condiciones definidas en otras partes del árbol para que puedan desencadenar su propia desaparición. Si bien compartir referencias de subárboles ayudaría a aliviar este dolor, sigue siendo contrario a lo que uno espera al mirar el árbol de comportamiento. De hecho, cometí el mismo error dos veces en un pico muy simple.
Por lo tanto, voy a seguir la otra ruta: soporte explícito para anticiparnos dentro del modelo de objetos, y un compuesto especial que permite que se ejecute un conjunto diferente de acciones cuando se produce la preferencia. Publicaré una respuesta por separado cuando tenga algo funcionando.
fuente
Preempt()
método que atravesaría el árbol. Sin embargo, lo único que realmente "manejaría" esto sería el compuesto preventivo, que cambiaría instantáneamente a su nodo secundario preventivo.Aquí está la solución que he decidido por ahora ...
Node
clase base tiene unInterrupt
método que, por defecto, no hace nadabool
(lo que implica que son rápidas de ejecutar y nunca necesitan más de una actualización)Node
expone una colección de condiciones por separado a su colección de nodos secundariosNode.Execute
ejecuta todas las condiciones primero y falla de inmediato si falla alguna condición. Si las condiciones tienen éxito (o no hay ninguna), llamaExecuteCore
para que la subclase pueda hacer su trabajo real. Hay un parámetro que permite omitir condiciones, por razones que verá a continuaciónNode
También permite que las condiciones se ejecuten de forma aislada a través de unCheckConditions
método. Por supuesto, enNode.Execute
realidad solo llamaCheckConditions
cuando necesita validar condicionesSelector
compuesto ahora llamaCheckConditions
a cada hijo que considera para la ejecución. Si las condiciones fallan, se mueve directamente hacia el siguiente hijo. Si pasan, verifica si ya hay un niño ejecutor. Si es así, llamaInterrupt
y luego falla. Eso es todo lo que puede hacer en este momento, con la esperanza de que el nodo actualmente en ejecución responda a la solicitud de interrupción, lo que puede hacer al ...Interruptible
nodo, que es una especie de decorador especial porque tiene el flujo regular de lógica como elemento secundario decorado, y luego un nodo separado para las interrupciones. Ejecuta su hijo regular hasta su finalización o falla siempre que no se interrumpa. Si se interrumpe, cambia inmediatamente a ejecutar su nodo secundario de manejo de interrupciones, que podría ser un subárbol tan complejo como sea necesarioEl resultado final es algo como esto, tomado de mi pico:
Lo anterior es el árbol de comportamiento para una abeja, que recolecta néctar y lo devuelve a su colmena. Cuando no tiene néctar y no está cerca de una flor que tiene algo, deambula:
Si este nodo no fuera interrumpible, nunca fallaría, por lo que la abeja deambularía perpetuamente. Sin embargo, dado que el nodo padre es un selector y tiene hijos de mayor prioridad, su elegibilidad para la ejecución se verifica constantemente. Si sus condiciones pasan, el selector genera una interrupción y el subárbol de arriba cambia inmediatamente a la ruta "interrumpida", que simplemente falla lo antes posible al fallar. Podría, por supuesto, realizar algunas otras acciones primero, pero mi pico realmente no tiene nada que hacer aparte de la fianza.
Sin embargo, para vincular esto con mi pregunta, se podría imaginar que el camino "Interrumpido" podría intentar revertir la animación sentada y, en su defecto, hacer que el soldado tropiece. Todo esto retrasaría la transición al estado de mayor prioridad, y ese era precisamente el objetivo.
Yo creo que estoy feliz con este enfoque - sobre todo las piezas centrales esbozo anterior - pero para ser honesto, que ha recaudado más preguntas sobre la proliferación de las implementaciones específicas de condiciones y acciones, y que atan el árbol de comportamiento en el sistema de animación. Ni siquiera estoy seguro de poder articular estas preguntas todavía, así que seguiré pensando / aumentando.
fuente
Solucioné el mismo problema inventando el decorador "When". Tiene una condición y dos comportamientos infantiles ("entonces" y "de lo contrario"). Cuando se ejecuta "When", verifica la condición y, dependiendo de su resultado, se ejecuta entonces / de otro modo secundario. Si el resultado de la condición cambia, se reinicia el elemento secundario en ejecución y se inicia el elemento secundario correspondiente a otra rama. Si el niño finaliza la ejecución, todo el "Cuándo" finaliza la ejecución.
El punto clave es que, a diferencia de la BT inicial en esta pregunta, donde la condición se verifica solo al inicio de la secuencia, mi "Cuándo" sigue verificando la condición mientras se está ejecutando. Entonces, la parte superior del árbol de comportamiento se reemplaza con:
Para un uso más avanzado de "Cuándo", uno también querría introducir la acción de "Esperar" que simplemente no hace nada durante un período de tiempo específico o de forma indefinida (hasta que se restablece por el comportamiento de los padres). Además, si solo necesita una rama de "Cuándo", la otra puede contener acciones de "Éxito" o "Fracaso", que respectivamente tienen éxito y fallan de inmediato.
fuente
Si bien llego tarde, pero espero que esto pueda ayudar. Principalmente porque quiero asegurarme de que personalmente no me he perdido algo, ya que he estado tratando de resolver esto también. Sobre todo tomé prestada esta idea
Unreal
, pero sin convertirla en unaDecorator
propiedad sobre una baseNode
o fuertemente vinculada con elBlackboard
, es más genérica.Esto introducirá un nuevo tipo de nodo llamado
Guard
que es como una combinación de aDecorator
,Composite
y tiene unacondition() -> Result
firma junto a unupdate() -> Result
Tiene tres modos para indicar cómo debe ocurrir la cancelación cuando
Guard
regresaSuccess
oFailed
, la cancelación depende realmente de la persona que llama. Entonces, para unaSelector
llamada aGuard
:.self
-> Solo cancele elGuard
(y su elemento secundario en ejecución) si se está ejecutando y la condición eraFailed
.lower
-> Solo cancele los nodos de menor prioridad si se están ejecutando y la condición eraSuccess
oRunning
.both
-> Ambos.self
y.lower
dependiendo de las condiciones y los nodos en ejecución. Desea cancelar self si se está ejecutando y condicionaríafalse
o cancelaría el nodo en ejecución si se consideran de menor prioridad según laComposite
regla (Selector
en nuestro caso) si la condición esSuccess
. En otras palabras, son básicamente ambos conceptos combinados.Me gusta
Decorator
y diferenteComposite
solo se necesita un solo hijo.A pesar de que
Guard
sólo tienen un solo niño, puede anidar ya que muchosSequences
,Selectors
o de otros tiposNodes
como usted desee, incluyendo otraGuards
oDecorators
.Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Sequence2 StandingOnGrass? Idle HumATune
En el escenario anterior, siempre que se
Selector1
actualice, siempre ejecutará verificaciones de condición en los guardias asociados con sus hijos. En el caso anterior,Sequence1
está guardado y debe verificarse antes deSelector1
continuar con elrunning
tareas.Siempre que se ejecute
Selector2
oSequence1
tan pronto comoEnemyNear?
regresesuccess
durante unaGuards
condition()
verificaciónSelector1
, emitirá una interrupción / cancelación alrunning
node
y luego continuar como de costumbre.En otras palabras, podemos reaccionar a la rama "inactiva" o "atacante" en función de algunas condiciones que hacen que el comportamiento sea mucho más reactivo que si nos conformamos con
Parallel
Esto también le permite proteger a los solteros
Node
que tienen mayor prioridad contra correrNodes
en el mismoComposite
Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Guard.both[StandingOnGrass?] Idle HumATune
Si
HumATune
es una ejecución largaNode
,Selector2
siempre verificará esa primero si no fuera por elGuard
. Entonces, si el npc se teletransportó a un parche de hierba, la próxima vez que seSelector2
ejecute, verificaráGuard
y cancelaráHumATune
para ejecutarIdle
Si se teletransporta fuera del parche de hierba, cancelará el nodo en ejecución (
Idle
) y se moverá aHumATune
Como puede ver aquí, la toma de decisiones depende de la persona que llama
Guard
y no deGuard
sí misma. Las reglas de quién se consideralower priority
permanecen con la persona que llama. En ambos ejemplos, esSelector
quien define lo que constituye comolower priority
.Si tuviera una
Composite
llamadaRandom Selector
, podría definir las reglas dentro de la implementación de ese específicoComposite
.fuente