Separación de dibujo y lógica en los juegos.


11

Soy un desarrollador que acaba de empezar a jugar con el desarrollo de juegos. Soy un tipo .Net, así que me he metido con XNA y ahora estoy jugando con Cocos2d para iPhone. Sin embargo, mi pregunta es más general.

Digamos que estoy construyendo un simple juego de Pong. Tendría una Ballclase y una Paddleclase. Viniendo del desarrollo del mundo de los negocios, mi primer instinto es no tener ningún código de dibujo o manejo de entrada en ninguna de estas clases.

//pseudo code
class Ball
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Move(){}
}

Nada en la clase de pelota maneja la entrada ni se ocupa del dibujo. Entonces tendría otra clase, mi Gameclase o mi Scene.m(en Cocos2d) que renovaría la bola, y durante el ciclo del juego, manipularía la bola según sea necesario.

Sin embargo, en muchos tutoriales para XNA y Cocos2d, veo un patrón como este:

//pseudo code
class Ball : SomeUpdatableComponent
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Update(){}
   void Draw(){}
   void HandleInput(){}
}

Mi pregunta es, ¿es esto correcto? ¿Es este el patrón que la gente usa en el desarrollo de juegos? De alguna manera va en contra de todo lo que estoy acostumbrado, que mi clase de Ball haga todo. Además, en este segundo ejemplo, donde mi Ballsabe cómo moverse, ¿cómo manejaría la detección de colisión con el Paddle? ¿La Ballnecesidad de tener conocimiento de la Paddle? En mi primer ejemplo, la Gameclase tendría referencias tanto a la Ballcomo a la Paddle, y luego las enviaría a algún CollisionDetectiongerente o algo así, pero ¿cómo trato con la complejidad de varios componentes, si cada componente individual hace todo por sí mismo? (Espero tener sentido .....)


2
Desafortunadamente, así es como se hace en muchos juegos. Sin embargo, no tomaría eso como una mejor práctica. Muchos de los tutoriales que está leyendo podrían estar dirigidos a principiantes, por lo que no van a comenzar a explicar modelos / vistas / controladores o patrones de yugo al lector.
tenpn

@tenpn, tu comentario parece sugerir que no crees que esto sea una buena práctica, me preguntaba si podrías explicarlo más. Siempre he pensado que era la disposición más adecuada debido a los métodos que contienen código que probablemente sea muy específico para cada tipo; pero tengo poca experiencia, así que me interesaría escuchar tus pensamientos.
sebf

1
@sebf no funciona si te gusta el diseño orientado a datos, y no funciona si te gusta el diseño orientado a objetos. Vea los principios SÓLIDOS del tío Bob: butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
tenpn

@tenpn. Ese artículo y documento vinculado dentro son muy interesantes, ¡gracias!
sebf

Respuestas:


10

Mi pregunta es, ¿es esto correcto? ¿Es este el patrón que la gente usa en el desarrollo de juegos?

Los desarrolladores de juegos tienden a usar lo que funcione. No es riguroso, pero bueno, se envía.

De alguna manera va en contra de todo lo que estoy acostumbrado, que mi clase de Ball haga todo.

Entonces, un idioma más generalizado para lo que tienes allí es handleMessages / update / draw. En los sistemas que hacen un uso intensivo de los mensajes (que tiene ventajas y desventajas, como cabría esperar), una entidad del juego toma los mensajes que le interesan, realiza la lógica en esos mensajes y luego se dibuja a sí mismo.

Tenga en cuenta que esa llamada a draw () podría no significar que la entidad llame a putPixel o lo que sea dentro de sí misma; en algunos casos, podría significar la actualización de datos que luego sondeados por el código responsable del dibujo. En casos más avanzados, se "dibuja" a sí mismo llamando a los métodos expuestos por un renderizador. En la tecnología en la que estoy trabajando en este momento, el objeto se dibujaría a sí mismo usando llamadas de renderizador y luego cada cuadro en el que el hilo de renderizado organiza todas las llamadas realizadas por todos los objetos, se optimiza para cosas como cambios de estado y luego dibuja todo (todo de que esto ocurra en un hilo separado de la lógica del juego, por lo que obtenemos una representación asincrónica).

La delegación de responsabilidad depende del programador; Para un simple juego de pong, tiene sentido que cada objeto maneje su propio dibujo (completo con llamadas de bajo nivel) por completo. Es una cuestión de estilo y sabiduría.

Además, en este segundo ejemplo, donde mi Bola sabe cómo moverse, ¿cómo manejaría la detección de colisión con la Paleta? ¿La pelota necesitaría tener conocimiento de la paleta?

Entonces, una forma natural de resolver el problema aquí es hacer que cada objeto tome todos los demás objetos como algún tipo de parámetro para verificar las colisiones. Esto se escala mal y puede tener resultados extraños.

Hacer que cada clase implemente algún tipo de interfaz Collidable y luego tener un código de alto nivel que simplemente recorra todos los objetos Collidable en la fase de actualización de su juego y maneje las colisiones, estableciendo las posiciones de los objetos según sea necesario.

