NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WINAPI C++ 2.4 (Crear ObjetoEscena_Records)

14 de Mayo del 2010 por Josep Antoni Bover, 25 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.4 (Crear ObjetoEscena_Records)

En este tutorial vamos a tener que usar lo que hemos visto sobre archivos y directorios para crear un objeto que sea capaz de guardar nuestas puntuaciones maximas en el disco duro.

Vamos a tener que usar el objeto ObjetoEscena_VentanaTranslucida para hacer el ultimo modelo de ventana translucida que nos faltara para tener listo el juego, y ademas vamos a simular un cursor al estilo ms-dos para introducir el nombre de la puntuacion.

Para este objeto nos vamos a tener que complicar algo mas que con los 3 últimos basados en ObjetoEscena_VentanaTranslucida.

Necesitamos es tener un objeto que cargue y guarde los records en un archivo además de mostrarlos en la escena. También debemos pensar que este objeto deberá ser capaz de obtener los caracteres pulsados del teclado, para poder guardar el nombre del record.

Lo primero será definir una clase para almacenar los datos de un record, veamos la declaración :

Archivo : ObjetoEscena_Records.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Clase que contiene los datos de una puntuación del juego
class Record_Snake {
public: // Miembros publicos
// -Constructor
Record_Snake(void) {
Nombre = NULL;
TamNombre = 0;
};
// -Destructor
~Record_Snake(void) {
if (Nombre != NULL) delete Nombre;
};
// -Puntos
UINT Puntos;
// -Total de cuadros recorridos
UINT Recorrido;
// -Nivel
UINT Nivel;
// -Nombre del record
TCHAR *Nombre;
// -Tamaño en caracteres del nombre
UINT TamNombre;
};

Como podemos observar tenemos una serie de variables para almacenar los datos del record. Lo único que hay que remarcar es que el string que guardara el nombre será dinamico y se creara ajustado a los caracteres que contenga.

Ahora que ya tenemos en mente los datos que necesitamos para un record podemos crear el ObjetoEscena_Records, veamos su declaración :

Archivo : ObjetoEscena_Records.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
28
29
30
31
32
33
34
35
// Clase que encapsula una ventana translucida que sirve para ver los records dentro de un ObjetoEscena
class ObjetoEscena_Records : public ObjetoEscena_VentanaTranslucida {
public : ////////////////////// Miembros publicos
// -Constructor
ObjetoEscena_Records(void);
// -Destructor
~ObjetoEscena_Records(void);
// -Función que carga el archivo de records
void CargarRecords(void);
// -Función que guarda el archivo de records
void GuardarRecords(void);
// -Función que agrega un caracter al nuevo nombre del record actual
void Agregar_Caracter(const TCHAR Tecla);
// -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
BOOL MostrarRecords(const UINT nPuntos, const UINT nRecorrido, const UINT nNivel);
// -Función que oculta la ventana de los records
void OcultarRecords(void);
// -Función que retorna si se esta introduciendo un nuevo record
inline BOOL IntroduciendoNuevoRecord(void) const { return IntroduciendoRecord; };
private: ////////////////////// Miembros privados
// -Vector que contiene los records del juego
std::vector<Record_Snake *> Records;
// -Posición del nuevo record
UINT PosNuevoRecord;
// -Valor que determina si se esta introduciendo un record
BOOL IntroduciendoRecord;
// -Milisegundos para hacer parpadear el cursor
DWORD MSParpadeo;
// -Valor que determina si el cursor sera visible o no
bool Parpadeo;
};

Podemos ver que hay una función para cargar los records, una para guardarlos, una para agregar caracteres (la cual se usara a la hora de escribir el nuevo record), una función Pintar_Terminado que usaremos para pintar los records, una función MostrarRecords que calculara la posición y mostrara la ventana de los records, una función OcultarRecords para ocultar la ventana y por ultimo una función IntroduciendoNuevoRecord que nos servirá para saber si se está introduciendo un nuevo record o no.

Veamos la función CargarRecords :

