NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WINAPI C++ 2.2 Crear tablero, marcador y mensaje

14 de Mayo del 2010 por Josep Antoni Bover, 0 visitas, 0 comentarios, 0 votos
Categorías : Windows, Programación, C y C++.
La web está en segundo plano, animación en pausa.
Cargando animación...
Tutorial WINAPI C++ 2.2 Crear tablero, marcador y mensaje

En el tutorial anterior se creo una base para poder hacer ventanas translucidas, ahora vamos a utilizar esa base para crear los objetos específicos que necesitaremos para el juego. Necesitamos varios objetos para el juego, pero por el momento empezaremos por estos tres : el Tablero, el Marcador y el Mensaje.

El tablero consistira en una parrilla de cuadrados que sera por donde se movera nuestra serpiente. El marcador se usara para contabilizar los puntos, el recorrido, la velocidad y los frames por segundo. El mensaje nos servira para advertir que el juego esta en pausa, que se ha pasado al siguiente nivel, o que el juego ha terminado

Para empezar veamos la declaración del ObjetoEscena_Marcador :

Archivo : ObjetoEscena_Marcador.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Clase que encapsula una ventana translucida que hace de tablero
class ObjetoEscena_Marcador : public ObjetoEscena_VentanaTranslucida {
public : //////////////////// Miembros publicos
// -Constructor
ObjetoEscena_Marcador(void);
// -Destructor
~ObjetoEscena_Marcador(void);
// -Función para mostrar el marcador
void MostrarMarcador(void);
// -Función que se llama antes de hacer el alphablend en la
// que podemos añadir mas graficos.
virtual void Pintar_AlphaBlend(HDC hDCDestino);
// -Puntuacion total del jugador
UINT Puntuacion;
// -Recorrido total del jugador
UINT Recorrido;
// -Frames por segundo
UINT FPS;
// -Velocidad en MS que tardara en hacer el movimiento la
// serpiente
UINT Velocidad;
};

Como podéis ver esta clase incluye una función MostrarMarcador, que será la encargada de colocar el marcador en la parte superior centrado, y de hacerlo visible. En segundo lugar se ha añadido la función Pintar_AlphaBlend que será donde pintaremos el marcador, y por ultimo se han añadido varios miembros que apuntaran : la puntuación total, el recorrido total, la velocidad en milisegundos que se tarda en hacer un movimiento, y los frames por segundo que se están mostrando.

Veamos la funcion Pintar_AlphaBlend :

Archivo : ObjetoEscena_Marcador.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Esta función se usara para pintar los graficos que se necesiten con AlphaBlend
// NOTA el hDCDestino es un backbuffer creado por la funcion Pintar, por ello no hace falta crear otro buffer de pintado.
void ObjetoEscena_Marcador::Pintar_AlphaBlend(HDC hDCDestino) {
// Declaracion de variables
HFONT Fuente = CreateFont( 16, 0, 0, 0, FW_BOLD, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );
HFONT VFuente = static_cast<HFONT>(SelectObject(hDCDestino, Fuente));
TCHAR Texto[256];
SetBkMode(hDCDestino, TRANSPARENT);
RECT RectaDestino = { 10, 8, Espacio.right, 40 };
RECT RectaDestinoSombra = { 11, 9, Espacio.right, 41 };
// Pintamos la parte izquierda del texto con su sombra
int TamTexto = swprintf_s(Texto, 256, TEXT("Puntuación : %d\nRecorrido : %d"), Puntuacion, Recorrido);
SetTextColor(hDCDestino, RGB(0, 0, 0));
DrawText(hDCDestino, Texto, TamTexto, &RectaDestinoSombra, DT_LEFT);
SetTextColor(hDCDestino, RGB(255, 255, 255));
DrawText(hDCDestino, Texto, TamTexto, &RectaDestino, DT_LEFT);
// Pintamos la parte derecha del texto con su sombra
RectaDestino.left = Espacio.right - 135;
RectaDestinoSombra.left = Espacio.right - 135;
TamTexto = swprintf_s(Texto, 256, TEXT("FPS : %d\nVelocidad : %d%%"), FPS, abs(static_cast<int>(100 - Velocidad)));
SetTextColor(hDCDestino, RGB(0, 0, 0));
DrawText(hDCDestino, Texto, TamTexto, &RectaDestinoSombra, DT_LEFT);
SetTextColor(hDCDestino, RGB(255, 255, 255));
DrawText(hDCDestino, Texto, TamTexto, &RectaDestino, DT_LEFT);
// Eliminamos los objetos GDI utilizados de la memoria
SelectObject(hDCDestino, VFuente);
DeleteObject(Fuente);
}

