DevLog #11 - Engine engineering

posted in StarDust DevLog
Published October 08, 2011
Advertisement
Last several days I've spent by separating engine from game by creating engine API.
Code for window handling and keyboard listening are nearly done.


Sounds strange to start speaking about creating an engine after many weeks of development. Bad for me, I haven't draw the line between engine part and game part at the beginning, and so no wonder that codes of both parts began to weave together. In fact when I needed draw something from the game part, I accessed directly the OpenGL API. It works, but creates a mess that is hard to maintain. Although there was some shallow intermediate level for more complex operations (e.g. depth sorting), the usage of OpenGL was in most cases direct.

[subheading]Window handling[/subheading]
I had tendency to make the engine more separated already before, but it was burnt at the first step - wrap the window management. Creating a window wrapper is easy so long until you reach WndProc (method processing window messages that had to be registered). To register the WndProc it had to be a static method. I was trying to search Google for help but at my earlier attempts I found only confirmations that there isn't a way how to use a member function for window message processing. This week I tried once more to find a solution on this and finally was lucky.

The WndProc will stay static, but the only thing it will do is to pass the message to a member function of the window instance. The only thing remaining is how to pass the window instance to static WndProc. Before we have the HWND available, we pass the pointer on our window class using LPARAM of window creation message.

[spoiler]HWND WINAPI CreateWindowEx(
__in DWORD dwExStyle,
__in_opt LPCTSTR lpClassName,
__in_opt LPCTSTR lpWindowName,
__in DWORD dwStyle,
__in int x,
__in int y,
__in int nWidth,
__in int nHeight,
__in_opt HWND hWndParent,
__in_opt HMENU hMenu,
__in_opt HINSTANCE hInstance,
__in_opt LPVOID lpParam // This will come as LPARAM of WM_NCCREATE message, so put pointer on window wrapper here
);
[/spoiler]

Whatever we set as LPARAM in the CreateWindowEx function will come as LPARAM of WM_NCCREATE message.
In WndProc we have to catch the message and read out the pointer on our window wrapper instance from the LPARAM.
We need to save the pointer now somewhere else. Because with the mesage came also HWND (window handle), we can save the pointer into it's USERDATA space. All later messages received by WndProc will read the instance of our window wrapper from the USERDATA in HWND.

[spoiler]LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
// Variable for our window wrapper instance
Window *window = NULL;

// This message comes after CreateWindowEx() is called
if ( uMsg == WM_NCCREATE )
{
// Read pointer on wrapper from LPARAM ...
window = reinterpret_cast( ((LPCREATESTRUCT)lParam)->lpCreateParams );
// ... and save it into USERDATA in HWND
SetWindowLong( hWnd, GWL_USERDATA, reinterpret_cast( window ) );
// Saving HWND into wrapper instace as well
window->SetHWnd( hWnd );
}
else
{
// Any other message received will read the wrapper instance from USERDATA in HWND
window = reinterpret_cast( GetWindowLong( hWnd, GWL_USERDATA ) );
}

if ( window )
{
// Pass the message to our own member function in window wrapper
return window->HandleWindowEvent( uMsg, wParam, lParam );
}
else
{
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
}
[/spoiler]

With the WndProc out of the way there is nothing what would prevent us from creating a window handling API, that can be easily used

Window* window = new Window();

window->Create( "My window", 1024, 768, 32, false );
window->Show();

// Switch from windowed to fullscreen
window->Destroy();
window->Create( "My window", 1024, 768, 32, true );
window->Show();


[spoiler]class Window
{
public:
Window();
~Window();
bool Create( char* title, int width, int height, int bits, bool fullscreen );
void Destroy();
void Show() const;

bool IsFullscreen() const;
HDC GetHDC() const;
void SetHWnd( HWND hWnd );

static LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam );

private:
LRESULT HandleWindowEvent( UINT uMsg, WPARAM wParam, LPARAM lParam ) const;
};
[/spoiler]

