Ir al contenido

publicidad

Foto

Curso MM: 5 Controlando juegos con teclado y ratón


Este tema ha sido archivado. Esto significa que no puedes responder en este tema.
No hay respuestas en este tema

#1

Escrito 08 agosto 2009 - 14:39

Controlando juegos con teclado y ratón

Evaluando la entrada de teclado en juegos


Win32 tiene mensajes llamados WM_KEYDOWN y WM_KEYUP que informan de que una tecla se ha presionado o soltado. Son muy fáciles de manejar en un programa de Windows pero tienen el problema de que el gestor de mensajes de Windows es muy lento con éste tipo de mensajes. En la función WinMain() de GameEngine() hay un bucle que se repite constantemente procesando los mensajes del programa. Cuando no procesa mensajes sigue en el sistema de ciclos del programa y ese tiempo en el que no hace nada se puede aprovechar para llevar a cabo tareas del juego. Ahí es donde podemos procesar los eventos del teclado en vez de esperar a que Windows nos envíe alguno.

Gestionando el ratón


En el caso del ratón los mensajes si son suficientemente rápidos para juegos.

WM_MOUSEMOVE— cualquier movimiento
WM_LBUTTONDOWN— botón izquierdo pulsado
WM_LBUTTONUP— botón izquierdo soltado
WM_RBUTTONDOWN— botón derecho presionado
WM_RBUTTONUP— botón derecho soltado
WM_MBUTTONDOWN— botón central presionado
WM_MBUTTONUP— botón central soltado

La posición del ratón se puede localizar en lParam lo procesamos en el método GameEngine::HandleEvent() . Se envían las coordenadas y se pueden extraer así:

[code:1]case WM_MOUSEMOVE:
WORD x = LOWORD(lParam);
WORD y = HIWORD(lParam);
return 0;[/code]

wParam tiene información del estado de los botones y teclado (shift o control):

MK_LBUTTON botón izquierdo pulsado.
MK_RBUTTON botón derecho pulsado.
MK_MBUTTON botón del medio pulsado.
MK_SHIFT pulsada la tecla shift.
MK_CONTROL pulsada la tecla control.

Todo lo anterior son banderas que pueden estar combinadas en wParam, para ver sólo una se usa AND (&):

if(wParam & MK_RBUTTON) entonces el botón derecho se ha pulsado.

Repasando el motor de juego para añadir entrada de datos

Hay aspectos generales del teclado que pueden ser incluidos en el motor del juego. Otros son específicos de cada juego y serán colocados en el código. La función GamePaint() es llamada cada vez que se redibuja la pantalla, es cosa de cada juego añadir si hace falta elementos a ésta función. Con el teclado pasa lo mismo.

- Añadiendo soporte para el teclado. Lo que hacemos es comprobar el teclado constantemente a través del motor del juego y éste llamará a una función para manejar las pulsaciones de teclado: HandleKeys(); Ésta función estará en el código del juego, no en el motor, si no hace falta entrada de teclado la dejamos en blanco. En WinMain() nos aseguramos de que ésta función es requerida lo más rápido posible para tener una buena respuesta. El código de WinMain() quedará así:

[code:1]if (iTickCount > iTickTrigger)
{
iTickTrigger = iTickCount + GameEngine::GetEngine()->GetFrameDelay();
HandleKeys();
GameCycle();
}[/code]

Justo antes de cada ciclo del juego llamamos a la función.

- Añadiendo soporte para ratón: el ratón lo tratamos a través de los mensajes de Windows. Los juegos deben soportar las siguientes tres funciones que son llamadas por el motor del juego:

[code:1]void MouseButtonDown(int x, int y, BOOL bLeft);
void MouseButtonUp(int x, int y, BOOL bLeft);
void MouseMove(int x, int y);[/code]

Para hacer ésto el motor debe tratar los mensajes del ratón y llamar a éstas funciones. bLeft será TRUE si se trata del botón izquierdo o FALSE si es el derecho. El método GameEngine::HandleEvent() debe contener el siguiente código:

[code:1]case WM_LBUTTONDOWN:
// Handle left mouse button press
MouseButtonDown(LOWORD(lParam), HIWORD(lParam), TRUE);
return 0;

case WM_LBUTTONUP:
// Handle left mouse button release
MouseButtonUp(LOWORD(lParam), HIWORD(lParam), TRUE);
return 0;

case WM_RBUTTONDOWN:
// Handle right mouse button press
MouseButtonDown(LOWORD(lParam), HIWORD(lParam), FALSE);
return 0;

