INTRODUCCIÓN A LA PROGRAMACIÓN WINDOWS 95/NT

Artículo 4: MULTITAREA Y MCI

Autor: (c) Santiago Romero.
Revista: Programación Actual (Prensa Técnica) nº 18, Septiembre-1998


En esta última entrega del curso vamos a tratar la organización del código en sí y el aprovechamiento de la multitarea mediante threads independientes, haciendo el bucle principal de nuestro juego más sencillo gracias al multiproceso y los eventos, así como el uso del MCI.

En las entregas anteriores hemos aprendido a crear nuestra ventana base, gestionar sus mensajes, tomar input del usuario y mostrar algo de información textual y gráfica en la misma. En la entrega de este mes (la última de nuestra serie de introducción a Windows) se trata la multitarea, los timers multimedia, la idea de animación, y un programa de ejemplo para ver la manera de organizar un programa en Windows.

Tras finalizar este curso podreis seguir el curso de programación mediante DirectX, apoyándonos en lo dado en esta introducción, ya que lo único que nos falta para hacer buenos programas gráficos en Windows es el acceso rápido al hardware de video, cosa que se conseguirá mediante las librerias de Microsoft: DirectX.


CONTROL DEL FLUJO DE LA ANIMACION

Como programadores de DOS hemos estado tentados muchas veces de utilizar el clásico método del uso del retrazado vertical para controlar la velocidad de nuestros programas, o de controlar el tiempo que pasa entre cada retrazado para establecer un número de frames por segundo constante (como pueda ser 35 fps).

En la programación en Windows esto es una mala aproximación, pues no tenemos seguridad de que windows nos ceda el control cuando nosotros creamos que es el momento de actualizar un frame en pantalla. En cambio, resulta mucho más recomendable basar la animación en nuestros programas (entendiendo por animación también el simple movimiento de un sprite) en términos de "elapsed time" o tiempo transcurrido. Para esto, en vez de mover un sprite 1 pixel cada fotograma, le dotamos de una cierta velocidad (por ejemplo: 10 pixels por segundo), y lo movemos de acuerdo con el tiempo que haya transcurrido desde el frame anterior: si un sprite se mueve a 100 pixels por segundo y ha transcurrido medio segundo desde el último trazado de pantalla, le corresponde un movimiento de 100*0.5 = 50 pixels (v=a*t).

¿Cuáles son las ventajas de este tipo de animación (tambien conocido como true physics al estar basado en ecuaciones físicas)? Mediante el establecimiento de velocidades para los objetos, y el uso de ecuaciones físicas como v=v0 + a·t y s=s0 + v·t + ½·a·t, donde el subindice 0 significa valores iniciales), se consigue una animación muy fluida en ordenadores potentes pero a igual velocidad en los más lentos. Es decir, un movimiento desde A hasta B tardará el mismo tiempo en todos los ordenadores, siendo más suave en los más potentes y menos en los de menos capacidad de proceso, pero obtendremos el mismo tiempo de ejecución en todas las máquinas.

No obstante, este tipo de animación hace más dificil (aunque más real) el control de nuestros sprites, sobre todo a la hora de las colisiones, pero eso escapa a nuestro curso. Por supuesto, los juegos en 3d deben basar casi siempre su animación en este método pues suelen implementar siempre ecuaciones físicas que incluso podemos variar (como la gravedad de Quake o el realismo en los juegos de coches 3D).


TIMERS (II)

¿Cómo implementar el método de incrementos de tiempo (elapsed time) en nuestras aplicaciones? Necesitamos temporizadores o sistemas de medición capaz de medir tiempos muy pequeños, ya que no podemos utilizar WM_TIMER debido a su relativa inexactitud. Para ello (y para cualquier otro proceso de temporización que requeramos) disponemos de diferentes opciones.