Archivo : ObjetoEscena_Records.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
// Función que carga los records del juego
void ObjetoEscena_Records::CargarRecords(void) {
// Borramos los records de la memoria
size_t i = 0;
for (i = 0; i < Records.size(); i++) delete Records[i];
Records.resize(0);
// Declaración de variables y objetos
ObjetoDirectoriosWindows Directorios;
ObjetoArchivo ArchivoRecords;
TCHAR RutaAppData[MAX_PATH]; // Directorio
TCHAR RutaAppDataTxt[MAX_PATH]; // Archivo de prueba
// Obtengo el directorio AppData
Directorios.AppData(RutaAppData);
// Añado el resto de la ruta
wcscat_s(RutaAppData, MAX_PATH, TEXT("\\Tutoriales www.devildrey33.es"));
// Creo un string con el nombre del archivo con el que se va a trabajar
wcscpy_s(RutaAppDataTxt, MAX_PATH, RutaAppData);
wcscat_s(RutaAppDataTxt, MAX_PATH, TEXT("\\Records.snake"));
Record_Snake *Tmp;
if (ArchivoRecords.AbrirArchivo(RutaAppDataTxt, false) == TRUE) {
for (i = 0; i < 10; i++) {
Tmp = new Record_Snake;
Records.push_back(Tmp);
ArchivoRecords.LeerUINT(Tmp->Puntos);
ArchivoRecords.LeerUINT(Tmp->Recorrido);
ArchivoRecords.LeerUINT(Tmp->Nivel);
Tmp->TamNombre = ArchivoRecords.LeerString(Tmp->Nombre);
}
ArchivoRecords.CerrarArchivo();
}
// Si no existe el archivo de records creo una tabla con 10 records a cero
else {
for (i = 0; i < 10; i++) {
Tmp = new Record_Snake;
Tmp->Puntos = 0;
Tmp->Recorrido = 0;
Tmp->Nivel = 0;
Tmp->Nombre = NULL;
Tmp->TamNombre = 0;
Records.push_back(Tmp);
}
}
}

La función CargarRecords lo primero que hace es obtener la ruta de trabajo mediante la función AppData del ObjetoDirectoriosWindows, luego le añade la parte final del directorio “\Tutoriales www.devildrey33.es” con la función wcscat_s. Una vez tenemos determinada la ruta de trabajo creamos otro string con dicha ruta mas el nombre del archivo, para ello utilizamos wcscpy_s y wcscat_s (que son las funciones Unicode seguras de strcopy y strcat). Cuando tenemos la ruta del archivo intentamos abrirlo y leer sus datos, en caso de que el archivo no exista crearemos un vector de 10 records con todo a cero.

Viendo la función CargarRecords creo que podemos omitir la explicación para la función GuardarRecords que en esencia hace algo similar, asi que pasemos a ver la función Pintar_Terminado :