case WM_RBUTTONUP:
// Handle right mouse button release
MouseButtonUp(LOWORD(lParam), HIWORD(lParam), FALSE);
return 0;

case WM_MOUSEMOVE:
// Handle mouse movement
MouseMove(LOWORD(lParam), HIWORD(lParam));
return 0;[/code]

Mejorando la clase Bitmap

Vamos a añadir transparencia a la clase Bitmap. Utilizaremos un color como transparente. Vamos a extender el método Bitmap::Draw() para que soporte la transparencia. Añadiremos dos nuevos argumentos:
- bTrans - un valor booleano que indica si el bitmap debe dibujarse con o sin transparencia, y
- crTransColor - que indica el color transparente del bitmap.

[code:1]void Draw(HDC hDC, int x, int y, BOOL bTrans = FALSE, COLORREF crTransColor = RGB(255, 0, 255));[/code]

El valor por defecto de bTrans es FALSE, es decir, por defecto no se hace con transparencia. El color especificado por defecto es el magenta. Si es con transparencia Draw() utilizará la función de Win32 TransparentBlt(), en caso contrario seguirá usando BitBlt().

Aquí dejo la función TransparentBlt(), la definición va en Bitmap.h y el código en Bitmap.cpp:


[code:1]void TransparentBlt(HDC hDC,int xdest,int ydest,int widthdest,
int heightdest,HDC hMemDC,int xsource,int ysource,int widthsource,
int heightsource,COLORREF crTransColor);[/code]

y la función modificada:


[code:1]void Bitmap::TransparentBlt(HDC hDC,int xdest,int ydest,
int widthdest,int heightdest, HDC hMemDC,int xsource,int ysource,
int widthsource,int heightsource, COLORREF crTransColor)
{
BITMAP bm;
COLORREF cColor;
HBITMAP bmAndBack, bmAndObject, bmAndMem, bmSave;
HBITMAP bmBackOld, bmObjectOld, bmMemOld, bmSaveOld;
HDC hdcMem, hdcBack, hdcObject, hdcSave;
POINT ptSize;

GetObject(m_hBitmap, sizeof(BITMAP), (LPSTR)&bm);
ptSize.x = bm.bmWidth; // Get width of bitmap
ptSize.y = bm.bmHeight; // Get height of bitmap
DPtoLP(hMemDC, &ptSize, 1); // Convert from device

// to logical points

// Create some DCs to hold temporary data.
hdcBack = CreateCompatibleDC(hDC);
hdcObject = CreateCompatibleDC(hDC);
hdcMem = CreateCompatibleDC(hDC);
hdcSave = CreateCompatibleDC(hDC);

// Create a bitmap for each DC. DCs are required for a number of
// GDI functions.

// Monochrome DC
bmAndBack = CreateBitmap(ptSize.x, ptSize.y, 1, 1, NULL);

// Monochrome DC
bmAndObject = CreateBitmap(ptSize.x, ptSize.y, 1, 1, NULL);

bmAndMem = CreateCompatibleBitmap(hDC, ptSize.x, ptSize.y);
bmSave = CreateCompatibleBitmap(hDC, ptSize.x, ptSize.y);

// Each DC must select a bitmap object to store pixel data.
bmBackOld = (HBITMAP)SelectObject(hdcBack, bmAndBack);
bmObjectOld = (HBITMAP)SelectObject(hdcObject, bmAndObject);
bmMemOld = (HBITMAP)SelectObject(hdcMem, bmAndMem);
bmSaveOld = (HBITMAP)SelectObject(hdcSave, bmSave);

// Set proper mapping mode.
SetMapMode(hMemDC, GetMapMode(hDC));

// Save the bitmap sent here, because it will be overwritten.
BitBlt(hdcSave, 0, 0, ptSize.x, ptSize.y, hMemDC, 0,0, SRCCOPY);

// Set the background color of the source DC to the color.
// contained in the parts of the bitmap that should be transparent
cColor = SetBkColor(hMemDC, crTransColor);

// Create the object mask for the bitmap by performing a BitBlt
// from the source bitmap to a monochrome bitmap.
BitBlt(hdcObject, 0, 0, ptSize.x, ptSize.y, hMemDC, 0, 0,
SRCCOPY);

// Set the background color of the source DC back to the original
// color.
SetBkColor(hMemDC, cColor);

// Create the inverse of the object mask.
BitBlt(hdcBack, 0, 0, ptSize.x, ptSize.y, hdcObject, 0, 0,
NOTSRCCOPY);

// Copy the background of the main DC to the destination.
BitBlt(hdcMem, xsource, ysource, ptSize.x, ptSize.y, hDC,
xdest, ydest, SRCCOPY);

// Mask out the places where the bitmap will be placed.
BitBlt(hdcMem, xsource, ysource, ptSize.x, ptSize.y,
hdcObject, xsource, ysource, SRCAND);

// Mask out the transparent colored pixels on the bitmap.
BitBlt(hMemDC, xsource, ysource, ptSize.x, ptSize.y, hdcBack,
xsource, ysource, SRCAND);

// XOR the bitmap with the background on the destination DC.
BitBlt(hdcMem, xsource, ysource, ptSize.x, ptSize.y, hMemDC,
xsource, ysource, SRCPAINT);

// Copy the destination to the screen.
BitBlt(hDC, xdest, ydest, widthdest, heightdest, hdcMem, xsource,
ysource, SRCCOPY);

// Place the original bitmap back into the bitmap sent here.
BitBlt(hMemDC, 0,0, ptSize.x, ptSize.y, hdcSave, 0, 0, SRCCOPY);

// Delete the memory bitmaps.
DeleteObject(SelectObject(hdcBack, bmBackOld));
DeleteObject(SelectObject(hdcObject, bmObjectOld));
DeleteObject(SelectObject(hdcMem, bmMemOld));
DeleteObject(SelectObject(hdcSave, bmSaveOld));

// Delete the memory DCs.
DeleteDC(hdcMem);
DeleteDC(hdcBack);
DeleteDC(hdcObject);
DeleteDC(hdcSave);
}[/code]