DWORD GetTickCount(void);
Devuelve el número de milisegundos pasados desde que se inició Windows. Veamos un ejemplo de esta función donde utilizaremos Sleep(n) para suspender la aplicación durante n ms):


 DWORD antes, despues, elapsed;
 antes = GetTickCount();
 Sleep(500);
 despues = GetTickCount();
 elapsed = despues - antes;

La función GetTickCount() es suficiente para nosotros si no necesitamos medir tiempos más pequeños de 55 milisegundos.

BOOL QueryPerformanceFrequency(lpliPerformanceFreq);
BOOL QueryPerformanceCounter(lpliPerformanceCount);

Estas 2 funciones devuelven la frecuencia y el valor actual del contador de alta resolución del sistema, si éste existe. El parámetro LpliPerformanceFreq apunta a una variable que la función especifica (en ticks por segundo) como el actual contador de alta resolución del sistema, devolviendo 0 en este puntero si el hardware no soporta contadores de alta resolución. Devuelven TRUE en caso de error y FALSE si el hardware actual soporta un contador de alta resolución. El parámetro lpliPerformanceCount apunta a una variable en que se devolverá el valor actual del contador de alta resolución del sistema:


 __int64 antes, despues, elapsed;
 QueryPerformanceCounter((LARGE_INTEGER*) &antes);
 Sleep(500);
 QueryPerformanceCounter((LARGE_INTEGER*) &despues);
 elapsed = despues - antes;

El ejemplo anterior devuelve el tiempo transcurrido (elapsed) en ticks dependiendo de la resolución actual del timer de hi-res del sistema. Si queremos obtener tiempos en milisegundos, veamos un ejemplo algo más elaborado, que implementa una función de inicialización y otra que devuelve (usando QueryPerformanceCounter()) el número de milisegundos que han pasado desde que se inició Windows, en vez de devolverlo en ticks:


 static LARGE_INTEGER frec;

 char InitTimer(void)
 {
  if (!QueryPerformanceFrequency(&frec)) return(0);
 }

 DWORD GetTimer(void)
 {
   LARGE_INTEGER count;
   QueryPerformanceCounter(&count);
   return ((DWORD) ((count.QuadPart*1000) / frec.QuadPart));
 }

 // En WinMain:
 DWORD starttime, endtime, elapsed;

 InitTimer();
 starttime = GetTimer();
 Sleep(500);
 endtime = GetTimer();
 elapsed = endtime-starttime;

Finalmente, disponemos de los timers multimedia, y con ellos las funciones timeBeginPeriod(), timeEndPeriod(), y timeSetEvent(), que sirven para lanzar procesos periódicos o también de 1 solo pulso, si el lector así lo desea puede utilizar la ayuda del compilador para usar estas funciones (sólo disponibles en Win32, no en 3.1).

Con todo lo anterior el tema de los timers bajo Windows puede considerarse totalmente cubierto, e incluso si usamos assembler también disponemos de la instrucción RDTSC, que devuelve un entero de 64 bits basado en la frecuencia de reloj del sistema, de forma análoga a QueryPerformanceCounter().


ACCESO A ARCHIVOS Y MEMORIA

El acceso a archivos por Windows no cambia en nada al realizado bajo MSDOS. Las funciones estándar de C (fopen(), fgetc(), fread(), etc) pueden continuar siendo usadas sin ningún problema bajo Windows, aunque si el lector lo desea (disponibles en la ayuda del compilador) puede utilizar funciones como CreateFile() (que realiza tanto creación como apertura), ReadFile(), GetOverlappedResult(), etc., para lectura asíncrona de ficheros.

Otro tanto puede definirse para la memoria, utilizar malloc(), free(), new o delete se presenta como una forma muy sencilla y compatible de evitar funciones de la API de Windows como GlobalAlloc(), GlobalRealloc(), GlobalSize() y GlobalFree(), que asignan, modifican, devuelven el tamño y liberan un bloque de memoria, respectivamente. Por otra parte, mediante la función GlobalMemoryStatus() se puede obtener información sobre la memoria disponible en el sistema.

Usando las funciones estándar de C para ficheros y memoria conseguimos un código más portable a otras plataformas, que puede ser adaptado fácilmente desde MSDOS si se decide dar el paso de DOS a Windows tras haber comenzado un proyecto bajo dicho sistema operativo.


MEDIA CONTROL INTERFACE

Sea nuestro programa del tipo que sea (juego, aplicación multimedia, utilidad), suele resultar muy conveniente a veces permitir al usuario la posibilidad de disfrutar de sonido durante la ejecución del mismo. Ya vimos la forma de sonar ficheros WAV de efectos mediante las funciones PlaySound() y SndPlaySound(), y puede hacersenos necesario utilizar sonidos MIDI o CD-AUDIO en nuestros programas. Esto se soluciona con el Media Control Interface (MCI), que nos permitirá gestionar los dispositivos multimedia de una manera muy sencilla, tras incluir en nuestro programa la librería multimedia winmm.lib.

Las funciones principales a utilizar para realizar llamadas al dispositivo multimedia son mciSendString() y mciSendCommand(). Veamos la primera de ellas (la más sencilla):

MCIERROR mciSendString( LPCTSTR command, LPTSTR returnstring, UINT returnstringlength, HANDLE hwndCallback );
El parámetro command es una cadena de texto con las ordenes que le enviamos al MCI, returnstring es un buffer donde queremos que nos deje los posibles mensajes de error (puede ser NULL) de tamaño máximo (en carácteres) returnstringlength, especificado por nosotros (es decir, le damos el tamaño del buffer de error para que no escriba fuera), este parámetro es ignorado si returnstring es NULL, y hwndCallback, que indica una ventana a la que enviarle los mensajes si hemos pedido algún comando susceptible de que nos envie mensajes.

De una manera más visual, veamos una sección de código que programa el MCI para que comience a reproducir música CD desde la pista 1 del CD que esté colocado en la bandeja CDROM. Por supuesto, puede cambiarse el nº de pista al deseado (donde estén situadas las pistas de música en el CD de nuestro juego o programa, ya que la pista 1 suele ser de datos).


void PlayCD( void )
{
 char blank[128];
 mciSendString("open cdaudio", blank, 127, NULL);
 mciSendString("play cdaudio from 1", blank, 127, NULL);
}

Veamos ahora un ejemplo más elaborado que utiliza sprintf() para generar las cadenas de comandos y cuya función es sonar un fichero MIDI en background (como la música FM de cualquier juego):


bool PlayMidi(char *filename)
{
  char buffer[256];

  wsprintf(buf, "open %s type sequencer alias MUSIC", filename);
  if ( mciSendString("close all", NULL, 0, NULL) != 0 )
     return false;
  if (mciSendString(buffer, NULL, 0, NULL) != 0)
     return false;
  if (mciSendString("play MUSIC from 0", NULL, 0, hwnd) != 0)
     return false;
 
  return true;
}

Basta con conocer los comandos de que nos provee el MCI (como stop, open, y demás) para implementar sencillas rutinas que comiencen a sonar un fichero MIDI (o CD-audio), hagan una pausa, paren, continuen la ejecución, etc. Los comandos que acepta el MCI son, entre otros:

  close device_name
  info device_name          [product] 
  open device_name          [alias device_alias] 
                            [shareable] 
                            [type device_type] 
  status device_name        [mode | ready]
  pause device_name	
  play device_name           [from position] 
                             [to position]
  stop device_name

A modo de ejemplo, veamos funciones para el control de música MIDI:


char OpenMusic( char *filename )
{
  char buffer[256];
  wsprintf(buffer, "open %s type sequencer alias MUSIC", filename );
  return( mciSendString( buffer, 0, 0, 0 ) == 0 );
}

char PlayMusic( void )
{
  return( mciSendString("play MUSIC from 0 notify", 0, 0, 0 ) == 0 );
}