Archivo : ObjetoEscena_Records.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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 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_Records::Pintar_Terminado(HDC hDCDestino) {
RECT EspacioCursor;
SIZE TamTexto;
// Fuente negrita
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") );
// Fuente normal
HFONT Fuente2 = CreateFont( 16, 0, 0, 0, FW_NORMAL, false, false, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_ROMAN, TEXT("Tahoma") );
// Fuente inicialmente seleccionada en el DC
HFONT VFuente = static_cast<HFONT>(SelectObject(hDCDestino, Fuente));
// Calculamos el cambio de parpadeo
if (MSParpadeo < GetTickCount()) {
Parpadeo = !Parpadeo;
MSParpadeo = GetTickCount() + 300;
}
TCHAR TmpTxt[256];
UINT TmpTam = 0;
UINT AnchoNombre = 0;
// Pintamos la cabecera
SetBkMode(hDCDestino, TRANSPARENT);
SetTextColor(hDCDestino, RGB(0, 0, 0));
TextOut(hDCDestino, 11, 11, TEXT("Puntos"), 6);
TextOut(hDCDestino, 66, 11, TEXT("Recorrido"), 9);
TextOut(hDCDestino, 139, 11, TEXT("Nivel"), 5);
TextOut(hDCDestino, 180, 11, TEXT("Nombre"), 6);
SetTextColor(hDCDestino, RGB(255, 255, 255));
TextOut(hDCDestino, 10, 10, TEXT("Puntos"), 6);
TextOut(hDCDestino, 65, 10, TEXT("Recorrido"), 9);
TextOut(hDCDestino, 138, 10, TEXT("Nivel"), 5);
TextOut(hDCDestino, 179, 10, TEXT("Nombre"), 6);
SelectObject(hDCDestino, Fuente2);
// Pintamos los valores de los records
for (size_t i = 0; i < Records.size(); i++) {
// Pintamos la sombra del texto
SetTextColor(hDCDestino, RGB(0, 0, 0));
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Puntos);
TextOut(hDCDestino, 11, (15 * i) + 26, TmpTxt, TmpTam);
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Recorrido);
TextOut(hDCDestino, 66, (15 * i) + 26, TmpTxt, TmpTam);
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Nivel);
TextOut(hDCDestino, 139, (15 * i) + 26, TmpTxt, TmpTam);
TextOut(hDCDestino, 180, (15 * i) + 26, Records[i]->Nombre, Records[i]->TamNombre);
if (i != PosNuevoRecord) { // Si no es la posición del record actual lo pintamos blanco
SetTextColor(hDCDestino, RGB(255, 255, 255));
}
else { // Si es la posición del record actual
// Si el parpadeo esta activado y estamos introduciendo un record, pintamos el cursor
if (Parpadeo == true && IntroduciendoRecord == TRUE) {
TamTexto.cx = 0;
if (Records[i]->Nombre != NULL)
GetTextExtentPoint32(hDCDestino, Records[i]->Nombre, Records[i]->TamNombre -1, &TamTexto);
EspacioCursor.left = 180 + TamTexto.cx;
EspacioCursor.right = EspacioCursor.left + 2;
EspacioCursor.top = (15 * i) + 25;
EspacioCursor.bottom = EspacioCursor.top + 15;
FillRect(hDCDestino, &EspacioCursor, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
}
SetTextColor(hDCDestino, RGB(0, 255, 255));
}
// Pintamos el texto
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Puntos);
TextOut(hDCDestino, 10, (15 * i) + 25, TmpTxt, TmpTam);
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Recorrido);
TextOut(hDCDestino, 65, (15 * i) + 25, TmpTxt, TmpTam);
TmpTam = swprintf_s(TmpTxt, 256, TEXT("%d"), Records[i]->Nivel);
TextOut(hDCDestino, 138, (15 * i) + 25, TmpTxt, TmpTam);
TextOut(hDCDestino, 179, (15 * i) + 25, Records[i]->Nombre, Records[i]->TamNombre);
}
SelectObject(hDCDestino, Fuente);
// Pintamos un texto o otro segun si se esta introduciendo o mirando un record.
if (IntroduciendoRecord == TRUE) {
SetTextColor(hDCDestino, RGB(0, 0, 0));
TextOut(hDCDestino, 11, 186, TEXT("Nuevo record!! Introduce tu nombre"), 40);
SetTextColor(hDCDestino, RGB(240, 240, 0));
TextOut(hDCDestino, 10, 185, TEXT("Nuevo record!! Introduce tu nombre"), 40);
}
else {
SetTextColor(hDCDestino, RGB(0, 0, 0));
TextOut(hDCDestino, 7, 186, TEXT("Presiona ESPACIO para volver a empezar"), 39);
SetTextColor(hDCDestino, RGB(240, 240, 0));
TextOut(hDCDestino, 6, 185, TEXT("Presiona ESPACIO para volver a empezar"), 39);
}
// Borramos objetos GDI
SelectObject(hDCDestino, VFuente);
DeleteObject(Fuente);
DeleteObject(Fuente2);
}

Para empezar creamos un par de fuentes con la API CreateFont, una normal y una negrita. Luego miramos los milisegundos del parpadeo del cursor con la API GetTickCount, de forma que cada 300 milisegundos la variable Parpadeo cambie de true a false.

Lo siguiente es pintar los textos con sombra de la cabecera de los records, para ello utilizamos SetBkMode para establecer que los textos se pintaran sin fondo, SetTextColor para asignar el color del texto y TextOut para pintar el texto.

Despues creamos un bucle que recorra todos los records para pintarlos de uno en uno. Dentro del bucle utilizamos swprintf_s para formatear cada valor y los pintamos con TextOut. Ademas comprobamos si el record actual es el que se está pintando, para determinar si tenemos que pintar el cursor o no. Para pintar el cursor utilizamos FillRect y la brocha la obtenemos del stock de brochas de Windows con GetStockObject.

Por último pintamos el texto “Nuevo Record!!” si se está introduciendo un record, o en caso contrario pintamos el texto “Pulsa ESPACIO para volver a empezar”, y eliminamos de memoria todos los objetos GDI.

Ahora veamos la función MostrarRecords :

