Tutorial WINAPI C++ 2.2 Crear tablero, marcador y mensaje
Categorías : Windows, Programación, C y C++.

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 :
// Clase que encapsula una ventana translucida que hace de tableroclass ObjetoEscena_Marcador : public ObjetoEscena_VentanaTranslucida {public : //////////////////// Miembros publicos// -ConstructorObjetoEscena_Marcador(void);// -Destructor~ObjetoEscena_Marcador(void);// -Función para mostrar el marcadorvoid 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 jugadorUINT Puntuacion;// -Recorrido total del jugadorUINT Recorrido;// -Frames por segundoUINT FPS;// -Velocidad en MS que tardara en hacer el movimiento la// serpienteUINT 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 :
// 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 variablesHFONT 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 sombraint 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 sombraRectaDestino.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 memoriaSelectObject(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 :
// Función para mostrar el marcadorvoid 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 :
// Clase que encapsula una ventana translucida que hace de tablero dentro de un ObjetoEscenaclass ObjetoEscena_Tablero : public ObjetoEscena_VentanaTranslucida {public : //////////////////// Miembros publicos// -ConstructorObjetoEscena_Tablero(void);// -Destructor~ObjetoEscena_Tablero(void);// -Función para crear el tablero// Esta funcion adaptara la ventana que contiene el ObjetoEscena al tamaño necesariovoid 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 serpientestd::vector<POINT> Serpiente;// -Vector de puntos para las bolasstd::vector<POINT> Bolas;// -Vector de puntos para el murostd::vector<POINT> Muro;// -Ancho en cuadros del tableroUINT Ancho;// -Alto en cuadros del tableroUINT 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 :
// -Función para crear el tablero// Esta funcion adaptara la ventana que contiene el ObjetoEscena al tamaño necesariovoid 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 altoEscenaPadre->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 :
// 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 distintaRECT 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 serpienteHPEN 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 memoriaDeleteObject(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 :
// 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 necesariasHBRUSH 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 murosfor (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 bolasHBRUSH 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 serpientefor (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 memoriaDeleteObject(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 :
// Clase que encapsula una ventana translucida que hace de tablero dentro de un ObjetoEscenaclass ObjetoEscena_Mensaje : public ObjetoEscena_VentanaTranslucida {public : //////////////////// Miembros publicos// -ConstructorObjetoEscena_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 mensajevoid MostrarMensaje(const TCHAR *nTexto, const bool Error = false);// -Función que oculta la ventana del mensajevoid OcultarMensaje(void);private: //////////////////// Miembros privados// -Texto del mensajeTCHAR *TextoMensaje;// -Tamaño del texto del mensajesize_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 :
// -Función que calcula el tamaño y hace visible la ventana del mensajevoid 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 mensajeTamTexto = wcslen(nTexto) + 1;TextoMensaje = new TCHAR[TamTexto];wcscpy_s(TextoMensaje, TamTexto, nTexto);// Inicio un DC y una fuente para obtener los tamaños del textoHDC 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 analizarif (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 guardamosif (static_cast<UINT>(PixelsTexto.cx) > nAncho) nAncho = PixelsTexto.cx;nAlto += (PixelsTexto.cy + 4);Contador = 0;}}// Calculamos la posicion centradanAlto += 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 memoriaSelectObject(hDC, VFuente);DeleteObject(Fuente);DeleteDC(hDC);// Si la ventana no es de error ponemos un fondo verdeif (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 rojoColorFondo = RGB(120, 20, 20);ColorBordeE = RGB(160, 60, 60);ColorBordeI = RGB(220, 120, 120);}// Hacemos visible el mensajeVisible = 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 |