Codificación de diferentes estados en juegos de aventura

12

Estoy planeando un juego de aventuras, y no puedo entender cuál es la forma correcta de implementar el comportamiento de un nivel dependiendo del estado de la progresión de la historia.

Mi juego para un jugador presenta un mundo enorme donde el jugador tiene que interactuar con personas de una ciudad en varios puntos del juego. Sin embargo, dependiendo de la progresión de la historia, se le presentarían diferentes cosas al jugador, por ejemplo, el Líder del Gremio cambiará de lugar desde la plaza del pueblo a varios lugares alrededor de la ciudad; Las puertas solo se desbloquearían en ciertos momentos del día después de terminar una rutina en particular; Los diferentes eventos de corte de pantalla / activación ocurren solo después de que se haya alcanzado un hito en particular.

Inicialmente pensé en usar una declaración switch {} inicialmente para decidir qué debería decir el NPC o en qué se podía encontrar, y hacer que los objetivos de búsqueda interactuaran solo después de verificar la condición de una variable global game_state. Pero me di cuenta de que rápidamente me encontraría con muchos estados de juego y casos de cambio diferentes para cambiar el comportamiento de un objeto. Esa declaración de cambio también sería enormemente difícil de depurar, y supongo que también podría ser difícil de usar en un editor de niveles.

Entonces pensé, en lugar de tener un solo objeto con múltiples estados, tal vez debería tener múltiples instancias del mismo objeto, con un solo estado. De esa manera, si uso algo como un editor de niveles, puedo poner una instancia del NPC en todas las diferentes ubicaciones en las que podría aparecer, y también una instancia para cada estado de conversación que tenga. Pero eso significa que habrá muchos objetos de juego invisibles e inactivos flotando alrededor del nivel, lo que podría ser un problema para la memoria o simplemente difícil de ver en un editor de niveles, no lo sé.

O simplemente, crea un nivel idéntico pero separado para cada estado del juego. Esta es la forma más limpia y libre de errores para hacer las cosas, pero se siente como un trabajo manual masivo para asegurarse de que cada versión del nivel sea realmente idéntica entre sí.

Todos mis métodos se sienten tan ineficientes, así que para recapitular mi pregunta, ¿hay una manera mejor o estandarizada de implementar el comportamiento de un nivel dependiendo del estado de la progresión de la historia?

PD: Todavía no tengo un editor de niveles, pensando en usar algo como JME SDK o hacer el mío.

Cardin
fuente

Respuestas:

9

Creo que lo que necesita en este caso es el Patrón de diseño de estado . En lugar de tener múltiples instancias de cada objeto del juego, cree una sola instancia, pero encapsule su comportamiento en una clase separada. Cree varias clases, una para cada comportamiento posible, y dé a todas las clases la misma interfaz. Asocia uno a tu objeto de juego (el estado inicial) y, cuando cambian las condiciones (se alcanza un hito, pasa la hora del día, etc.), cambias el estado de ese objeto (es decir, asócialo con un objeto diferente dependiendo de la lógica de tu juego) y actualice sus propiedades si corresponde.

Un ejemplo de cómo se vería una interfaz de estado (completamente inventada, solo para ilustrar el nivel de control que le brinda este esquema):

interface NPCState {
    Scene whereAmI(NPC o);
    String saySomething(NPC o);
}

Y dos clases de implementación:

class Busy implements NPCState {
    Scene whereAmI(NPC o) {
        return o.getWorkScene();
    }
    String saySomething(NPC o) {
        return "Can't talk now, I'm busy!";
    }
}

class Available implements NPCState {
    Scene whereAmI(NPC o) {
        return TAVERN;
    }
    String saySomething(NPC o) {
        String[] choices = o.getRandomChat();
        return choices[RANDOM.getInt(choices.length)];
    }
}

Y estados de conmutación:

