Scripting y cinemática sin subprocesos

12

He estado luchando con la forma de implementar secuencias de comandos en mi motor de juego. Solo tengo unos pocos requisitos: debe ser intuitivo, no quiero escribir un lenguaje, analizador e intérprete personalizados, y no quiero usar hilos. (Estoy seguro de que hay una solución más simple; no necesito la molestia de múltiples hilos de lógica de juego). Aquí hay un script de ejemplo, en Python (también conocido como pseudocódigo):

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    alice.walk_to(bob)
    if bob.can_see(alice):
        bob.say("Hello again!")
    else:
        alice.say("Excuse me, Bob?")

Esa pieza épica de narración plantea problemas de implementación. No puedo evaluar todo el método a la vez, porque walk_tolleva tiempo de juego. Si regresa de inmediato, Alice comenzaría a caminar hacia Bob y (en el mismo marco) saludaría (o sería saludada). Pero si walk_toes una llamada de bloqueo que regresa cuando llega a Bob, entonces mi juego se atasca, porque está bloqueando el mismo hilo de ejecución que haría que Alice caminara.

Pensé en hacer que cada función pusiera en acción una acción: alice.walk_to(bob)empujaría un objeto a una cola, que se despegaría después de que Alice llegara a Bob, donde sea que estuviera. Eso está más sutilmente roto: la iframa se evalúa de inmediato, por lo que Bob podría saludar a Alice incluso si le da la espalda.

¿Cómo manejan otros motores / personas las secuencias de comandos sin crear hilos? Estoy empezando a buscar ideas en áreas que no sean de desarrollo de juegos, como las cadenas de animación jQuery. Parece que debería haber algunos buenos patrones para este tipo de problema.

ojrac
fuente
1
+1 para la pregunta, pero especialmente para "Python (también conocido como pseudocódigo)" :)
Ricket
Python es como tener una superpotencia.
ojrac

Respuestas:

3

La forma en que algo como Panda hace esto es con devoluciones de llamada. En lugar de bloquearlo sería algo así como

def dramatic_scene(actors):
    alice = actors["alice"]
    bob = actors["bob"]

    def cb():
        if bob.can_see(alice):
            bob.say("Hello again!")
        else:
            alice.say("Excuse me, Bob?")
    alice.walk_to(bob, cb)

Tener una devolución de llamada completa le permite encadenar este tipo de eventos tan profundamente como desee.

EDITAR: ejemplo de JavaScript ya que tiene una mejor sintaxis para este estilo:

function dramatic_scene(actors) {
    var alice = actors.alice;
    var bob = actors.bob;
    alice.walk_to(bob, function() {
        if(bob.can_see(alice)) {
            bob.say('Hello again!');
        } else {
            alice.say('Excuse me, Bob?');
        }
     });
}
coderanger
fuente
Funciona lo suficiente para un +1, solo creo que está mal para diseñar contenido. Estoy dispuesto a hacer un trabajo extra de mi parte para que los guiones se vean simples y claros.
ojrac
1
@ojrac: En realidad, así es mejor, porque aquí, en el mismo guión, puedes decirle a varios actores que comiencen a caminar al mismo tiempo.
Bart van Heukelom el
Ugh, buen punto.
ojrac
Sin embargo, para mejorar la legibilidad, la definición de devolución de llamada podría anidarse dentro de la llamada walk_to (), o colocarse después (para ambos va: si el idioma lo admite) para que el código que se llama más tarde se vea más tarde en la fuente.
Bart van Heukelom el
Sí, desafortunadamente, Python no es realmente bueno para este tipo de sintaxis. Se ve mucho mejor en JavaScript (ver arriba, no se puede usar el formato de código aquí).
coderanger
13

El término que desea buscar aquí es " corutinas " (y generalmente la palabra clave del idioma o el nombre de la función es yield).

Las rutinas son componentes del programa que generalizan las subrutinas para permitir múltiples puntos de entrada para suspender y reanudar la ejecución en ciertas ubicaciones.

La implementación dependerá en primer lugar de su idioma. Para un juego, desea que la implementación sea lo más liviana posible (más liviana que los hilos o incluso las fibras). La página de Wikipedia (vinculada) tiene algunos enlaces a varias implementaciones específicas del idioma.

