Patrón para realizar acciones de juego

11

¿Existe un patrón generalmente aceptado para realizar diversas acciones dentro de un juego? Una forma en que un jugador puede realizar acciones y también que una IA puede realizar acciones, como movimiento, ataque, autodestrucción, etc.

Actualmente tengo una BaseAction abstracta que usa genéricos .NET para especificar los diferentes objetos que devuelven las diferentes acciones. Todo esto se implementa en un patrón similar al Comando, donde cada acción es responsable de sí misma y hace todo lo que necesita.

Mi razonamiento para ser abstracto es que puedo tener un único ActionHandler, y AI puede poner en cola diferentes acciones implementando la acción base. Y la razón por la que es genérico es para que las diferentes acciones puedan devolver información de resultados relevante para la acción (ya que diferentes acciones pueden tener resultados totalmente diferentes en el juego), junto con algunas implementaciones comunes de beforeAction y afterAction.

Entonces ... ¿hay una forma más aceptada de hacer esto, o suena bien?

Arkiliknam
fuente
Suena bien, la pregunta es ¿qué quieres decir con cola? ¿La mayoría de los juegos tienen una respuesta muy rápida? "La IA puede poner en cola diferentes acciones"
AturSams
Buen punto. No hay cola Solo necesita saber si está ocupado y, si no, ejecutar la acción.
Arkiliknam

Respuestas:

18

No creo que haya una forma aceptada de la aplicación de este concepto, pero me gusta mucho compartir cómo me suelen tratar con esto en mis juegos. Es una combinación del patrón de diseño Command y el patrón de diseño Composite .

Tengo una clase base abstracta para acciones que no es más que un contenedor alrededor de un Updatemétodo que se llama cada cuadro, y un Finishedindicador para indicar cuándo la acción ha terminado de ejecutarse.

abstract class Action
{
    abstract void Update(float elapsed);
    bool Finished;
}

También utilizo el patrón de diseño compuesto para crear un tipo de acciones que sea capaz de alojar y ejecutar otras acciones. Esta también es una clase abstracta. Se reduce a:

abstract class CompositeAction : Action
{
    void Add(Action action) { Actions.Add(action); }
    List<Action> Actions;
}

Luego tengo dos implementaciones de acciones compuestas, una para ejecución paralela y otra para ejecución secuencial . Pero la belleza es que, dado que el paralelo y la secuencia son acciones en sí mismas, pueden combinarse para crear flujos de ejecución más complejos.

class Parallel : CompositeAction
{
    override void Update(float elapsed) 
    {
        Actions.ForEach(a=> a.Update(elapsed));
        Actions.RemoveAll(a => a.Finished);
        Finished = Actions.Count == 0;
    }
}

Y el que gobierna las acciones secuenciales.

class Sequence : CompositeAction
{
    override void Update(float elapsed) 
    {
        if (Actions.Count > 0) 
        {
            Actions[0].Update(elapsed);
            if (Actions[0].Finished)
                Actions.RemoveAt(0);
        }
        Finished = Actions.Count == 0;
    }
 }

Con esto en su lugar, se trata simplemente de crear implementaciones de acciones concretas y usar las acciones Parallely Sequencepara controlar el flujo de ejecución. Terminaré con un ejemplo:

// Create a parallel action to work as an action manager
Parallel actionManager = new Parallel();

// Send character1 to destination
Sequence actionGroup1 = new Sequence();
actionGroup1.Add(new MoveAction(character1, destination));
actionGroup1.Add(new TalkAction(character1, "Arrived at destination!"));
actionManager.Add(actionGroup1);

// Make character2 use a potion on himself
Sequence actionGroup2 = new Sequence();
actionGroup2.Add(new RemoveItemAction(character2, ItemType.Potion));
actionGroup2.Add(new SetHealthAction(character2, character2.MaxHealth));
actionGroup2.Add(new TalkAction(character2, "I feel better now!"));
actionManager.Add(actionGroup2);

// Every frame update the action manager
actionManager.Update(elapsed);

He utilizado con éxito este sistema para conducir todo el juego en una aventura gráfica antes, pero probablemente debería funcionar para casi cualquier cosa. También fue lo suficientemente simple como para agregar otros tipos de acciones compuestas, que se utilizaron para crear bucles de ejecución y condicionales.

David Gouveia
fuente
Esa parece una muy buena solución. Por curiosidad, ¿cómo le haces saber a la UI qué dibujar? ¿Sus objetos de juego (como los personajes) contienen un estado que se utiliza para identificar lo que sucedió con fines de representación, o es la acción misma lo que hace eso?
Arkiliknam
1
Por lo general, mis acciones solo cambian el estado de las entidades, y cualquier cambio en la salida representada se produce como consecuencia de ese cambio de estado, no a través de las acciones en sí. Por ejemplo, con un renderizador de modo inmediato no se requiere un paso adicional ya que el Drawmétodo ya está construido sobre el estado de la entidad y los cambios son automáticos. En un renderizador en modo retenido como Flash, puede usar el patrón observable para realizar cambios en las entidades que se propagan a los objetos de visualización, o realizar la conexión manualmente dentro de la propia entidad.
David Gouveia
1
En la primera situación, supongamos que su Characterclase tiene una Positionpropiedad y un Drawmétodo que lee cuál es el valor actual de Positiony dibuja la imagen correcta allí. En esta situación, solo necesita actualizar el valor de Positionque el resultado se verá automáticamente en la pantalla.
David Gouveia
1
La segunda situación es, cuando Charactertiene una Positionpropiedad, pero delega la representación a algún tipo de Spriteobjeto que se representa automáticamente mediante un gráfico de escena o algo así. En esta situación, debes asegurarte de que tanto la posición del personaje como la posición del sprite estén siempre sincronizadas, lo que implica un poco más de trabajo. Aún así, en cualquier caso, no veo por qué el administrador de acciones debería tener algo que ver con eso. :)
David Gouveia
1
Ambos métodos tienen ventajas y desventajas. Fui con el segundo método para mi juego 2D, y a veces me arrepiento porque es significativamente más complicado mantener todo sincronizado. Pero también hay ventajas, por ejemplo, cuando se intenta detectar en qué entidad se hizo clic, o qué se debe dibujar o no, porque todo lo que se representará está contenido dentro de la misma estructura de datos en lugar de estar disperso entre N tipos de entidades.
David Gouveia