Archivo : ObjetoEscena_Records.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
BOOL ObjetoEscena_Records::MostrarRecords(const UINT nPuntos, const UINT nRecorrido, const UINT nNivel) {
// Cargo los records y miro si el nuevo record es mejor que alguno.
IntroduciendoRecord = FALSE;
CargarRecords();
PosNuevoRecord = 10;
size_t i = 0;
for (i = 0; i < 10; i++) {
// Si tiene mas puntos que este record es mejor
if (nPuntos > Records[i]->Puntos) {
PosNuevoRecord = i;
break;
}
// Si tiene los mismos puntos miramos el recorrido
else if (nPuntos == Records[i]->Puntos) {
// Si el recorrido es el mismo o mas pequeño, este record sera mejor
if (nRecorrido <= Records[i]->Recorrido) {
PosNuevoRecord = i;
break;
}
}
}
// Si la posicion del nuevo record no es 10, es que hay que guardarlo
// Ademas moveremos los records que sean inferiores una posicion en el array
if (PosNuevoRecord != 10) {
IntroduciendoRecord = TRUE;
Record_Snake *Tmp = new Record_Snake;
Tmp->Nivel = nNivel;
Tmp->Puntos = nPuntos;
Tmp->Recorrido = nRecorrido;
delete Records[9];
for (i = 9; i > PosNuevoRecord; i--) Records[i] = Records[i -1];
Records[PosNuevoRecord] = Tmp;
}
// Calculo la posicion centrada dentro de la escena
RECT RectaEscena;
GetClientRect(EscenaPadre->hWnd(), &RectaEscena);
Espacio.left = (RectaEscena.right - 280) / 2;
Espacio.top = (RectaEscena.bottom - 210) / 2;
Espacio.right = Espacio.left + 280;
Espacio.bottom = Espacio.top + 210;
Visible = TRUE;
ColorFondo = RGB(20, 120, 20);
ColorBordeE = RGB(60, 160, 60);
ColorBordeI = RGB(120, 220, 120);
return static_cast<BOOL>(IntroduciendoRecord);
}

El objetivo de esta función no es solo mostrar los records. Su finalidad también es ordenar los records de forma que se valore la puntuación por encima de todo, y luego si hay varias puntuaciones iguales que las ordene según la idea de que la que tenga menor recorrido es mejor.

El primer bucle determina si el record está entre los 10 primeros. Mas tarde partiendo de que sabemos la posición del record ordenamos los otros records de forma que borraremos el ultimo y añadiremos el nuevo en la posición que le toque. Por último se calcula la posición centrada para esta ventana y se pone el estado de visibilidad a TRUE.

Ya solo nos queda ver una función mas, Agragar_Caracter :

Archivo : ObjetoEscena_Records.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
// -Función que agrega un caracter al nuevo nombre del record actual
void ObjetoEscena_Records::Agregar_Caracter(const TCHAR Tecla) {
TCHAR *TmpTxt = Records[PosNuevoRecord]->Nombre;
switch (Tecla) {
// Tecla Intro
case VK_RETURN :
IntroduciendoRecord = FALSE;
return;
// Tecla borrar
case VK_BACK :
if (Records[PosNuevoRecord]->TamNombre > 0) Records[PosNuevoRecord]->TamNombre --;
if (Records[PosNuevoRecord]->TamNombre == 0) {
delete Records[PosNuevoRecord]->Nombre;
Records[PosNuevoRecord]->Nombre = NULL;
}
else {
Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre -1] = TEXT('\0');
}
return;
// Las demas teclas
default :
if (Records[PosNuevoRecord]->Nombre == NULL) {
Records[PosNuevoRecord]->Nombre = new TCHAR[2];
Records[PosNuevoRecord]->Nombre[0] = Tecla;
Records[PosNuevoRecord]->Nombre[1] = TEXT('\0');
Records[PosNuevoRecord]->TamNombre = 2;
}
else {
Records[PosNuevoRecord]->TamNombre ++;
Records[PosNuevoRecord]->Nombre = new TCHAR[Records[PosNuevoRecord]->TamNombre];
wcscpy_s(Records[PosNuevoRecord]->Nombre, Records[PosNuevoRecord]->TamNombre, TmpTxt);
delete TmpTxt;
Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre - 2] = Tecla;
Records[PosNuevoRecord]->Nombre[Records[PosNuevoRecord]->TamNombre - 1] = TEXT('\0');
}
return;
}
}

Esta función tiene por objetivo almacenar los caracteres introducidos por el teclado para formar una cadena que será el nombre del nuevo record.

Basicamente miramos si se ha presionado Intro para terminar con la introducción del nuevo record, si se ha pulsado la tecla borrar para borrar el ultimo carácter de la cadena, o si se ha pulsado cualquier otra tecla la cual añadiremos a la cadena.

En el ejemplo 2.4 podemos ver dentro de la escena una ventana que muestra los records, y que esta preparada para recibir pulsaciones de teclado y anotar el nombre del record.

Ya casi estamos al final del segundo tutorial, ahora nos las veremos con el objeto que cargara los niveles para el Snake: 2.5 - Tutorial Creación del ObjetoSnake_Nivel.

Descargar tutorial WinAPI completo Snake compilada