Olá!
Este tutorial tem como objetivo a criação do esqueleto de uma engine 2D utilizando C++/DirectX no Windows. A parte 1, que buscava discutir um pouco de tecnologia (enrolar), encontra-se aqui.
Sem enrolação porque já me arrependi de escrever isso, vamos lá.
Sumário
1. Configurando o Visual Studio
2. Iniciando o Win32
3. Constantes e Tratamento de Erros
4. Inputs
5. Gráficos, Imagens e Texturas
6. Um Jogo Genérico
7. As Regras do Pokémon
8. Adicionando mais coisas e polindo o jogo
Configurando o Visual Studio
Bom, na parte 1, avisei que era necessário ter instalado os kits de desenvolvimento do DX e do Windows 7. Não tem muito mistério, basta baixar e apertar nos botões de seguir em frente. O que eu falo aqui é pro Visual Studio 2010 Express, mas serve também para outras IDEs como Eclipse e tudo mais. O objetivo inicial aqui é inserirmos todas as libs relacionadas ao Windows 7 e DX para o compilador usá-las (afinal, as libs não ficam por padrão inseridas no linker do compilador).
Crie um projeto e vá em Project e, logo em seguida, Properties. No topo da nova janela, sete a configuração para "Active" (posteriormente, repita todo o processo aqui para a Debug também).
Agora expanda a opção Linker e vá para a opção General. Em Additional Library Directories, insira, antes de todos os outros, a linha "$(DXSDK_DIR)\Lib\x86;" (sem aspas). Já na opção "Input", no campo Additional Dependencies, é necessário incluir as seguintes libs (no início de tudo): "d3d9.lib;d3dx9.lib;winmm.lib;xinput.lib;"Agora na opção C/C++ no menu, a qual você deve expandir, siga para General e em Additional Include Directories, insira "$(DXSDK_DIR)\Include;".
Pronto, o Visual Studio está perfeitamente configurado para compilar aplicações que usem o Win32 e o DirectX. Se você quiser, pode incluir as libs da versão x64 também, mas só recomendo fazer isso se souber as consequências.
Iniciando o Win32
Win32 é uma API que permite você se utilizar de funções para fazer janelas, botões e afins no Windows. Vale lembrar que ela é uma função quase legada, visto que se você quiser um aplicativo Metro, conforme é no Windows 8/10, você utiliza outras tecnologias (como XAML, até onde sei).
De qualquer forma, o primeiro passo aqui é setar as configurações que queremos da janela e depois chamar a função de inicialização. Como existem muitos headers para adicionar, utilizamos a diretiva WIN32_LEAN_AND_MEAN que contém as seguintes informações:
Começando, crie um arquivo .cpp no Visual Studio; pode chamá-lo de como quiser, aqui vamos chamar de winmain.cpp. Vou inserindo o código por partes e discutindo o que cada trecho faz.
A primeira diretiva e a biblioteca <crtdbg.h> tem como funções a detecção de possíveis memory leaks na versão debug do código. Por mais que não tenhamos, no futuro, ao adicionar mais funções, pode ser interessante investigá-los. O pokemon.h trata-se de nosso "Jogo Específico" (discutido mais a frente) e a stdlib.h é a biblioteca padrão da linguagem C que, se você está lendo isso, já a deve conhecer.
Quanto aos protótipos de funções, existem coisas interessantes aqui. Todo programa Windows é inicializado pela função WinMain (diferente do "main" padrão em C ou Java, de forma que esta não existe aí). A WinProc recebe todos os comandos inseridos por nós (cliques, teclas e afins) e a CreateMainWindow é apenas uma função que será mais discutida a frente e tem a finalidade de gerar a janela de fato.
Vamos tentar destrinchar o que está acontecendo aqui, mas não vou me aprofundar muito em detalhes sobre a API ou isso daria um livro. O nosso jogo funciona no esquema de loops: enquanto o loop estiver rodando (ou seja, o while(!done) for verdadeiro), as ações continuam sendo sempre monitoradas. Se há uma mensagem (input, pedido de saída, etc), então isso é passado posteriormente para o WinProc que, posteriormente, vai retornar uma função que será executada no jogo.
Existe tratamento de erros aí que é meio intuitivo o que está acontecendo; o GameError é tratado mais a frente.
O código para o Safe_Delete (inserido em um constants.h em breve) é o seguinte:
Por fim, nosso winmain.cpp termina da seguinte forma:
O código está bem comentado e é legível por si só, mas vale explicar alguns pontos. WNDCLASSEX é basicamente uma estrutura que define as janelas do windows por padrão. É necessário preenchê-la de parâmetros e registrá-la para que o Windows a reconheça como um processo em execução (neste caso, como sistema operacional mesmo). O que definirá o estilo da janela em si é a função CreateWindow (do próprio Windows) que está recheado de parâmetros ali, inclusive mostrando sempre que qualquer ação deve ser dirigida ao WinProc, que já foi discutido anteriormente.
Por fim, a função que exibe a janela de fato é a ShowWindow. Se você precisa escondê-la, basta enviar o parâmetro SW_HIDE.
Constantes e Tratamento de Erros
Antes de prosseguirmos, é interessante definir algumas constantes que usaremos mais a frente e funções para tratamento de erros. Lembrando sempre que a necessidade dessas constantes, diferente aqui, vai aparecendo com o tempo; porém, o "passo-a-passo" ficaria demasiado longo. O objetivo aqui é entender o que há por trás de um motor de jogo, afinal.
Criemos agora um arquivo constants.h.
Começamos com as prediretivas, includes e macros para evitar repetições no compilador:
O código em si é autoexplicativo; queremos evitar que memória se acumule sem ser descartada e acabe gerando um overflow. O interessante está na TRANSCOLOR, que basicamente vai definir que cor queremos para a transparência na hora de renderizar nossos sprites/tiles e afins; nesse caso, é a magenta. Se mudássemos tudo para 0, teríamos que preto é equivalente.
Ainda em nosso constants.h:
Nosso jogo consiste apenas em um background e um personagem se movimentando - nada mais. De forma a facilitar nossa vida, simplesmente definimos os locais dos arquivos em uma constante. Pode-se fazer isso lendo um arquivo de dados ou similar, por exemplo.
As configurações da janela foram todas chamadas na função CreateWindow, no tópico anterior; tanto faz setá-las como constantes ou invocá-las diretamente pela função, passando-as como parâmetro. Isso é apenas para facilitar o trabalho. Aproveitamos e definimos também a estrutura do sprite de nosso personagem (3 colunas e 4 linhas) e detalhes como FPS e demos nomes mais simples às teclas para o input.
Para fins de informação, este é o sprite que usaremos para o nosso personagem:
O background pode ser qualquer um, mas dei preferência por um em 640x480.
Criemos agora um gameError.h.
Primeiro, um namespace que indica a gravidade do erro; juntamente à biblioteca <exception> do C++.
A partir daqui, de forma a facilitar nosso trabalho, simplesmente criamos uma classe GameError (que herda de exception) e fazemos os construtores e dando overloading nos operadores.
Inputs
Os inputs nada mais são que as entradas do jogo - pode ser mouse, teclado ou mesmo joysticks. A XInput é o componente do DirectX responsável por as entradas. No nosso caso, para demonstrar o poder dele, estaremos adicionando entradas de teclado, mouse e controle de Xbox 360 - o jogo em si só vai chamar o teclado, mas as funções pros outros dos tipos existem.
Criemos um input.h.
Com isso, podemos ir para o input.cpp e inserir o código:
Este módulo de inputs tem suporte a entradas de teclado, mouse e controles de Xbox 360. No entanto, nosso exemplo utilizará apenas teclado comum. De certa forma, o conteúdo do header - contendo declarações sobre bits no nosso controle acaba sendo mais importante. O nosso módulo de Inputs procura ler apenas as informações de X e Y para o mouse e saber quando uma (ou um conjunto) tecla foi pressionada, sem se preocupar em tomar alguma ação quando certa tecla especificamente for pressionada. Algumas teclas estão mapeadas no constant.h, como o Esc, Alt, dentre outras, que poderemos utilizar mais a frente em outras zonas do jogo.
Este tutorial tem como objetivo a criação do esqueleto de uma engine 2D utilizando C++/DirectX no Windows. A parte 1, que buscava discutir um pouco de tecnologia (enrolar), encontra-se aqui.
Sem enrolação porque já me arrependi de escrever isso, vamos lá.
Sumário
1. Configurando o Visual Studio
2. Iniciando o Win32
3. Constantes e Tratamento de Erros
4. Inputs
5. Gráficos, Imagens e Texturas
6. Um Jogo Genérico
7. As Regras do Pokémon
8. Adicionando mais coisas e polindo o jogo
Configurando o Visual Studio
Bom, na parte 1, avisei que era necessário ter instalado os kits de desenvolvimento do DX e do Windows 7. Não tem muito mistério, basta baixar e apertar nos botões de seguir em frente. O que eu falo aqui é pro Visual Studio 2010 Express, mas serve também para outras IDEs como Eclipse e tudo mais. O objetivo inicial aqui é inserirmos todas as libs relacionadas ao Windows 7 e DX para o compilador usá-las (afinal, as libs não ficam por padrão inseridas no linker do compilador).
Crie um projeto e vá em Project e, logo em seguida, Properties. No topo da nova janela, sete a configuração para "Active" (posteriormente, repita todo o processo aqui para a Debug também).
Agora expanda a opção Linker e vá para a opção General. Em Additional Library Directories, insira, antes de todos os outros, a linha "$(DXSDK_DIR)\Lib\x86;" (sem aspas). Já na opção "Input", no campo Additional Dependencies, é necessário incluir as seguintes libs (no início de tudo): "d3d9.lib;d3dx9.lib;winmm.lib;xinput.lib;"Agora na opção C/C++ no menu, a qual você deve expandir, siga para General e em Additional Include Directories, insira "$(DXSDK_DIR)\Include;".
Pronto, o Visual Studio está perfeitamente configurado para compilar aplicações que usem o Win32 e o DirectX. Se você quiser, pode incluir as libs da versão x64 também, mas só recomendo fazer isso se souber as consequências.
Iniciando o Win32
Win32 é uma API que permite você se utilizar de funções para fazer janelas, botões e afins no Windows. Vale lembrar que ela é uma função quase legada, visto que se você quiser um aplicativo Metro, conforme é no Windows 8/10, você utiliza outras tecnologias (como XAML, até onde sei).
De qualquer forma, o primeiro passo aqui é setar as configurações que queremos da janela e depois chamar a função de inicialização. Como existem muitos headers para adicionar, utilizamos a diretiva WIN32_LEAN_AND_MEAN que contém as seguintes informações:
Código:
#ifndef WIN32_LEAN_AND_MEAN
#include <cderr.h>
#include <dde.h>
#include <ddeml.h>
#include <dlgs.h>
#ifndef _MAC
#include <lzexpand.h>
#include <mmsystem.h>
#include <nb30.h>
#include <rpc.h>
#endif
#include <shellapi.h>
#ifndef _MAC
#include <winperf.h>
#include <winsock.h>
#endif
#ifndef NOCRYPT
#include <wincrypt.h>
#include <winefs.h>
#include <winscard.h>
#endif
#ifndef NOGDI
#ifndef _MAC
#include <winspool.h>
#ifdef INC_OLE1
#include <ole.h>
#else
#include <ole2.h>
#endif /* !INC_OLE1 */
#endif /* !MAC */
#include <commdlg.h>
#endif /* !NOGDI */
#endif /* WIN32_LEAN_AND_MEAN */
Começando, crie um arquivo .cpp no Visual Studio; pode chamá-lo de como quiser, aqui vamos chamar de winmain.cpp. Vou inserindo o código por partes e discutindo o que cada trecho faz.
Código:
#define _CRTDBG_MAP_ALLOC // detectar memory leaks
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <stdlib.h> // detectar memory leaks
#include <crtdbg.h> // detectar memory leaks
#include "pokemon.h"
// Protótipos de funções
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int);
bool CreateMainWindow(HWND &, HINSTANCE, int);
LRESULT WINAPI WinProc(HWND, UINT, WPARAM, LPARAM);
// Ponteiros
Pokemon *game = NULL;
HWND hwnd = NULL;
A primeira diretiva e a biblioteca <crtdbg.h> tem como funções a detecção de possíveis memory leaks na versão debug do código. Por mais que não tenhamos, no futuro, ao adicionar mais funções, pode ser interessante investigá-los. O pokemon.h trata-se de nosso "Jogo Específico" (discutido mais a frente) e a stdlib.h é a biblioteca padrão da linguagem C que, se você está lendo isso, já a deve conhecer.
Quanto aos protótipos de funções, existem coisas interessantes aqui. Todo programa Windows é inicializado pela função WinMain (diferente do "main" padrão em C ou Java, de forma que esta não existe aí). A WinProc recebe todos os comandos inseridos por nós (cliques, teclas e afins) e a CreateMainWindow é apenas uma função que será mais discutida a frente e tem a finalidade de gerar a janela de fato.
Código:
//=============================================================================
// Ponto inicial para uma aplicação Windows
//=============================================================================
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
// Verificando por memory leaks se estivermos compilando uma versão DEBUG
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif
MSG msg;
// Crie o jogo, configura o mensageiro
game = new Pokemon;
// Cria a janela principal
if (!CreateMainWindow(hwnd, hInstance, nCmdShow))
return 1;
try{
game->initialize(hwnd); // Dá GameError
// Loop principal de mensagem
int done = 0;
while (!done) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// procurar por uma mensagem de saída
if (msg.message == WM_QUIT)
done = 1;
// decodar e passar mensagem para o WinProc
TranslateMessage(&msg);
DispatchMessage(&msg);
} else
game->run(hwnd); // rodar o loop do jogo
}
SAFE_DELETE (game); // limpar memória antes de sair
return msg.wParam;
}
catch(const GameError &err) {
game->deleteAll();
DestroyWindow(hwnd);
MessageBox(NULL, err.getMessage(), "Error", MB_OK);
}
catch(...){
game->deleteAll();
DestroyWindow(hwnd);
MessageBox(NULL, "Erro desconhecido aconteceu no jogo.", "Error", MB_OK);
}
SAFE_DELETE (game); // limpar memória antes de sair
return 0;
}
Vamos tentar destrinchar o que está acontecendo aqui, mas não vou me aprofundar muito em detalhes sobre a API ou isso daria um livro. O nosso jogo funciona no esquema de loops: enquanto o loop estiver rodando (ou seja, o while(!done) for verdadeiro), as ações continuam sendo sempre monitoradas. Se há uma mensagem (input, pedido de saída, etc), então isso é passado posteriormente para o WinProc que, posteriormente, vai retornar uma função que será executada no jogo.
Existe tratamento de erros aí que é meio intuitivo o que está acontecendo; o GameError é tratado mais a frente.
O código para o Safe_Delete (inserido em um constants.h em breve) é o seguinte:
Código:
#define SAFE_DELETE(ptr) { if (ptr) { delete(ptr); (ptr)=NULL; } }
Por fim, nosso winmain.cpp termina da seguinte forma:
Código:
//=============================================================================
// Função de callback da janela
//=============================================================================
LRESULT WINAPI WinProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
return (game->messageHandler(hwnd, msg, wParam, lParam));
}
//=============================================================================
// Cria a janela
// Em caso de erro, retorna falso
//=============================================================================
bool CreateMainWindow(HWND &hwnd, HINSTANCE hInstance, int nCmdShow) {
WNDCLASSEX wcx;
// Preenche a janela com a estrutura de parâmetros que descrevem a janela principal
wcx.cbSize = sizeof(wcx); // tamanho da estrutura
wcx.style = CS_HREDRAW | CS_VREDRAW; // redesenhar se o tamanho mudar na horizontal ou vertical
wcx.lpfnWndProc = WinProc; // aponta para o procedimento WinProc
wcx.cbClsExtra = 0; // sem memória extra de classe
wcx.cbWndExtra = 0; // sem memória extra de classe
wcx.hInstance = hInstance; // handle to instance
wcx.hIcon = NULL;
wcx.hCursor = LoadCursor(NULL,IDC_ARROW); // cursor predefinido
wcx.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // bg negro
wcx.lpszMenuName = NULL; // nome do menu
wcx.lpszClassName = CLASS_NAME; // nome da classe da janela
wcx.hIconSm = NULL; // ícone da janela pequeno
// Registra a classe da janela
// RegisterClassEx retorna 0 em erro.
if (RegisterClassEx(&wcx) == 0) // se erro
return false;
// configurar a janela em janelado ou fullscreen?
DWORD style;
if (FULLSCREEN)
style = WS_EX_TOPMOST | WS_VISIBLE | WS_POPUP;
else
style = WS_OVERLAPPEDWINDOW;
// Gerar janela
hwnd = CreateWindow(
CLASS_NAME, // nome da classe da janela
GAME_TITLE, // texto da barra de título
style, // estilo da janela
CW_USEDEFAULT, // posição horizontal padrão da janela
CW_USEDEFAULT, // posição vertical padrão da janela
GAME_WIDTH, // largura of window
GAME_HEIGHT, // altura of the window
(HWND) NULL, // sem janela pai
(HMENU) NULL, // sem menu
hInstance, // lidar para o parâmetro da aplicação
(LPVOID) NULL); // sem parâmetros de janela
// se houve um erro ao criar a janela
if (!hwnd)
return false;
if(!FULLSCREEN) { // se janelado
// Ajustar tamanho da janela para que a área fique GAME_WIDTH x GAME_HEIGHT
RECT clientRect;
GetClientRect(hwnd, &clientRect); // get size of client area of window
MoveWindow(hwnd,
0, // Esquerda
0, // Cima
GAME_WIDTH+(GAME_WIDTH-clientRect.right), // Direita
GAME_HEIGHT+(GAME_HEIGHT-clientRect.bottom), // Baixo
TRUE); // Redesenhar janela
}
// Mostra a janela
ShowWindow(hwnd, nCmdShow);
return true;
}
Por fim, a função que exibe a janela de fato é a ShowWindow. Se você precisa escondê-la, basta enviar o parâmetro SW_HIDE.
Constantes e Tratamento de Erros
Antes de prosseguirmos, é interessante definir algumas constantes que usaremos mais a frente e funções para tratamento de erros. Lembrando sempre que a necessidade dessas constantes, diferente aqui, vai aparecendo com o tempo; porém, o "passo-a-passo" ficaria demasiado longo. O objetivo aqui é entender o que há por trás de um motor de jogo, afinal.
Criemos agora um arquivo constants.h.
Começamos com as prediretivas, includes e macros para evitar repetições no compilador:
Código:
#ifndef _CONSTANTS_H
#define _CONSTANTS_H
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
//-----------------------------------------------
// Macros úteis
//-----------------------------------------------
// Deletar com segurança item (ponteiro) referenciado
#define SAFE_DELETE(ptr) { if (ptr) { delete (ptr); (ptr)=NULL; } }
// Liberar com segurança item (ponteiro) referenciado
#define SAFE_RELEASE(ptr) { if(ptr) { (ptr)->Release(); (ptr)=NULL; } }
// Deletar com segurança array de ponteiro referenciado
#define SAFE_DELETE_ARRAY(ptr) { if(ptr) { delete [](ptr); (ptr)=NULL; } }
// Chamar com segurança onLostDevice
#define SAFE_ON_LOST_DEVICE(ptr) { if(ptr) { ptr->onLostDevice(); } }
// Chamar com segurança onResetDevice
#define SAFE_ON_RESET_DEVICE(ptr) { if(ptr) { ptr->onResetDevice(); } }
#define TRANSCOLOR SETCOLOR_ARGB(0,255,0,255) // cor transparente (magenta)
O código em si é autoexplicativo; queremos evitar que memória se acumule sem ser descartada e acabe gerando um overflow. O interessante está na TRANSCOLOR, que basicamente vai definir que cor queremos para a transparência na hora de renderizar nossos sprites/tiles e afins; nesse caso, é a magenta. Se mudássemos tudo para 0, teríamos que preto é equivalente.
Ainda em nosso constants.h:
Código:
//-----------------------------------------------
// Constantes
//-----------------------------------------------
// imagens
const char MAP_IMAGE[] = "pictures\\bg.jpg"; // background
const char CHAR_IMAGE[] = "pictures\\red.png"; // RED
// configurações da janela
const char CLASS_NAME[] = "Pokemon";
const char GAME_TITLE[] = "Pokemon - Tech Demo 1.0";
const bool FULLSCREEN = false; // janelado ou fullscreen
const UINT GAME_WIDTH = 640; // largura do jogo em pixels
const UINT GAME_HEIGHT = 480; // altura do jogo em pixels
const int CHAR_START_FRAME = 0; // frame inicial da animação
const int CHAR_END_FRAME = 2; // último frame da animação do personagem
const float CHAR_ANIMATION_DELAY = 0.2f; // tempo entre frames
const int CHAR_COLS = 3; // textura dos personagens tem duas colunas
const int CHAR_WIDTH = 16; // largura da imagem do personagem
const int CHAR_HEIGHT = 32; // altura da imagem do personagem
const float ROTATION_RATE = 180.0f; // graus por segundo
const float SCALE_RATE = 0.2f; // % mudança por segundo
const float CHAR_SPEED = 100.0f; // pixels por segundo
const float CHAR_SCALE = 1.5f; // escala inicial do personagem
// jogo
const double PI = 3.14159265;
const float FRAME_RATE = 200.0f; // o framerate alvo (frames/sec)
const float MIN_FRAME_RATE = 10.0f; // framerate mínimo
const float MIN_FRAME_TIME = 1.0f/FRAME_RATE; // tempo mínimo desejado para 1 frame
const float MAX_FRAME_TIME = 1.0f/MIN_FRAME_RATE; // tempo máximo usado nos cálculos
// Teclas
// Usamos simples constantes para o mapeamento de teclas. Se variáveis fossem usadas, seria possível salvar e
// restaurar o mapeamento de teclas de um arquivo de dados.
const UCHAR ESC_KEY = VK_ESCAPE; // Esc
const UCHAR ALT_KEY = VK_MENU; // Alt
const UCHAR ENTER_KEY = VK_RETURN; // Enter
const UCHAR CHAR_LEFT_KEY = VK_LEFT; // Direcional esquerda
const UCHAR CHAR_RIGHT_KEY = VK_RIGHT; // Direcional direita
const UCHAR CHAR_UP_KEY = VK_UP; // Direcional cima
const UCHAR CHAR_DOWN_KEY = VK_DOWN; // Direcional baixo
#endif
Nosso jogo consiste apenas em um background e um personagem se movimentando - nada mais. De forma a facilitar nossa vida, simplesmente definimos os locais dos arquivos em uma constante. Pode-se fazer isso lendo um arquivo de dados ou similar, por exemplo.
As configurações da janela foram todas chamadas na função CreateWindow, no tópico anterior; tanto faz setá-las como constantes ou invocá-las diretamente pela função, passando-as como parâmetro. Isso é apenas para facilitar o trabalho. Aproveitamos e definimos também a estrutura do sprite de nosso personagem (3 colunas e 4 linhas) e detalhes como FPS e demos nomes mais simples às teclas para o input.
Para fins de informação, este é o sprite que usaremos para o nosso personagem:
Criemos agora um gameError.h.
Código:
// Classe de Erro jogada pela engine
#ifndef _GAMEERROR_H
#define _GAMEERROR_H
#define WIN32_LEAN_AND_MEAN
#include <string>
#include <exception>
namespace gameErrorNS {
// Códigos de erros
// Números negativos são erros fatais que podem necessitar que o jogo seja fechado
// Números positivos são alertas que não requerem que o jogo seja desligado
const int FATAL_ERROR = -1;
const int WARNING = 1;
}
A partir daqui, de forma a facilitar nosso trabalho, simplesmente criamos uma classe GameError (que herda de exception) e fazemos os construtores e dando overloading nos operadores.
Código:
class GameError : public std::exception {
private:
int errorCode;
std::string message;
public:
// construtor padrão
GameError() throw() :errorCode(gameErrorNS::FATAL_ERROR), message("Erro indefinido no jogo.") {}
// construtor cópia
GameError(const GameError& e) throw(): std::exception(e), errorCode(e.errorCode), message(e.message) {}
// construtor com argumentos
GameError(int code, const std::string &s) throw() :errorCode(code), message(s) {}
// operador de taxa
GameError& operator= (const GameError& rhs) throw() {
std::exception::operator=(rhs);
this->errorCode = rhs.errorCode;
this->message = rhs.message;
}
// destrutor
virtual ~GameError() throw() {};
// substituir o what da classe base
virtual const char* what() const throw() { return this->getMessage(); }
const char* getMessage() const throw() { return message.c_str(); }
int getErrorCode() const throw() { return errorCode; }
};
#endif
Inputs
Os inputs nada mais são que as entradas do jogo - pode ser mouse, teclado ou mesmo joysticks. A XInput é o componente do DirectX responsável por as entradas. No nosso caso, para demonstrar o poder dele, estaremos adicionando entradas de teclado, mouse e controle de Xbox 360 - o jogo em si só vai chamar o teclado, mas as funções pros outros dos tipos existem.
Criemos um input.h.
Código:
#ifndef _INPUT_H
#define _INPUT_H
#define WIN32_LEAN_AND_MEAN
class Input;
#include <windows.h>
#include <WindowsX.h>
#include <string>
#include <XInput.h>
#include "constants.h"
#include "gameError.h"
// para mouse de alta definição
#ifndef HID_USAGE_PAGE_GENERIC
#define HID_USAGE_PAGE_GENERIC ((USHORT) 0x01)
#endif
#ifndef HID_USAGE_GENERIC_MOUSE
#define HID_USAGE_GENERIC_MOUSE ((USHORT) 0x02)
#endif
namespace inputNS {
const int KEYS_ARRAY_LEN = 256; // tamanho do array de teclas
// what tem valor de clear(), flag de bit
const UCHAR KEYS_DOWN = 1;
const UCHAR KEYS_PRESSED = 2;
const UCHAR MOUSE = 4;
const UCHAR TEXT_IN = 8;
const UCHAR KEYS_MOUSE_TEXT = KEYS_DOWN + KEYS_PRESSED + MOUSE + TEXT_IN;
}
const DWORD GAMEPAD_THUMBSTICK_DEADZONE = (DWORD)(0.20f * 0X7FFF); // padrão 20% de intervalo em zona morta
const DWORD GAMEPAD_TRIGGER_DEADZONE = 30; // range de ativação de 0-255
const DWORD MAX_CONTROLLERS = 4; // Número máximo de controles suportados
// Bits correspondentes aos botões do gamepad em state.Gamepad.wButtons
const DWORD GAMEPAD_DPAD_UP = 0x0001;
const DWORD GAMEPAD_DPAD_DOWN = 0x0002;
const DWORD GAMEPAD_DPAD_LEFT = 0x0004;
const DWORD GAMEPAD_DPAD_RIGHT = 0x0008;
const DWORD GAMEPAD_START_BUTTON = 0x0010;
const DWORD GAMEPAD_BACK_BUTTON = 0x0020;
const DWORD GAMEPAD_LEFT_THUMB = 0x0040;
const DWORD GAMEPAD_RIGHT_THUMB = 0x0080;
const DWORD GAMEPAD_LEFT_SHOULDER = 0x0100;
const DWORD GAMEPAD_RIGHT_SHOULDER = 0x0200;
const DWORD GAMEPAD_A = 0x1000;
const DWORD GAMEPAD_B = 0x2000;
const DWORD GAMEPAD_X = 0x4000;
const DWORD GAMEPAD_Y = 0x8000;
struct ControllerState {
XINPUT_STATE state;
XINPUT_VIBRATION vibration;
float vibrateTimeLeft; // mSec
float vibrateTimeRight; // mSec
bool connected;
};
class Input {
private:
bool keysDown[inputNS::KEYS_ARRAY_LEN];
bool keysPressed[inputNS::KEYS_ARRAY_LEN];
std::string textIn;
char charIn;
bool newLine;
int mouseX, mouseY;
int mouseRawX, mouseRawY;
RAWINPUTDEVICE Rid[1];
bool mouseCaptured;
bool mouseLButton;
bool mouseMButton;
bool mouseRButton;
bool mouseX1Button;
bool mouseX2Button;
ControllerState controllers[MAX_CONTROLLERS];
public:
Input();
virtual ~Input();
void initialize(HWND hwnd, bool capture);
void keyDown(WPARAM);
void keyUp(WPARAM);
void keyIn(WPARAM);
bool isKeyDown(UCHAR vkey) const;
bool wasKeyPressed(UCHAR vkey) const;
bool anyKeyPressed() const;
void clearKeyPress(UCHAR vkey);
void clear(UCHAR what);
void clearAll() {clear(inputNS::KEYS_MOUSE_TEXT);}
void clearTextIn() {textIn.clear();}
// Retorna entrada de texto como string
std::string getTextIn() {return textIn;}
// Retorna último caractere inserido
char getCharIn() {return charIn;}
// Lê a posição do mouse em X, Y
void mouseIn(LPARAM);
void mouseRawIn(LPARAM);
// Salva estado do botão do mouse
void setMouseLButton(bool b) { mouseLButton = b; }
void setMouseMButton(bool b) { mouseMButton = b; }
void setMouseRButton(bool b) { mouseRButton = b; }
void setMouseXButton(WPARAM wParam) {mouseX1Button = (wParam & MK_XBUTTON1) ? true:false;
mouseX2Button = (wParam & MK_XBUTTON2) ? true:false;}
// Retorna posição X do mouse
int getMouseX() const { return mouseX; }
// Retorna posição Y do mouse
int getMouseY() const { return mouseY; }
int getMouseRawX() const { return mouseRawX; }
int getMouseRawY() const { return mouseRawY; }
// Retorna estado do botão esquerdo / meio / direito do mouse
bool getMouseLButton() const { return mouseLButton; }
bool getMouseMButton() const { return mouseMButton; }
bool getMouseRButton() const { return mouseRButton; }
// Retorna estado do botão X1 / X2 do mouse
bool getMouseX1Button() const { return mouseX1Button; }
bool getMouseX2Button() const { return mouseX2Button; }
// Atualiza status de conexão dos controles.
void checkControllers();
// Salva entrada de controles conectados.
void readControllers();
// Retorna estado especificado de controle.
const ControllerState* getControllerState(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return &controllers[n];
}
// Retorna estado dos n botões do controle.
const WORD getGamepadButtons(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return controllers[n].state.Gamepad.wButtons;
}
// Retorna estado do D-PAD cima / baixo / esquerda / direita / start / back / etc do controle n
bool getGamepadDPadUp(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return ((controllers[n].state.Gamepad.wButtons&GAMEPAD_DPAD_UP) != 0);
}
bool getGamepadDPadDown(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return ((controllers[n].state.Gamepad.wButtons&GAMEPAD_DPAD_DOWN) != 0);
}
bool getGamepadDPadLeft(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return ((controllers[n].state.Gamepad.wButtons&GAMEPAD_DPAD_LEFT) != 0);
}
bool getGamepadDPadRight(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_DPAD_RIGHT) != 0);
}
bool getGamepadStart(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_START_BUTTON) != 0);
}
bool getGamepadBack(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_BACK_BUTTON) != 0);
}
bool getGamepadLeftThumb(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_LEFT_THUMB) != 0);
}
bool getGamepadRightThumb(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_RIGHT_THUMB) != 0);
}
bool getGamepadLeftShoulder(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_LEFT_SHOULDER) != 0);
}
bool getGamepadRightShoulder(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_RIGHT_SHOULDER) != 0);
}
bool getGamepadA(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_A) != 0);
}
bool getGamepadB(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_B) != 0);
}
bool getGamepadX(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_X) != 0);
}
bool getGamepadY(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return bool((controllers[n].state.Gamepad.wButtons&GAMEPAD_Y) != 0);
}
BYTE getGamepadLeftTrigger(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.bLeftTrigger);
}
BYTE getGamepadRightTrigger(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.bRightTrigger);
}
SHORT getGamepadThumbLX(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.sThumbLX);
}
SHORT getGamepadThumbLY(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.sThumbLY);
}
SHORT getGamepadThumbRX(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.sThumbRX);
}
SHORT getGamepadThumbRY(UINT n) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
return (controllers[n].state.Gamepad.sThumbRY);
}
// Vibra o motor esquerdo / direito do controle n.
// Esquerda é vibração de baixa frequência, direito é alta frequência.
// Velocidade 0=desligado, 65536=100 porcento
// sec é o tempo para vibrar em segundos
void gamePadVibrateLeft(UINT n, WORD speed, float sec) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
controllers[n].vibration.wLeftMotorSpeed = speed;
controllers[n].vibrateTimeLeft = sec;
}
void gamePadVibrateRight(UINT n, WORD speed, float sec) {
if(n > MAX_CONTROLLERS-1)
n=MAX_CONTROLLERS-1;
controllers[n].vibration.wRightMotorSpeed = speed;
controllers[n].vibrateTimeRight = sec;
}
void vibrateControllers(float frameTime);
};
#endif
Com isso, podemos ir para o input.cpp e inserir o código:
Código:
#include "input.h"
//=============================================================================
// construtor padrão
//=============================================================================
Input::Input() {
// limpar teclas seguradas (key down)
for (size_t i = 0; i < inputNS::KEYS_ARRAY_LEN; i++)
keysDown[i] = false;
// limpar teclas apertadas (key pressed)
for (size_t i = 0; i < inputNS::KEYS_ARRAY_LEN; i++)
keysPressed[i] = false;
newLine = true; // iniciar nova linha
textIn = ""; // limpar textIn
charIn = 0; // limpar charIn
// mouse data
mouseX = 0; // X da tela
mouseY = 0; // Y de tela
mouseRawX = 0; // X de alta definição
mouseRawY = 0; // Y de alta definição
mouseLButton = false; // verdade se o mouse esquerdo está apertado
mouseMButton = false; // verdade se o botão do meio está apertado
mouseRButton = false; // verdade se o mouse direito está apertado
mouseX1Button = false; // verdade se o botão X1 do mouse está apertado
mouseX2Button = false; // verdade se o botão X2 do mouse está apertado
for(int i=0; i<MAX_CONTROLLERS; i++)
{
controllers[i].vibrateTimeLeft = 0;
controllers[i].vibrateTimeRight = 0;
}
}
//=============================================================================
// destrutor
//=============================================================================
Input::~Input() {
if(mouseCaptured)
ReleaseCapture(); // libera mouse
}
//=============================================================================
// Inicializar mouse e input do controle
// Setar capture=true para capturar mouse
//=============================================================================
void Input::initialize(HWND hwnd, bool capture) {
try{
mouseCaptured = capture;
// registrar o mouse de alta definição
Rid[0].usUsagePage = HID_USAGE_PAGE_GENERIC;
Rid[0].usUsage = HID_USAGE_GENERIC_MOUSE;
Rid[0].dwFlags = RIDEV_INPUTSINK;
Rid[0].hwndTarget = hwnd;
RegisterRawInputDevices(Rid, 1, sizeof(Rid[0]));
if(mouseCaptured)
SetCapture(hwnd); // capturar mouse
// Limpar estados do controle
ZeroMemory( controllers, sizeof(ControllerState) * MAX_CONTROLLERS );
checkControllers(); // Checar por controles conectados
}
catch(...) {
throw(GameError(gameErrorNS::FATAL_ERROR, "Erro ao inicializar sistema de input"));
}
}
//=============================================================================
// Coloca true nos arrays keysDown e keysPressed para esta tecla
// Pre: wParam contém o código virtual da tecla (0--255)
//=============================================================================
void Input::keyDown(WPARAM wParam) {
// ter certeza que o código da tecla está dentro do buffer
if (wParam < inputNS::KEYS_ARRAY_LEN) {
keysDown[wParam] = true; // atualizar o array keysDown
// tecla foi "apertada", limpar com o clear()
keysPressed[wParam] = true; // atualizar o array keysPressed
}
}
//=============================================================================
// Coloca falso no array keysDown para este tecla
//=============================================================================
void Input::keyUp(WPARAM wParam) {
// ter certeza que o código da tecla está dentro do buffer
if (wParam < inputNS::KEYS_ARRAY_LEN)
// atualizar tabela de estados
keysDown[wParam] = false;
}
//=============================================================================
// Salva o caractere inserido na string textIn
// Pre: wParam contém o caractere
//=============================================================================
void Input::keyIn(WPARAM wParam) {
if (newLine) { // se o início de uma nova linha
textIn.clear();
newLine = false;
}
if (wParam == '\b') { // se espaço
if(textIn.length() > 0) // se caracteres existem
textIn.erase(textIn.size()-1); // apagar o último inserido
}
else {
textIn += wParam; // adicionar caractere ao textIn
charIn = wParam; // salvar último caractere inserido
}
if ((char)wParam == '\r') // se retornar
newLine = true; // iniciar nova linha
}
//=============================================================================
// Retorna true se a tecla virtual está pressionada; caso contrário, falso.
//=============================================================================
bool Input::isKeyDown(UCHAR vkey) const
{
if (vkey < inputNS::KEYS_ARRAY_LEN)
return keysDown[vkey];
else
return false;
}
//=============================================================================
// Retorna true se a tecla virtual especificada foi pressionada no frame
// mais recente. Apertos de tecla são apagados no fim de cada frame.
//=============================================================================
bool Input::wasKeyPressed(UCHAR vkey) const {
if (vkey < inputNS::KEYS_ARRAY_LEN)
return keysPressed[vkey];
else
return false;
}
//=============================================================================
// Retorna true se qualquer tecla virtual especificada foi apertada no frame mais recente.
// Apertos são apagados após o fim de cada frame.
//=============================================================================
bool Input::anyKeyPressed() const {
for (size_t i = 0; i < inputNS::KEYS_ARRAY_LEN; i++)
if(keysPressed[i] == true)
return true;
return false;
}
//=============================================================================
// Limpar a tecla apertada especificada
//=============================================================================
void Input::clearKeyPress(UCHAR vkey) {
if (vkey < inputNS::KEYS_ARRAY_LEN)
keysPressed[vkey] = false;
}
//=============================================================================
// Limpar os buffers de inputs especificados.
// Veja input.h para quais valores
//=============================================================================
void Input::clear(UCHAR what) {
if(what & inputNS::KEYS_DOWN) {
for (size_t i = 0; i < inputNS::KEYS_ARRAY_LEN; i++)
keysDown[i] = false;
}
if(what & inputNS::KEYS_PRESSED) {
for (size_t i = 0; i < inputNS::KEYS_ARRAY_LEN; i++)
keysPressed[i] = false;
}
if(what & inputNS::MOUSE) {
mouseX = 0;
mouseY = 0;
mouseRawX = 0;
mouseRawY = 0;
}
if(what & inputNS::TEXT_IN)
clearTextIn();
}
//=============================================================================
// Lê a posição do mouse em mouseX e mouseY
//=============================================================================
void Input::mouseIn(LPARAM lParam) {
mouseX = GET_X_LPARAM(lParam);
mouseY = GET_Y_LPARAM(lParam);
}
//=============================================================================
// Lê o dado cru do mouse em mouseRawX, mouseRawY
// Esta rotina é compatível com um mouse de alta definição
//=============================================================================
void Input::mouseRawIn(LPARAM lParam) {
UINT dwSize = 40;
static BYTE lpb[40];
GetRawInputData((HRAWINPUT)lParam, RID_INPUT,
lpb, &dwSize, sizeof(RAWINPUTHEADER));
RAWINPUT* raw = (RAWINPUT*)lpb;
if (raw->header.dwType == RIM_TYPEMOUSE) {
mouseRawX = raw->data.mouse.lLastX;
mouseRawY = raw->data.mouse.lLastY;
}
}
//=============================================================================
// Procurar por controles conectados
//=============================================================================
void Input::checkControllers()
{
DWORD result;
for( DWORD i = 0; i <MAX_CONTROLLERS; i++) {
result = XInputGetState(i, &controllers[i].state);
if(result == ERROR_SUCCESS)
controllers[i].connected = true;
else
controllers[i].connected = false;
}
}
//=============================================================================
// Ler o estado dos controles conectados
//=============================================================================
void Input::readControllers()
{
DWORD result;
for( DWORD i = 0; i <MAX_CONTROLLERS; i++) {
if(controllers[i].connected) {
result = XInputGetState(i, &controllers[i].state);
if(result == ERROR_DEVICE_NOT_CONNECTED) // se desconectado
controllers[i].connected = false;
}
}
}
//=============================================================================
// Vibrar controles conectados
//=============================================================================
void Input::vibrateControllers(float frameTime) {
for(int i=0; i < MAX_CONTROLLERS; i++) {
if(controllers[i].connected) {
controllers[i].vibrateTimeLeft -= frameTime;
if(controllers[i].vibrateTimeLeft < 0) {
controllers[i].vibrateTimeLeft = 0;
controllers[i].vibration.wLeftMotorSpeed = 0;
}
controllers[i].vibrateTimeRight -= frameTime;
if(controllers[i].vibrateTimeRight < 0) {
controllers[i].vibrateTimeRight = 0;
controllers[i].vibration.wRightMotorSpeed = 0;
}
XInputSetState(i, &controllers[i].vibration);
}
}
}
Este módulo de inputs tem suporte a entradas de teclado, mouse e controles de Xbox 360. No entanto, nosso exemplo utilizará apenas teclado comum. De certa forma, o conteúdo do header - contendo declarações sobre bits no nosso controle acaba sendo mais importante. O nosso módulo de Inputs procura ler apenas as informações de X e Y para o mouse e saber quando uma (ou um conjunto) tecla foi pressionada, sem se preocupar em tomar alguma ação quando certa tecla especificamente for pressionada. Algumas teclas estão mapeadas no constant.h, como o Esc, Alt, dentre outras, que poderemos utilizar mais a frente em outras zonas do jogo.