CComponent_Particle_Emitter

El componente de emisión de partículas se encarga de crear efectos visuales tales como pantallas de humo, fuegos artificiales, disparos, etc.

Para ello, hemos optado por un diseño bastante sencillo compuesto de 2 partes:

  • Una cantidad determinada de partículas: cada una se tratará como una entidad (OJO, entidad, no objeto) que tendrá su posición, su velocidad, su aceleración, su velocidad angular, etc.
  • Un contenedor que gestionará dichas partículas en cada iteración del bucle principal, calculando sus nuevas propiedades (posición, velocidad, color, vida…) y mostrándolas por pantalla en función de dichas propiedades.

particle_emitter

class CComponent_Particle_Emitter: public CComponent
{
  private:
    class CParticle
    {
      private:
        bool active;
        GLfloat life;

        colorf_t color;
        vector3f_t position, velocity, acceleration;
        GLfloat angle, angle_velocity, angle_acceleration;
        vector3f_t scale;
        GLfloat scale_factor;

        string material_name;
    };

    vector<CParticle*> particles;
    void NewParticle(CParticle* p, vector3f go_pos);

  public:
    vector3f last_pos;

    uint max_particles;
    string material_name;

    bool freeze; // Congelar partículas.
    bool stop;   // Dejar de emitir partículas.

    // Cono
    vector3f_t direction;
    GLfloat angle_spread;

    vector3f gravity;
    colorf_t color_adder;

    GLfloat max_vel, min_vel;
    GLfloat max_angle_vel, min_angle_vel;
    GLfloat max_scale, min_scale;

    // Valores iniciales
    GLfloat start_max_life_time, start_min_life_time;
    GLfloat start_max_distance, start_min_distance;
    GLfloat start_max_angle, start_min_angle;
    GLfloat start_max_angle_vel, start_min_angle_vel;
    GLfloat start_max_vel, start_min_vel;
    GLfloat start_max_scale, start_min_scale;
    GLfloat start_max_scale_factor, start_min_scale_factor;
    colorf_t start_max_color, start_min_color;

  public:
    void Start();

    void Stop();
    void Resume();

    void Freeze();
    void UnFreeze();

    void OnRender();
    void OnLoop();
};

Una partícula, en este caso, es una textura en el mundo 3D que siempre mira a la cámara (billboard). Por tanto, una partícula se define con las siguientes propiedades:

  1. Una posición en el espacio.
  2. Una velocidad.
  3. Una aceleración.
  4. Un ángulo de rotación.
  5. Una velocidad angular.
  6. Una aceleración angular (no implementado).
  7. Una escala.
  8. Un factor de escala.
  9. Un color.
  10. Un material o textura.
  11. Un tiempo de vida.

Estos valores vendrán definidos por el componente que gestionará dichas partículas, con propiedades como:

  1. Valores máximos y mínimos actuales (restricciones).
  2. Valores máximos y mínimos iniciales (aleatorio).
  3. Numero máximo de partículas.
  4. Etc.

Finalmente, el funcionamiento del emisor de partículas es el siguiente:

Para iniciar:

Desde i = 0 hasta max_particles
    Crear partícula i con propiedades aleatorias.

Para mostrar por pantalla

Para cada p en particles:Si (p.life > 0) saltamos al siguiente p
    Aplicar textura y color a opengl
    Trasladarse a p.position
    Hacer que la partícula se genere como un sprite o billboard
    Rotar p.angle
    Escalar p.scale
    Dibujar p

Para iterar:

Si está congelado (freeze), salir.
Para cada p en particles:Incrementar p.position a partir de p.velocity
    Incrementar p.velocity a partir de p.acceleration
    // Idem para los ángulos y similar para la escala
    Decrementar el tiempo de vida de p con el tiempo delta (diferencia de tiempo entre última iteración e iteración actual).
    Si la vida es < 0 y no está en modo parado (stop, sin generar nuevas partículas)
    Generar nueva partícula p.
Advertisements

CComponent_Transform

El componente de transformación representa la posición, la orientación y la escala de nuestro objeto en el mundo.

SAMSUNG

En un principio, se representaban estos 3 vectores con un vector de 3 componentes flotantes, tales como:

vec3 position;
vec3 angle;
vec3 scale;

