Separar Game Engine del código del juego en juegos similares, con versiones

15

Tengo un juego terminado, que quiero rechazar en otras versiones. Estos serían juegos similares, con más o menos el mismo tipo de diseño, pero no siempre, básicamente las cosas pueden cambiar, a veces poco, a veces a lo grande.

Me gustaría que el código principal se versione por separado del juego, de modo que si digo que soluciono un error encontrado en el juego A, la solución estará presente en el juego B.

Estoy tratando de encontrar la mejor manera de manejar eso. Mis ideas iniciales son las siguientes:

  • Crear un engine módulo / carpeta / lo que sea, que contenga todo lo que se pueda generalizar y sea 100% independiente del resto del juego. Esto incluiría algo de código, pero también activos genéricos que se comparten entre los juegos.
  • Coloque este motor en su propio gitrepositorio, que se incluirá en los juegos comogit submodule

La parte con la que estoy luchando es cómo administrar el resto del código. Digamos que tiene su escena de menú, este código es específico del juego, pero también la mayoría tiende a ser genérico y podría reutilizarse en otros juegos. No puedo ponerlo en el engine, pero volver a codificarlo para cada juego sería ineficiente.

Tal vez usar algún tipo de variación de ramas git podría ser efectivo para manejar eso, pero no creo que esta sea la mejor manera de hacerlo.

¿Alguien tiene algunas ideas, experiencia para compartir o algo al respecto?

Malharhak
fuente
¿En qué idioma está tu motor? Algunos idiomas tienen administradores de paquetes dedicados que pueden tener más sentido que usar submódulos git. Por ejemplo, NodeJS tiene npm (que puede apuntar a repositorios de Git como fuentes).
Dan Pantry
¿Es su pregunta sobre la mejor forma de administrar el código genérico o cómo administrar el código "semi-genérico" o cómo diseñar el código, cómo diseñar el código o qué?
Dunk
Esto puede variar en cada entorno de lenguaje de programación, pero puede considerar no solo el software de la versión de control, sino también saber cómo comenzar a dividir el motor del juego del código del juego (como paquetes, carpetas y API), y más adelante , aplique la versión de control.
umlcat
Cómo tener un historial limpio de una carpeta en una rama: refactorice su motor para que los repositorios separados (futuros) estén en carpetas separadas, esa es su última confirmación. Luego haga una nueva rama, elimine todo lo que esté fuera de esa carpeta y confirme. Luego vaya a la primera confirmación del repositorio y fusione eso con su nueva sucursal. Ahora tiene una rama con solo esa carpeta: llévela a otros proyectos y / o fusione de nuevo con su proyecto existente. Esto me ayudó mucho con la separación de motores en ramas, si su código ya está separado. No necesito módulos git.
Barry Staes

Respuestas:

13

Cree un módulo de motor / carpeta / lo que sea, que contenga todo lo que pueda generalizarse y sea 100% independiente del resto del juego. Esto incluiría algo de código, pero también activos genéricos que se comparten entre los juegos.

Coloque este motor en su propio repositorio git, que se incluirá en los juegos como un submódulo git

Eso es exactamente lo que hago y funciona muy bien. Tengo un marco de aplicación y una biblioteca de renderizado, y cada uno de estos se trata como submódulos de mis proyectos. Encuentro SourceTree es útil cuando se trata de submódulos, ya que los maneja bien y no le permite olvidar nada, por ejemplo, si actualizó el submódulo del motor en el proyecto A, le notificará que retire los cambios en el proyecto B.

Con la experiencia viene el conocimiento de qué código debe estar en el motor frente a qué debe ser por proyecto. Sugiero que si está un poco inseguro, lo mantenga en cada proyecto por ahora. A medida que pase el tiempo, verá entre sus diversos proyectos lo que sigue siendo el mismo y luego podrá incorporarlo gradualmente en el código del motor. En otras palabras: duplica el código hasta el momento en que estés casi 100% seguro de que no está cambiando discretamente por proyecto, luego generalízalo.

Nota sobre control de origen y binarios

Solo recuerde que si espera que sus activos binarios cambien con frecuencia, es posible que no desee ponerlos en el control de fuente basado en texto como git. Solo digo ... hay mejores soluciones para binarios. Lo más simple que puede hacer por ahora para ayudar a mantener limpio y eficiente su repositorio "fuente de motor" es tener un repositorio "binarios de motor" separado que contenga solo binarios, que también incluye como submódulo en su proyecto. De esta manera, usted mitiga el daño de rendimiento causado a su repositorio "motor-fuente", que está cambiando todo el tiempo y por lo tanto necesita iteraciones rápidas: commit, push, pull, etc. Los sistemas de gestión de control de fuente como git operan en deltas de texto , y tan pronto como introduce binarios, introduce deltas masivos desde una perspectiva de texto, lo que finalmente le cuesta tiempo de desarrollo.Anexo GitLab . Google es tu amigo.

