Pregunta

Como un pequeño ejercicio Estoy intentando escribir un motor de juego muy pequeño y sencillo que se encarga sólo entidades (en movimiento, AI básica etc.)

Como tal, estoy tratando de pensar cómo un juego de manijas las versiones de todas las entidades, y estoy recibiendo un poco confundido (Probablemente porque voy sobre él en el camino equivocado)

Así que decidí publicar esta pregunta aquí para mostrar mi manera actual de pensar en ello, y para ver si alguien puede sugerir a mí una mejor forma de hacerlo.

Actualmente, tengo una clase CEngine que toman punteros a otras clases que necesita (por ejemplo una clase CWindow, CEntityManager clase, etc.)

Tengo un bucle de juego que a su pseudo código sería algo así (Dentro de la clase CEngine)

while(isRunning) {
    Window->clear_screen();

    EntityManager->draw();

    Window->flip_screen();

    // Cap FPS
}

Mi clase CEntityManager veía así:

enum {
    PLAYER,
    ENEMY,
    ALLY
};

class CEntityManager {
    public:
        void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc.
        void delete_entity(int entityID);

    private:
        std::vector<CEntity*> entityVector;
        std::vector<CEntity*> entityVectorIter;
};

Y mi clase CEntity veía así:

class CEntity() {
    public:
        virtual void draw() = 0;
        void set_id(int nextEntityID);
        int get_id();
        int get_type();

    private:
        static nextEntityID;
        int entityID;
        int entityType;
};

Después de eso, me gustaría crear clases, por ejemplo, por un enemigo, y darle una hoja de sprites, sus propias funciones, etc.

Por ejemplo:

class CEnemy : public CEntity {
    public:
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void draw(); // Implement draw();
        void handle_input();
};

Todo esto funcionaba bien para simplemente dibujar sprites en la pantalla.

Pero entonces llegó al problema de la utilización de las funciones que existen en una sola entidad, pero no en otro.

En el ejemplo de código de pseudo anteriormente, do_ai_stuff (); y handle_input ();

Como se puede ver en mi ciclo del juego, se hace un llamado a EntityManager-> draw (); Esto sólo itera a través de la entityVector y llamó al empate (); la función para cada entidad - que funcionaba bien ya que todas las entidades tienen un empate (); función.

Pero luego pensé, ¿y si se trata de una entidad de jugador que necesita de entrada mango? ¿Cómo funciona eso?

no he probado pero supongo que no puedo acaba de recorrer como lo hice con la función draw (), porque entidades como enemigos no tendrán una función handle_input ().

Yo podría utilizar una sentencia if para comprobar el entityType, así:

for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) {
    if((*entityVectorIter)->get_type() == PLAYER) {
        (*entityVectorIter)->handle_input();
    }
}

Pero no sé cómo la gente normalmente van a escribir estas cosas así que no estoy seguro de la mejor manera de hacerlo.

Me escribió mucho aquí y no me preguntó nada concreto, así que voy a aclarar lo que estoy buscando aquí:

  • ¿Es cierto que he presentado / mi código diseñado bien, y es práctico?
  • ¿Hay una mejor manera más eficiente para mí actualizar mi entidades y funciones de llamada que otras entidades no pueden tener?
  • ¿Está utilizando una enumeración para realizar un seguimiento de una entidades escribir una buena manera de identificar las entidades?
¿Fue útil?

Solución

Te estás poniendo muy cerca de la forma en la mayoría de los juegos en realidad lo hacen (aunque el rendimiento experto curmudgeon Mike Acton menudo quejas acerca de que ).

Por lo general se vería algo como esto

class CEntity {
  public:
     virtual void draw() {};  // default implementations do nothing
     virtual void update() {} ;
     virtual void handleinput( const inputdata &input ) {};
}

class CEnemy : public CEntity {
  public:
     virtual void draw(); // implemented...
     virtual void update() { do_ai_stuff(); }
      // use the default null impl of handleinput because enemies don't care...
}

class CPlayer : public CEntity {
  public:
     virtual void draw(); 
     virtual void update();
     virtual void handleinput( const inputdata &input) {}; // handle input here
}

y luego el gestor de la entidad pasa a través de las llamadas y actualización (), handleinput (), y dibujar () en cada entidad en el mundo.