Programa de ejemplo: ufo.h , ufo.cpp

En ufo.h podemos ver _iMAXSPEED que establece la velocidad máxima del platillo, es decir cuántos pixeles puede viajar en una dirección cada ciclo. _pBackground y _pSaucer almacenan los gráficos del fondo y el platillo. También vemos las coordenadas del platillo y la velocidad en los ejes x,y que indica cuántos pixeles debe moverse en esas direcciones por cada ciclo.

En ufo.cpp empezamos viendo que GameInitialize() pone los frames a 30 para que vaya muy suave. GameStart() carga los gráficos y pone la velocidad inicial y coordenadas del platillo. GamePaint() es muy sencilla, dibuja el fondo y luego dibuja el platillo con transparencia. La función GameCycle() actualiza la posición del platillo y obliga a redibujar con la función InvalidateRect(). Ahora viene la siguiente función:

[code:1]1: void HandleKeys()
2: {
3: // Change the speed of the saucer in response to arrow key presses
4: if (GetAsyncKeyState(VK_LEFT) < 0)
5: _iSpeedX = max(-_iMAXSPEED, --_iSpeedX);
6: else if (GetAsyncKeyState(VK_RIGHT) < 0)
7: _iSpeedX = min(_iMAXSPEED, ++_iSpeedX);
8: if (GetAsyncKeyState(VK_UP) < 0)
9: _iSpeedY = max(-_iMAXSPEED, --_iSpeedY);
10: else if (GetAsyncKeyState(VK_DOWN) < 0)
11: _iSpeedY = min(_iMAXSPEED, ++_iSpeedY);
12: }[/code]

Lo que hace es comprobar si están pulsado los cursores y actuar en consecuencia. Utilizamos los virtual key codes. Y ahora vemos la función del ratón:

[code:1]1: void MouseButtonDown(int x, int y, BOOL bLeft)
2: {
3: if (bLeft)
4: {
5: // Set the saucer position to the mouse position
6: _iSaucerX = x - (_pSaucer->GetWidth() / 2);
7: _iSaucerY = y - (_pSaucer->GetHeight() / 2);
8: }
9: else
10: {
11: // Stop the saucer
12: _iSpeedX = 0;
13: _iSpeedY = 0;
14: }
15: }[/code]

Aquí lo que hacemos es que si se pulsa el botón izquierdo ponemos el ovni en la posición del ratón. Si es el botón derecho hacemos que se pare.

Los archivos Resource.h y ufo.rc son fáciles de entender si habéis leido los anteriores capítulos. Los cambios en GameEngine.cpp podéis verlos en WinMain() (se añade la llamada a la función de ufo.cpp que gestiona el teclado) y en GameEngine::HandleEvent() (se añade el tratamiento de los mensajes del ratón). Los cambios en Bitmap.cpp son la posibilidad de mostrar bitmaps con transparencias y la función TransparentBlt(). Realmente no necesitáis saber cómo funcionan porque las ocultamos del código del juego precisamente para que todo sea más sencillo.
Nos centraremos ahora en ufo.h y ufo.cpp

ufo.h
[code:1]//-----------------------------------------------------------------
// UFO Application
// C++ Header - UFO.h
//-----------------------------------------------------------------

#pragma once

//-----------------------------------------------------------------
// Include Files
//-----------------------------------------------------------------
#include
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"

//-----------------------------------------------------------------
// Global Variables
//-----------------------------------------------------------------
HINSTANCE _hInstance;
GameEngine* _pGame;
const int _iMAXSPEED = 8; //velocidad máxima.
Bitmap* _pBackground; //el bitmap de fondo.
Bitmap* _pSaucer; //el bitmap del platillo.
int _iSaucerX, _iSaucerY; //posición del platillo.
int _iSpeedX, _iSpeedY; //velocidad vertical y horizontal.[/code]

ufo.cpp
[code:1]//-----------------------------------------------------------------
// UFO Application
// C++ Source - UFO.cpp
//-----------------------------------------------------------------

//-----------------------------------------------------------------
// Include Files
//-----------------------------------------------------------------
#include "UFO.h"

//-----------------------------------------------------------------
// Game Engine Functions
//-----------------------------------------------------------------
BOOL GameInitialize(HINSTANCE hInstance)
{
// Create the game engine
_pGame = new GameEngine(hInstance, TEXT("UFO"),
TEXT("UFO"), IDI_UFO, IDI_UFO_SM, 500, 400);
if (_pGame == NULL)
return FALSE;

// Set the frame rate
_pGame->SetFrameRate(30);

// Store the instance handle
_hInstance = hInstance;

return TRUE;
}

void GameStart(HWND hWindow)
{
// Create and load the background and saucer bitmaps
HDC hDC = GetDC(hWindow);
_pBackground = new Bitmap(hDC, IDB_BACKGROUND, _hInstance);
_pSaucer = new Bitmap(hDC, IDB_SAUCER, _hInstance);

// Posición y velocidad inicial del platillo.
_iSaucerX = 250 - (_pSaucer->GetWidth() / 2);
_iSaucerY = 200 - (_pSaucer->GetHeight() / 2);
_iSpeedX = 0;
_iSpeedY = 0;
}

void GameEnd()
{
// Cleanup the background and saucer bitmaps
delete _pBackground;
delete _pSaucer;

// Cleanup the game engine
delete _pGame;
}

void GameActivate(HWND hWindow)
{
}

void GameDeactivate(HWND hWindow)
{
}

void GamePaint(HDC hDC)
{
// Draw the background and saucer bitmaps
_pBackground->Draw(hDC, 0, 0);
_pSaucer->Draw(hDC, _iSaucerX, _iSaucerY, TRUE);
}

void GameCycle()
{
// Update the saucer position
// Si la velocidad es negativa se moverá hacia arriba o a la izquierda.
// Si la velocidad es positiva se moverá hacia derecha o abajo.
// Éste código tiene en cuenta los bordes también, por eso se ve un poco raro.
// max y min son funciones con dos parámetros que devuelven el mayor o el menor.
_iSaucerX = min(500 - _pSaucer->GetWidth(), max(0, _iSaucerX + _iSpeedX));
_iSaucerY = min(320, max(0, _iSaucerY + _iSpeedY));

// Esta función invalida toda la pantalla del programa y así fuerza un redibujado.
InvalidateRect(_pGame->GetWindow(), NULL, FALSE);
}

void HandleKeys()
{
// Cambiar la velocidad del platillo en respuesta a la pulsación de teclas.
if (GetAsyncKeyState(VK_LEFT) < 0)
_iSpeedX = max(-_iMAXSPEED, --_iSpeedX);
else if (GetAsyncKeyState(VK_RIGHT) < 0)
_iSpeedX = min(_iMAXSPEED, ++_iSpeedX);
if (GetAsyncKeyState(VK_UP) < 0)
_iSpeedY = max(-_iMAXSPEED, --_iSpeedY);
else if (GetAsyncKeyState(VK_DOWN) < 0)
_iSpeedY = min(_iMAXSPEED, ++_iSpeedY);
}

void MouseButtonDown(int x, int y, BOOL bLeft)
{
if (bLeft)
{
// Poner el platillo en la posición del ratón.
_iSaucerX = x - (_pSaucer->GetWidth() / 2);
_iSaucerY = y - (_pSaucer->GetHeight() / 2);
}
else
{
// Parar el platillo.
_iSpeedX = 0;
_iSpeedY = 0;
}
}

void MouseButtonUp(int x, int y, BOOL bLeft)
{
}

void MouseMove(int x, int y)
{
}[/code]

Código fuente


Este tema ha sido archivado. Esto significa que no puedes responder en este tema.
publicidad