// The time of day passed from "afternoon" to "evening"
NPCState available = new Available();
for ( NPC o : list ) {
    Scene oldScene = o.state.whereAmI(o);
    o.state = available;
    Scene newScene = o.state.whereAmI(o);
    moveGameObject(o, oldScene, newScene);
    ...

Los NPC importantes pueden tener sus estados personalizados, la lógica de elección de estado puede ser más personalizable y puede tener diferentes estados para diferentes facetas del juego (en este ejemplo, utilicé una sola clase para indicar la ubicación y el chat, pero podría separar ellos y hacer muchas combinaciones).

Esto también funciona bien con los editores de nivel: puede tener un cuadro combinado simple para cambiar el estado "global" de un nivel, luego agregar y reposicionar los objetos del juego como desee que aparezcan en ese estado. El motor del juego solo sería responsable de "agregar" ese objeto a la escena cuando tenga el estado correcto, pero sus parámetros aún serían editables de una manera fácil de usar.

(Descargo de responsabilidad: tengo poca experiencia en el mundo real con los editores de juegos, por lo que puedo decir con confianza sobre cómo funcionan los editores profesionales; pero mi punto sobre el Patrón de Estado aún se mantiene, organizar su código de esta manera debería ser limpio, fácil de mantener y no desperdiciar el sistema recursos)

mgibsonbr
fuente
ya sabes, podrías combinar este patrón de diseño de estado con la matriz asociativa que describí. Puede codificar los objetos de estado como se describe aquí, y luego elegir entre diferentes objetos de estado utilizando una matriz asociativa como sugerí.
jhocking
Estoy de acuerdo, también es bueno separar el juego de su motor, y la lógica del juego de codificación fortalece el acoplamiento entre ellos (reduciendo la posibilidad de reutilización). Sin embargo, hay una compensación, ya que dependiendo de la complejidad de su comportamiento previsto al tratar de "codificar" todo puede resultar en un desorden innecesario . En este caso, podría ser deseable un enfoque mixto (es decir, tener una lógica de transición de estado "genérica", pero que también permita incorporar código personalizado)
mgibsonbr
Entonces, según tengo entendido, hay mapeo uno a uno entre NPCState y GameState. Luego, pondré los NPC en una matriz e iteraré a través de ellos, asignando el nuevo NPCState cuando se observe un cambio en el estado del juego. El NPCState tiene que ser capaz de saber cómo manejar cada NPC diff que se le envía, ¿entonces esencialmente el NPCState contiene el comportamiento de todos los NPC para un estado dado? Me gusta que todos los comportamientos se almacenen limpiamente en un solo NPCState, que se asigna limpiamente a la implementación del editor del juego, pero hace que el NPCState sea bastante grande.
Cardin
Oh, creo que entendí mal tus respuestas. Lo cambié un poco para incluir observadores. Por lo tanto, es un NPCState diff para cada NPC diff, excepto los súper genéricos como Crowd NPC que pueden compartir estado. Para cada estado del juego, el NPC se registrará a sí mismo y a su NPCState con un observador. Por lo tanto, el Observador sabrá exactamente qué NPC está registrado para cambiar el comportamiento en qué estado del juego, y simplemente iterará a través de ellos. Y en el lado del editor del juego, el editor del juego solo tiene que pasar una señal al Observador para cambiar el estado de todo el nivel.
Cardin
1
Sí, esa es la idea! Los NPC importantes tendrán muchos estados, y la transición de estado dependerá principalmente de los hitos completados. Los NPC genéricos también pueden reaccionar a los hitos a veces, e incluso hacer que su estado elegido dependa de una propiedad interna (digamos que todos los NPC tienen un estado inicial predeterminado, y cuando hablas con uno de ellos por primera vez se presenta, luego ingresa el ciclo normal de cambio de estado).
mgibsonbr
2

Las opciones que consideraría son hacer que los objetos individuales respondan a diferentes estados de juego o servir diferentes niveles en diferentes estados de juego. La elección entre esos dos dependerá de qué es exactamente lo que estoy tratando de hacer en el juego (¿cuáles son los diferentes estados? ¿Cómo hará la transición del juego entre estados? Etc.)

De cualquier manera, sin embargo, no lo haría codificando los estados en el código del juego. En lugar de una declaración de cambio masivo en objetos NPC, en lugar de los comportamientos NPC cargados en una matriz asociativa desde un archivo de datos y luego usar esa matriz asociativa para ejecutar un comportamiento diferente para sus estados asociados, algo como esto:

if (state in behaviors) {
  behaviors[state]();
}
jhocking
fuente
¿Sería ese archivo de datos una especie de lenguaje de script? Creo que un archivo de datos de texto sin formato podría no ser suficiente para describir el comportamiento. En cualquier caso, tiene razón en que debería cargarse dinámicamente. No puedo pensar en usar un editor de juegos para generar código Java válido, definitivamente debe analizarse un poco.
Cardin
1
Bueno, ese fue mi pensamiento inicial, pero después de ver la respuesta de mgibsonbr, me di cuenta de que podía codificar los diferentes comportamientos como clases separadas y luego, en el archivo de datos, simplemente decir qué clases de comportamiento van con cada estado. Cargue esos datos en una matriz asociativa en tiempo de ejecución.
jhocking
Oh .. Sí, eso es definitivamente más simple! : D Comparado con el escenario de incrustar algo como Lua jaja ..
Cardin
2

¿Qué pasa con el uso de un patrón de observador para buscar cambios importantes? Si ocurre un cambio, alguna clase lo reconocería y manejaría, por ejemplo, un cambio que debe hacerse a un npc.

En lugar del patrón de diseño de estado mencionado, usaría un patrón de estrategia.

Si un npc tiene n formas de interactuar con el personaje ym posiciones donde podría estar, hay un máximo de (m * n) +1 clases que debes diseñar. Usando el patrón de estrategia, terminaría con n + m + 1 clases, pero estas estrategias también podrían ser utilizadas por otros npcs.

Entonces, podría haber una clase manejando los hitos, y clases que observen esta clase y manejen npc o enemigos o lo que sea que deba cambiarse. Si los observadores se actualizan, decidirán si tienen que cambiar algo a las instancias que gobiernan. La clase NPC, por ejemplo, en el constructor, informaría al NPC-Manager cuándo debe actualizarse y qué debe actualizarse ...

TOAOGG
fuente
El patrón del observador parece interesante. Creo que podría dejar limpiamente todas las responsabilidades con la APN para registrarse con el observador estatal. El patrón de estrategia se parece mucho a los comportamientos de disparo e IA de Unity Engine, que se utiliza para compartir el comportamiento entre diferentes objetos del juego (creo). Suena factible. No estoy seguro de cuáles son los pros / contras en este momento, pero que Unity también use el mismo método es algo tranquilizador jaja ...
Cardin
Solo utilicé estos dos patrones varias veces, por lo que no puedo informarle sobre los inconvenientes: - / Pero creo que es bueno en caso de responsabilidad individual y la disponibilidad para probar cada estrategia :) Puede ser confuso si su utilizando una estrategia en muchas clases diferentes y desea encontrar todas las clases que la utilizan.
TOAOGG
0

