Nuevo sistema auxiliar: Mixer

Con la necesidad de añadir sonido a nuestro motor gráfico, hemos creado un nuevo sistema auxiliar llamado CSystem_Mixer para encapsular la librería de OpenAL. El funcionamiento del sistema de sonido es bastante sencillo de cara al usuario: si un objeto necesita reproducir un sonido o una pieza musical, entonces deberá usar el componente CComponent_Audio_Source (que explicaré en próximos posts), y este hará uso de manera cómoda del sistema mencionado anteriormente, como en el siguiente esquema:

SAMSUNG

Para ello, se definirá el sistema con las siguientes características en el sistema:


class CSystem_Mixer: public CSystem
{
  friend class CComponent_Audio_Source;

  private:
    // Buffers reservados "one shots"...
    vector< ALuint > oneshot_used;
    vector< ALuint > oneshot_unused;

    // Buffers normales
    vector< ALuint > sources_used;
    vector< ALuint > sources_unused;

    uint NUMBER_SOURCES;
    uint NUMBER_SOURCES_ONESHOT;

    ALuint GetFreeSource();
    void AddFreeSource(ALuint source);

    ALuint GetFreeOneShot();

  public:
    CGameObject* listener;

  public:
    CSystem_Mixer(): CSystem() {};

    bool Init();
    void Close();

    void OnLoop();
};

Primero, se definen 4 vectores:

  • Vectores para sources o emisores de OpenAL que serán usados de manera continua, y se generarán un total de NUMBER_SOURCES (225 por defecto), cargadas de la variable __SOUND_NUMBER_SOURCES del sistema CSystem_Data_Storage. Se dfinirán dos vectores: sources ocupados o usados y sources sin usar. Si el mixer se queda sin sources, devuelve un error por consola.
  • Vectores para sources a los que hemos llamado OneShots, que consisten en sonidos que se se reproducirán una vez y se liberará una vez haya acabado, teniendo un total de  NUMBER_SOURCES_ONESHOT (30 por defecto), cargadas de la variable __SOUND_NUMBER_SOURCES_ONESHOT. Esto es ideal para sonidos cortos que se reproducen continuamente (explosiones, disparos…). Al igual que el anterior, se definen dos vectores: oneshots que están siendo usados y sin usar. Si se intenta emitir un sonido de este tipo y no quedan oneshots disponibles, se devuelve un error por consola. Si se prevee que no hay suficientes oneshots para una determinada instancia, se puede aumentar el número de oneshots cambiando la variable mencionada anteriormente, y los cambios se verán reflejados en la próxima vez que se llame a Init() del Mixer.
También, se definirá un único oyente por instancia para que funcione de manera correcta, que se encargará de recibir los sonidos cercanos y reproducirlos en función de su posición, orientación (no implementado) y velocidad (no implementado). Si no se especifica ninguno, se tomará como oyente a la cámara principal del sistema. Si no se ha definido ninguna cámara, entonces el sistema Mixer no funcionará. 
El objetivo del sistema es asignar sources normales a los componentes AudioSource que soliciten por medio de su método Bind() ocupar un source. Si se llama a Unbind() en dicho componente, se liberará el source que ocupa. De igual manera, hará algo similar con los oneshots, sólo que los componentes solicitarán reproducir un sonido de este tipo con PlayOneShot() (o con PlayAt(pos) (no implementado)), que el sistema Mixer comprobará en cada iteración si debe liberar el source o no, moviéndolo de un vector a otro.
Finalmente, el Mixer debe actualizar en cada iteración la posición del oyente de OpenAL con respecto a la de nuestro oyente o listener, además de actualizar la posición de los sources (no los oneshots) con las propiedades de transformación del objeto que contiene el componente AudioSource.

Vacaciones + Exámenes

Hemos tenido un largo periodo para descansar (dulces fiestas de navidad) y otro periodo aún más largo para estudiar para los exámenes, y por consiguiente, presentarnos a ellos.

Por supuesto, hemos tenido algo de tiempo libre para programar cosillas relacionadas con este proyecto. Pero, con pocas ganas de trabajar mucho, y pudiendo gastar el tiempo en cosas que nos “apetecen más”, habíamos decidido darle una pausa a este proyecto. Por supuesto, ahora que empieza el nuevo cuatrimestre, usaremos el tiempo que tengamos para trabajar en él, viendo el progreso que esperamos que tenga.

lazyprogrammer_gyxvv

Salud y buenas tardes.

CComponent_GUI_Texture

Otro componente interesante relacionado con el render es mostrar una interfaz gráfica de usuario por medio de una “capa” mostrada en nuestra pantalla. Se podrán mostrar texturas (implementado en este componente) y texto (no implementado aún) de tal forma que se ajusten de manera cómoda a la pantalla.

gui

El funcionamiento de este componente tiene 2 partes: una nueva funcionalidad en el Render y otra parte en el componente en sí:

  • En el Render, se ha definido una nueva cámara, llamada __GUI_Camera, encargada de renderizar el contenido de los componentes GUI. Dicha cámara se caracteriza por tener una proyección del tipo ortográfica en el origen de nuestro mundo. Por tanto, se renderizarán de la siguiente forma:
1. Desactivar el buffer de profundidad (GL_DEPTH_BUFFER).
2. Ajustar el viewport de la pantalla.
3. Cargar matriz ortográfica en la matriz GL_PROJECTION.
4. Para cada componente GUI hallado en el primer recorrido de objetos (main camera):
   5. Renderizar el componente GUI.

  • En el componente GUI se guarda un color, una textura o material, un ancho y un alto (valores entre 0 y 1) y un offset dado por píxeles. Y ahora decimos… ¿Cómo se guarda la posición de la textura en la pantalla? Para ello, usamos el componente Transform, que nos serviará para guarda la posición en pantalla (X e Y con valores entre 0 y 1, siendo el 0 la izquierda, el 1 la derecha para la X, y el 0 abajo y el 1 arriba para la Y, y un valor de Z que nos dirá qué elementos GUI van delante y cuales van detrás (uno con un valor Z menor que otro se dibujará después que otro), la escala (como se expande la textura o texto) y la rotación (como se rota el elemento con respecto a su centro). Para el renderizado, se hace lo siguiente:
1. Trasladarse a la posición dada por el componente Transform del objeto (position).
2. Rotar por un ángulo dado por el componente Transform del objeto (angle).
3. Escalar por un factor dado por el componente Transform del objeto (scale).
4. Seleccionar textura y color.
5. Dibujar aplicando offsets.

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.

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).