[subheading]Keyboard listener[/subheading]
For listening keyboard events I am using window messages sent to WndProc. But reaction on pressed keys is game-dependent. So the message processing function have to pass the information about keys to some game method. So when user "click" on a key on keyboard, the game gets notified about it, but the game often needs to know also if the key is "held". There are two methods to know if key is still pressed I know about:
1) use of boolean array and set the appropriate bool to TRUE on WM_KEYDOWN message and set it to FALSE on WM_KEYUP message
2) ask on actual status of key by GetAsyncKeyState() method

I've picked the first approach - array of 256 bool values keeping track of all keys. The KeyboardListener stores this array and also provides API for game to register for pressed keys or check actual status of the keys.

// Example of KeyboardListener usage

// Game functions that should react on different keys
void Funct_A() { ... }
void Funct_Shift_A() { ... }
void Funct_Ctrl_Shift_A() { ... }


void test()
{
KeyboardListener keyboardListener;
// Each function is now registered for a specific key (and possibly for combination of special keys like CTRL, ALT and SHIFT)
keyboardListener.RegisterCallback( 'A', &Funct_A );
keyboardListener.RegisterCallback( 'A', SPECIAL_KEY_SHIFT, &Funct_Shift_A );
keyboardListener.RegisterCallback( 'A', SPECIAL_KEY_CTRL | SPECIAL_KEY_SHIFT, &Funct_Ctrl_Shift_A );

// The registered functions are automatically called everytime their key (or combination) is pressed
// If some code should execute for the duration of some key being held, it have to check the the key status
if ( keyboardListener.IsPressed( 'A', SPECIAL_KEY_ALT | SPECIAL_KEY_CTRL ) )
{
...
}

}


[spoiler]class KeyboardListener
{
public:
KeyboardListener();
~KeyboardListener();

void KeyPressed ( unsigned char key, unsigned short specialKeyMask = 0 );
void KeyReleased( unsigned char key );
bool IsPressed ( unsigned char key, unsigned short specialKeyMask = 0 );

void RegisterCallback ( unsigned char key, unsigned short specialKeyMask, void(*callback)() );
void UnregisterCallback( unsigned char key, unsigned short specialKeyMask );

private:
KeyHandler* keys[ 256 ];
};



class KeyHandler
{
public:
KeyHandler();
~KeyHandler();

void Press( unsigned short specialKeyMask );
void Release();
bool IsPressed( unsigned short specialKeyMask );

void RegisterCallback( void (*function)(void), unsigned short specialKeyMask = 0 );
void UnregisterCallback( unsigned short specialKeyMask = 0 );

private:
bool pressed;
std::map< unsigned short, void(*)(void) > callbacks;
};
[/spoiler]

Every key can have a callback function. The game registers the callback function through KeyboardListener API. When the listener is notified by WndProc about pressed key, it checks if the key has a callback function registered and calls it. The thing gets a little bit complicated when we want to react on SHIFT, ALT and CTRL combinations. To check a combination of these special keys is used a 9 digit mask (unsigned short):

1 - Ctrl (any)
2 - Alt (any)
4 - Shift (any)
8 - Left Ctrl
16 - Right Ctrl
32 - Left Alt
64 - Rigth Alt
128 - Left Shift
256 - Right Shift

If a game needs some function to react on Ctrl + A, but does not bother if it is left or right Ctrl, it registers the function in KeyboardListener for key 'A' and a special key mask "000000001":

keyboardListener.RegisterCallback( 'A', binary(000000001) , &Function );

When player hits a left Ctrl and A for example, the WndProc catches 'A' key pressed message and looks for state of Ctrl, Alt and Shift keys. It creates the following mask:

000000001 // because VK_CONTROL is pressed
000001000 // because VK_LCONTROL is pressed
----------------
000001001


As you see, although the Ctrl was pressed, the two masks are not equal. The KeyboardListener needs to split this mask into two parts - general part and LR (left-right) part. The general part takes last 3 digits of mask, the L-R part takes first 6 digits of the mask. These two parts are then taken as the masks to compare against what was registered by game.

General mask: 000000001 - equal to registered mask to call Function
L-R mask: 000001000 - no function is registered with this mask

This enables the game to make differences between left and right control keys, or don't bother about their side at all.
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement