eMc2H 7 - Tutor practico de desarrollo de videojuegos (parte 1)
Autor: kobe
eMc2H
INDICE
- Introduccion a este texto
- Requerimientos
- Introduccion a la creacion de videojuegos
- Iniciando la programacion con la Api Win32
- Empezando a pintar sobre la pantalla
- Trabajando con graficos
- Empezando a crear nuestro scroll
- Despedida
1. Introduccion a este texto
Bienvenidos a esta pequeña guia practica de creacion de videojuegos, les anticipo que en esta guia se tratara de manera practica la creacion de un juego de naves en 2D, pero el 80% de los elementos vistos en este tutor se aplican sin problemas a juegos de otro tipo (plataformas, peleas, rpg, etc) Este tutor esta planeado a salir en 3 o 4 entregas, ya que la programacion de videojuegos en realidad es un tema largo, pero esperemos que despues de estas entregas quede todo bien entendido.
Tambien creo que es importante señalar que este texto estara basado en el libro "Como programar videojuegos en Windows" del autor Antonio Ruiz. En capitulos avanzados si se nos hara necesario incluir algunas rutinas y funciones nuevas, ya que el autor de este libro no profundizo mucho, y solo dejo las bases. Mas adelante les mostrare un par de ideas que el autor no incluyo, y hablaremos tambien de programacion de videojuegos en 3D (otra cosa que el autor no hizo). Aun asi, este libro me ha sido muy util aprendiendo sobre programacion de videojuegos y usaremos el codigo de su juego de naves en nuestros ejemplos (pero repito, en capitulos posteriores agregare ciertas cosas para hacerlo mas bonito a nuestro juego : -)
He escogido un juego de naves porque es complejo y sencillo a la vez, como esta eso ? pues bien, es complejo porque requiere de la mayoria de los elementos que todo buen juego ocupa (desde fondos hasta inteligencia artificial) pero es sencillo ya que los veremos de una forma que sean facilmente entendibles, dicho sea de paso, el codigo usado en este tutor se puede decir que esta en un nivel 'Medio' de dificultad, se pudieron haber hecho cosas mas facilmente, pero serian ejecutadas mas lentamente por el ordenador, tambien se pudieron usar algoritmos mas complejos, pero este tutor ha alcanzado un buen punto entre dificultad y desempeño.
Aunque este tutor esta diseñado para un juego de naves en 2D, dependiendo de la respuesta de la gente no descarto una version de este texto usando 3D.
2. Requerimientos
Bueno, en este tutor usaremos los siguientes recursos:
- Windows 98, 2K, ME o XP
- Conocimientos de C
- Usaremos VisualC++ 6.0 (Borland C++ tambien deberia trabajar de manera correcta)
- DirectX SDK 8.1: http://download.microsoft.com/download/whistler/dx/8.1/W982KMeXP/EN-US/DX81SDK_FULL.exe
Evitare la programacion orientada a objetos para facilitar el tutor, ademas que en este caso no resulta indispensable.
NOTA: anexo los archivos .LIB necesarios para compilar nuestros codigos de ejemplo, cuando crees un proyecto tan solo inserta estos archivos en tu WorkSpace, o agrega en Tools->Options->Directories el directorio donde hayas instalado el Directx DSK, añade la carpeta de INCLUDE del SDK asegurandote que este seleccionado INCLUDE FILES y añade la carpeta LIB seleccionando primero LIBRARY FILES, despues te vas a Project->Settings->Link y agrega:
- ddraw.lib
- dinput.lib
- dsound.lib
- dxguid.lib
- Winmm.lib
3. Introduccion a la creacion de videojuegos
Desde la aparicion del ATARI, la programacion de videojuegos ha sido un tema de interes comun para la mayoria de los programadores. El hecho de ser capaz de crear tu propio mundo en un ordenador y regirlo bajo tus reglas resulta demasiado atractivo como para ignorarlo.
Podemos decir que la programacion de videojuegos es un arte, el arte de poder experimentar y aventurarnos en 1001 mundos sin morir en el intento, es por eso que los famosos videojuegos como Zelda, Metroid, Castlevania entre otros transmiten un sentimiento tan especial, transmiten el esfuerzo que han puesto los creadores por crear un universo paralelo.
Ahora, tu tambien tendras la oportunidad de ser una especie de dios y crear tu propio heroe y tu concepto del mundo ideal lleno de magia y aventura, seras el mago y creador de tu mundo y podras mover las piezas de este mundo a tu antojo.
Les recomiendo a todos los que lean este texto probar tantas cosas como se les ocurra durante la creacion de sus aventuras.
3.1: Elementos basicos de los videojuegos
bueno, solo les enlisto los elementos que estaran presentes en nuestro juego y que aparecen en practicamente en todos los juegos creados
- Engine o Mecanismo grafico.
- Fondos
- Animaciones
- Mecanismo de entrada (control)
- Sonido
- Colisiones
- Efectos especiales
- Inteligencia artificial
4. Iniciando la programacion con la Api Win32
Bueno, hora de empezar a meter mano sobre el codigo, aquellos que ya hayan creado aplicaciones usando la API de Windows con C/C++ estaran muy familiarizados con el contenido de este capitulo.
Cabe señalar que no es necesario memorizar todo lo visto en este capitulo, ya que la mayoria de los elementos que usaremos para crear nuestro videojuego sera usando elementos comunes en C (structs punteros condiciones etc). Tambien el programa de ejemplo usado en este capitulo podra ser usado con facilidad en otros juegos que crees, ya que no se han usado caracteristicas especificas.
El programa de este capitulo es una simple inicializacion de ventana, el resultado de ejecutarlo es.. virtualmente Nada, asi es, no pasa nada al ejecutarlo. Lo unico que va a pasar es que el puntero del raton va a desaparecer y no tendremos control de la pc hasta que pulsemos alguna tecla, es decir, crea una ventana a pantalla completa sin bordes u otros elementos visibles.
4.1: Diferencias entre una aplicacion comun y un videojuego que usan la API de WIN32
Bueno, como ya mencione, nuestro juego se ejecutara a pantalla completa, a diferencia de ejecutarse en una ventana del escritorio como lo hacen los programas comunes de windows.
Otra diferencia importante es la forma en como manejan los mensajes, en la mayoria de las aplicaciones de windows cuando no se esta haciendo algo, ya sea mover el mouse o manejando el programa con el teclado, el programa pasa a un estado de espera para asi no consumir recursos. Por otro lado, los videojuegos aunque no reciba ningun evento por parte del usuario, se supone que los personajes siguen ejecutando sus acciones sin pasar nunca al estado de espera.
4.2: Nuestro primer programa
Bueno, hora de empezar a trabajar con nuestro programa de ejemplo, al ejecutar este programa la primera funcion que se va a llamar es WinMain (como en cualquier aplicacion de windows), una vez en WinMain se llamara a nuestra funcion "doInit" que crea la ventana usando una funcion de las librerias de windows, "CreateWindosEx" (Aqui es donde podemos indicar las caracteristicas de la ventana) Si la ventana fue creada directamente la funcion "doInit" nos devolvera TRUE en caso contrario devolvera FALSE y el programa no se ejecutara.
Nuestra tercer funcion es WindowProc (las otras dos son WinMain y doInit, tmb llamaremos a una funcion llamada "game" pero estara vacia de momento) En Esta funcion de WindowProc se gestionan los mensajes introducidos por el usuario, en este ejemplo solo se procesara el crear una aplicacion y el de salirse al pulsar una tecla. Aunque dentro de nuestra funcion WinMain haremos uso de la funcion "WaitMessage", cuando se active el programa (osease cuando bActive valga TRUE) ya no vuelve a ejecutarse esta funcion y nuestro programa consumira el maximo de recursos posibles.
4.3: El codigo fuente de nuestro ejemplo
He incluido comentarios en varias partes explicando el significado del codigo para facilitar la comprension
---------------------<recorta de aqui para abajo>------------------
//Algunos Includes necesarios y definiciones no tan necesarias pero utiles.
#define NAME "TUTOR 1"
#define TITLE "Tutor practico de desarrollo de videojuegos"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <stdlib.h>
#include <stdarg.h>
#include "resource.h"
HWND hwnd; //el manejador de nuestra ventana
BOOL bActive; //almacena si el juego esta activo
//funcion de nuestro grandioso juego, vacio por ahora
void game(void)
{
}
/* En esta funcion se analizan los mensajes que lleguen a nuestro juego,
se recibe un mensaje (contenido en la variable message) y usando un simple
switch-case se decide que se va a hacer, al ejecutarse la aplicacion,
bActive pasa a valer TRUE, y no se hace ninguna otra cosa en especial,
tal como lo indica WM_KEYDOWN al presionarse una tecla se envia un mensaje
de cerrar la aplicacion (WM_CLOSE)*/
LRESULT CALLBACK WindowProc( HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam )
{
switch( message )
{
case WM_ACTIVATEAPP:
bActive = TRUE;
break;
case WM_CREATE:
break;
case WM_SETCURSOR:
SetCursor(NULL);
return TRUE;
case WM_KEYDOWN:
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
/*En esta funcion es donde se crea y actualiza la ventana, no te preocupes
por entender todos los parametros que se presentan aqui, ya que no son
indispensables en la creacion de videojuegos, aun asi incluire un apartado
mas abajo con el significado de algunas de las estructuras usadas aqui*/
static BOOL doInit( HINSTANCE hInstance, int nCmdShow )
{
//una estructura de datos para nuestra ventana
WNDCLASS wc;
//se rellenan los valores de dicha estructura
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground = NULL;
wc.lpszMenuName = NAME;
wc.lpszClassName = NAME;
RegisterClass( &wc );
//se crea la ventana en si
hwnd = CreateWindowEx(
WS_EX_TOPMOST,
NAME,
TITLE,
WS_POPUP,
0, 0,
GetSystemMetrics( SM_CXSCREEN ),
GetSystemMetrics( SM_CYSCREEN ),
NULL,
NULL,
hInstance,
NULL );
if( !hwnd )
{
return FALSE;
}
ShowWindow( hwnd, nCmdShow );
UpdateWindow( hwnd );
return TRUE;
}
//el punto de partida de nuestro juego, desde aqui se llama a las demas funciones
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
lpCmdLine = lpCmdLine;
hPrevInstance = hPrevInstance;
if( !doInit( hInstance, nCmdShow ) )
{
return FALSE;
}
//se ejecuta siempre
while( 1 )
{
if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ))
{
if( !GetMessage( &msg, NULL, 0, 0 ) ) return msg.wParam;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
//si el juego esta activo se llama al nucleo de este
if( bActive )
{
game();
}
else
{
WaitMessage();
}
}
}
<----------------------------------FIN------------------------------------->
4.4: Off-Topic, Estructuras de la WinAPI
Antes que nada, este apartado solo deben leerlo aquellos que esten interesados en profundizar mas sobre la creacion de aplicaciones usando la API de Windows, no planeo entrar en muchos detalles aqui ya que la API es bastante grande y no es necesario conocer todo su funcionamiento para hacer un videojuego en 2D.
4.4.1 Estructura WNDCLASS
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
bueno, ahora hare un pequeño comentario de cada campo sin profundizar mucho, si estas interesado en crear aplicaciones para windows puedes buscar en google sobre la win api para detallar.
- style: Es el estilo de la ventala, que toma valores constantes prefijados por cs_ se puede usar el separador | (OR) para combinar estilos
- lpfnWndProc: puntero a nuestra funcion de procedimiento de ventana (WindowProc)
- cbClsExtra: Especifíca cuantos bytes extra se reservaran a continuación de la estructura de la clase, en la mayoria de los programas sera 0
- cbWndExtra: Parecido al cbClsExtra, aqui tambien casi siempre ira en 0
- hInstance: Aqui se indica a que instancia pertenece nuestra ventana segun los parametros que usemos, por lo normal se llama hInstance
- hIcon: Aqui se carga el icono de nuestra aplicacion, como en nuestro videojuego no estamos muy interesados en meternos a fondo con los detalles de las ventanas hemos usado la funcion LoadIcon(hInstance, IDI_APPLICATION); IDI_APPLICATION representa un icono por defecto que trae windows.
- hCursor: Similar a hIcon, esta es para manejar el cursor que usa el mouse, usando la funcion LoadCursor() una manera simple de manejarlo es como lo hicimos nosotros, con LoadCursor(NULL, IDC_ARROW);
- hbrBackground: Aqui se especifica la forma en que sera pintada nuestra ventana, como nosotros no queremos a Windows dibujando sobre ella lo hemos puesto a NULL
- lpszMenuName: Aqui se especifica el nombre del menu que se va a anexar a tu ventana, o puedes setearlo a NULL si no deseas usar alguno
- lpszClassName: simplemente el nombre que deseemos darle a nuestra clase.
listo, hemos finalizado con la estructura de la ventana, no te preocupes si te quedaron partes sin comprender, en la creacion de juegos no es necesario dominar todos estos parametros.
4.4.2 Funcion CreateWindowEx
Con esta funcion hemos creado la ventana, aqui esta la definicion:
HWND CreateWindowEx(
DWORD dwExStyle,
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
Brevemente, en dwExStyle se especifica el estilo extendido de la v entana, nosotros usamos el parametro WS_EX_TOPMOST para indicar quenuestra ventana debera estar siempre sobre los demas
En lpClassName y lpWindowName usamos los valores contenidos en nuestros defines NAME y en TITLE para especificar el nombre y titulo de nuestra ventana en dwStyle indicamos el estilo de la ventana y nosotros usamos uno del tipo WS_POPUP
x, y, n Width
y n Height
simplemente indican la posicion horizontal, posicion vertical, el ancho y el alto de la ventana (en ese orden) Nosotros usamos los parametros 0, 0, para indicar que se creara al principio de la pantalla y llamamos a la funcion GetSystemMetrics para obtener el tamaño de la pantalla del sistema
hWndParent: Aqui indicamos la ventana padre de nuestra ventana, es opcional cuando usamos WS_POPUP en nuestro estilo
- hMenu: El menu de nuestra ventana, no usamos ninguno asi que lo ponemos en NULL
- hInstance: la instancia que hemos usado para nuestra ventana
- lpParam: Esto no se usa por lo normal en la creacion de juegos, por eso lo hemos puesto en NULL. Serviria para hacer aplicaciones que requieran multiples interfaces para sus documentos.
4.5 Despedida de este capitulo
pues bien, hemos terminado de ver los detalles de nuestra ventana en WINAPI para el siguiente capitulo empezaremos a trabajar con DirectX y a pintar sobre nuestra pantalla, aparte crearemos un nuevo archivo fuente para nuestro engine o mecanismo de graficos.
5. Empezando a pintar sobre la pantalla
5.1 Pequeño previo
Bueno, si se les hizo dificil lo visto en el capitulo 4 con la WinAPI mejor repanselo las veces necesarias, ya que ahora si viene la programacion con DirectX en realidad no va a ser tan duro ya que no usaremos todos sus elementos y vamos a ir de poco en poco, no voy a poner todo el codigo fuente ya que esto se haria bastante largo y tedioso viendo las mismas cosas una y otra vez, lo que voy a hacer es ir explicando lo que vamos a agregar a nuestro archivo fuente y en que parte lo vamos a agregar, ademas de hacer sus comentarios obviamente. Junto a este texto anexare los archivos de codigo finales que nos resulten, que incluira las rutinas graficas y el scroll que mas adelante haremos. No olvides que puedes cambiar el nombre de las variables o las funciones que use aqui, tan solo asegurate que los cambios sean consistentes en todo el programa, es decir, si cambias el nombre de la variable PEPE a JUAN a asegurate de cambiarlo en todo el codigo.
5.2 Objetos de DirectX
Bueno, aunque la api de windows nos ha servido para crear la ventana resulta insuficiente para hacer un juego de calidad, usando directX podremos manejar de una forma mas directa y rapida a la pantalla sobre la cual trabajamos, podremos cambiar la resolucion y colores de la pantalla y pintar sobre cualquier punto de esta.
En este texto usaremos 640 x 480 usando colores de 16 bits ya que es una de las resoluciones mas usadas (por cierto, en este tipo de resolucion cada punto en pantalla ocupara 2 bytes)
Lo primero que haremos sera agregar un archivo include, el ddraw.h que tiene los prototipos y tipos necesarios para hacer uso de directx, tambien tendremos que incluir el ddraw.lib (al final del apartado incluire el codigo fuente que agreguemos)
Ahora que ya hemos incluido a directdraw, hay que definir cuantas pantallas vamos a ocupar, necesariamente ocuparemos una que es la que vemos en el monitor pero podemos crear muchas mas para almacenar otros graficos, en este texto vamos a usar dos pantallas, ya que los graficos los almacenaremos en estructuras de datos que posteriormente vamos a crear.
Entonces, usaremos una FrontSurface (esta es la principal) y una BackSurface (pantalla de fondo), usaremos dos pantallas ya que nos apegaremos al siguiente algoritmo:
- Se borra la pantalla de fondo
- Se mueven los personajes
- Se pintan los personajes en la pantalla de fondo
- Se coppia completamente la pantalla de fondo sobre la principal
- Se devuelve al paso 1
y porque no hacerlo todo directamente sobre la pantalla principal?
pues bien, si se hiciera de esta manera, se veria un parpadeo en cada ciclo del programa, asi, al pintar primero sobre la pantalla de fondo evitamos esto y posteriormente copiamos todo su contenido sobre la pantalla principal.
pues bien, para crear estas dos pantallas DirectX nos ofrece un par de objetos que nos haran muy facil el realizar esto, tambien ocuparemos un objeto para inicializar directdraw y otro para la descripcion de la pantalla, he aqui el nuevo codigo de lo visto hasta ahora:
--------------------Inicio
//Algunos includes que ocuparemos
#include <stdio.h>
#include <conio.h>
#include "c:\dxsdk\include\ddraw.h" //asumo que instalaste el SDK en C:\dxsdk,
//cambia esto si no es asi
//Objetos para utilizar Direct-Draw
LPDIRECTDRAW lpDD; // Objeto directdraw principal
LPDIRECTDRAWSURFACE lpDDSPrimary; // pantalla principal (frontsurface)
LPDIRECTDRAWSURFACE lpDDSBack; // pantalla de fondo (backsurface)
DDSURFACEDESC ddsd; // descriptor de la pantalla
--------------------FIN
recuerda que estas variables deben ser declaradas globalmente, hechale un ojo al codigo de ejemplo anexado junto a este texto (game.cpp)
5.3: Empezando nuestro motor grafico
Bueno, hora de agregar otro archivo fuente a nuestro proyecto, se trata del engine.cpp que vamos a hacer, en este archivo vamos a incluir funciones y estructuras para nuestro motor grafico que posteriormente podremos usar en otros proyectos En estos primeros pasos de nuestro motor grafico simplemente crearemos una estructura que nos permitira poner pixeles en cualquier punto de la pantalla que deseemos, recuerda que el punto X=0 , Y=0 es el extremo de arriba a la izquierda, y el punto X=639, Y=479 es el que esta en el extremo inferior derecho.
Para saber que posicion de memoria ocupa un pixel en pantalla haremos uso de una formula simple, solamente se multiplica el valor de 'Y' por el ancho de la pantalla y se le suma el valor de 'X', por ejemplo, si quieres pintar en el punto X=50, Y=150 usa algo asi: (150*640)+50 y el resultado de esto lo multiplicamos por 2, debido a que cada pixel en la pantalla ocupa 2 bytes
El metodo que nosotros usaremos es mas practico, usaremos una serie de punteros cada uno apuntando al principio de una linea de la pantalla, asi para nuestra pantalla de 640x480 pixeles usaremos 480 punteros cada uno apuntando a una linea de 640 puntos
Tambien usaremos typedef para crear un tipo de dato llamado "Screenlines" que solamente es un puntero a una linea, recuerda que como cada punto en pantalla ocupa dos bytes usaremos el tipo de dato basico WORD para este tipo Screenlines. La estructura llamada "Screen" que es el tipo que contendra la informacion de nuestra pantalla tiene 3 valores, AX y AY que representan el alto y el ancho y un puntero a las lineas de la pantalla. Por motivos practicos tambien vamos a hacer una funcion llamada "Error" que recibe un mensaje de error y lo imprime a continuacion el codigo de lo que se ha visto nuevo (recuerda que esto va en un nuevo archivo llamado engine.cpp)
------------- engine.cpp
typedef WORD *ScreenLines;
typedef struct{
WORD AX,AY;
ScreenLines *Lines;
} Screen;
void Error(char *c,char *buf)
{
MessageBox( hwnd, buf, c, MB_OK );
exit(-1);
}
------------- engine.cpp
ahora que disponemos de una estructura para crear pantallas, es hora de crearlas de momento, vamos a crear solo dos pantallas aunque posteriormente haremos mas para almacenar los graficos. Como posteriormente usaremos mas pantallas tambien se hara necesario el tener un puntero a la pantalla sobre la cual se va a estar trabajando, asi, aunque tengamos una gran cantidad de pantallas, solo bastara asignarle la direccion en memoria de una de estas al puntero "pantallaenuso" para que todas las acciones se realizen sobre esta.
----------- incluyelo en engine.cpp
//estas estructuras son globales
Screen pantallafisica;
Screen *pantallaenuso;
void SetActiveScreen(Screen &s)
{
pantallaenuso=&s;
}
----------- fin de lo nuevo
Preparados porque vienen una lista de funciones para nuestro motor grafico, hasta ahora solo tenemos un par de estructuras y unos tipos, asi como una funcion de error y otra para establecer la pantalla activa pero es hora de crear funciones que nos permitan manejar a las estructuras que creamos.
Recuerdas que creamos una pantalla principal (front surface) y una pantalla de fondo (back surface) usando unos objetos de directdraw ? Pues bien, si no lo recuerdas tienes muy mala memoria ya que hace poco lo vimos, pero el punto es que estas pantallas pueden ser accedidas desde punteros y es esa forma de trabajar la que usaremos, una pantalla ocupa en total 614, 400 bytes de memoria, ((640*480)*2) multiplicamos por dos debido a que cada pixel de la pantalla ocupa 2 bytes de memoria, para poder acceder a estas posiciones de memorias sera necesario hacerle un LOCK a nuestra pantalla para que no cambie su posicion de memoria, ya que esta posicion cambia constantemente en Direct-Draw.
El unico problema de hacerle un LOCK a una pantalla es que no puede permanecer mucho tiempo en este estado, ya que eso podria ocasionar fallos en el sistema, asi que tendremos que cambiar nuestro algoritmo para pintar dentro del juego:
- Se bloquea la pantalla de fondo haciendole un LOCK
- Se obtiene su posicion de memoria y se almacena en un puntero
- Se pintan los graficos sobre la pantalla de fondo
- Se desbloquea la pantalla de fondo
- Se copia el contenido de la pantalla de fondo a la pantalla principal
- Se vuelve al paso Num. 1
Es hora de hacer algunas funciones para el manejo de las estructuras Screen en nuestro engine grafico.
Empezaremos con la funcion "NewScreenWin" que recibe dos parametros, una estructura del tipo Screen y el tamaño de la pantalla (que por lo normal sera de 640, 480), en esta funcion reservaremos memoria usando la funcion "new" para cada uno de los 480 punteros que contendra al principio de cada linea.
Para el paso #1 y #2 de nuestro ciclo de juego crearemos una nueva funcion llamada "PrepareRealScreen" la cual se llama al principio de cada ciclo del programa, que hace un LOCK al backsurface y guarda la direccion de memoria del primer punto de pantalla en un puntero a WORD (palabra, 2 bytes) llamado screenpointer, despues de esto llamaremos a una funcion que tmb vamos a crear de nombre "UpdateScreenPointers".
Esta funcion crea una tabla de 480 punteros cada uno con la direccion de memoria donde esta el principio de cada linea de la pantalla, esta funcion recibe dos parametros, una estructura del tipo "Screen" y un puntero al primer pixel de la pantalla (tipo WORD)
La funcion "UpdateRealScreen" es en la que se desbloquea la pantalla de fondo y se copia su contenido a la pantalla principal usando la funcion "Blt" de DirectDraw Para poder copiar el contenido de la pantalla de fondo a la pantalla principal, la pantalla de fondo NO puede estar bloqueada
Recuerda que estas funciones deben ir en nuestro mecanismo grafico, engine.cpp asi que no olvides anexarlo a lo poco que ya llevamos.
------------------------------- mas para el engine.cpp
void NewScreenWin(Screen& SCR,WORD AX,WORD AY)
{
if ((SCR.Lines=new ScreenLines[AY])==NULL)
{
Error("no hay memoria en newscreen","");
}
else
{
SCR.AX=AX;
SCR.AY=AY;
}
}
void UpdateScreenPointers(Screen& SCR,WORD *pointer)
{
int Y;
for (Y=0;Y<SCR.AY;Y++)
{
SCR.Lines[Y]=pointer;
pointer+=(long)(SCR.AX);
}
}
/* Esta es una funcion sencilla, no recibe parametros y solamente
se emplea una variable tipo puntero a palabra llamado screenpointer
mediante la funcion Lock que nos ofrece direct draw podremos bloquear
la pantalla de fondo, y llamaremos a una funcion llamada UpdateScreenPointers*/
void PrepareRealScreen()
{
WORD *screenpointer;
lpDDSBack->Lock(NULL, &ddsd, 0, NULL);
screenpointer=(WORD *)ddsd.lpSurface;
UpdateScreenPointers(pantallafisica,screenpointer);
}
void UpdateRealScreen()
{
//Se crean algunas estructuras de datos necesarias
HRESULT ddrval;
RECT rcRect;
LPDIRECTDRAWSURFACE pdds;
lpDDSBack->Unlock(ddsd.lpSurface);
rcRect.left = 0;
rcRect.top = 0;
rcRect.right = 640;
rcRect.bottom = 480;
pdds = lpDDSBack;
while(1)
{
ddrval = lpDDSPrimary->Blt(&rcRect, pdds, &rcRect, 0, 0);
if(ddrval == DD_OK)
{
break;
}
}
}
--------------------------------- fin de lo nuevo
Ok, ahora que podemos reservar memoria para nuestras pantallas y actualizar los punteros a sus lineas ocuparemos una funcion que sea capaz de dibujar sobre estas, para eso vamos a crear la funcion "PutPixel" que sera la ultima que creemos para nuestro engine.cpp en este capitulo.
Esta sencilla funcion lo unico que hace es recibir 3 parametros, que son las coordenadas (X, Y) y el color que usara para pintar ese punto en la pantalla, el color debe ser un numero de 0 a 65535.
---------------------------->agregalo a engine.cpp
void PutPixel(WORD x, WORD y, WORD color)
{
(*(pantallaenuso->Lines[y]+x))=color;
}
----------------------------> Fin de lo nuevo
como ves, fue facil pintar sobre nuestro monitor ahora que disponemos de varias funciones para el acceso a la pantalla, es hora de volver a nuestro archivo principal (game.cpp) y hacerle los cambios necesarios para que aproveche estas nuevas rutinas de graficos, ademas de, esta vez iniciar DIrectX
5.4 Levantando a DirectX
Bueno, es hora de volver al game.cpp y a usar directx, hace un par de capitulos definimos unos objetos de directx en nuestro codigo fuente, pero el hacer referencia a estos objetos en el codigo no quiere decir que ya estemos haciendo uso de directx.
Para hacer uso de directx y sus funciones es necesario recurrir a una serie de funciones propias de esta libreria, que en realidad no son necesarias entender perfectamente para poder programar juegos, es tal como la programacion que hicimos con la Api WIN32... aunque nuestros juegos lleven estos codigos no es necesario entenderlos al 100% para poder sacarles provecho.
Antes que nada, vamos a crear una funcion que finaliza los objetos de directx que iniciemos.
Porque hacer primero una funcion que finalize los objetos antes que una que los inicialize?
Pues bien, esto se hace porque en caso de crear los objetos y no finalizarlos es casi un hecho que el sistema se bloquearia al no poder recuperar el control de la pantalla.
Aparte de la funcion "finiObjects" que es la que se encargara de finalizar nuestros objetos directx sera necesario añadir un par de variables globales a nuestro archivo game.cpp, estas variables seran "programstatus" y las variables X, Y, y Color que usaremos para la funcion PutPixel.
La variable programstatus la usaremos para indicarle a nuestro juego en que estado está, el primer estado es un estado de inicializacion y el segundo estado es el estado de ejecucion de las funciones propias del juego. Mas adelante crearemos una funcion que haga uso de esta variable de estado
Aqui estan las cosas nuevas que se han hablado hastaahora:
--------------------------------> Esto es para el game.cpp
//Se incluyen las funciones y estructuras que creamos
#include "engine.cpp"
//Estas variables deben ser declaradas globalmente
long programstatus=1;
WORD x,y,color;
//Esta funcion es llamada al finalizar el programa y en ella
//se destruyen objetos que habian sido creados tales como
//las pantallas de directdraw y el propio objeto DirectDraw
static void finiObjects( void )
{
if( lpDD != NULL )
{
if( lpDDSPrimary != NULL )
{
lpDDSPrimary->Release();
lpDDSPrimary = NULL;
}
lpDD->Release();
lpDD = NULL;
}
}
--------------------------------> Fin de lo nuevo
Ahora, vamos a crear 2 funciones mas en nuestro archivo game.cpp La primera de ellas se trata de la funcion GameInit() esta es una funcion muy simple la cual no recibe ningun parametro y solo llama a 2 funciones que ya creamos con anterioridad en nuestro engine y se trata de NewScreenWin y SetActiveScreen, esto con el fin de reservar la memoria para los punteros que apuntan al principio de cada linea que usa la pantalla de fondo, como no se ha reservado ninguna otra pantalla sera esta la que se use como pantalla activa y asi todas las operaciones graficas que hagamos re realizaran sobre la pantalla de fondo.
La siguiente funcion sera la funcion "game" , esta funcion en realidad ya la habiamos creado desde el capitulo sobre la WinAPI, pero no habiamos puesto nada en el (checa el capitulo anterior para que lo verifiques) Es en esta funcion game() donde se hace uso de la variable global programstatus y esto es debido a que nuestro juego tiene varios estados (o los tendra mas bien)
En el estado 1 lo usaremos para inicializacion, e inmediatamente pasaremos al estado 2 donde se hace una llamada a la funcion que creamos "PrepareRealScreen" que era donde obteniamos el puntero a nuestra pantalla, despues de obtener el puntero vamos a generar coordenadas X y Y de forma aleatoria usando el rand(), tambien vamos a obtener un color de forma aleatoria llamando nuevamente a rand()una vez hecho esto se copia la pantalla de fondo sobre la pantalla principal usando la funcion UpdateRealScreen.
---------------------------->Nuevo para game.cpp
//Aqui estaran las funciones de inicializacion que creemos
void GameInit()
{
//Primero creamos una tabla de 480 punteros que apuntan a cada linea
NewScreenWin(pantallafisica,640,480);
//Y aqui activamos la pantallafisica como la pantalla en uso
SetActiveScreen(pantallafisica);
}
/* El nucleo de nuestro juego, tiene dos estados por ahora, en el primer
estado solo pasamos al estado #2 por ahora, en el estado dos vamos a obtener
el puntero a la pantalla y posteriormente vamos a obtener coordenadas
aleatorios donde pintaremos puntos de distintos colores. despues de esto
actualizamos el contenido de la pantalla usando la funcion UpdateRealScreen */
void game(void)
{
if(programstatus==1)
{
programstatus=2;
}
else
if(programstatus==2)
{
PrepareRealScreen();
x=rand()%640;
y=rand()%480;
color=rand();
PutPixel(x,y,color);
UpdateRealScreen();
}
}
---------------------------->Fin de lo nuevo
Bueno, pero aun falta algo mas. este capitulo esta dedicado a DirectX y aun nos falta algo.. que sera.. !! no hemos inicializado directx! solo creamos sus objetos y funciones para manejar el contenido de la pantalla, pero es hora de hacer la inicializacion de directx antes de concluir con este capitulo.
pues bien, tal como paso en el capitulo de WinAPI la inicializacion de directx va a llevar una serie de pasos y parametros que aqui no vamos a explicar a fondo, pero no olvides que puedes checar la Documentacion de DirectDraw contenida en el SDK de DirectX. A nosotros por ahora solo nos interesa activar las dos pantallas que creamos en una resolucion de 640 x 480 x 16 bits de color y checar que todo este funcionando correctamente.
Para esto, lo primero que haremos sera crear un objeto DirectDraw haciendo una llamada a la funcion "DirectDrawCreate" que es una de las funciones de libreria de directx despues vamos a establecer el nivel de Cooperacion de nuestro juego haciendo una llamada a "SetCooperativeLevel" donde usaremos los parametros "DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN" que nos indican que el juego funcion de forma exclusiva y a pantalla completa, esto lo hacemos para evitar que alguna otra aplicacion pueda dañar el desempeño de nuestro juego.
La resolucion y el color lo definiremos con la funcion "SetDisplayMode" que como dijimos sera de 640x480x16, posteriormente crearemos nuestras dos pantallas, el FrontSurface y el BackSurface. Aqui cabe aclarar que aunque se pudo haber creado las dos pantallas en la memoria de la tarjeta de video solo el frontsurface se ha creado ahi, mientras que el BackSurface se ha creado en la propia memoria RAM de la computadora, asi, los acceso al BackSurface seran mas rapidos en la mayoria de las ocasiones (a menos que tengas una tarjeta de nueva generacion) y para compatibilidad con tarjetas de video antiguas.
Si todas estas inicializaciones salieron bien, finalmente se llama a la funcion que creamos "GameInit" donde se realizan otras inicializaciones que ya vimos y despues se devuelve TRUE, en caso de que algo haya salido mal se imprime un mensaje de error y se finalizan los objetos usando nuestra funcion "finiObjects" despues destruimos la ventana y devolveremos FALSE. Todo esto, va en la funcion "doInit" que creamos en nuestro capitulo de la WinAPI, asi que habra que borrar esta funcion de nuestro source code y remplazarla con la que pondre aqui, que ya incluye el codigo para inicializar directX o devolver un mensaje de error en caso de que no se pueda inicializar.
-------------------------> remplaza la funcion doInit del game.cpp con esta:
static BOOL doInit( HINSTANCE hInstance, int nCmdShow )
{
//Estructuras de datos necesarias para crear la ventana.
WNDCLASS wc;
//Estructuras usadas por Direct-Draw.
DDSCAPS ddscaps;
HRESULT ddrval;
//para manejar los posibles mensajes de error
char buf[256];
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hbrBackground = NULL;
wc.lpszMenuName = NAME;
wc.lpszClassName = NAME;
RegisterClass( &wc );
hwnd = CreateWindowEx(
WS_EX_TOPMOST,
NAME,
TITLE,
WS_POPUP,
0, 0,
GetSystemMetrics( SM_CXSCREEN ),
GetSystemMetrics( SM_CYSCREEN ),
NULL,
NULL,
hInstance,
NULL );
if( !hwnd )
{
return FALSE;
}
ShowWindow( hwnd, nCmdShow );
UpdateWindow( hwnd );
//creamos el objeto DirectDraw
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
//se comprueba si salio bien
if( ddrval == DD_OK )
{
//activa el modo exclusivo y a pantalla completa
ddrval = lpDD->SetCooperativeLevel( hwnd,
DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );
if(ddrval == DD_OK )
{
//queremos una resolucion de 640x480x16
ddrval = lpDD->SetDisplayMode( 640, 480, 16 );
if( ddrval == DD_OK )
{
//Creamos el frontsurface y el backsurface
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP | DDSCAPS_SYSTEMMEMORY |
DDSCAPS_COMPLEX;
//Se crea una sola pantalla de fondo cuya memoria estara en la RAM
ddsd.dwBackBufferCount = 1;
ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval == DD_OK )
{
//se obtiene el puntero al BackSurface
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps,
&lpDDSBack);
if( ddrval == DD_OK )
{
//si todo salio bien se inicializa el resto del juego
//y devuelve TRUE
GameInit();
return TRUE;
}
}
}
}
}
//En caso de error imprimimos un mensaje y destruimos lo que se haya creado
wsprintf(buf, "Fallo al inicializar Direct-Draw (%08lx)\n", ddrval );
MessageBox( hwnd, buf, "ERROR", MB_OK );
finiObjects();
DestroyWindow( hwnd );
return FALSE;
}
------------------------------------> Fin
Listo, nuestro "juego" esta completo y funcionando, lo unico que hace es imprimir puntos de colores en coordenadas aleatorias, no es nada espectacular aun no ? bueno esto va por pasos, y el siguiente paso para el proximo capitulo sera crear rutinas que puedan leer y cargar graficos que tengamos en el disco duro.
Un comentario sobre este capitulo, hay ocasiones en que aunque tu codigo esta bien, el compilador nos manda errores, esto es un error comun y se debe a que el codigo no esta ordenado como deberia de ser, por ejemplo, es necesario crear los objetos LPDIRECTDRAWSURFACE y demas antes de llamar al #include engine.cpp porque ? pues bien, en el engine.cpp hacemos uso de estos objetos tmb, y si aun no hemos declarado el frontsurface y el backsurface entonces va a mandar errores, es por eso que primero debemos declarar los objetos DirectDraw y despues llamar al engine.cpp asi que si despues de copiar el codigo de este texto te manda mensajes de error, fue probablemente porque algo no esta en su lugar correcto, porque el codigo de que compila compila.
EOC (end of chapter)
6. Trabajando con graficos
Bien, si eres un fanatico de los juegos dudo mucho que el haber imprimido puntos de colores te haya hecho gritar de felicidad, pero no te preocupes que las cosas estan a punto de cambiar. En este capitulo veremos varias cosas nuevas, analizaremos el formato de imagenes "TGA" y crearemos funciones para cargar este tipo de imagenes. Tambien crearemos mas funciones para manejar las pantallas en nuestro engine grafico y crearemos arreglos de pantallas para poder almacenar grafico en c/u de ellas.
6.1: Formato de color
Para poder cargar las imagenes .TGA que usaremos el programa tiene que leer cada uno de sus pixeles y convertirlos a un formato de 16 bits de color, esto es un poco complejo ya que algunas tarjetas usan un formato de 5 bits para el rojo, 5 bits para el verde y 5 bits para el azul (total de 15 bits) en estos casos, hay que desplazar cada componente 3 bits a la derecha, esto se nos complica mas teniendo en cuenta que tambien muchas tarjetas usan 6 bits para el verde con un total de 64 tonos.
Para saber si la tarjeta usa 5 o 6 bits de verde tendremos que usar algunos objetos y funciones de DirectDraw. Con dicha informacion, se obtienen 3 mascaras de bits (rojo verde y azul). En esta parte, llamaremos a una funcion que aun no vamos a crear, se trata de la funcion "GetBits" con la cual podremos obtener el numero de la posicion del ultimo bit puesto a 1, en otras palabras, al pasarle una de las 3 mascaras de color obtendremos el ultimo bit de dicho color, los dos formatos mas usados en las tarjetas de video es 5-5-5 y 5-6-5 y es muy poco probable que uses alguna tarjeta distinta a esto.
Ten cuidado ala hora de agregar estos fragmentos de codigo, ya que si no van en el lugar preciso podria producirse algun error a la hora de compilar el juego, algunas variables ya les habiamos incluido en capitulos anteriores pero lo vuelvo a hacer para facilitar la comprension de donde deberiamos agregar el codigo, pero evita repetir declaraciones de variables, la ultima parte del codigo que anexo debe estar al final de la funcio doInit, aqui en esta parte es donde reconocemos el formato de color de nuestra tarjeta. Te recomiendo checar el archivo fuente que anexo para poder ver bien en que parte va ese fragmento de codigo.
Para mayor detalle respecto a formato de pixeles y colores checa la documentacion de directx o enviame un mail pidiendo un articulo respecto a este tema y no dudes que hago un pequeño texto dedicado a eso : -)
---------------------------------> Agrega al game.cpp donde se indique
//Esta variable desde el 1er capitulo la hemos usado
//No la declares 2 veces, la vuelvo a anotar para indicar en que parte va lo demas
HWND hwnd;
//Objetos para utilizar Direct-Draw
LPDIRECTDRAW lpDD; // Objeto directdraw principal
LPDIRECTDRAWSURFACE lpDDSPrimary; // pantalla principal (frontsurface)
LPDIRECTDRAWSURFACE lpDDSBack; // pantalla de fondo (backsurface)
DDSURFACEDESC ddsd; // descriptor de la pantalla
BOOL bActive; // almacena si la aplicacion esta activa
DDPIXELFORMAT PixelFormat; //Esta parte es nueva y sirve para
//almacenar el formato de color
//contendra los bits de rojo, verde y azul que usa la tarjeta grafica
long bitsrojo,bitsverde,bitsazul;
//Se incluyen las estructuras de datos y funciones que se
//han creado para hacer juegos.
#include "engine.cpp"
//Variable que almacena el estado de nuestro juego
long programstatus=1;
//estas variables las usaremos solo para este capitulo
long x,y,x2,y2,p_count;
//Antes en doInit solo llamabamos a GameInit en esta parte
//Con esto ahora tmb sabremos que formato de color usa la tarjeta
//La funcion GetBits la crearemos un poco mas adelante en este capitulo
//y repito, checa el archivo fuente que incluyo para evitar confusiones
//respecto a como queda acomodada la funcion doInit
if( ddrval == DD_OK )
{
//A continuacion se hace una comprobacion del formato
//de pixel que usa la tarjeta grafica en la resolucion
//de 16 bits de color, ya que existen tarjetas que
//usan 5 bits para el verde y otras que usan 6 bits
lpDDSBack->Lock (NULL, &ddsd,
DDLOCK_SURFACEMEMORYPTR | DDLOCK_WAIT, NULL);
PixelFormat = ddsd.ddpfPixelFormat;
lpDDSBack->Unlock (ddsd.lpSurface);
//bitsazul bits para azul, siempre deberia ser 5
bitsazul = GetBits(PixelFormat.dwBBitMask);
//bitsverde contiene el numero de bits para el verde
//que podra ser 5 o 6, dependiendo de tu tarjeta
bitsverde = GetBits(PixelFormat.dwGBitMask)-bitsazul;
//bitsrojo son los bits para el rojo, siempre 5.
bitsrojo = GetBits(PixelFormat.dwRBitMask)-bitsazul-bitsverde;
//si todo salio bien se inicializa el resto del juego y
//devuelve TRUE
GameInit();
return TRUE;
}
}
}
}
}
//En caso de error imprimimos un mensaje y destruimos lo que se haya creado
wsprintf(buf, "Fallo al inicializar Direct-Draw (%08lx)\n", ddrval );
MessageBox( hwnd, buf, "ERROR", MB_OK );
finiObjects();
DestroyWindow( hwnd );
return FALSE;
}
-------------------------------------------->Fin de lo nuevo
6.2 Trabajando con multiples pantallas y mejorando nuestro engine
Bueno, es hora de hacer cosas un poco mas grandes, esta vez vamos a crear una tabla (array) de pantallas tipo Screen en cada una de las cuales podremos almacenar imagenes.
Hasta ahora, hemos trabajado con dos surfaces:
- La pantalla fisica o FronBuffer la cual esta almacenadaen la memoria de nuestra tarjeta grafica y es controlada por DirectDraw
- La pantalla de fondo o BackBuffer, tambien manejada por DirectDraw pero a la cual podemos accesar mediante nuestra estructura del tipo "Screen" que creamos anteriormente.
Ahora, procederemos a crear una tabla de pantallas en nuestro engine.cpp, con una par de sencillas lineas de codigo:
-------------------------------------------> Agregalo a engine.cpp
//No olvides que primero debe estar declarada la estructura Screen en nuestro fuente
//o DirectDraw no reconocera nuestro tipo de datos
//Esto debe ir despues de declarar la estructura "Screen"
#define maxpantallas 999
//espacio reservado para las pantallas que crearemos
Screen pantalla[maxpantallas];
------------------------------------------->Fin de lo nuevo
Ahora que disponemos de una tabla de pantallas, ocupamos un par de funciones nuevas que nos permitiran hacer cosas muy interesnates con ellas. Anteriormente, creamos una funcion llamada "NewScreenWin" ahora crearemos una funcion similar, llamada "NewScreen"
Cual es la diferencia entre estas dos funciones ? bien, con NewScreenWin solo puede ser usada para acceder a la pantalla de fondo o BackSurface mientras que la funcion NewScreen sirve para reservar la memoria para todas las pantallas adicionales que nosotros creemos (como el arreglo de pantallas que acabamos de definir)
Supongamos que quieras crear una pantalla nueva con dimensiones de 300 x 100 la funcion "NewScreen" hace lo siguiente:
- a) Almacena en AX y AY el ancho y el alto de la pantalla
- b) Reserva memoria para el numero de punteros que es igual al numero de lineas de altura (100 en este caso, como cada puntero usa 4 bytes seran 400 bytes)
- c) Reserva memoria para cada una de las lineas, (300 en este caso, cada linea es de tipo WORD que equivale a 2 bytes asi que seran 600 bytes por linea)
Si deseas saber la cantidad total de memoria que va a reservar esta funcion recurrimos a una simple formula:
400 + ( 600 bytes * 100 lineas ) = 60,400 bytes
Antes de proceder aver el codigo de esta funcion es necesario hacer algunas aclaraciones, antes que nada recuerda que no podras acceder a ninguna de nuestras pantallas si no haz reservado su memoria previamente con nuestra funcion "NewScreen", tampoco se puede reservar memoria para la misma pantalla dos veces, y finalmente, se necesita limpiar la memoria de cada pantalla que creemos, para esto vamos a crear una funcion llamada "ClearScreen" que es muy sencilla, solo recibe un parametro (Una estructura Screen) y usando un par de ciclos for y el "delete" limpiara la memoria que habiamos reservado.
He aqui el codigo fuente de este par de funciones(puedes poner estas funciones despues de "NewScreenWin" que creamos anteriormente)
-------------------------------agregalo al engine.cpp ----------------
//Esta funcion reserva la memoria para una pantalla. A diferencia de la
//funcion NewScreenWin, aqui es necesario reservar la memoria para cada una
//de las lineas, ya que las pantallas creadas en esta funcion estan
//contenidas en la memoria del ordenador y no se corresponden con ninguna
//pantalla fisica.
void NewScreen(Screen& SCR,WORD AX,WORD AY)
{
WORD Y;
//Se reserva la memoria para los punteros a lineas.
//Si no se puede reservar la memoria, se genera un error.
if ((SCR.Lines=new ScreenLines[AY])==NULL)
{
Error("no hay memoria en newscreen","");
}
//Estas variables contienen el tamaño de la pantalla, por lo general
//sera 640*480
else
{
SCR.AX=AX;
SCR.AY=AY;
//Se recorren todas la lineas.
for (Y=0;Y<AY;Y++)
{
//Se reserva la memoria para cada una de las lineas.
SCR.Lines[Y]=new WORD[AX];
//Se rellena cada linea con el valor 0, que es el color negro.
//El numero de bytes a rellenar es el ancho*2 (porque cada pixel
//emplea un total de 2 bytes).
_fmemset(SCR.Lines[Y],0,AX*2);
}
}
}
//Esta funcion sirve para borrar una pantalla que ha sido creada previamente
//con la funcion NewScreen. Su utilidad es liberar memoria por si dicha pantalla
//va a ser usada de nuevo.
//NOTA: Intentar borrar una pantalla que no ha sido previamente creada puede
//ocasionar un error en la aplicacion.
void ClearScreen(Screen& SCR)
{
WORD Y;
//Se recorren todas la lineas.
for (Y=0;Y<SCR.AY;Y++)
//Se libera la memoria de cada una de las lineas.
if (SCR.Lines[Y]!=NULL)
delete SCR.Lines[Y];
//Se libera la memoria de los punteros a lineas.
if (SCR.Lines!=NULL)
delete SCR.Lines;
}
--------------------------------------- Fin de lo nuevo
Un par de funciones simples pero muy potentes, que nos permitiran el uso de varias imagenes en nuestro mismo juego de ejemplo, como ves su estructura es simple y sus parametros tambien, antes de concluir con estas funciones es importante señalar que si deseas cargar un grafico que no quepa por completo en la pantalla, el programa producira un error y se va a colgar. Por ejemplo, si tratas de cargar un grafico de 640 x 480 en una pantalla de 300 x 100 esto no sera posible y va a producir un error de acceso ilegal a memoria. Ahora pasemos a un par de funciones mas para nuestro engine grafico.
A diferencia de las dos funciones anteriores, la siguiente funcion que veremos si tiene un cierto grado de dificultad. El nombre de esta funcion es "ScreenCopy" que, como su nombre lo indica copia una area rectangular de una pantalla a otra. Con una funcion como esta, es facil hacer animaciones almacenando cada grafico en distintas pantallas con las funciones que creamos anteriormente y despues vamos copiando cada una de esas pantallas al Back Surface con nuestra funcion ScreenCopy.
La complejidad de esta funcion radica en si, en la cantidad y nombre de variables que recibe, puesto que su cuerpu tambien es breve y facil de entender una vez que sepamos de que trata cada variable, a continuacion pongo la lista de parametros que recibe la funcion y para que nos sirve cada uno.
- Screen &Sc: Estructura tipo screen de origen
- Screen &Dt: Estructura tipo screen de destino
- int Xo, Yo, Xo1, Yo1: Estas cuatro variables se refieren a lo mismo, sirven para indicar el area rectangular que se va a copiar que esta en las coordenadas (Xo, Yo)(Xo1, Yo1)
- int Xd, Yd: con este par de variables indicamos las coordenadas (Xd,Yd) de destino para el rectangulo que copiamos.
A continuacion el codigo de esta funcion, puedes ponerla debajo de las 2 funciones que acabamos de crear en el engine.cpp (NewScreen y ClearScreen)
------------------------------ una funcion mas para el engine.cpp
void ScreenCopy(Screen& Sc,Screen& Dt,int Xo,int Yo,int Xo1,int Yo1,
int Xd,int Yd)
{
//Variables de uso local
WORD A,AY=Yo1-Yo+1;
//Recorremos todas las lineas de la pantalla y usamos una funcion
//de copiar memoria (_fmemcpy) ya que es mucho mas rapido que hacerlo
//pixel por pixel. El tamaño en bytes de cada linea es su ancho por 2
int aux=(abs(Xo1-Xo)+1)*2;
for(A=0;A<AY;A++)
_fmemcpy(Dt.Lines[Yd+A]+Xd,Sc.Lines[Yo+A]+Xo,aux);
}
----------------------------- fin de lo nuevo
Listo, ahora solo nos falta un pequeño detalle, si recuerdas hace un par de temas atras (para ser mas exactos, cuando nos metimos en el tema del formato de color usado por nuestra tarjeta de video) dejamos pendientes el creado de una funcion. y ya es hora de hacerla, se trata de la funcion "GetBits". Esta es una funcion muy sencilla que devuelve el numero de bits que usa algun numero, esto lo hace con una serie de divisiones consecutivas.
el codigo de esta funcion es el siguiente:
-------------------------------------->Agregalo al final del engine.cpp
long GetBits(long n)
{
long cont=0;
while(n>0)
{
n=n/2;
cont++;
}
return cont;
}
-------------------------------------->Fin de lo nuevo
6.3: Imagenes .TGA
En este juego vamos a trabajar con imagenes de tipo .TGA porque .TGA ? pues por la sensilla razon de que es bastante facil de manejar a la hora de la programacion de videojuegos, usaremos imagenes .TGA SIN compresion y con color de 24 o 32 bits
Un TGA de 24 bits tiene un byte para el color rojo, un byte para el color verde y un byte para el color azul ( no olvides que un byte son 8 bits ) el TGA de 32 bits hace lo mismo pero el ultimo byte suele ser ignorado o se usa para almacenar un valor alfa o de intensidad que nosotros no ocuparemos Algo peculiar del formato de imagen .TGA es que las imagenes pueden estar invertidas, esto es debido a un bit que existe en la cabecera del archivo (no olvides que una imagen es una serie de puntos acomodados en forma especifica mas su cabecera de archivo) el cual puede activar esta opcion, asi se nos hara necesario, el leer la cabecera del archivo TGA para determinar sus caracteristicas.
Pues bien, hora de crear la estructura que pueda cargar la informacion de una imagen TGA, en esta estructura se carga la cabecera del archivo y con la funcion LoadTGA volcamos todos sus datos a memoria usando elementos basicos de C, como lo son el manejo de archivos y el uso de ciclos for. Si estas interesado en usar otros formatos de imagenes como el jpg, ocuparas algo de documentacion sobre su cabecera, esta documentacion la puedes localizar en internet facilmente, si realmente deseas ayuda con otros formatos manda un email a kobe@emc2h.com y para siguientes ediciones de esta revista publicare las alternativas usando otro tipos de imagenes : ]
Agrega esto al final de lo que llevas del engine.cpp:
----------------------------------------- engine.cpp
//cabecera TGA
typedef struct
{
BYTE idsize; //Normamente 0.
BYTE colormaptype; //Tiene que ser 0.
BYTE image; //Tiene que ser 2.
BYTE colormapspec[5]; //Se ignora.
WORD Xorigin; //Origen X.
WORD Yorigin; //Origen Y.
WORD AX; //Ancho.
WORD AY; //Alto.
BYTE bitsperpixel; //Tiene que ser 24 o 32.
BYTE imagedesc; //Se ignora.
} TGA;
//Esta funcion carga una pantalla TGA que este sin comprimir y en
//formato de 24 o 32 bits sobre la pantalla en uso a partir de las
//coordenadas indicadas por X e Y.
void LoadTGA(char *name,long x,long y)
{
TGA h;
FILE *f;
//Variables de uso interno en la funcion.
long pixels,size,xx,yy;
WORD rojo,verde,azul,color;
BYTE *buffer,*pointer,id[256];
//si no encontramos el archivo, imprimimos un error
if ((f=fopen(name,"rb"))==NULL)
Error("fichero no encontrado en LoadTGA:",name);
//Se leen los 18 bits que forman la cabecera del fichero.
fread(&h,18,1,f);
//En este caso, el formato TGA no esta soportado y se genera un error.
if( (h.colormaptype!=0)||(h.image!=2) )
Error("error al leer un TGA",name);
//Si la pantalla TGA no esta en 24 bits ni en 32 bits, se genera un error.
if( (h.bitsperpixel!=24)&&(h.bitsperpixel!=32) )
Error("error al leer un TGA, numero incorrecto de bits",name);
//En caso de que exista una descripcion del fichero TGA, se lee.
if(h.idsize!=0)
fread(id,h.idsize,1,f);
//Se calcula el numero de pixeles que tiene la pantalla (ancho por alto).
pixels=((long)h.AX)*((long)h.AY);
//Se calcula el numero de bytes que usa la pantalla (pixeles*bits_por_pixel/8).
size=pixels*((long)h.bitsperpixel)/8;
//Se reserva la memoria para contener la pantalla en si.
if ((buffer=new BYTE[size])==NULL)
{
Error("no hay memoria en LoadTGA","");
}
//Se lee la totalidad de la pantalla dentro de buffer.
fread(buffer,size,1,f);
//Pointer es un puntero temporal que es actualizado con el valor de buffer.
pointer=buffer;
//Con dos bucles anidados se recorren todos los pixeles de la pantalla.
for(yy=0;yy<h.AY;yy++)
for(xx=0;xx<h.AX;xx++)
{
//Se obtienen los colores azul, rojo y verde en valores comprendidos
//entre 0 (minimo) y 255 (maximo).
azul=(WORD)(*pointer++);
verde=(WORD)(*pointer++);
rojo=(WORD)(*pointer++);
//Los valores de rojo, verde y azul son divididos entre cuatro para
//facilitar su conversion (0=minimo, 63=maximo).
azul=(azul>>2);
verde=(verde>>2);
rojo=(rojo>>2);
//Ahora la variable color va a contener el resultado de convertir los
//tres colores a un solo valor de 16 bits. Este valor tendra que ser
//calculado de manera diferente si la tarjeta grafica usa 6 bits o
//5 bits de color verde.
if(bitsverde==6) //6 bits de verde.
color=(azul>>1)+(verde<<5)+((rojo>>1)<<11);
else
if(bitsverde==5) //5 bits de verde.
color=(azul>>1)+((verde>>1)<<5)+((rojo>>1)<<10);
//Ahora se imprime cada pixel en pantalla.
//Si el bit 5 de h.imagedesc es igual a 0, la imagen estara almacenada
//de abajo a arriba. En el otro caso, la imagen esta almacenada de arriba
//a abajo.
if((h.imagedesc&&0x20)==0)
PutPixel(xx+x,(h.AY-yy-1)+y,color);
else
PutPixel(xx+x,yy+y,color);
//Si la pantalla esta en modo de 32 bits, el cuarto y ultimo byte de cada
//pixel tiene que ser saltado, ya que no contiene ningun color.
if(h.bitsperpixel==32)
pointer++;
}
//Se libera la memoria usada por el buffer temporal.
delete buffer;
//Se cierra el fichero.
fclose(f);
}
-------------------------------------------------------- fin de lo nuevo
6.4 Modificando nuestra funcion game()
Ahora que tenemos todas estas funciones, solo nos resta hacer algunos cambios a nuestro programa principal paraque pueda hacer uso de ellas, como ya lo comentamos nuestra funcion game() tendra dos estados.
En el primer estado, vamos a reservar 3 pantallas y despues vamos a ir imprimiendo un grafico en cada una de ellas. Para esto primero crearemos cada pantalla usando la funcion "NewScreen", posteriormente la estableceremos como la pantalla activa usando "SetActiveScreen" y finalmente cargaremos en ella una imagen usando nuestra funcion "LoadTGA".
En el segundo estado del programa, se realiza la impresion de cada imagen de forma ciclica, cada imagen se va a imprimir unay otra vez. Usaremos la funcion "ScreenCopy" para copiar cada una de estas imagenes a la pantalla fisica en cuadros de 40 x 40 puntos. Aunque lo hacemos en cuadros de 80x80 pudimos haberlo hecho usando el tamaño que queramos, siempre y cuando no se exceda del tamaño de nuestras pantallas reservadas, solo recuerda que mientras mas pequeño sea el area que vamos a ir copiando, mas lenta sera la impresion del grafico (usando dimensiones de 20 x 20 seria algo lento el ciclo de nuestras pantallas que cambian)
El codigo con algunos comentarios es el siguiente:
---------------------------Funcion "game" en el game.cpp
void game(void)
{
//El primer estado es de inicializacion.
//Pasa al estado numero dos despues de realizar dicha inicializacion.
if(programstatus==1)
{
//Se reserva memoria para la pantalla 1.
NewScreen(pantalla[1],640,480);
//La pantalla activa pasa a ser la pantalla 1. Es necesario para que los
//graficos que se van a cargar pasen a esta pantalla.
SetActiveScreen(pantalla[1]);
//Se carga el grafico "imagen1.TGA" en la pantalla activa (que es la 1).
LoadTGA("imagen1.TGA",0,0);
//Se repite el mismo proceso con la pantalla 2.
NewScreen(pantalla[2],640,480);
SetActiveScreen(pantalla[2]);
LoadTGA("imagen2.TGA",0,0);
//Igual con la pantalla 3.
NewScreen(pantalla[3],640,480);
SetActiveScreen(pantalla[3]);
LoadTGA("imagen3.TGA",0,0);
//La pantalla fisica pasa a ser la pantalla activa.
SetActiveScreen(pantallafisica);
//Se inicializan los valores de X e Y.
x=0;
y=0;
//El contador de nuestras pantallas lo inicializamos a 1
p_count=1;
//Se pasa al estado de programa numero 2.
programstatus=2;
}
else
if(programstatus==2)
{
//Obtiene el puntero a la pantalla
PrepareRealScreen();
//Incrementa la coordenada X
x++;
//Cuando la coordenada X pasa de 7, se pone a 0 y se incrementa en uno
//la coordenada Y.
if(x>7)
{
x=0;
y++;
//Cuando la coordenada Y pasa de 5, se pone a 0 y se incrementa en uno
//el contador de pantalla p_count (rotamos de imagenes)
if(y>5)
{
y=0;
p_count++;
//Cuando p_count pasa de 3, se pone a 1. Con esto las pantallas se
//imprimen de forma ciclica
if(p_count>3)
p_count=1;
}
}
//Se va a copiar la pantalla en trozos cuadrados de 80 pixeles.
//X va de 0 hasta 7.
//7*80=560 sera el principio del ultimo cuadro de cada linea.
//Y va de 0 hasta 5.
//5*80=400 sera el principio del ultimo cuadro de cada columna.
x2=x*80;
y2=y*80;
//Como cada cuadrado es de 80 pixeles, la coordenada final de X y de Y
//sera X+79 e Y+79 respectivamente.
ScreenCopy(pantalla[p_count],pantallafisica,x2,y2,x2+79,y2+79,x2,y2);
//Actualiza la pantalla, copiando el BackSurface sobre el FrontSurface
UpdateRealScreen();
}
}
--------------------------- Fin de lo nuevo
Bueno, con esto damos por terminado este capitulo que fue algo extenso, pero muy provechoso. Ahora ya estas listo para empezar a imprimir tus graficos en pantalla y algunas animaciones. Algo que olvidaba mencionar, es que los archivos "Imagen1.tga" "Imagen2.tga" e "Imagen3.tga" deben estar en la misma carpeta que nuestro codigo fuente, yo incluire algunas imagenes de ejemplo para esta labor, pero puedes remplazarla siempre y cuando te apegues a lo que hemos dicho respecto a los formatos de imagenes.
7. Empezando a crear nuestro scroll
El capitulo exterior fue algo extenso y vimos varias cosas nuevas, pero este sera breve. En este capitulo solo veremos una funcion nueva para nuestro engine y algunas modificaciones a nuestra funcion "game". De hecho, en realidad solo vamos a mejorar el concepto de lo que vimos el capitulo anterior, pero como ya se habian visto varias cosas nuevas de golpe, considere que era mejor tratar esto en un capitulo nuevo.
Si eres curioso, probablemente ya hiciste algunos experimentos con imagenes propias en nuestro programa anterior, y quizas ya trataste hacerle algunos cambios al codigo fuente. Probablemente en tus experimentos hayas obtenido algunos errores de acceso ilegal a memoria, debido a que tratabamos de imprimir en un area de la pantalla mas grande que la propia pantalla que habiamos reservado.
En este capitulo resolveremos algunos de estos problemas, con una mejora a la funcion "ScreenCopy". Crearemos una funcion nueva llamada "ScreenCopyClip" la cual es similar a ScreenCopy pero con la diferencia de que no genera ningun error de memoria aun si algunos de los trozos que quieras copiar quedan fuera de la pantalla.
Pero bueno, esto no es tan sencillo como parece, ya que la funcion ScreenCopyClip es algo compleja y confusa, ya que inclue bastantes IFs anidados, si el area queda totalmente fuera, no se realiza ninguna copia. Con los demas IFs se recorta el area a copiar a los limites de la pantalla de origen. Para lograr entender bien todos los IFs sera necesario leer una y otra vez el contenido de esta funcion, pero este es el inicio para crear un scroll en movimiento. El codigo de la funcion ScreenCopyClip lo presento a continuacion, agrega esta funcion a tu engine.cpp despues de la funcion "ScreenCopy"
-------------------------------------- Nuevo al engine.cpp
//Funcion que recorta el area a copiar a los limites de cada pantalla
//y despues llama a la funcion ScreenCopy con los valores recortados
void ScreenCopyClip(Screen& Sc,Screen& Sd,int Xo,int Yo,int Xo1,int Yo1,
int Xd,int Yd)
{
int Xd1=Xd+(Xo1-Xo),Yd1=Yd+(Yo1-Yo);
//Primer y ultimo pixel valido de cada pantalla en sus coordenadas X e Y.
int ScClipX=0,ScClipX1=Sc.AX-1;
int ScClipY=0,ScClipY1=Sc.AY-1;
int SdClipX=0,SdClipX1=Sd.AX-1;
int SdClipY=0,SdClipY1=Sd.AY-1;
//Si el area a copiar esta totalmente fuera de la pantalla de origen
//o de la pantalla de destino, no se copia nada.
if( (Xo>ScClipX1)||(Yo>ScClipY1)||(Xo1<ScClipX)||(Yo1<ScClipY) )
return;
if( (Xd>SdClipX1)||(Yd>SdClipY1)||(Xd1<SdClipX)||(Yd1<SdClipY) )
return;
//Se recorta el area a copiar a los limites de la pantalla de origen.
if(Xo<ScClipX)
{
Xd+=(ScClipX-Xo);
Xo=ScClipX;
}
if(Xo1>ScClipX1)
{
Xd1-=(Xo1-ScClipX1);
Xo1=ScClipX1;
}
if(Yo<ScClipY)
{
Yd+=(ScClipY-Yo);
Yo=ScClipY;
}
if(Yo1>ScClipY1)
{
Yd1-=(Yo1-ScClipY1);
Yo1=ScClipY1;
}
//Se recorta el area a copiar a los limites de la pantalla de destino.
if(Xd<SdClipX)
{
Xo+=(SdClipX-Xd);
Xd=SdClipX;
}
if(Xd1>SdClipX1)
{
Xo1-=(Xd1-SdClipX1);
Xd1=SdClipX1;
}
if(Yd<SdClipY)
{
Yo+=(SdClipY-Yd);
Yd=SdClipY;
}
if(Yd1>SdClipY1)
{
Yo1-=(Yd1-SdClipY1);
Yd1=SdClipY1;
}
//De nuevo se comprueba si el area queda totalmente fuera de la pantalla
//origen.
if( (Xo>ScClipX1)||(Yo>ScClipY1)||(Xo1<ScClipX)||(Yo1<ScClipY) )
return;
//Se hace la misma comprobacion para la pantalla de destino.
if( (Xd>SdClipX1)||(Yd>SdClipY1)||(Xd1<SdClipX)||(Yd1<SdClipY) )
return;
//Se llama a ScreenCopy ya con el area recortada por dichos limites
ScreenCopy(Sc,Sd,Xo,Yo,Xo1,Yo1,Xd,Yd);
}
------------------------------------------ Fin de lo nuevo
Ahora ya que contamos con esta funcion, solo nos resta modificar nuestra funcion game para hacer uso de ella. En nuestro programa principal vamos a imprimir una pantalla que estara fija y no va a cambiar o a moverse, debido a eso la imprimimos usando la funcion "ScreenCopy" antigua, aunque pudimos haber usado "ScreenCopyClip" y no habria problemas. Tambien, imprimiremos dos imagenes mas, esta vez si usando a nuestra nueva funcion "ScreenCopyClip" que entran por un extremo del monitor y salen por el otro. La pantalla fija que tenemos tambien nos sirve para que se limpie la pantalla completa, en caso de quitarla veremos como las otros dos pantallas al moverse van dejando una marca a su paso. Un experimento sugerido seria modificar el programa principal para que imprima una pantalla mucho mas grande que la pantalla fisica, algo asi como un terreno montañoso o algunas baldosas para simular un Scroll de algun escenario de gran tamaño, como no soy muy bueno con el diseño grafico, esta sera tarea para los lectores de este texto.
He aqui el codigo de la nueva funcion game:
-----------------------------------Agregalo al game.cpp
void game(void)
{
if(programstatus==1)
{
//Se reserva memoria para la pantalla 1.
NewScreen(pantalla[1],640,480);
//La pantalla activa pasa a ser la pantalla 1. Es necesario para que los
//graficos que se van a cargar pasen a esta pantalla.
SetActiveScreen(pantalla[1]);
//Se carga el grafico "imagen1.TGA" en la pantalla activa (que es la 1).
LoadTGA("imagen1.TGA",0,0);
//Se repite el mismo proceso con la pantalla 2.
NewScreen(pantalla[2],640,480);
SetActiveScreen(pantalla[2]);
LoadTGA("imagen2.TGA",0,0);
//Igual con la pantalla 3.
NewScreen(pantalla[3],640,480);
SetActiveScreen(pantalla[3]);
LoadTGA("imagen3.TGA",0,0);
//La pantalla fisica pasa a ser la pantalla activa.
SetActiveScreen(pantallafisica);
//Estos valores son las coordenadas iniciales para las dos pantallas que
//se iran moviendo sobre la pantalla fisica.
x=-700;
y=-500;
x2=0;
y2=0;
//Se pasa al estado de programa numero 2.
programstatus=2;
}
else
//El segundo estado reliza la impresion de un punto en pantalla.
if(programstatus==2)
{
//Obtiene el puntero a la pantalla
PrepareRealScreen();
//Se incrementa la coordenada X en la cual se pintara una pantalla.
x+=7;
//Al llegar a un tope, vuelve al valor inicial.
if(x>700)
x=-700;
//Se incrementa la coordenada Y donde se pintara una pantalla
y+=5;
if(y>500)
y=-500;
//Decrementa la coordenada X donde se pintara otra pantalla
x2-=7;
if(x2<-700)
x2=700;
//Se incrementa la coordenada Y donde se pintara otra pantalla
y2+=5;
if(y2>500)
y2=-500;
//La primera pantalla se pinta de forma fija, borrando todo el contenido
//que habia en la pantalla fisica, por eso usamos a ScreenCopy
ScreenCopy(pantalla[1],pantallafisica,0,0,639,479,0,0);
//Las dos pantallas que se pintan ahora, una aparece por el lado
//izquierdo y va bajando hacia la derecha y la otra aparece por el
//lado derecho bajando hacia la izquierda.
//Debido a que parte del area que se copia queda fuera de la pantalla
//fisica, es necesario usar "ScreenCopyClip".
ScreenCopyClip(pantalla[2],pantallafisica,0,0,639,479,x,y);
ScreenCopyClip(pantalla[3],pantallafisica,0,0,639,479,x2,y2);
//Actualiza la pantalla, copiando el BackSurface sobre el FrontSurface
UpdateRealScreen();
}
}
------------------------------------ Fin de lo nuevo
8. Despedida
Bien, esta es una despedida temporal, ya que esto aun no puede llamarse un juego y nos requerira un par de textos mas antes de concluirlo. Por mientras puedes ir practicando con lo visto hasta ahora, para el siguiente texto hay preparadas cosas como: Graficos con transparencia, Manejo de Sprites y animaciones, un scroll mas real, enemigos, y como contralar nuestra nave con el teclado, y quizas algunas cosas mas. Si tienes alguna sugerencia que hacer para este texto, o alguna correccion o mejora que hacerle al codigo fuente, tan solo mandame un email.
Nos vemos hasta entonces
kobe, para emc2h ezine. kobe@emc2h.com