El comando notify agregado a la orden play pide al MCI que al finalizar el play del fichero nos envie un mensaje WM_MCINOTIFY para que sepamos que el fichero ha terminado su sonorización y podamos cargar otro fichero MIDI o repetir el mismo. El comando from (opcional) permite especificar desde donde sonar el fichero, especificado como desde el principio del mismo (0) en esta función.


char StopMusic( void )
{  return( mciSendString("stop MUSIC", 0, 0, 0 ) == 0 );  }

char ContinueMusic( void )
{  return( mciSendString("play MUSIC notify", 0, 0, 0 ) == 0 ); }

char CloseMusic( void )
{
  // para permitir la reprod. de otro MIDI.
  return( mciSendString("close all", 0, 0, 0 ) == 0 );
}

El uso de estas funciones es nuestra mejor alternativa (aparte de los stream midis, midis que se pueden gestionar como flujos (streams), aunque estos son mucho más complicados) hasta que llegue la futura DirectMusic a DirectX. Para los sonidos WAV, DirectSound hace un buen papel excepto en algunas tarjetas, sin tener tanta latencia como el MCI.


MULTITHREADING

La principal característica de Windows 95/NT respecto a Windows 3.1 es que la multitarea es mucho más real y eficiente (aunque sea de tipo apropiativo). Bajo Windows 95/NT disponemos de la posibilidad de crear/gestionar threads (hebras, en castellano), que son procesos independientes capaces de ejecutarse paralelamente a nuestro programa. En palabras sencillas, tenemos la posibilidad de ejecutar funciones en background (aprovechando la multitarea) mientras sigue ejecutándose nuestro programa principal, su bucle de mensajes, etc.

La utilidad de esto es muy extensa: desde implementar todo tipo de técnicas de programación de juegos y aplicaciones, como pensadores de inteligencia artificial para juegos que preparan su jugada, próximo movimiento o acción mientras el usuario hace lo propio, para controlar aspectos concretos de una aplicación, o incluso para cargar ficheros en background esperando un mensaje de finalización (mediante SendMessage()) en el thread principal.

Lo primero que se debe hacer es cambiar nuestra aplicación (que por defecto es Single Threaded, o de un sólo proceso), a Multithreaded (multiproceso). Esto se hace en el menú BUILD, opción Settings, pestaña C/C++, Category->Code generation, y cambiar Single threaded a Debug Multithreaded o Multithreaded, según estemos creando la versión debug o release, tras lo cual pulsaremos OK. A partir de este momento ya podemos usar las funciones _beginthread() y _endthread() para definir procesos multitarea. A _beginthread() se le pasan diferentes parámetros, entre los cuales está un puntero a la función a ejecutar en background (la que va a constituir el proceso). La función _endthread() no tiene parámetros y es simplemente el return() de un thread. Para utilizar estas funciones necesitaremos además incluir el archivo de cabecera <process.h>.

Veamos primero un ejemplo y después daremos las declaraciones formales de ambas funciones:


void mi_thread( void *params )
{
  MessageBox( NULL, "Test", "Info", MB_OK );
  _endthread();
}

// en alguna parte del resto del programa:
_beginthread( mi_thread, 0, NULL);

Este código crearía un thread independiente (que contendría un MessageBox) que se ejecutaría al mismo tiempo que continúa la ejecución de nuestro programa (en este caso el focus lo mantiene nuestra ventana).

unsigned long _beginthread( void( __cdecl *start_address )(void *), unsigned stack_size, void *arglist );
Inicia un nuevo thread. Devuelve -1 si hay un error en la creación del thread. El parámetro start_address es la dirección de la rutina a usar como thread, stack_size el tamaño de la pila para el nuevo thread (o 0 para usar el valor por defecto), y arglist es una lista de argumentos que pasar al nuevo thread (o NULL si no hay argumentos que pasar). void _endthread( void );
Finaliza un thread iniciado con _beginthread(). Aparte de estas 2 funciones disponemos de funciones más nuevas (pero más complejas) dentro de la API de Windows, tales como CreateThread(), ExitThread(), etc, aunque valgan como introducción las 2 funciones que hemos utilizado, que nos va a permitir gran cantidad de ahorro de código y facilidad de organización del mismo.


