Técnicas de gestión del estado del juego?

24

En primer lugar, no me refiero a la gestión de la escena; Estoy definiendo el estado del juego libremente como cualquier tipo de estado en un juego que tiene implicaciones sobre si la entrada del usuario debe estar habilitada o no, o si ciertos actores deben deshabilitarse temporalmente, etc.

Como ejemplo concreto, digamos que es un juego del clásico Battlechess. Después de hacer un movimiento para tomar la pieza de otro jugador, se reproduce una breve secuencia de batalla. Durante esta secuencia, el jugador no debe poder mover piezas. Entonces, ¿cómo rastrearías este tipo de transición de estado? ¿Una máquina de estados finitos? ¿Un simple cheque booleano? Parece que este último solo funcionaría bien para un juego con muy pocos cambios de estado de este tipo.

Puedo pensar en muchas formas directas de manejar esto usando máquinas de estados finitos, pero también puedo ver que se salen de control rápidamente. Tengo curiosidad por saber si hay una forma más elegante de realizar un seguimiento de los estados / transiciones del juego.

vargonian
fuente
¿Has echado un vistazo a gamedev.stackexchange.com/questions/1783/game-state-stack y gamedev.stackexchange.com/questions/2423/… ? Es como saltar sobre el mismo concepto, pero no puedo pensar en nada que sea mejor que una máquina de estado para el estado del juego.
michael.bartnett

Respuestas:

18

Una vez me encontré con un artículo que resuelve su problema con bastante elegancia. Es una implementación básica de FSM, que se llama en su bucle principal. He esbozado el resumen básico del artículo en el resto de esta respuesta.

Su estado de juego básico se ve así:

class CGameState
{
    public:
        // Setup and destroy the state
        void Init();
        void Cleanup();

        // Used when temporarily transitioning to another state
        void Pause();
        void Resume();

        // The three important actions within a game loop
        void HandleEvents();
        void Update();
        void Draw();
};

Cada estado del juego está representado por una implementación de esta interfaz. Para su ejemplo de Battlechess, esto podría significar estos estados:

  • animación de introducción
  • menú principal
  • animación de configuración del tablero de ajedrez
  • entrada de movimiento del jugador
  • animación de movimiento del jugador
  • animación de movimiento del oponente
  • pausa menú
  • pantalla final

Los estados se administran en su motor de estado:

class CGameEngine
{
    public:
        // Creating and destroying the state machine
        void Init();
        void Cleanup();

        // Transit between states
        void ChangeState(CGameState* state);
        void PushState(CGameState* state);
        void PopState();

        // The three important actions within a game loop
        // (these will be handled by the top state in the stack)
        void HandleEvents();
        void Update();
        void Draw();

        // ...
};

Tenga en cuenta que cada estado necesita un puntero al CGameEngine en algún momento, por lo que el estado mismo puede decidir si se debe ingresar un nuevo estado. El artículo sugiere pasar CGameEngine como parámetro para HandleEvents, Update y Draw.

Al final, su ciclo principal solo trata con el motor de estado:

int main ( int argc, char *argv[] )
{
    CGameEngine game;

    // initialize the engine
    game.Init( "Engine Test v1.0" );

    // load the intro
    game.ChangeState( CIntroState::Instance() );

    // main loop
    while ( game.Running() )
    {
        game.HandleEvents();
        game.Update();
        game.Draw();
    }

    // cleanup the engine
    game.Cleanup();
    return 0;
}
fantasma
fuente
17
C para la clase? Ew. Sin embargo, ese es un buen artículo: +1.
El pato comunista
Por lo que puedo deducir, este es el tipo de cosas sobre las que la pregunta es explícitamente -no- preguntando. Eso no quiere decir que no pueda manejarlo de esta manera, como ciertamente podría, pero si todo lo que quería hacer era deshabilitar la entrada temporalmente, creo que es excesivo y malo para el mantenimiento derivar una nueva subclase de CGameState que va a ser 99% idéntico a otra subclase.
Kylotan
Creo que esto depende en gran medida de cómo se junta el código. Puedo imaginar una separación limpia entre seleccionar una pieza y un destino (principalmente indicadores de UI y manejo de entrada), y una animación de la pieza de ajedrez hacia ese destino (una animación de tablero completo donde otras piezas se mueven fuera de su camino, interactúan con el movimiento pieza, etc.), haciendo que los estados estén lejos de ser idénticos. Esto separa la responsabilidad, lo que permite un fácil mantenimiento e incluso la reutilización (demostración de introducción, modo de reproducción). Creo que esto también responde a la pregunta al mostrar que usar un FSM no necesita ser una molestia.
fantasma
Esto es realmente genial, gracias. Un punto clave que hizo fue en su último comentario: "el uso de un FSM no tiene por qué ser una molestia". Había imaginado erróneamente que usar un FSM implicaría usar declaraciones de cambio, lo que no es necesariamente cierto. Otra confirmación clave es que cada estado necesita una referencia al motor del juego; Me preguntaba cómo funcionaría esto de otra manera.
Vargonian
2