Lo primero que hacemos es definir las variables entre las cuales creamos una fuente para pintar el texto que necesitara el marcador con la API CreateFont. Luego utilizamos la función swprintf_s para formatear el texto y añadirle los valores que necesitamos mostrar. La función swprintf_s es especial para VisualC++, y se basa en la antigua función swprintf (que es la versión Unicode de sprintf). Esta función se utiliza prácticamente igual, lo único que tiene de mas es que debemos especificar el tamaño de nuestro buffer.

Una vez formateado el texto necesario para pintar la parte izquierda utilizamos la API SetTextColor para asignar el color del texto, y la API DrawText para pintar el texto. La API DrawText nos permite pintar cadenas con varias líneas, cosa que nos facilitara algo el trabajo.

Repetimos el mismo proceso para el texto que se alineara a la derecha, y por ultimo eliminamos la fuente anteriormente creada de la memoria.

Veamos la función MostrarMarcador :

Archivo : ObjetoEscena_Marcador.cpp
1
2
3
4
5
6
7
8
9
10
// Función para mostrar el marcador
void ObjetoEscena_Marcador::MostrarMarcador(void) {
RECT RectaEscena;
GetWindowRect(EscenaPadre->hWnd(), &RectaEscena);
Espacio.left = 10;
Espacio.top = 10;
Espacio.bottom = 60;
Espacio.right = (RectaEscena.right - RectaEscena.left) - 30;
Visible = TRUE;
}

Lo primero que hacemos es calcular el tamaño de la ventana que contiene la escena, para ello utilizamos la API GetWindowRect que nos devuelve tanto la posición como el ancho y la altura de la ventana. La razón de utilizar GetWindowRect teniendo GetClientRect es que GetWindowRect nos devuelve el ancho y la altura de TODA la ventana incluyendo bordes y la barra de titulo, GetClientRect por el contrario nos devuelve el ancho y el alto del área cliente omitiendo los bordes y la barra de titulo.

Una vez calculada la posición asignamos TRUE a la variable Visible para que el marcador se pinte en la escena.

Con esto ya tenemos hecho el marcador, ahora veamos el Tablero y su declaración :

Archivo : ObjetoEscena_Tablero.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Clase que encapsula una ventana translucida que hace de tablero dentro de un ObjetoEscena
class ObjetoEscena_Tablero : public ObjetoEscena_VentanaTranslucida {
public : //////////////////// Miembros publicos
// -Constructor
ObjetoEscena_Tablero(void);
// -Destructor
~ObjetoEscena_Tablero(void);
// -Función para crear el tablero
// Esta funcion adaptara la ventana que contiene el ObjetoEscena al tamaño necesario
void MostrarTablero(const UINT nAncho, const UINT nAlto);
// -Función que se llama antes de hacer el alphablend en la que podemos añadir mas graficos.
virtual void Pintar_AlphaBlend(HDC hDCDestino);
// -Función que se llama al final de pintar todos los graficos
// (Los graficos pintados no seran modificados con AlphaBlend).
virtual void Pintar_Terminado(HDC hDCDestino);
// -Vector de puntos para la serpiente
std::vector<POINT> Serpiente;
// -Vector de puntos para las bolas
std::vector<POINT> Bolas;
// -Vector de puntos para el muro
std::vector<POINT> Muro;
// -Ancho en cuadros del tablero
UINT Ancho;
// -Alto en cuadros del tablero
UINT Alto;
};

Como veis la declaración es muy similar a la declaración del Marcador, y consiste en una función MostrarTablero, dos funciones de pintado Pintar_AlphaBlend y Pintar_Terminado, varios vectores para contener la serpiente, el muro y las bolas, y dos variables que nos indican el ancho y la altura del tablero EN CUADROS.

Veamos la función MostrarTablero :

Archivo : ObjetoEscena_Tablero.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// -Función para crear el tablero
// Esta funcion adaptara la ventana que contiene el ObjetoEscena al tamaño necesario
void ObjetoEscena_Tablero::MostrarTablero(const UINT nAncho, const UINT nAlto) {
Ancho = nAncho +2;
Alto = nAlto +2;
RECT RectaEscena;
GetWindowRect(EscenaPadre->hWnd(), &RectaEscena);
// Muevo la ventana padre acorde al tamaño del tablero añadiendo 40 pixeles de ancho y 120 de alto
EscenaPadre->Mover(RectaEscena.left, RectaEscena.top, (Ancho * 10) + 40, (Alto * 10) + 120);
Espacio.left = 10;
Espacio.top = 70;
Espacio.right = Espacio.left + (Ancho * 10);
Espacio.bottom = Espacio.top + (Alto * 10);
Visible = TRUE;
}

Esta función es la encargada de redimensionar el tamaño de la escena según el ancho y el alto del tablero, y por ello la funcion MostrarTablero debe ser la primera en ejecutarse cuando vayamos a mostrar la escena. Como necesitamos mover la ventana de la escena, requerimos saber su posición X y posición Y, o en caso contrario no podremos usar la función Mover que internamente utiliza la API MoveWindow. Para averiguar la posición de la ventana escena utilizamos la API GetWindowRect. Y por ultimo asignamos el estado de visibilidad para esta ventana a TRUE.

Ahora veamos la función Pintar_AlphaBlend :

Archivo : ObjetoEscena_Tablero.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Esta función se usara para pintar los graficos que se necesiten con AlphaBlend
// NOTA el hDCDestino es un backbuffer creado por la funcion Pintar, por ello no hace falta crear otro buffer de pintado.
void ObjetoEscena_Tablero::Pintar_AlphaBlend(HDC hDCDestino) {
UINT nx = 0;
UINT ny = 0;
// Pinto el fondo por donde se movera la serpiente con una tonalidad de azul distinta
RECT EspacioTablero = { 11, 11, (Espacio.right - Espacio.left) - 10, (Espacio.bottom - Espacio.top) - 10 };
HBRUSH Brocha = CreateSolidBrush(RGB(40, 40, 140));
FillRect(hDCDestino, &EspacioTablero, Brocha);
// Pinto lineas para formar una rejilla dentro del espacio donde se movera la serpiente
HPEN Pluma = CreatePen(PS_SOLID, 1, RGB(120, 120, 220));
HPEN Viejo = static_cast<HPEN>(SelectObject(hDCDestino, Pluma));
for (ny = 1; ny < Alto; ny ++) {
MoveToEx(hDCDestino, 11, ny * 10, NULL);
LineTo(hDCDestino, (Alto * 10) - 10, ny * 10);
}
for (nx = 1; nx < Ancho; nx ++) {
MoveToEx(hDCDestino, nx * 10, 11, NULL);
LineTo(hDCDestino, nx * 10, (Ancho * 10) - 10);
}
// Elimino objetos GDI de la memoria
DeleteObject(Brocha);
SelectObject(hDCDestino, Viejo);
DeleteObject(Pluma);
}

Esta función la usamos para remarcar el fondo donde se moverá el juego, y lo hacemos pintando un fondo azul con una tonalidad distinta y unas líneas que formaran una rejilla. Para pintar el fondo utilizamos las APIs CreateSolidBrush y FillRect, luego para pintar la rejilla creamos una pluma con la API CreatePen, seleccionamos esa pluma con la API SelectObject, nos guardamos la pluma inicialmente seleccionada y por ultimo hacemos un bucle en el que usamos las APIs MoveToEx y LineTo que son las que nos permitirán pintar líneas, y por ultimo eliminamos los objetos GDI de la memoria con la API DeleteObject.

La API MoveToEx viene a ser como la funcion gotoxy que antiguamente se usaba en MS-DOS, pero bueno para los que no habeis usado nunca gotoxy esta funcion situaba el cursor en la posición indicada de la pantalla. MoveToEx en esencia apunta unas coordenadas X, Y donde empezaran todos los LineTo. Si por ejemplo hacemos MoveToEx 0,0 y luego LineTo 1024,0 estaremos haciendo una línea horizontal en la parte superior de la pantalla, si luego queremos hacer una línea en la parte izquierda solo nos hace falta llamar a LineTo 0, 768 ya que inicialmente se ha colocado el cursor con MoveToEx a 0,0.

Ahora nos toca ver la funcion Pintar_Terminado :