EL FLUJO DEL PROGRAMA

Tras el comentario y comprensión de la necesidad del bucle de mensajes en la función WinMain(), muchos lectores es posible que se pregunten donde han de poner el código específico de sus programas, ya que suele ser complicado buscar un hueco para nuestro código si ha de estar ejecutandose el bucle de recogida de mensajes. La idea es que el bucle de mensajes sólo se debe ejecutar cuando haya algún mensaje pendiente, y que durante el resto del tiempo debe ejecutarse el código de nuestro programa.

La manera de la que se va a proceder es la siguiente: si hay algún mensaje disponible, se procesa, chequeando si el mensaje es WM:QUIT para salir del bucle. Si no hay ningún mensaje, no lo esperamos sino que procedemos a actualizar nuestra aplicación (siempre que no estemos en estado minimizado, que como vimos en la anterior entrega puede deducirse del mensaje WM_ACTIVATEAPP, donde deberemos modificar el valor de alguna variable externa para informar de eso a nuestro bucle de mensajes):


int quit=0, minimised=0;
MSG msg;
 
  // etc... 
while (!quit)
{
  while ( PeekMessage(&msg,NULL,0,0,PM_REMOVE) )
  {
    if (msg.message == WM_QUIT) { quit=1; break; }
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  if (quit) break;

  if (minimised)
    WaitMessage();
  else
  {
     VisualizarPantallaVirtual();
     MoverSpritesEnPantVirtual();
     MasFunciones();
  }
}

Mediante un bucle como el anterior, Windows nos dará más tiempo del habitual para nuestros procesos, y otras aplicaciones correrán de manera más lenta. Ya hemos comentado que el objetivo de este curso es para la programación de juegos y aplicaciones gráficas, por lo que no debería importarnos que se ralenticen otras aplicaciones del sistema, es más debemos copar la CPU todo lo que podamos, ya que nuestro programa va a estar en primer plano y es el más importante. Además, si se utiliza un bucle de estas características, sólo perderemos aproximadamente un 5% de la velocidad a la que se ejecutaría bajo DOS, aunque bajo Windows obtendremos más velocidad que bajo DOS gracias a la potente caché que posee y a otras funcionalidades de 32 bits. Por otra parte la función WaitMessage() se dedica a esperar un mensaje de Windows pasandole todo el tiempo de ejecución al resto de aplicaciones. Esto lo hacemos así porque cuando nuestra ventana está minimizada (en un icono en la barra de tareas) no necesitamos actualizar el juego (modo de pausa) y damos nuestro tiempo a las aplicaciones que nos necesiten, favoreciendo la filosofía multitarea. Por otra parte, mediante dicho bucle ya pueden implementarse todos los programas de MSDOS, fácilmente adaptables ahora bajo Windows utilizando los conocimientos obtenidos durante el curso.

La única dificultad con que nos encontramos a diferencia de MSDOS es la realización de inicializaciones de datos, carga de ficheros y de cómo trabajan las funciones (por ejemplo si queremos hacer un menú gráfico propio mediante el ratón). En MSDOS es posible permanecer dentro de una función el tiempo que se desee hasta que la misma termine (como esperar que el usuario seleccione una opción del menú con el ratón, en un bucle en que se obtiene la posición del ratón y el estado de los botones y se actúa en consecuencia). Bajo Windows una función de ese estilo haría creer a Windows que nuestra ventana se ha "colgado", pues al estar en la función no estamos ejecutando el bucle de mensajes y no estamos contestando a Windows. Lo mismo ocurre para cargar archivos de datos al principio del programa: no es bueno que estas funciones tomen mucho si se van a ejecutar una vez antes del bucle de mensajes. Este problema es fácilmente solucionable usando funciones que realicen estas tareas en pequeños incrementos de la misma, de forma que realicen una fracción de su tarea (por ejemplo, cargar una parte de un archivo o hacer un sólo redibujado del menu, movimiento y chequeo del mouse) y vuelva a ejecutarse el bucle de mensajes. Podemos ayudarnos de una variable que nos indique donde estamos para organizar el bucle de mensajes:


 char estado;
 #define ESTADO_CARGANDO 0
 #define (etc...)

// función WinMain
while (!quit)
{
  while( PeekMessage(...) )
  {
   Translate/Dispatch/testear_WM_QUIT()
  }

  switch (estado)
  {
   case ESTADO_CARGANDO:
             CargarUnaFraccion();
             break;
   case ESTADO_MINIMIZADO: 
             WaitMessage();
             break;
   case ESTADO_MENU:
             FraccionDeMenu();
             break;
   case ESTADO_JUEGO:
             HazUnFrame();
             break;
  }
 }

Aunque parezca ineficaz el uso del switch, muchas veces éste se compila como una tabla de saltos (más rápido que combinaciones if/else), en cuyo caso dicho switch() no resultará perceptible a la hora de la ejecución del programa. Además podemos utilizar variables globales para informarnos (desde otros puntos del programa), de qué archivo hemos finalizado de cargar, o qué acción está preparada para ejecutarse, por ejemplo.

Otra opción para la carga de archivos es utilizar un proceso (thread) independiente (aprovechando la multitarea), que cargue el fichero mientras nuestro thread (el principal) continua contestando mensajes. Para que el programa principal sepa cuándo ha finalizado la carga del archivo para disponer de los datos del mismo, puede enviarse un mensajes WM_USER desde el thread de carga al principal, que gestionaremos en el WndProc del mismo.

Para ello, basta con comentar que Windows dispone de una serie de identificadores de mensajes privados para que las aplicaciones puedan enviarse mensajes, que van desde el WM_USER (id de un valor numñerico) hasta el nº 0x7FFF. Por otra parte, para enviar un mensaje se usa la función LRESULT SendMessage(hwnd, uMsg, wParam, lParam), donde hwnd es el handle de la ventana destino, uMsg es un entero con mensaje a enviar (por ejemplo WM_USER o WM_USER+2)y el resto los parámetros a enviar (por si queremos enviarle alguna información a la aplicación).

A modo de ejemplo de todo lo visto hasta ahora está el listado 1, un StarField-3d (parecido al salvapantallas de Windows) que demuestra el acceso al GDI, al MCI y el uso de un bucle general y de timers para procesado de datos, así como un thread independiente con un MessageBox de información extra.

  LISTADO 1: El ejemplo del mes.

//-----------------------------------------------
// Listado1 (ej4) .CPP  -  StarField 3d.
// (c) 1998, S. Romero AKA NoP/Compiler,
//-----------------------------------------------
#include <windows.h>
#include <process.h>
#include <stdlib.h>

//--- Declaración de funciones del programa -----
void InitStarField( void );
void DrawStarField( HDC );
void MoveStarField (int, int, int );
void mi_thread( void * );

#define RANDOM(min,max) ((rand()%(int)(((max)+1)-(min)))+(min))
#define MAX_STARS 2000

//--- Declaración de variables del programa ------------------------------
struct Star
{
    int x, y, z;
    int oldx, oldy;
} StarField[ MAX_STARS ]; 

int quit=0, minimised=0;
int num_stars = 200, speed = -4, xinc = 0;
char cadena[80];
DWORD start, end, frames;
MSG msg;
HDC hdc;

//=== Función principal WinMain() ========================================
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
				    LPSTR lpCmdLine, int nCmdShow )
{

 //Código de creación de la ventana:
  RegisterClass_Y_CreateWindow_etc();

  InitStarField();
  ShowWindow( hwnd, nCmdShow );
  UpdateWindow( hwnd );

  CloseMusic();
  OpenMusic("musica.mid");
  PlayMusic();
  start = GetTickCount();
  _beginthread( mi_thread, 0, NULL);

 while (!quit)
 {
  while (PeekMessage(&msg,NULL,0,0,PM_REMOVE) )
  {
    if (msg.message == WM_QUIT) { quit=1; break; }
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
   if (quit) break;
   if (minimised)  WaitMessage(); 
   else
   {
      DrawStarField(hdc=GetDC(hwnd));
      ReleaseDC(hwnd, hdc);
      MoveStarField(xinc,0,speed);
   }

   frames++; // para calcular f.p.s:
 }

  end = GetTickCount();
  frames = (frames / ((end-start)/1000L));
  if( end != start )
    wsprintf(cadena, "Fps: %d", frames );
  MessageBox( NULL, cadena, "Info", MB_OK );

  return( msg.wParam );
}

//---------------------------------------------
void mi_thread( void *params )
{
  MessageBox( NULL, "Info", "Info", MB_OK );
  _endthread();
}

//=== Función WndProc() ========================
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
 switch( message )
 {
  case WM_ACTIVATEAPP:
    minimised = !wParam;
    if( minimised )
       StopMusic();
    else
       ContinueMusic();
     break;

  case WM_KEYDOWN:
     switch( wParam )
     {
       case VK_ESCAPE:
       case VK_F12:  DestroyWindow( hwnd ); break;
       case VK_ADD:
         if( num_stars < MAX_STARS-10 ) num_stars+=10;
       break;
     };
     return(0);
	
  case WM_LBUTTONDOWN:
      if( speed > -6 ) speed-=1; break;
  case WM_RBUTTONDOWN:
      if( speed < 6 ) speed+=1; break;
  case WM_MBUTTONDOWN:
      speed=0; break;

  case WM_DESTROY:
      StopMusic();  CloseMusic();
      PostQuitMessage(0); break;

  default:
      return( DefWindowProc( hwnd, message, wParam, lParam ) );
   }
 return(0);
}