Una vez más, solo tienes que desarrollar un sentido (o decidir) cómo se debe delegar la responsabilidad de las diferentes cosas en tu juego. La representación es una actividad solitaria y puede ser manejada por un objeto en el vacío; colisión y física generalmente no pueden.

En mi primer ejemplo, la clase de Juego tendría referencias tanto a la Bola como a la Paleta, y luego las enviaría a algún administrador de Detección de Colisión o algo así, pero ¿cómo trato la complejidad de varios componentes, si cada componente individual lo hace? todo ellos mismos?

Vea lo anterior para una exploración de esto. Si está en un lenguaje que admite interfaces fácilmente (es decir, Java, C #), esto es fácil. Si estás en C ++, esto puede ser ... interesante. Estoy a favor de usar la mensajería para resolver estos problemas (las clases manejan los mensajes que pueden, ignoran a los demás), a otras personas les gusta la composición de componentes.

De nuevo, lo que funcione y sea más fácil para usted.


2
Ah, y recuerda siempre: YAGNI (no lo vas a necesitar) es realmente importante aquí. Puede perder mucho tiempo tratando de encontrar el sistema flexible ideal para manejar todos los problemas posibles, y luego nunca hacer nada. Quiero decir, si realmente quisieras un buen backbone multijugador para todos los resultados posibles, usarías CORBA, ¿verdad? :)
ChrisE

5

Recientemente hice un juego simple de Space Invadors usando un 'sistema de entidad'. Es un patrón que separa atributos y comportamientos extremadamente bien. Me tomó algunas iteraciones entenderlo completamente, pero una vez que se diseñaron algunos componentes, se vuelve extremadamente simple componer nuevos objetos usando sus componentes existentes.

Deberías leer esto:

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

Se actualiza con frecuencia por un tipo extremadamente conocedor. También es la única discusión del sistema de entidades con ejemplos de códigos concretos.

Mis iteraciones fueron las siguientes:

La primera iteración tenía un objeto "EntitySystem" que era como Adam describe; sin embargo, mis componentes todavía tenían métodos: mi componente 'renderizable' tenía un método paint (), y mi componente de posición tenía un método move (), etc. Cuando comencé a desarrollar las entidades, me di cuenta de que necesitaba comenzar a pasar el mensaje entre componentes y ordenar la ejecución de actualizaciones de componentes ... demasiado desordenado.

Entonces, volví y releí el blog de T-machines. Hay mucha información en los hilos de comentarios, y en ellos él realmente enfatiza que los componentes no tienen comportamientos, los comportamientos son proporcionados por los sistemas de la entidad. De esta manera, no necesita pasar mensajes entre los componentes y ordenar actualizaciones de componentes porque el orden está determinado por el orden global de ejecución del sistema. Okay. Quizás eso es demasiado abstracto.

De todos modos para la iteración # 2, esto es lo que obtuve del blog:

EntityManager: actúa como la "base de datos" de componentes, que se puede consultar para las entidades que contienen ciertos tipos de componentes. Esto incluso puede ser respaldado por una base de datos en memoria para un acceso rápido ... vea la parte 5 de t-machine para más información.

EntitySystem: cada sistema es esencialmente un método que funciona en un conjunto de entidades. Cada sistema utilizará el componente x, y y z de una entidad para realizar su trabajo. Por lo tanto, consultaría al administrador de entidades con componentes x, y y z y luego pasaría ese resultado al sistema.

Entidad: solo una identificación, como una larga. La entidad es lo que agrupa un conjunto de instancias de componentes en una 'entidad'.

Componente: un conjunto de campos ... ¡sin comportamientos! cuando comienzas a agregar comportamientos, comienza a complicarse ... incluso en un simple juego de Space Invadors.

Editar : por cierto, 'dt' es el tiempo delta desde la última invocación del bucle principal

Entonces mi bucle principal de Invadors es este:

Collection<Entity> entitiesWithGuns = manager.getEntitiesWith(Gun.class);
Collection<Entity> entitiesWithDamagable =
manager.getEntitiesWith(BulletDamagable.class);
Collection<Entity> entitiesWithInvadorDamagable = manager.getEntitiesWith(InvadorDamagable.class);

keyboardShipControllerSystem.update(entitiesWithGuns, dt);
touchInputSystem.update(entitiesWithGuns, dt);
Collection<Entity> entitiesWithInvadorMovement = manager.getEntitiesWith(InvadorMovement.class);
invadorMovementSystem.update(entitiesWithInvadorMovement);

Collection<Entity> entitiesWithVelocity = manager.getEntitiesWith(Velocity.class);
movementSystem.move(entitiesWithVelocity, dt);
gunSystem.update(entitiesWithGuns, System.currentTimeMillis());
Collection<Entity> entitiesWithPositionAndForm = manager.getEntitiesWith(Position.class, Form.class);
collisionSystem.checkCollisions(entitiesWithPositionAndForm);

