¿Cuáles son las desventajas de usar DrawableGameComponent para cada instancia de un objeto de juego?


25

He leído en muchos lugares que DrawableGameComponents debe guardarse para cosas como "niveles" o algún tipo de gerentes en lugar de usarlos, por ejemplo, para personajes o mosaicos (como dice este tipo aquí ). Pero no entiendo por qué esto es así. Leí esta publicación y tenía mucho sentido para mí, pero son minoría.

Por lo general, no prestaría demasiada atención a cosas como estas, pero en este caso me gustaría saber por qué la mayoría aparente cree que este no es el camino a seguir. Tal vez me estoy perdiendo algo.


Tengo curiosidad por saber por qué, dos años después, ¿ha eliminado la marca de "aceptado" de mi respuesta ? Normalmente no me importa realmente - pero en este caso se produce de VeraShackle exasperante tergiversación de mi respuesta a la burbuja a la parte superior :(
Andrew Russell

@ AndrewRussell Leí este hilo no hace mucho (debido a algunas respuestas y votos) y pensé que no estaba calificado para dar una opinión sobre si su respuesta es correcta o no. Sin embargo, como usted dice, no creo que la respuesta de VeraShackle sea la que tenga más votos positivos, por lo que estoy marcando su respuesta como correcta nuevamente.
Kenji Kina

1
Puedo condensar mi respuesta aquí de la siguiente manera: "El uso de DrawableGameComponentbloqueos en un solo conjunto de firmas de métodos y un modelo específico de datos retenidos. Estas son típicamente la elección incorrecta inicialmente, y el bloqueo dificulta significativamente la evolución del código en el futuro". "Pero déjame decirte lo que está pasando aquí ...
Andrew Russell

1
DrawableGameComponentes parte de la API central de XNA (nuevamente: principalmente por razones históricas). Los programadores novatos lo usan porque está "allí" y "funciona" y no saben nada mejor. Ven mi respuesta diciéndoles que están " equivocados " y, debido a la naturaleza de la psicología humana, lo rechazan y eligen el otro "lado". Que resulta ser la no respuesta argumentativa de VeraShackle; que cobró impulso porque no lo llamé de inmediato (ver esos comentarios). Siento que mi eventual refutación sigue siendo suficiente.
Andrew Russell

1
Si alguien está interesado en cuestionar la legitimidad de mi respuesta sobre la base de votos a favor: si bien es cierto que (GDSE está enfadado con los novatos antes mencionados), la respuesta de VeraShackle ha logrado obtener un par de votos más que los míos en los últimos años, no se olvide que tengo miles de votos a favor en toda la red, principalmente en XNA. He implementado la API XNA en múltiples plataformas. Y soy un desarrollador de juegos profesional que usa XNA. El pedigrí de mi respuesta es impecable.
Andrew Russell

Respuestas:


22

Los "Componentes del juego" fueron una parte muy temprana del diseño de XNA 1.0. Se suponía que debían funcionar como controles para WinForms. La idea era que podría arrastrarlos y soltarlos en su juego usando una GUI (algo así como el bit para agregar controles no visuales, como temporizadores, etc.) y establecer propiedades en ellos.

Entonces, ¿podría tener componentes como una cámara o un ... contador FPS? ... o ...?

¿Lo ves? Desafortunadamente, esta idea realmente no funciona para los juegos del mundo real. El tipo de componentes que deseas para un juego no son realmente reutilizables. Es posible que tenga un componente de "jugador", pero será muy diferente al componente de "jugador" del juego de los demás.

Me imagino que fue por esta razón, y por una simple falta de tiempo, que la GUI se retiró antes de XNA 1.0.

Pero la API todavía es utilizable ... He aquí por qué no deberías usarla:

Básicamente se trata de lo que es más fácil de escribir, leer, depurar y mantener como desarrollador de juegos. Haciendo algo como esto en su código de carga:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

Es mucho menos claro que simplemente hacer esto:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Especialmente cuando, en un juego del mundo real, podrías terminar con algo como esto:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(Es cierto que podría compartir la cámara y el spriteBatch usando una Servicesarquitectura similar . Pero esa también es una mala idea por la misma razón).

Esta arquitectura, o falta de ella , es mucho más ligera y flexible!

Puedo cambiar fácilmente el orden de dibujo o agregar múltiples ventanas gráficas o agregar un efecto de objetivo de renderizado, simplemente cambiando algunas líneas. Para hacerlo en el otro sistema me requiere piense qué están haciendo todos esos componentes, distribuidos en varios archivos, y en qué orden.

Es como la diferencia entre la programación declarativa y la imperativa. Resulta que, para el desarrollo de juegos, la programación imperativa es mucho más preferible.


2
Tenía la impresión de que eran para la programación basada en componentes , que aparentemente se usa ampliamente para la programación de juegos.
BlueRaja - Danny Pflughoeft

3
Las clases de componentes del juego XNA no son realmente directamente aplicables a ese patrón. Ese patrón se trata de componer objetos a partir de múltiples componentes. Las clases XNA están orientadas a un componente por objeto. Si encajan perfectamente en su diseño, entonces utilícelas por supuesto. Sin embargo, usted debe no construir su diseño en torno a ellos! Vea también mi respuesta aquí : busque dónde menciono DrawableGameComponent.
Andrew Russell

1
Estoy de acuerdo en que la arquitectura de GameComponent / Service no es tan útil y parece que los conceptos de Office o de aplicaciones web están siendo forzados a un marco de juego. Pero preferiría usar el player.DrawOrder = 100;método sobre el imperativo para el orden de dibujo. Estoy terminando un juego de Flixel en este momento, que dibuja objetos en el orden en que los agregas a la escena. El sistema FlxGroup alivia algo de esto, pero tener algún tipo de clasificación de índice Z integrado hubiera sido bueno.
michael.bartnett

@ michael.bartnett Tenga en cuenta que Flixel es una API en modo retenido, mientras que XNA (excepto, obviamente, el sistema de componentes del juego) es una API en modo inmediato. Si utiliza una API en modo retenido o implementa la suya propia , la capacidad de cambiar el orden de los sorteos sobre la marcha es definitivamente importante. De hecho, la necesidad de cambiar el orden de dibujo sobre la marcha es una buena razón para hacer algo en modo retenido (me viene a la mente el ejemplo de un sistema de ventanas). Pero si puede escribir algo como modo inmediato, si la API de la plataforma (como XNA) está construida de esa manera, entonces debería hacerlo.
Andrew Russell el


26

Hmmm Tengo miedo de desafiar a este por un poco de equilibrio.

Parece que hay 5 cargos en la respuesta de Andrew, así que trataré de tratar cada uno por turno:

1) Los componentes son un diseño obsoleto y abandonado.

La idea del patrón de diseño de componentes existía mucho antes de XNA; en esencia, es simplemente una decisión de hacer que los objetos, en lugar del objeto general del juego, sean el hogar de su propio comportamiento de Actualización y Dibujo. Para los objetos en su colección de componentes, el juego llamará a estos métodos (y a algunos otros) automáticamente en el momento apropiado; crucialmente, el objeto del juego no tiene que 'conocer' nada de este código. Si desea reutilizar sus clases de juego en diferentes juegos (y, por lo tanto, diferentes objetos del juego), este desacoplamiento de objetos sigue siendo una buena idea desde el punto de vista financiero. Un vistazo rápido a las últimas demostraciones (producidas profesionalmente) de XNA4.0 de AppHub confirma mi impresión de que Microsoft todavía está (con razón, en mi opinión) firmemente detrás de esta.

2) Andrew no puede pensar en más de 2 clases de juegos reutilizables.

Yo puedo. De hecho, puedo pensar en cargas! Acabo de comprobar mi lib compartida actual, y comprende más de 80 componentes y servicios reutilizables . Para darle un sabor, podría tener:

  • Game State / Screen Managers
  • Emisores de Partículas
  • Primitivas dibujables
  • Controladores AI
  • Controladores de entrada
  • Controladores de animación
  • Vallas publicitarias
  • Gerentes de Física y Colisión
  • Cámaras

Cualquier idea de juego en particular necesitará al menos 5-10 cosas de este conjunto, garantizado, y pueden ser completamente funcionales en un juego completamente nuevo con una línea de código.

3) Controlar el orden de dibujo por el orden de las llamadas de dibujo explícitas es preferible a usar la propiedad DrawOrder del componente.

Bueno, básicamente estoy de acuerdo con Andrew en este caso. PERO, si deja todas las propiedades DrawOrder de sus componentes como valores predeterminados, aún puede controlar explícitamente su orden de dibujo por el orden en que los agrega a la colección. Esto es realmente idéntico en términos de 'visibilidad' al pedido explícito de sus llamadas de extracción manual.

4) Llamar una carga de métodos de extracción de objetos usted mismo es más "ligero" que hacer que los llame un método de marco existente.

¿Por qué? Además, en todos los proyectos, excepto en los más triviales, su lógica de actualización y el código de Draw eclipsarán por completo su método de llamadas en términos de éxito en cualquier caso.

5) Colocar su código en un archivo es preferible, al depurar, que tenerlo en el archivo del objeto individual.

Bueno, en primer lugar, cuando estoy depurando en Visual Studio, la ubicación de un punto de interrupción en términos de archivos en el disco es casi invisible en estos días: el IDE trata la ubicación sin ningún drama. Pero también considere por un momento que tiene un problema con su dibujo de Skybox. Ir al método de dibujo de Skybox es realmente más oneroso que localizar el código de dibujo de skybox en el maestro (presumiblemente muy largo) método de dibujo en el objeto Juego? No, en realidad no, de hecho diría que es todo lo contrario.

En resumen, por favor, eche un vistazo a los argumentos de Andrew aquí. No creo que realmente den bases sustanciales para escribir código de juego enredado. Las ventajas del patrón de componente desacoplado (usado juiciosamente) son enormes.


2
Acabo de darme cuenta de esta publicación, y debo emitir una fuerte refutación: ¡no tengo ningún problema con las clases reutilizables! (Que puede tener métodos de sorteo y actualización, etc.) Estoy teniendo un problema específico con el uso de ( Drawable) GameComponentpara proporcionar su arquitectura de juego, debido a la pérdida del control explícito sobre el orden de sorteo / actualización (que está de acuerdo en el n. ° 3) de sus interfaces (que no se abordan).
Andrew Russell

1
Su punto # 4 toma mi uso de la palabra "ligero" para significar rendimiento, donde realmente me refiero claramente a la flexibilidad arquitectónica. ¡Tus puntos restantes # 1, # 2, y especialmente el # 5 parecen erigir al absurdo hombre de paja que creo que la mayoría / todos los códigos deberían introducirse en tu clase de juego principal! Lea mi respuesta aquí para ver algunos de mis pensamientos más detallados sobre arquitectura.
Andrew Russell

55
Finalmente: si va a publicar una respuesta a mi respuesta, diciendo que estoy equivocado, habría sido cortés agregar también una respuesta a mi respuesta para ver una notificación al respecto, en lugar de tener que tropezar que dos meses más tarde.
Andrew Russell

Puede que haya entendido mal, pero ¿cómo responde esto a la pregunta? ¿Debo usar un GameComponent para mis sprites o no?
Excelente


4

No estoy de acuerdo un poco con Andrew, pero también creo que hay un momento y un lugar para todo. No usaría un Drawable(GameComponent)para algo tan simple como un Sprite o un Enemigo. Sin embargo, lo usaría para un SpriteManagero EnemyManager.

Volviendo a lo que Andrew dijo sobre el orden de dibujo y actualización, creo Sprite.DrawOrderque sería muy confuso para muchos sprites. Además, es relativamente fácil configurar un DrawOrdery UpdateOrderpara una pequeña (o razonable) cantidad de Gerentes que lo son (Drawable)GameComponents.

Creo que Microsoft los habría eliminado si realmente fueran tan rígidos y nadie los usara. Pero no lo han hecho porque funcionan perfectamente bien, si el papel es el correcto. Yo mismo los uso para desarrollar Game.Servicesque se actualizan en segundo plano.


2

Estaba pensando en esto también, y se me ocurrió: por ejemplo, para robar el ejemplo en su enlace, en un juego RTS podría hacer de cada unidad una instancia de componente del juego. Bueno.

Pero también puede crear un componente UnitManager, que mantiene una lista de unidades, las dibuja y las actualiza según sea necesario. Supongo que la razón por la que quieres convertir tus unidades en componentes del juego es para compartimentar el código, entonces, ¿esta solución lograría adecuadamente ese objetivo?


2

En los comentarios, publiqué una versión resumida de mi respuesta anterior:

El uso lo DrawableGameComponentbloquea en un solo conjunto de firmas de métodos y un modelo específico de datos retenidos. Estas son típicamente la elección incorrecta inicialmente, y el bloqueo dificulta significativamente la evolución del código en el futuro.

Aquí intentaré ilustrar por qué este es el caso publicando un código de mi proyecto actual.


El objeto raíz que mantiene el estado del mundo del juego tiene un Updatemétodo que se ve así:

void Update(UpdateContext updateContext, MultiInputState currentInput)

Hay varios puntos de entrada para Update , dependiendo de si el juego se ejecuta en la red o no. Es posible, por ejemplo, que el estado del juego se revierta a un punto anterior en el tiempo y luego se vuelva a simular.

También hay algunos métodos adicionales de actualización, como:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

Dentro del estado del juego hay una lista de actores para actualizar. Pero esto es más que una simple Updatellamada. Hay pases adicionales antes y después para manejar la física. También hay algunas cosas elegantes para el desove / destrucción diferida de actores, así como transiciones de nivel.

Fuera del estado del juego, hay un sistema de interfaz de usuario que debe administrarse de forma independiente. Pero algunas pantallas en la interfaz de usuario también deben poder ejecutarse en un contexto de red.


Para dibujar, debido a la naturaleza del juego, existe un sistema de clasificación y selección personalizado que toma entradas de estos métodos:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

Y luego, una vez que descubre qué dibujar, llama automáticamente:

virtual void Draw(DrawContext drawContext, int tag)

los tag parámetro es obligatorio porque algunos actores pueden aferrarse a otros actores y, por lo tanto, deben poder dibujar tanto por encima como por debajo del actor retenido. El sistema de clasificación asegura que esto suceda en el orden correcto.

También existe el sistema de menús para dibujar. Y un sistema jerárquico para dibujar la interfaz de usuario, alrededor del HUD, alrededor del juego.


Ahora compare esos requisitos con lo que DrawableGameComponentproporciona:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

No hay una forma razonable de pasar de un sistema DrawableGameComponental sistema que describí anteriormente. (Y esos no son requisitos inusuales para un juego de complejidad incluso modesta).

Además, es muy importante tener en cuenta que esos requisitos no surgieron simplemente al comienzo del proyecto. Evolucionaron durante muchos meses de trabajo de desarrollo.


Lo que la gente suele hacer, si comienzan por la DrawableGameComponentruta, es comenzar a construir sobre hacks:

  • En lugar de agregar métodos, hacen algunos cambios feos a su propio tipo base (¿por qué no usarlo directamente?).

  • En lugar de reemplazar el código para ordenar e invocar Draw/ Update, hacen algunas cosas muy lentas para cambiar los números de pedido sobre la marcha.

  • Y lo peor de todo: en lugar de agregar parámetros a los métodos, comienzan a "pasar" "parámetros" en ubicaciones de memoria que no son la pila (el uso de Servicesesto es popular). Esto es complicado, propenso a errores, lento, frágil, raramente seguro para subprocesos y es probable que se convierta en un problema si agrega redes, crea herramientas externas, etc.

    También hace que la reutilización del código sea más difícil , porque ahora sus dependencias están ocultas dentro del "componente", en lugar de publicarse en el límite de la API.

  • O, casi ven lo loco que es todo esto, y comienzan a hacer "gerente" DrawableGameComponentpara solucionar estos problemas. Excepto que todavía tienen estos problemas en los límites del "gerente".

    Invariablemente, estos "gerentes" podrían reemplazarse fácilmente con una serie de llamadas a métodos en el orden deseado. Cualquier otra cosa es demasiado complicada.

Por supuesto, una vez que comience a emplear esos hacks, se necesita un trabajo real para deshacer esos hacks una vez que se da cuenta de que necesita más de lo que DrawableGameComponentproporciona. Te vuelves " encerrado ". Y a menudo la tentación es seguir acumulando hacks, lo que en la práctica lo aturdirá y limitará los tipos de juegos que puede hacer a los relativamente simples.


Muchas personas parecen llegar a este punto y tienen una reacción visceral, posiblemente porque usan DrawableGameComponenty no quieren aceptar estar "equivocados". Entonces se aferran al hecho de que al menos es posible hacer que "funcione".

¡Por supuesto que se puede hacer que funcione! Somos programadores, hacer que las cosas funcionen bajo restricciones inusuales es prácticamente nuestro trabajo. Pero esta restricción no es necesaria, útil o racional ...


Lo que es particularmente flabbergasting sobre es que es sorprendentemente trivial lado a paso todo este asunto. Aquí, incluso te daré el código:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(Es simple agregar en la clasificación según DrawOrdery UpdateOrder. Una forma simple es mantener dos listas y usarlas List<T>.Sort(). También es trivial manejar Load/ UnloadContent.)

No es importante lo que el código realmente es . Lo importante es que puedo modificarlo trivialmente .

Cuando me doy cuenta de que necesito un sistema de sincronización diferente para gameTime , o necesito más parámetros (o un UpdateContextobjeto completo con alrededor de 15 cosas), o necesito un orden de operaciones completamente diferente para dibujar, o necesito algunos métodos adicionales para diferentes pases de actualización, puedo seguir adelante y hacer eso .

Y puedo hacerlo de inmediato. No necesito preocuparme por elegirDrawableGameComponent mi código. O peor: piratearlo de alguna manera que lo hará aún más difícil la próxima vez que surja esa necesidad.


En realidad, solo hay un escenario en el que es una buena opción usar DrawableGameComponent: y eso es si literalmente está haciendo y publicando el estilo de componente de arrastrar y soltar middleware de para XNA. Cuál fue su propósito original. Lo cual, agregaré, ¡ nadie lo está haciendo!

(Del mismo modo, solo tiene sentido usarloServices al hacer tipos de contenido personalizados para ContentManager).


Si bien estoy de acuerdo con sus puntos, tampoco veo nada particularmente malo en el uso de elementos que se heredan de DrawableGameComponentciertos componentes, especialmente para elementos como pantallas de menú (menús, muchos botones, opciones y demás), o las superposiciones de FPS / depuración Mencionaste. En estos casos, generalmente no necesita un control detallado sobre el orden de dibujo y termina con menos código que necesita escribir manualmente (lo cual es bueno, a menos que necesite escribir ese código). Pero supongo que es más o menos el escenario de arrastrar y soltar que estabas mencionando.
Groo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.