//---------------------------------------------
void InitStarField( void )
{
  for (int f=0; f<MAX_STARS; f++)
  {
     StarField[f].x = RANDOM (-400, 400);
     StarField[f].y = RANDOM (-300, 300);
     StarField[f].z = rand()%2000;
     StarField[f].oldx = -1;
     StarField[f].oldy = -1;
  }
}

//---------------------------------------------
void DrawStarField (HDC hdc)
{
   int sx, sy, sz;
   for (int f=0; f<num_stars; f++)
   {
     SetPixel(hdc, StarField[f].oldx, StarField[f].oldy, RGB (0, 0, 0));
     sx = ((StarField[f].x*256)/StarField[f].z)+(400/2);
     sy = ((StarField[f].y*256)/StarField[f].z)+(300/2);
     sz = 255-(StarField[f].z/8);
     SetPixel(hdc, sx, sy, RGB (sz, sz, sz));
     StarField[f].oldx = sx;
     StarField[f].oldy = sy;
   }
}

//---------------------------------------------
void MoveStarField (int xinc, int yinc, int zinc)
{
  for (int f=0; f<num_stars; f++)
  {
     StarField[f].x += xinc;
     StarField[f].y += yinc;
     StarField[f].z += zinc;

     if( StarField[f].z > 2000 )
       StarField[f].z -= 2000;
     if( StarField[f].z < 1 )
       StarField[f].z += 2000;
  }
}


EN RESUMEN

A lo largo del curso se ha tratado de proporcionar los fundamentos de programación bajo Windows, los sistemas de mensajes, la organización del flujo del programa, e incluso se han tratado temas como el audio (tanto CD y wavefiles como música FM) y el input del usuario (mouse/teclado), permitiendo el desarrollo de potentes utilidades y aplicaciones, y asentando una base para el posterior uso de DirectX.

Pero como siempre, a programar se aprende programando.

Pulse aquí para bajarse los ejemplos y listados del artículo (21 Kb).

Santiago Romero


Volver a la tabla de contenidos.