Ingeniero
fuente
Realmente no cambian a menudo, pero eso me interesa. No sé nada sobre versiones binarias. ¿Qué soluciones hay?
Malharhak
@Malharhak Editado para responder tu comentario.
Ingeniero
@Malharak Aquí hay un poco de información sobre este tema.
Ingeniero
1
+1 para mantener las cosas en el proyecto el mayor tiempo posible. El código común otorga una mayor complejidad. Debe evitarse hasta que sea absolutamente necesario.
Gusdor
1
@Malharhak No, particularmente porque su objetivo es solo mantener "copias" hasta que note que dicho código es inmutable y puede ser considerado como común. Gusdor reiteró esto, se advierte, se puede perder fácilmente un montón de tiempo factorizando las cosas demasiado pronto, luego tratando de encontrar formas de mantener ese código lo suficientemente general como para mantenerse común, pero lo suficientemente adaptable para adaptarse a varios proyectos ... terminas con una gran cantidad de parámetros e interruptores y se convierte en un feo desastre que todavía no es lo que necesitas porque terminas cambiándolo por proyecto nuevo de todos modos . No descarte demasiado temprano . Tener paciencia.
Ingeniero
6

En algún momento, un motor DEBE especializarse y saber cosas sobre el juego. Iré por una tangente aquí.

Tomar recursos en un RTS. Un juego puede tener Creditsy Crystalotro MetalyPotatoes

Debes usar los conceptos de OO correctamente y elegir max. reutilización de código. Está claro que Resourceaquí existe un concepto de .

Entonces decidimos que los recursos tienen lo siguiente:

  1. Un gancho en el bucle principal para aumentar / disminuir
  2. Una forma de obtener la cantidad actual (devuelve un int)
  3. Una forma de restar / sumar arbitrariamente (jugadores transfiriendo recursos, compras ...)

¡Tenga en cuenta que esta noción de a Resourcepodría representar asesinatos o puntos en un juego! No es muy poderoso.

Ahora pensemos en un juego. Podemos tener moneda al negociar centavos y agregar un punto decimal a la salida. Lo que no podemos hacer son recursos "instantáneos". Como decir "generación de red eléctrica"

Digamos que agrega una InstantResourceclase con métodos similares. Ahora está (comenzando) a contaminar su motor con recursos.


El problema

Tomemos el ejemplo de RTS nuevamente. Supongamos que el jugador dona lo que sea Crystala otro jugador. Quieres hacer algo como:

if(transfer.target == engine.getPlayerId()) {
    engine.hud.addIncoming("You got "+transfer.quantity+" of "+
        engine.resourceDictionary.getNameOf(transfer.resourceId)+
        " from "+engine.getPlayer(transfer.source).name);
}
engine.getPlayer(transfer.target).getResourceById(transfer.resourceId).add(transfer.quantity)
engine.getPlayer(transfer.source).getResourceById(transfer.resourceId).add(-transfer.quantity)

Sin embargo, esto es realmente bastante desordenado. Es de uso general, pero desordenado. ¡Aunque ya impone una resourceDictionaryque significa que ahora tus recursos tienen que tener nombres! Y es por jugador, por lo que ya no puede tener recursos del equipo.

Esta es una abstracción "demasiado" (no es un ejemplo brillante, lo admito), en su lugar, debe llegar a un punto en el que acepte que su juego tiene jugadores y cristal, y luego puede tener (por ejemplo)

engine.getPlayer(transfer.target).crystal().receiveDonation(transfer)
engine.getPlayer(transfer.source).crystal().sendDonation(transfer)

Con una clase Playery una clase CurrentPlayerdonde CurrentPlayerel crystalobjeto mostrará automáticamente las cosas en el HUD para la transferencia / envío de donaciones.

Esto contamina el motor con cristal, la donación de cristal, los mensajes en el HUD para los jugadores actuales y todo eso. Es más rápido y fácil de leer / escribir / mantener (lo cual es más importante, ya que no es significativamente más rápido)


Observaciones finales

