Estoy desarrollando un juego de mesa que tiene una clase de juego que controla el flujo del juego y jugadores vinculados a la clase de juego. El tablero es solo una clase visual, pero el control de los movimientos es todo por el juego.
El jugador puede intentar hacer movimientos falsos que conduzcan al juego diciéndole al jugador que este tipo de movimiento no está permitido (y decir la razón detrás de esto). Pero en algunos casos, el jugador puede hacer un movimiento y darse cuenta de que no es el mejor movimiento, o simplemente hacer clic en el tablero o simplemente intentar otro enfoque.
El juego se puede guardar y cargar, por lo que un enfoque podría ser almacenar todos los datos antes del movimiento y no permitir una reversión, pero permitir al usuario cargar el último turno guardado automáticamente. Esto parece un buen enfoque, pero involucra la interacción del usuario, y el rediseño del tablero podría ser tedioso para el usuario. ¿Hay una mejor manera de hacer este tipo de cosas o la arquitectura realmente importa en esto?
La clase de juego y la clase de jugador no son complicadas, por lo que es una buena idea clonar las clases, o separar los datos del juego de las reglas del juego para un mejor enfoque, o el guardado / carga (incluso automático en una reversión solicitada) está bien.
ACTUALIZACIÓN: cómo funciona este juego: tiene un tablero principal donde haces movimientos (movimientos simples) y un tablero de jugador que reacciona en los movimientos que hace en el tablero principal. También tiene movimientos de reacción de acuerdo con los movimientos de otros jugadores y sobre ti mismo. También puedes reaccionar cuando no sea tu turno, haciendo cosas en tu tablero. Tal vez no pueda deshacer cada movimiento, pero me gusta la idea de deshacer / rehacer flotando en una de las respuestas actuales.
fuente
Respuestas:
¿Por qué no almacenar el historial de todos los movimientos realizados (así como cualquier otro evento no determinista)? De esa manera siempre puedes reconstruir cualquier estado de juego dado.
Esto tomará significativamente menos espacio de almacenamiento que almacenar todos los estados del juego, y sería bastante simple de implementar.
fuente
Construir un sistema de deshacer es conceptualmente bastante simple. Solo necesita hacer un seguimiento de los cambios. Querrás una pila y un tipo de objeto que describa una parte del estado del juego. Su pila debe ser una pila de matrices / listas de objetos de estado del juego.
El truco es, no grabar movimientos que la gente hace . Veo esto en las otras respuestas, y se puede hacer, pero es mucho más complicado. En cambio, registre cómo se veía el tablero de juego antes de realizar el movimiento. Por ejemplo, cuando mueve una pieza, ha realizado dos cambios: la pieza dejó un cuadrado y la pieza se colocó en un nuevo cuadrado. Si crea una matriz que muestra cómo se veían esos dos cuadrados antes de que comenzara el movimiento, puede deshacer el movimiento simplemente restaurando esos dos cuadrados a la forma en que estaban antes del movimiento.
O, si su estado de juego se mantiene en las piezas y no en los cuadrados, lo que registra es la posición de la pieza antes de que se mueva. (Y si su pieza interactúa con otras piezas, como una captura en ajedrez, registre dónde estaban esas otras piezas antes de que se cambiaran).
Cuando ocurre cualquier movimiento:
Cuando el usuario dice Deshacer:
fuente
Una buena manera de implementar esto es encapsular sus movimientos en forma de objetos Command. Piense en una interfaz de comando que tiene los métodos
move(Position newPosition)
yundo
. Una clase concreta puede implementar esta interfaz de tal manera que para elmove
método, puede almacenar la posición actual en el tablero (la posición es una clase que contiene los valores de fila y columna probablemente), y luego hacer un movimiento a la fila y columna identificada pornewPosition
. Después de esto, el método agregará el comando (this
) a una pila global de objetos Command.Ahora, cuando el usuario necesite retroceder al paso anterior, simplemente saque la última instancia de Command de la pila y llame a su
undo
método. Esto también le brinda la capacidad de retroceder a todos los pasos que necesite.Espero que ayude.
fuente
Tocando hilos antiguos pero la forma más fácil que he encontrado para resolver este problema, al menos en escenarios complejos, es simplemente copiar cada maldita cosa antes de la operación del usuario ...
... suena un desperdicio, ¿verdad? Excepto si es realmente un desperdicio, entonces su próximo paso es hacer que la copia sea más barata para que las partes que no se cambiarán en el siguiente paso no se copien en profundidad.
En mi caso, a menudo trabajo con gigabytes de datos, pero el usuario a menudo solo toca megabytes para una operación, por lo que copiar todo normalmente sería extremadamente costoso y ridículamente derrochador ... pero he configurado estas estructuras de datos girando alrededor de poco profundas copiando lo que no cambia, y considero que esta es la solución más fácil y también abre muchas posibilidades nuevas, como estructuras de datos persistentes inmutables, redes nodales más seguras y múltiples hilos que ingresan algo y generan algo nuevo, edición no destructiva, creación de instancias objetos (poder copiar superficialmente, por ejemplo, sus costosos datos de malla pero tener el clon tiene su propia posición única en el mundo mientras apenas toma memoria), etc.
Todos los proyectos desde entonces han seguido este patrón básico en lugar de tener que registrar cuidadosamente cada pequeño cambio de estado y para docenas de diferentes tipos de datos que no encajan en un modelo homogéneo (imagen / pintura de textura, cambios de propiedad, cambios de malla, cambios de peso, cambios jerárquicos de escena, cambios de sombreador que no estaban relacionados con la propiedad, como intercambiar uno con otro, cambios de envolvente, etc., etc.). En cambio, si eso es demasiado derrochador en términos de memoria y tiempo, entonces no se nos ocurre un sistema de deshacer más sofisticado, sino que buscamos optimizar la copia para que se puedan hacer copias parciales en todas las estructuras de datos más caras. Eso lo deja como un detalle de optimización en el que puede tener una implementación de deshacer que funcione correctamente de inmediato que siempre se mantendrá correcta, y siempre se mantendrá correcto es increíble cuando, en el pasado,
La otra forma es registrar los deltas (cambios) individualmente, y solía hacerlo en el pasado, pero para un software a gran escala muy complejo, a menudo era muy propenso a errores y tedioso, ya que era realmente fácil para un desarrollador de complementos para olvidar registrar algún cambio en la pila de deshacer cuando introdujeron nuevos conceptos en el sistema.
Ahora, una cosa, independientemente de si copia todo el estado del juego o registra deltas es evitar los punteros (suponiendo C o C ++) cuando sea posible o de lo contrario complicará mucho las cosas. Si usa índices, todo se vuelve más simple, ya que los índices no se invalidan con copias (ya sea del estado completo de la aplicación o de una parte del mismo). Como ejemplo con una estructura de datos de gráfico, si desea copiarlo en su totalidad o simplemente registrar deltas cuando los usuarios hacen nuevas conexiones o rompen conexiones en el gráfico, el estado querrá almacenar enlaces a los nodos antiguos y puede encontrarse con problemas de invalidación y cosas por el estilo. Es mucho más fácil si las conexiones usan índices relativos en una matriz, ya que es mucho más fácil evitar que esos índices sean invalidantes que los punteros.
fuente
Mantendría una lista de deshacer de los movimientos (válidos) realizados por el jugador.
Cada elemento de la lista debe contener suficiente información para restaurar el estado del juego a lo que era justo antes de que se realizara el movimiento. Dependiendo de la cantidad de estado de juego que haya y la cantidad de pasos para deshacer que desee admitir, esto podría ser una instantánea del estado del juego o información que le diga al motor del juego cómo revertir el movimiento.
fuente