Comienzo manejando este tipo de cosas de la manera más simple posible.

bool isPieceMoving;

Luego agregaré los cheques contra esa bandera booleana en los lugares relevantes.

Si luego descubro que necesito más casos especiales que este, y solo eso, vuelvo a factorizar en algo mejor. Generalmente hay 3 enfoques que tomaré:

  • Refactorice cualquier indicador exclusivo que represente un subestado en enumeraciones. p.ej. enum { PRE_MOVE, MOVE, POST_MOVE }y agregue las transiciones donde sea necesario. Entonces puedo verificar con esta enumeración donde solía verificar con la bandera booleana. Este es un cambio simple pero que reduce la cantidad de cosas con las que tiene que verificar, le permite usar declaraciones de cambio para administrar el comportamiento de manera efectiva, etc.
  • Apague los subsistemas individuales según lo necesite. Si la única diferencia durante la secuencia de batalla es que no puedes mover piezas, puedes llamar pieceSelectionManager->disable()o algo similar al comienzo de la secuencia, y pieceSelectionManager->enable(). Esencialmente, todavía tiene banderas, pero ahora están almacenadas más cerca del objeto que controlan, y no necesita mantener ningún estado adicional en su código de juego.
  • La parte anterior implica la existencia de un PieceSelectionManager: de manera más general, puede descomponer partes del estado y el comportamiento de su juego en objetos más pequeños que manejan un subconjunto del estado general de manera coherente. Cada uno de estos objetos tendrá algo de su propio estado que determina su comportamiento, pero es fácil de administrar ya que está aislado de los otros objetos. ¡Resiste la tentación de permitir que tu objeto de estado de juego o bucle principal se convierta en un vertedero de pseudoglobales y factoriza esas cosas!

En términos generales, nunca necesito ir más allá de esto cuando se trata de subestados de casos especiales, por lo que no creo que exista el riesgo de que 'se salga de control rápidamente'.

Kylotan
fuente
1
Sí, me imagino que hay una línea entre ir con todos los estados y solo usar un bool / enums cuando sea apropiado. Pero conociendo mis tendencias pedantes, probablemente terminaré haciendo de cada estado su propia clase.
Vargonian
Haces que parezca que una clase es más correcta que las alternativas, pero recuerda que es subjetiva. Si comienza a crear demasiadas clases pequeñas para cosas que pueden ser representadas más fácilmente por otras construcciones de lenguaje, entonces puede oscurecer la intención del código.
Kylotan
1

¡http://www.ai-junkie.com/architecture/state_driven/tut_state1.html es un hermoso tutorial para la gestión del estado del juego! Puede usarlo para entidades de juego o para un sistema de menús como el anterior.

Comienza a enseñar sobre el patrón de diseño estatal , y luego implementa un State Machine, y lo extiende sucesivamente más y más. ¡Es una muy buena lectura! ¡Le dará una comprensión sólida de cómo funciona todo el concepto y cómo aplicarlo a nuevos tipos de problemas!

Zolomon
fuente
1

Intento no utilizar máquinas de estado y booleanos para este propósito, porque ambos no son escalables. Ambos se convierten en un desastre cuando crece el número de estados.

Por lo general, diseño el juego como una secuencia de acciones y consecuencias, cualquier estado del juego es algo natural sin necesidad de definirlo por separado.

Por ejemplo, en su caso con la deshabilitación de la entrada del jugador: tiene algún controlador de entrada del usuario y alguna indicación visual en el juego de que la entrada está deshabilitada, debe convertirlos en un solo objeto o componente, por lo que para deshabilitar la entrada simplemente deshabilita todo el objeto, no es necesario sincronícelos en alguna máquina de estados o reaccione a algún indicador booleano.

Filipp Keks
fuente