El caso de los recursos no es brillante. Sin embargo, espero que todavía puedas ver el punto. En todo caso, he demostrado que "los recursos no pertenecen al motor", ya que lo que un juego específico necesita y lo que es aplicable a todas las nociones de recursos son cosas MUY diferentes. Lo que generalmente encontrarás son 3 (o 4) "capas"

  1. El "núcleo": esta es la definición del motor del libro de texto, es un gráfico de escena con ganchos de eventos, se trata de sombreadores y paquetes de red y una noción abstracta de jugadores
  2. El "GameCore": es bastante genérico para el tipo de juego, pero no para todos los juegos, por ejemplo, recursos en RTS o municiones en FPS. La lógica del juego comienza a filtrarse aquí. Aquí es donde estaría nuestra noción anterior de recursos. Hemos agregado estas cosas que tienen sentido para la mayoría de los recursos RTS.
  3. "GameLogic" MUY específico para el juego real que se está haciendo. Encontrarás variables con nombres como creatureo shipo squad. El uso de la herencia que obtendrá clases que abarcan las 3 capas (por ejemplo, Crystal es una Resource la cual es un GameLoopEventListener ejemplo)
  4. Los "activos" son inútiles para cualquier otro juego. Tomemos, por ejemplo, los scripts de AI combinados en Half Life 2, no se usarán en un RTS con el mismo motor.

Hacer un nuevo juego a partir de un motor viejo

Esto es MUY común. La fase 1 es extraer las capas 3 y 4 (y 2 si el juego es de un tipo TOTALMENTE diferente) Supongamos que estamos haciendo un RTS a partir de un viejo RTS. Todavía tenemos recursos, simplemente no cristal y demás, por lo que las clases base en las capas 2 y 1 todavía tienen sentido, todo ese cristal al que se hace referencia en 3 y 4 se puede descartar. Así lo hacemos Sin embargo, podemos verificarlo como referencia de lo que queremos hacer.


Contaminación en la capa 1

Esto puede suceder. La abstracción y el rendimiento son enemigos. UE4, por ejemplo, proporciona muchos casos de composición optimizados (por lo que si desea X e Y, alguien escribió un código que hace X e Y juntos muy rápido, sabe que está haciendo ambas cosas) y, como resultado, es REALMENTE bastante grande. Esto no es malo, pero lleva mucho tiempo. La capa 1 decidirá cosas como "cómo pasar los datos a los sombreadores" y cómo animar las cosas. Hacerlo de la mejor manera para su proyecto SIEMPRE es bueno. Simplemente intente y planifique para el futuro, reutilizar el código es su amigo, herede donde tenga sentido.


Clasificar capas

POR ÚLTIMO (lo prometo) no tengas demasiado miedo a las capas. Motor es un término arcaico de los viejos tiempos de las tuberías de funciones fijas donde los motores funcionaban casi de la misma manera gráfica (y como resultado tenían mucho en común) la tubería programable le dio vueltas y como tal la "capa 1" se contaminó con los efectos que los desarrolladores quisieran lograr. La IA era la característica distintiva (debido a la gran cantidad de enfoques) de los motores, ahora es la IA y los gráficos.

Su código no debe archivarse en estas capas. Incluso el famoso motor de Unreal tiene MUCHAS versiones diferentes, cada una específica para un juego diferente. Hay pocos archivos (aparte de las estructuras de datos similares) que no habrían cambiado. ¡Esto esta bien! Si quieres hacer un juego nuevo desde otro, te llevará más de 30 minutos. La clave es planificar, saber qué bits copiar y pegar y qué dejar atrás.

Alec Teal
fuente
1

Mi sugerencia personal sobre cómo manejar el contenido que es una mezcla de genérico y específico es hacerlo dinámico. Tomaré la pantalla de tu menú como ejemplo. Si entendí mal lo que pedías, avísame qué querías saber y adaptaré mi respuesta.

Hay 3 cosas que (casi) siempre están presentes en una escena del menú: el fondo, el logotipo del juego y el menú en sí. Estas cosas suelen ser diferentes según el juego. Lo que puede hacer por este contenido es crear un MenuScreenGenerator en su motor, que toma 3 parámetros de objeto: BackGround, Logo y Menú. La estructura básica de estas 3 partes también es parte de su motor, pero su motor en realidad no dice cómo se generan estas partes, solo qué parámetros debe darles.

Luego, en su código de juego real, crea objetos para un BackGround, un Logo y un Menú, y lo pasa a su MenuScreenGenerator. Nuevamente, tu juego en sí no maneja cómo se genera el menú, eso es para el motor. Tu juego solo necesita decirle al motor cómo debería ser y dónde debería estar.

Esencialmente, su motor debe ser una API que el juego le dice qué mostrar. Si se hace correctamente, su motor debería hacer el trabajo duro y su juego solo debería decirle al motor qué activos usar, qué acciones tomar y cómo se ve el mundo.

Nzall
fuente