I am starting out with the goal to make a simple video game called "Anna's Afterlife". I also want to use it for rich client application development: some educational software for my son and other kids, and maybe as a front end to an accounting system for work. Therefor, my architecture should have these goals.
To meet these goals, I've created a very simple architecture. Essentially, the user of this framework will create an instance of a directApplication, and feed one or more boards into it. A board literally means a board like in a game - it has a hooks for getting keystrokes and mouse movements and also also knows how to draw itself. boards are designed to be written portably, and the direct2dApplication works through those boards.
| Layer | Classes | Purpose |
|---|---|---|
| Rendering | direct2dContext | Is the place where Direct 2d renders to. |
| Window | directApplication | Contains the Window handling, message loop, application lifetime. |
| Data Transfer | pointDto,solidBrushDto, etc | provides a platform neutral set of types that boards can use to talk to the application with. |
| Board | board | portable rendering and event handling logic |
Right now, there's just a testBoard, that draws a square and spins the name around in code. Eventually, there will be a derived board class that has a scene graph and serialization in it.
To get rolling with Direct2d, first you need a Direct2d factory and a DirectWrite factory, as shown below.
direct2dContext::direct2dContext( ) :
d2DFactory( NULL ), wicFactory( NULL ), dWriteFactory( NULL ), renderTarget( NULL )
{
CoInitialize( NULL );
HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &d2DFactory);
if (SUCCEEDED(hr))
hr = CoCreateInstance( CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wicFactory) );
if (SUCCEEDED(hr)) // Create a DirectWrite factory.
hr = DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(dWriteFactory), reinterpret_cast<IUnknown **>(&dWriteFactory) );
}
Now we need a render target. The rendertarget is just place for the system to write to. In our case, we want the render target
to be part of a Window.
bool direct2dContext::createRenderTarget( HWND hwnd )
{
RECT rc;
GetClientRect( hwnd, &rc);
D2D1_SIZE_U size = D2D1::SizeU( rc.right - rc.left, rc.bottom - rc.top );
HRESULT hr = d2DFactory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(hwnd, size), &renderTarget );
return SUCCEEDED(hr);
}
In Direct2d it is more efficient to create objects once, and paint with them repeatedly. However, Direct2d has an implementation quirk, in that, these objects can go away out from underneath you. So, you have to periodically recreate your objects so that you can use your brushes and geometries and others, repeatedly. To help with mananging this, the direct2d application creates a cache of the various object types and manages them on the board's behalf. This takes place in two steps.
First, we look at the end paint of our render target and see if we have to recreate it. If we do, we blow away the Direct2d side of things. Note the use of the new C++0x lambda expressions in Visual C++ 2010.
void directApplication::endDraw() {
if (!currentBoard)
return;
HRESULT hr = renderTarget->EndDraw();
if (hr == D2DERR_RECREATE_TARGET) {
std::for_each( brushes.begin(), brushes.end(), [ this ]( std::pair<std::string, deviceDependentAssetBase *> ib ) {
ib.second->release();
});
Then, recreate the items in the board when the render target doesn't exist.
void directApplication::beginDraw( )
{
if (!currentBoard)
return;
if (!renderTarget && createRenderTarget( hwnd )) {
std::for_each( bitmaps.begin(), bitmaps.end(), [ this ]( std::pair<std::string, deviceDependentAssetBase *> ib ) {
ib.second->create(this);
});
std::for_each( brushes.begin(), brushes.end(), [ this ]( std::pair<std::string, deviceDependentAssetBase *> ib ) {
ib.second->create(this);
});
Taken together, these items allow us to present to the board a consistent view of the brushes and items in the system. I'll delve more into the specifics of Direct2d objects in the next article.
Finally, our direct2dApplication is a window, and manages the Direct2d binding to it, and handles the message loop, and forwards items to the board.
Our message loop looks like
setBoard( _firstBoard );
::ShowWindow(hwnd, SW_SHOWNORMAL );
::UpdateWindow( hwnd );
::QueryPerformanceFrequency( (LARGE_INTEGER *)&performanceFrequency );
::QueryPerformanceCounter( (LARGE_INTEGER *)&lastCounter );
while (true) {
if (::PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE)) {
if (!::GetMessage( &msg, NULL, 0, 0 ))
break;
::TranslateMessage( &msg );
::DispatchMessage( &msg );
} else {
__int64 counter;
::QueryPerformanceCounter( (LARGE_INTEGER *)&counter );
double elapsedSeconds = (double)( counter - lastCounter ) / (double)performanceFrequency;
double totalSeconds = (double)( counter - startCounter ) / (double)performanceFrequency;
lastCounter = counter;
if (currentBoard->update( elapsedSeconds, totalSeconds )) {
::InvalidateRect( hwnd, NULL, TRUE );
::UpdateWindow( hwnd );
}
}
}
Handling the messages themselves delegates to the board, and does some rendertarget bookkeeping.
case WM_PAINT: beginDraw(); if (currentBoard) currentBoard->drawFrame(); endDraw(); ::ValidateRect( hwnd, NULL ); return 0; // <------------mouse management-------------------> case WM_LBUTTONDOWN: point.x = GET_X_LPARAM(lParam); point.y = GET_Y_LPARAM(lParam); if (currentBoard) currentBoard->mouseClick( &point ); break; case WM_MOUSEMOVE: point.x = GET_X_LPARAM(lParam); point.y = GET_Y_LPARAM(lParam); if (currentBoard) currentBoard->mouseMove( &point ); break;
So, we've got a basic architecture of our system, with a working object cache and message pump. In the next article, I'll talk about how creating Direct2d objects actually works for each kind, from an implementation perspective, and the board's.