Archivo : ObjetoEscena_Tablero.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Esta función se usara para pintar los graficos que se necesiten. (NO SE INCLUIRAN EN EL ALPHABLEND)
// NOTA el hDCDestino es un backbuffer creado por la funcion Pintar, por ello no hace falta crear otro buffer de pintado.
void ObjetoEscena_Tablero::Pintar_Terminado(HDC hDCDestino) {
int i;
RECT EspacioTmp;
// Creación de las brochas necesarias
HBRUSH Brocha = CreateSolidBrush(RGB(0, 200, 0));
HBRUSH BrochaH = CreateSolidBrush(RGB(0, 255, 0));
HBRUSH BrochaM = CreateSolidBrush(RGB(100, 50, 20));
HBRUSH Brocha2 = CreateSolidBrush(RGB(255, 0, 0));
// Pinto los muros
for (i = 0; i < static_cast<int>(Muro.size()); i++) {
EspacioTmp.left = (Muro[i].x * 10);
EspacioTmp.top = (Muro[i].y * 10);
EspacioTmp.right = EspacioTmp.left + 11;
EspacioTmp.bottom = EspacioTmp.top + 11;
FillRect(hDCDestino, &EspacioTmp, BrochaM);
}
// Pinto las bolas
HBRUSH VBrocha = static_cast<HBRUSH>(SelectObject(hDCDestino, Brocha2));
for (size_t i2 = 0; i2 < Bolas.size(); i2++) {
Ellipse(hDCDestino, (Bolas[i2].x * 10) + 1, (Bolas[i2].y * 10) + 1, (Bolas[i2].x * 10) + 10, (Bolas[i2].y * 10) + 10);
}
SelectObject(hDCDestino, VBrocha);
// Pinto la serpiente
for (i = static_cast<int>(Serpiente.size()) -1; i > -1; i--) {
EspacioTmp.left = (Serpiente[i].x * 10) + 1;
EspacioTmp.top = (Serpiente[i].y * 10) + 1;
EspacioTmp.right = EspacioTmp.left + 9;
EspacioTmp.bottom = EspacioTmp.top + 9;
if (i == 0) FillRect(hDCDestino, &EspacioTmp, BrochaH);
else FillRect(hDCDestino, &EspacioTmp, Brocha);
}
// Elimino las brochas de la memoria
DeleteObject(Brocha);
DeleteObject(Brocha2);
DeleteObject(BrochaH);
DeleteObject(BrochaM);
}

Esta funcion pintara las bolas, la serpiente y el muro OPACOS. No hace falta dar mucha explicación del código ya que se usan las API's de siempre exceptuando la API Ellipse la cual utilizamos para pintar las bolas. Solo remarcar que estos graficos no se incluirán en ningún AlphaBlend.

Con esto ya tenemos el ObjetoEscena_Tablero listo. Ahora veamos la definición para el ObjetoEscena_Mensaje :

Archivo : ObjetoEscena_Mensaje.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Clase que encapsula una ventana translucida que hace de tablero dentro de un ObjetoEscena
class ObjetoEscena_Mensaje : public ObjetoEscena_VentanaTranslucida {
public : //////////////////// Miembros publicos
// -Constructor
ObjetoEscena_Mensaje(void);
// -Destructor
~ObjetoEscena_Mensaje(void);
// -Función que se llama al final de pintar todos los graficos
// (Los graficos pintados no seran modificados con AlphaBlend).
virtual void Pintar_Terminado(HDC hDCDestino);
// -Función que calcula el tamaño y hace visible la ventana del mensaje
void MostrarMensaje(const TCHAR *nTexto, const bool Error = false);
// -Función que oculta la ventana del mensaje
void OcultarMensaje(void);
private: //////////////////// Miembros privados
// -Texto del mensaje
TCHAR *TextoMensaje;
// -Tamaño del texto del mensaje
size_t TamTexto;
};

La definición es muy parecida a las dos clases anteriores lo único a destacar es que también se ha añadido una función para ocultar el mensaje. La función Pintar_Terminado no hace falta ni volverla a ver ya que simplemente pinta el texto del mensaje con sombra, pero la funcion MostrarMensaje es algo mas complicada ya que requerimos calcular el tamaño que necesitamos para mostrar el texto, para luego calcular la posición centrada de la ventana con el mensaje.

Veamos la funcion MostrarMensaje :