Parece un poco raro al principio, pero es increíblemente flexible. También es muy fácil de optimizar; para diferentes tipos de componentes, puede tener diferentes almacenes de datos de respaldo para acelerar la recuperación. Para la clase 'form' puede respaldarlo con un quadtree para acelerar el acceso para la detección de colisiones.

Soy como tú; Soy un desarrollador experimentado pero no tenía experiencia escribiendo juegos. Pasé un tiempo investigando patrones de desarrollo, y este me llamó la atención. De ninguna manera es la única forma de hacer las cosas, pero lo he encontrado muy intuitivo y robusto. Creo que el patrón se discutió oficialmente en el libro 6 de la serie "Gemas de programación de juegos": http://www.amazon.com/Game-Programming-Gems/dp/1584500492 . No he leído ninguno de los libros, pero escuché que son la referencia de facto para la programación de juegos.


1

No hay reglas explícitas que deba seguir al programar juegos. Considero que ambos patrones son perfectamente aceptables, siempre y cuando sigas el mismo patrón de diseño en todo el juego. Te sorprendería que hay muchos más patrones de diseño para juegos, varios de los cuales ni siquiera están por encima de OOP.

Ahora, según mi preferencia personal, prefiero separar el código por comportamiento (una clase / hilo para dibujar, otro para actualizar), mientras tengo objetos minimalistas. Me resulta más fácil paralelizar, y a menudo me encuentro trabajando en menos archivos al mismo tiempo. Yo diría que esta es más una forma procesal de hacer las cosas.

Pero si vienes del mundo de los negocios, probablemente te sientas más cómodo escribiendo clases que sepan hacer todo por sí mismos.

Una vez más, te recomiendo que escribas lo que sientes más natural para ti.


Creo que entendiste mal mi pregunta. Estoy diciendo que NO ME GUSTA tener clases que hagan todo por sí mismos. Caí como una bola debe ser sólo una representación lógica de una pelota, sin embargo, debe saber nada sobre el bucle del juego, la manipulación de entrada, etc ...
bfree

Para ser justos, en la programación de negocios si se reduce, hay dos trenes: objetos de negocios que se cargan, persisten y realizan lógica en sí mismos, y DTO + AppLogic + Repository / Persitance Layer ...
Nate

1

El objeto 'Bola' tiene atributos y comportamientos. Creo que tiene sentido incluir los atributos y comportamientos únicos del objeto en su propia clase. La forma en que se actualiza un objeto Ball es diferente de la forma en que se actualizaría una pala, por lo que tiene sentido que estos comportamientos únicos garanticen su inclusión en la clase. Lo mismo con el dibujo. A menudo hay una diferencia única en la forma en que se dibuja una paleta frente a una Pelota y me resulta más fácil modificar el método de dibujo de un objeto individual para satisfacer estas necesidades que resolver evaluaciones condicionales en otra clase para resolverlas.

Pong es un juego relativamente simple en términos de cantidad de objetos y comportamientos, pero cuando ingresas a docenas o cientos de objetos de juego únicos, es posible que tu segundo ejemplo sea un poco más fácil de modularizar todo.

La mayoría de la gente usa una clase de entrada cuyos resultados están disponibles para todos los objetos del juego a través de un Servicio o una clase estática.


0

Creo que lo que probablemente estás acostumbrado en el mundo de los negocios es separar la abstracción y la implementación.

En el mundo de desarrollo de juegos, generalmente quieres pensar en términos de velocidad. Cuantos más objetos tenga, más cambios de contexto ocurrirán, lo que puede ralentizar las cosas dependiendo de la carga actual.

Esta es solo mi especulación ya que soy un desarrollador de juegos aspirante pero un desarrollador profesional.


0

No, no es 'correcto' o 'incorrecto', es solo una simplificación.

Parte del código comercial es exactamente el mismo, a menos que esté trabajando con un sistema que separa por la fuerza la presentación y la lógica.

Aquí, un ejemplo de VB.NET, donde la "lógica empresarial" (agregando un número) se escribe en un controlador de eventos GUI: http://www.homeandlearn.co.uk/net/nets1p12.html

¿Cómo trato la complejidad de varios componentes, si cada componente individual hace todo por sí mismo?

Si y cuando se vuelve tan complejo, puede factorizarlo.

class Ball
{
    GraphicalModel ballVisual;
    void Draw()
    {
        ballVisual.Draw();
    }
};

0

Según mi experiencia en el desarrollo, no hay una forma correcta o incorrecta de hacer las cosas, a menos que haya una práctica aceptada en el grupo con el que trabajas. Me he encontrado con la oposición de los desarrolladores que son reacios a separar los comportamientos, las máscaras, etc. Y estoy seguro de que dirán lo mismo sobre mi reserva para escribir una clase que incluya todo bajo el sol. Recuerde que no solo tiene que escribirlo, sino poder leer su código mañana y en una fecha futura, depurarlo y posiblemente construirlo en la próxima iteración. Debe elegir un camino que funcione para usted (y su equipo). Elegir una ruta que otros usen (y sea contra-intuitiva para usted) lo retrasará.

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.