Por supuesto, tener un montón de estas funciones, la mayoría de los cuales no hacen nada cuando se les llama, puede ser bastante derrochador, especialmente para funciones virtuales. Por lo que he visto algunos otros enfoques también.

Uno es almacenar por ejemplo los datos de entrada en una global (o como un miembro de una interfaz global, o un singleton, etc). A continuación, reemplace la función de actualización () de los enemigos para que do_ai_stuff (). y la actualización () de los jugadores para que no la manipulación por el sondeo del mundial.

Entrada

Otra es utilizar alguna variación en la Listener patrón , por lo que todo lo que se preocupa por la entrada hereda de una clase oyente común, y se registra todos los oyentes con un InputManager. A continuación, el InputManager llama a cada oyente a su vez cada cuadro:

class CInputManager
{
  AddListener( IInputListener *pListener );
  RemoveListener( IInputListener *pListener );

  vector<IInputListener *>m_listeners;
  void PerFrame( inputdata *input ) 
  { 
     for ( i = 0 ; i < m_listeners.count() ; ++i )
     {
         m_listeners[i]->handleinput(input);
     }
  }
};
CInputManager g_InputManager; // or a singleton, etc

class IInputListener
{
   virtual void handleinput( inputdata *input ) = 0;
   IInputListener() { g_InputManager.AddListener(this); }
   ~IInputListener() { g_InputManager.RemoveListener(this); }
}

class CPlayer : public IInputListener
{
   virtual void handleinput( inputdata *input ); // implement this..
}

Y hay otras formas más complicadas de hacer las cosas. Pero todos los que trabajo y he visto cada uno de ellos en algo que realmente transportado y vendido.

Otros consejos

Usted debe mirar a los componentes, en lugar de herencia para esto. Por ejemplo, en mi motor, he (simplificado):

class GameObject
{
private:
    std::map<int, GameComponent*> m_Components;
}; // eo class GameObject

Tengo varios componentes que hacen cosas diferentes:

class GameComponent
{
}; // eo class GameComponent

class LightComponent : public GameComponent // represents a light
class CameraComponent : public GameComponent // represents a camera
class SceneNodeComponent : public GameComponent // represents a scene node
class MeshComponent : public GameComponent // represents a mesh and material
class SoundComponent : public GameComponent // can emit sound
class PhysicsComponent : public GameComponent // applies physics
class ScriptComponent : public GameComponent // allows scripting

Estos componentes se pueden añadir a un objeto de juego para inducir comportamiento. Se pueden comunicar a través de un sistema de mensajería, y las cosas que requieren una actualización durante el bucle principal de registrar un detector de trama. Pueden actuar de forma independiente y ser añadido en forma segura / eliminado en tiempo de ejecución. Me parece un sistema muy extensible.

EDIT: Disculpas, voy a la carne esta un poco, pero estoy en medio de algo ahora:)

Se podría realizar esta funcionalidad mediante el uso de la función virtual, así:

class CEntity() {
    public:
        virtual void do_stuff() = 0;
        virtual void draw() = 0;
        // ...
};

class CEnemy : public CEntity {
    public:
        void do_stuff() { do_ai_stuff(); }
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void do_stuff() { handle_input(); }
        void draw(); // Implement draw();
        void handle_input();
};

1 Una pequeña cosa - ¿por qué cambiar el ID de una entidad? Normalmente, esto es constante e inicie durante la construcción, y eso es todo:

class CEntity
{ 
     const int m_id;
   public:
     CEntity(int id) : m_id(id) {}
}

Para las otras cosas, hay diferentes enfoques, la elección depende de la cantidad específica del tipo de funciones están allí (y qué tan bien puede repdict ellos).


Añadir a todo

El método más simple es simplemente añadir todos los métodos de la interfaz de base, y ponerlas en práctica como no-op en las clases que no lo soportan. Ese sonido pueden gustar mal consejo, pero es una desnormalización acceptabel, si hay muy pocos métodos que no se aplican, y se puede asumir el conjunto de métodos no crecerá significativamente con las necesidades futuras.

Incluso MAYN implementar un tipo básico de "mecanismo de descubrimiento", por ejemplo.

 class CEntity
 {
   public:
     ...
     virtual bool CanMove() = 0;
     virtual void Move(CPoint target) = 0;
 }