Escuché que Lua tiene soporte incorporado para las corutinas. También GameMonkey.

UnrealScript implementa esto con lo que llama "estados" y "funciones latentes".

Si usa C #, podría mirar esta publicación de blog de Nick Gravelyn.

Además, la idea de "cadenas de animación", aunque no es lo mismo, es una solución viable para el mismo problema. Nick Gravelyn también tiene una implementación C # de esto .

Andrew Russell
fuente
Buena captura, Tetrad;)
Andrew Russell
Esto es realmente bueno, pero no estoy seguro de que me lleve al 100% del camino. Parece que las rutinas le permiten ceder al método de llamada, pero quiero una forma de ceder desde un script Lua, todo el camino hasta el código C # sin escribir while (walk_to ()! = Done) {yield}.
ojrac
@ojrac: No sé acerca de Lua, pero si está utilizando el método C # de Nick Gravelyn, podría devolver un delegado (o un objeto que contenga uno) que contenga el condicional para que su administrador de scripts verifique (el código de Nick solo devuelve una hora que es implícitamente un condicional). Incluso puede hacer que las funciones latentes devuelvan el delegado, para que pueda escribir: yield return walk_to();en su script.
Andrew Russell
El rendimiento en C # es increíble, pero estoy optimizando mi solución para secuencias de comandos simples y difíciles de enredar. Me será más fácil explicar las devoluciones de llamada que el rendimiento, por lo que aceptaré la otra respuesta. Haría +2 si pudiera.
ojrac
1
Normalmente no tiene que explicar la llamada de rendimiento, por ejemplo, puede ajustar eso en la función "walk_to".
Kylotan
3

no ir enhebrado es inteligente.

La mayoría de los motores de juego funcionan como una serie de etapas modulares con cosas en la memoria que conducen cada etapa. Para su 'caminar al ejemplo', generalmente tiene una etapa de IA donde sus personajes caminados se encuentran en un estado en el que no deberían buscar enemigos para matar, una etapa de animación donde deberían ejecutar la animación X, una etapa de física (o etapa de simulación) donde se actualiza su posición real, etc.

en su ejemplo anterior, 'alice' es un actor compuesto por piezas que viven en muchas de estas etapas, por lo que un actor de bloqueo. walk_to call (o una rutina que llame next () una vez por cuadro) probablemente no tenga el contexto adecuado para tomar muchas decisiones

En cambio, una función 'start_walk_to' probablemente haría algo como:

def start_cutscene_walk_to(actor,target):
    actor.ai.setbrain(cutscene_brain)
    actor.physics.nocoll = 1
    actor.anims.force_anim('walk')
    # etc.

Luego, su bucle principal ejecuta su tic ai, su tic de física, su tic de animación y su tic de escena, y la escena de corte actualiza el estado de cada uno de los subsistemas en su motor. El sistema de escena definitivamente debería rastrear lo que está haciendo cada una de sus escenas, y un sistema impulsado por la rutina para algo lineal y determinista como una escena custscene podría tener sentido.

La razón de esta modularidad es que solo mantiene las cosas agradables y simples, y para algunos sistemas (como la física y la inteligencia artificial), debe conocer el estado de todo al mismo tiempo para resolver las cosas correctamente y mantener el juego en un estado consistente .

¡espero que esto ayude!

Aaron Brady
fuente
Me gusta lo que está buscando, pero en realidad siento mucho el valor de actor.walk_to ([otro actor, una posición fija o incluso una función que devuelve una posición]). Mi objetivo es proporcionar herramientas simples y comprensibles y manejar toda la complejidad lejos de la parte de creación de contenido del juego. También me ayudaste a darme cuenta de que todo lo que realmente quiero es una forma de tratar cada script como una máquina de estados finitos.
ojrac
me alegro de poder ayudar! Sentí que mi respuesta estaba un poco fuera de tema :) definitivamente estoy de acuerdo con el valor de una función actor.walk_to para lograr sus objetivos, espero escuchar sobre su implementación.
Aaron Brady
Parece que iré con una combinación de devoluciones de llamada y funciones encadenadas al estilo jQuery. Ver respuesta aceptada;)
ojrac