Esto nos permite operar de forma sencilla sobre esos valores, con operaciones del tipo traslación, rotación y escalado (locales al eje actual del objeto, y globales (con respecto al eje X, Y, Z).

No obstante, la representación de estos valores (en concreto, la orientación con “angle”) daba una serie de problemas. Uno de ellos era un manejo lento, complicado, pesado y poco eficiente. De cara al usuario, trabajar con grados sobre los ejes X, Y, Z (ángulos de Euler) era muy cómodo, pero de cara al sistema, lento. Además, hay un fenómeno muy molesto llamado “Gimbal lock“, que, dada una combinación de rotaciones en los ejes X, Y, Z, se anula una componente de las rotaciones (es decir, rotando en 2 ejes distintos, el objeto rota sobre el mismo eje en nuestro mundo). Como esto es chapuza problemática, he decidido buscar una mejor solución.

En todos los sitios relacionados al desarrollo de videojuegos hablaban de los cuaterniones, y lo peor es que hablaban mal de ellos, como una pesadilla, lo cual me empujaba a buscar otras soluciones. Después de numerosos intentos fallidos, he decidido usar los cuaterniones para la representación interna de la orientación de un objeto. Así, evitamos el efecto “Gimbal lock” y ganamos en eficiencia para hacer rotaciones (simplemente toca multiplicar la matriz GL_MODELVIEW actual por la matriz generada por el cuaternión actual).

No obstante, como esta representación es un cáncer de cáncer bastante confusa, tenemos una simple solución:

De cara al sistema, se trabajará con cuaterniones con pura eficiencia.
De cara al usuario (el que programará sus juegos), se le dará la posibilidad de trabajar con ángulos de Euler (intuitivos y lentos) o cuaterniones (rápidos). No obstante, en ambos casos se trabajará de manera indirecta con el segundo sistema (en el primer caso, se pasará de ángulos de Euler a cuaterniones).

Como estos cálculos son otro cáncer de cáncer, hemos decidido ampliar nuestro repositorio de librerías y utilizar “GLM” (Graphics Library Maths), una librería de sólo cabeceras para hacer operaciones matemáticas con gráficos 3D (lo usaremos para los cuaterniones, básicamente). Seguramente, usemos varias funciones de esta librería para “inflar” nuestro sistema “CSystem_Math“.

Por ende, ahora se representará el componente como:

vec3 position;
vec3 scale;
quat angle;

Enlaces de interés:

Sistemas auxiliares

Como hemos visto en el post “sistemas“, existen dos tipos de sistema en nuestro proyecto. El segundo, que describiremos en este post, son los sistemas auxiliares.

Un sistema auxiliar se caracteriza por ofrecer servicios o contenidos a los objetos y a otros sistemas para que puedan operar de una manera más cómoda o amplia. De momento, poseemos la siguiente lista de sistemas auxiliares:

  • Time
class CSystem_Time: public CSystem
{
  private:
    Uint32 deltaT;
    Uint32 lastT;

    GLfloat Tscale;

  public:
    // ...

    void SetTimeScale(GLfloat tscale);
    GLfloat timeScale();

    Uint32 deltaTime();
    GLfloat deltaTime_s();

    Uint32 GetTicks();

    GLfloat GetTicks_s();

    // ...
};
Time tendrá 3 objetivos principales: proporcionar un tiempo delta (deltaT) que define el lapso de tiempo transcurrido entre el fotograma (frame) o iteración anterior y el último fotograma (actual). Esto es útil para realizar operaciones que dependen del tiempo (por ejemplo, mover un objeto 8 unidades por segundo), para que funcione igual en todas las máquinas (ya que algunas ejecutarán más fotogramas o iteraciones por segundo que otras). Otro objetivo es proporcionar una escala de tiempo (Tscale), que servirá para escalar el tiempo. Esto simplemente modifica el valor devuelto por las funciones deltaTime y deltaTime_s, multiplicando dicho valor por la escala de tiempo (valor entre 0.0 (tiempo parado) e infinito (tiempo acelerado)(usar una escala de tiempo negativa puede dar resultados inesperados, aunque teóricamente es posible). El último objetivo es proporcionar el tiempo transcurrido desde la ejecución del programa (útil para realizar temporizadores).

  • Math

Este sistema proporcionará una serie de funciones matemáticas (seno, coseno, calcular producto cartesiano entre vectores, raíz cuadrada…) para operar de forma cómoda. De momento, el sistema está vacío (ni siquiera existe), pero en cuanto necesitemos añadir funciones, las colocaremos sin problema.

  • Data_Storage
#include "_globals.h"
#include "_system.h"

class CSystem_Data_Storage: public CSystem
{
  protected:
    map<string, string> strings;
    map<string, int> ints; //GLint
    map<string, float> floats; //GLfloat

  public:
    // ...

    // Cargar y guardar encriptados?
    void SetString(string name_id, string value);
    void SetInt(string name_id, int value);
    void SetFloat(string name_id, float value);

    string GetString(string name_id);
    int GetInt(string name_id);
    float GetFloat(string name_id);

    bool RemoveString(string name_id);
    bool RemoveInt(string name_id);
    bool RemoveFloat(string name_id);

    void RemoveAll();
    void RemoveUserVars();
};

Simplemente se trata de un contenedor capaz de almacenar enteros, flotantes y cadenas de caracteres mediante un identificador (un std::map para cada tipo). Así pues, puede haber un entero y un flotante con un mismo nombre, pero no dos enteros con un mismo nombre. Las variables se podrán guardar en ficheros, al igual que cargarlas (por defecto, en users.cfg). Además, se distinguen dos tipos de variables: variables de sistema, que empiezan por “__“, y variables de usuario, que equivalen al resto. Por ejemplo, “__Ejemplo” sería una variable de sistema, y “UN__EJEMPLO” sería una variable de usuario. Hay que advertir que se distingue entre mayúsculas y minúsculas. Estas variables se pueden usar para cualquier propósito: almacenar datos de sistema, de usuario, de objetos, etc…

  • Debug
class CSystem_Debug: public CSystem
{
  protected:
    // Debug
    bool opened;
    FILE* file;

    // ...

  public:

    // log.txt File
    void log(const char* fmt, ...);
    void error(const char* fmt, ...);
    void raw_log(const char* fmt, ...);

    // Message Boxes
    // Flags: debug::error for error, debug::warning for warnings, debug::information for info
    void msg_box(const char* title, const char* message, Uint32 flags = debug::error);
    void msg_box(Uint32 flags, const char* title, const char* fmt, ...);

    // Console
    void console_msg(const char* fmt, ...);
    void console_error_msg(const char* fmt, ...);
    void console_warning_msg(const char* fmt, ...);
    void console_custom_msg(GLfloat r, GLfloat g, GLfloat b, GLfloat a, const char* fmt, ...);

    void OnEvent();
    void OnRender();

  protected:
      // Console commands
    // General
    void Console_command__UNKNOWN_COMMAND(string arguments);
    void Console_command__HELP(string arguments);

    // ...

Este es el más amplio de los antes mencionados. Se encarga de permitirnos depurar nuestro programa. Principalmente, tiene 3 tipos funciones de depuración: la primera, escribe en un fichero log.txt (log escribe la hora de llamada a la función seguido de un mensaje, error escribe la hora y un error, y raw_log sólo escribe un mensaje); la segunda consiste en mostrar una ventana con un mensaje en su interior (tipo ventanas de error de error de windows); la tercera y última consiste en la gestión de una consola. Se podrá escribir mensajes en el buffer de la consola desde cualquier objeto. Además, el usuario podrá interactuar con la consola, escribiendo mensajes en ella. Un ejemplo de cómo se vería la consola es el siguiente:

console

  • Resources
/**
 * Formato fichero de resources (.rc):
 * ----------------------------------
 * # Comentario
 *
 * # Linea superior en blanco
 * # Formato de resource:
 * # tipo: nombre_rc ruta_al_fichero
 * mesh: nombre ruta/fichero
 * # Fin de fichero
 */

class CSystem_Resources: public CSystem
{
  protected:
    map<string, CResource*> resource_list;
    bool InitEngineResources();            // Recursos de sistema (ERROR Texture, ERROR model, ERROR sound, etc...)

  public:

    bool LoadResourceFile(string rc_file);
      bool LoadResource(string name, string rc_file, resources::types_t type, flags_t flags = 0x00);
    void AddEmpty(string name);

    void ClearResources();
      void ClearResource(string name);

    CResource_Mesh* GetMesh(string id);
    CResource_Texture* GetTexture(string id);
    CResource_Sound* GetSound(string id);
    // ...
};

Este sistema se encargará de gestionar los recursos de nuestro juego, entendiéndose por recursos texturas, sonidos, modelos… Y dependiendo del tipo de recurso que sea, se almacenará y se tratará de una forma u otra. Estos recursos podrán ser asociados a objetos de manera cómoda y sencilla.

Por cada instancia del juego, se cargará un fichero de recursos (por ejemplo, fase1.rc) que contendrá todos los archivos y recursos necesarios por dicha instancia (el formato del fichero se explica en el comentario del código superior).

(Sinceramente, llevo escribiendo esto desde hace un buen rato y creo que está más que explicado. No me apetece nada seguir documentando estas cosas, la verdad).