Todos los enfoques dados son válidos. Depende de la situación en la que se encuentre en un momento dado. Muchas aventuras o MMOs usan una combinación de estos.

Por ejemplo, si un evento crucial cambia una gran parte del nivel (por ejemplo, un cobrador de deudas limpia su apartamento y todos los que están en él son arrestados), generalmente es más fácil reemplazar toda la habitación con una segunda habitación que se ve similar.

OTOH, si los personajes caminan por el mapa y hacen cosas diferentes en diferentes lugares, a menudo tienes un solo actor que rota a través de diferentes objetos de comportamiento (por ejemplo, caminar hacia adelante / sin conversaciones frente a pie / conversación sobre la muerte de Mitch), que podría incluir "oculto" si su propósito se ha cumplido.

Dicho esto, generalmente tener duplicados de un objeto que creas manualmente no debería causar ningún problema. ¿Cuántos objetos puedes crear? Si puedes crear más objetos de los que tu juego puede recorrer, mira su propiedad "oculta" y salta, tu motor es demasiado lento. Así que no me preocuparía demasiado por eso. Muchos juegos en línea realmente hacen esto. Ciertos personajes o elementos siempre están ahí, pero no se muestran a los personajes que no tienen la misión correspondiente.

Incluso puede combinar los enfoques: tenga dos puertas en su edificio de apartamentos. Uno conduce al apartamento "antes del cobrador de deudas", uno al apartamento después. Cuando ingresas al corredor, solo se muestra el que se aplica a tu progresión en la historia. De esa manera, puede tener un mecanismo genérico para "el elemento es visible en el punto actual de la historia" y una puerta con un único destino. Alternativamente, puede hacer puertas más complicadas que pueden tener comportamientos que se pueden intercambiar, y una de ellas es "ir al departamento completo", la otra "ir al departamento vacío". Esto puede parecer absurdo si realmente cambia el destino de la puerta, pero si su apariencia también cambia (por ejemplo, una gran cerradura frente a la puerta que primero tiene que romper),

uli testigo
fuente