No se exceda! Es fácil empezar de esta manera, y luego se adhieren a ella, incluso cuando se crea un gran lío de su código. Se puede sugarcoated como "desnormalización intencional de la jerarquía de tipos" - pero al final es jsut un hack que permite resolver algunos problemas con rapidez, pero rápidamente duele cuando la aplicación crece

.

Tipo verdadero descubrimiento

y utilizando dynamic_cast, puede convertir de forma segura el objeto de CEntity a CFastCat. Si la entidad es en realidad un CReallyUnmovableBoulder, el resultado será un puntero nulo. De esa manera usted puede investigar un objeto por su tipo real, y reaccionar a ella en consecuencia.

CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

Este mecanismo funciona bien si sólo hay poca lógica ligada a los métodos específicos de tipo. Es no una buena solución si al final con cadenas donde sonda para muchos tipos, y actuar en consecuencia:

// -----BAD BAD BAD BAD Code -----
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ;
if (bigDog != 0)
   bigDog->Bark();

CPebble * pebble = dynamic_cast<CPebble *>(entity) ;
if (pebble != 0)
   pebble->UhmWhatNoiseDoesAPebbleMake();

Que por lo general significa que los métodos virtuales no son elegidos cuidadosamente.


Interfaces

Por encima se puede extender a las interfaces, cuando la funcionalidad de tipo específico no es métodos individuales, pero los grupos de métodos. Aren # t soportado muy bien en C ++, pero es soportable. P.ej. sus objetos tienen diferentes características:

class IMovable
{
   virtual void SetSpeed() = 0;
   virtual void SetTarget(CPoint target) = 0;
   virtual CPoint GetPosition() = 0;
   virtual ~IMovable() {}
}

class IAttacker
{
   virtual int GetStrength() = 0;
   virtual void Attack(IAttackable * target) = 0;
   virtual void SetAnger(int anger) = 0;
   virtual ~IAttacker() {}
}

Sus diferentes objetos heredan de la clase base y una o más interfaces:

class CHero : public CEntity, public IMovable, public IAttacker 

Y de nuevo, puede utilizar dynamic_cast a la sonda para interfaces en cualquier entidad.

Esto es muy extensible, y por lo general la forma más segura para ir cuando no está seguro. Es un poco mroe detallado de lo anterior soluciones, pero puede hacer frente bastante bien con los cambios inesperados en el futuro. funcionalidad factor en la interfaz es no fácil, se necesita algo de experiencia para conseguir una idea de ella.


patrón de Visitantes

El visitante patrón requiere escribir mucho, pero le permite añadir funcionalidad a las clases sin modificar esas clases.

En su contexto, que significa que usted puede construir su estructura de la entidad, pero poner en práctica sus actividades por separado. Esto se utiliza generalmente cuando tiene operaciones muy distintas en sus entidades, no se puede modificar libremente las clases, o la adición de la funcionalidad de las clases violarían fuertemente el single-responsabilidad-principio.

Esto puede hacer frente a prácticamente todas las necesidades de cambio (siempre sus propias entidades están bien factorizada).

(sólo estoy vincular a ella, porque se necesita la mayoría de la gente un rato para envolver su cabeza alrededor de ella, y que no recomendaría usarlo a menos que haya experimentado las limitaciones de otros métodos)

En general, su código es bastante bueno, como otros han señalado.

Para responder a su tercera pregunta: En el código que nos mostró, que no utiliza el tipo de enumeración a excepción de la creación. No parece bien (aunque me pregunto si un método "createPlayer ()" "createEnemy ()" y así sucesivamente llenar no ser más fácil de leer). Pero tan pronto como usted tiene el código que utiliza si o incluso cambiar a hacer cosas diferentes en función del tipo, entonces usted está violando algunos principios OO. A continuación, debe utilizar el poder de los métodos virtuales para asegurar que hacer lo que tienen que hacerlo. Si usted tiene que "encontrar" un objeto de un cierto tipo, que también podría almacenar un puntero a la derecha del jugador objeto especial cuando se crea.

También podría considerar la sustitución de las identificaciones con los punteros primas si sólo tiene un identificador único.

Por favor, considere esto como indicios de que podría ser apropiado en función de lo que realmente necesita.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top