Archivo : ObjetoEscena_Mensaje.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// -Función que calcula el tamaño y hace visible la ventana del mensaje
void ObjetoEscena_Mensaje::MostrarMensaje(const TCHAR *nTexto, const bool Error) {
UINT nAncho = 0;
UINT nAlto = 0;
// Eliminamos el anterior texto de la memoria (si es que existe)
if (TextoMensaje != NULL) delete TextoMensaje;
// Asignamos el nuevo texto del mensaje
TamTexto = wcslen(nTexto) + 1;
TextoMensaje = new TCHAR[TamTexto];
wcscpy_s(TextoMensaje, TamTexto, nTexto);
// Inicio un DC y una fuente para obtener los tamaños del texto
HDC hDC = CreateCompatibleDC(NULL);
HFONT Fuente = CreateFont( 16, 0, 0, 0, FW_BOLD, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );
HFONT VFuente = static_cast<HFONT>(SelectObject(hDC, Fuente));
// Escaneamos el texto para calcular el tamaño en pixeles de cada linea
// Para ello separaremos el texto por lineas y obtendremos el ancho para cada linea.
TCHAR Linea[512];
size_t Contador = 0;
SIZE PixelsTexto;
for (size_t i = 0; i < TamTexto; i++) {
Linea[Contador] = TextoMensaje[i];
Contador ++;
// Terminamos la linea que queremos analizar
if (TextoMensaje[i] == TEXT('\n') || TextoMensaje[i] == TEXT('\0')) {
Linea[Contador] = TEXT('\0');
GetTextExtentPoint32(hDC, Linea, Contador, &PixelsTexto);
// El ancho de la nueva linea es superior, por ello lo guardamos
if (static_cast<UINT>(PixelsTexto.cx) > nAncho) nAncho = PixelsTexto.cx;
nAlto += (PixelsTexto.cy + 4);
Contador = 0;
}
}
// Calculamos la posicion centrada
nAlto += 10;
nAncho += 10;
RECT RectaEscena;
GetClientRect(EscenaPadre->hWnd(), &RectaEscena);
Espacio.left = (RectaEscena.right - nAncho) / 2;
Espacio.top = (RectaEscena.bottom - nAlto) / 2;
Espacio.right = Espacio.left + nAncho;
Espacio.bottom = Espacio.top + nAlto;
// Eliminamos objetos GDI de la memoria
SelectObject(hDC, VFuente);
DeleteObject(Fuente);
DeleteDC(hDC);
// Si la ventana no es de error ponemos un fondo verde
if (Error == false) {
ColorFondo = RGB(20, 120, 20);
ColorBordeE = RGB(60, 160, 60);
ColorBordeI = RGB(120, 220, 120);
}
else { // Si la ventana es de error ponemos un fondo rojo
ColorFondo = RGB(120, 20, 20);
ColorBordeE = RGB(160, 60, 60);
ColorBordeI = RGB(220, 120, 120);
}
// Hacemos visible el mensaje
Visible = TRUE;
}

Lo primero que hacemos es eliminar el texto anterior de la memoria, y luego asignamos el nuevo texto para el mensaje. Para calcular el ancho del texto necesitaremos crear un DC compatible con la API CreateCompatibleDC, y una fuente que crearemos con la API CreateFont. Una vez creados estos objetos la idea es utilizar la API GetTextExtentPoint32 para obtener el ancho de nuestro texto, pero se nos presenta un problema ya que dicha API no mira si la cadena tiene varias líneas. Por ello debemos separar la cadena en líneas y analizar cada línea con GetTextExtentPoint32 de forma que nos quedemos con el ancho de la línea mas larga.

Una vez tenemos el ancho de la línea mas larga y sabemos el numero de líneas, podemos calcular el tamaño para el mensaje, y luego podemos calcular la posición centrada para el mensaje.

Por ultimo si se ha especificado Error a false pondremos el fondo de color verde, si por el contrario se ha especificado Error a true pondremos el fondo de color rojo. Y para terminar hacemos visible el mensaje.

En el ejemplo 2.2 podemos ver una venanta ObjetoEscena con las tres ventanas translucidas que se han creado en este tutorial.

Ya solo nos queda una ventana translucida mas por crear, que será la que llevara el tema de los records, pero antes necesitamos saber mas sobre archivos y directorios en Windows, asi que os invito a ver el siguiente tutorial : 2.3 - Archivos y Directorios.

Descargar tutorial WinAPI completo Snake compilada