Windows Touch Game Programming in DirectX 10

 My CodeProject Entries

In late 2009, Ian Backlund and I released a game we were working on that showcased Windows Touch.  You can download the sample associated with this project on the Code Gallery page for Windows Touch Game Programming in DirectX 10.

The following post is just a re-release of the documentation.

Game Overview

Gameplay is very simple: the player uses rotation gestures to rotate pieces of a puzzle and uses a pinch or zoom gesture to trigger pieces to insert into the stack. Keyboard input is supported using the keys on the numeric keypad. The challenge is to finish placing all the pieces before time runs out.

The following screen shows the game being played.

A rotate gesture spins the outer gear. Pinching or zooming when the gear is in place causes the outer gear to attach to the inner gear. If time runs out, the game presents a Game Over screen. 

How it Works

The game has three key components:

  • Rendering
  • Gameplay
  • Manipulations

The rendering portion of the game is powered by the Skinnable Mesh sample, which incorporates the DXUT utility and is accessed using the CogResourceLoader. The gameplay is controlled through a custom interface, the Game Manager. Manipulations are driven using the Windows Touch Manipulation Processor.

The following diagram illustrates interaction for the various classes.

 

Rendering Details

The CogResourceLoader class sets up the cogs, textures, camera, and lighting. Each type of cog (rusty, greasy, gold, and so on) has its own rendering and lighting settings that are handled through utility functions in that class.

Cog Meshes

The components of the cogs were all created in 3D Studio Max. Portions of the components are added or removed to create the shape geometry. Because the project targets DirectX 10, which handles meshes differently than DirectX 9, the cog components (meshes) must be converted before they can be rendered. This operation is handled by the D3DXMeshToD3DX10Mesh when the game loads.

Cog Textures

Textures are created to dress up the meshes to create an interesting variety of shapes. Shaders are used to render the textures on the cogs.

Game Camera

The camera is controlled programmatically to zoom out as gears are successfully aligned and put together.

Lighting

A flat light is placed behind the camera to give the textures a shiny appearance.

Rendering Details

The CogResourceLoader class sets up the cogs, textures, camera, and lighting. Each type of cog (rusty, greasy, gold, and so on) has its own rendering and lighting settings that are handled through utility functions in that class.

Cog Meshes

The components of the cogs were all created in 3D Studio Max. Portions of the components are added or removed to create the shape geometry. Because the project targets DirectX 10, which handles meshes differently than DirectX 9, the cog components (meshes) must be converted before they can be rendered. This operation is handled by the D3DXMeshToD3DX10Mesh when the game loads.

Cog Textures

Textures are created to dress up the meshes to create an interesting variety of shapes. Shaders are used to render the textures on the cogs.

Game Camera

The camera is controlled programmatically to zoom out as gears are successfully aligned and put together.

Lighting

A flat light is placed behind the camera to give the textures a shiny appearance.

Gameplay Details

The GameManager class orchestrates the game user interface, settings, sound, state, and mode.

User Interface and Menus

The user interface (UI) is handled through the GUIManager class. Although the GameManager sets up the object interfaces for DirectX, the GUIManager class uses these interfaces to render the various buttons that the user sees when starting the game.

The GUIManager is responsible for positioning and controlling game menu objects. The GUIManager has an associated state (Main Menu, Loading, Solving a Stack, and so on) that is switched through based on the user actions. When the application is in a mode where there are buttons, the GUIManager positions the buttons and handles the actions when the buttons are pressed. The GUIManager also hides menu screens when the user plays the game.

The following code shows how the GUIManager class renders the main menu of the game.

void GUIManager::EnableMainMenu()

{

m_GameState = MainMenu;

//Set the background texture

m_pDiffuseVariable->SetResource( m_pTextureMainMenuRV );

//Turn on main menu buttons

m_GameUI.GetButton(IDC_BEGINGAME)->SetEnabled(true);

m_GameUI.GetButton(IDC_BEGINGAME)->SetVisible(true);

m_GameUI.GetButton(IDC_TOGGLEFULLSCREEN)->SetEnabled(true);

m_GameUI.GetButton(IDC_TOGGLEFULLSCREEN)->SetVisible(true);

m_GameUI.GetButton(IDC_CREDITS)->SetEnabled(true);

m_GameUI.GetButton(IDC_CREDITS)->SetVisible(true);

m_GameUI.GetButton(IDC_PRACTICE)->SetEnabled(true);

m_GameUI.GetButton(IDC_PRACTICE)->SetVisible(true);

m_GameUI.GetButton(IDC_CHANGEDEVICE)->SetEnabled(true);

m_GameUI.GetButton(IDC_CHANGEDEVICE)->SetVisible(true);

m_GameUI.GetButton(IDC_MAINMENU)->SetEnabled(false);

m_GameUI.GetButton(IDC_MAINMENU)->SetVisible(false);

m_GameUI.GetButton(IDC_CONTINUEGAME)->SetEnabled(false);

m_GameUI.GetButton(IDC_CONTINUEGAME)->SetVisible(false);

}

Gameplay

The GameManager controls rendering of the game elements during play. When the user starts a game level, the GameManager sets up the stack of cogs and then positions the projection matrix, which controls what the user sees. The cog position is controlled by the GameManager using the GameManager::SpinCog method and GameManager::PushCog method which interacts with the Cog objects that are stored in a CogStack class. The CogStack class contains a stack of Cog objects. The Cog class controls rendering for a particular cog and stores the rotation and position state. When a cog is added to the stack for the user to play, the cog is initialized with a random rotation amount in the CogStack class and random component pieces using the Cog::CreatePatternEx method. The following code shows how the stack is initialized in the CogStack class.

HRESULT CogStack::CreateCogStack(int numberOfLevels, int difficulty, ID3D10Device* pd3dDevice, CogResourceLoader* p_CogResourceLoader)

{

HRESULT hr = S_OK;

//Set the convenience pointer

m_pd3dDevice = pd3dDevice;

m_CogResourceLoader = p_CogResourceLoader;

m_numOfLevels = numberOfLevels;

firstCog = new Cog();

firstCog->CreateCogPatternEx();//Counterintuitively, the patterns must be set up before calling loadMesh

firstCog->LoadMesh( pd3dDevice, Rusty, m_CogResourceLoader );//device, type, resouceLoader

firstCog->setPosition(D3DXVECTOR3(0,1,0));

firstCog->setScale(D3DXVECTOR3(1,1,1));

firstCog->setCameraPosition(D3DXVECTOR3(0,0,0));//The camera is not used for the first cog

float lastCameraY = 0;

float backdistance2 =0;

Cog* lastCog = firstCog;

float scale = 1;

float position = 1;

int practiceCogNumber = 0;

//Create a new cog for each level.

for(int i =1;i<numberOfLevels; i++)

{

Cog* tempCog = new Cog();

//Create the exterior cog pattern

tempCog->CreateCogPatternEx();

//Create the interior pattern for this cog

tempCog->CreateCogPatternInt(lastCog->exteriorSpokes);

//Pick the type of cog randomly

int cogTypeNumber = (rand()*5) / (RAND_MAX);//this could be tweeked to have weirder cog types appear more frequently in later levels

if(difficulty == 0)//If the game is in practice mode

cogTypeNumber = practiceCogNumber++;//One of each cog type

switch(cogTypeNumber)

{

case 0:

tempCog->LoadMesh( pd3dDevice, Normal, m_CogResourceLoader );

break;

case 1:

tempCog->LoadMesh( pd3dDevice, Gold, m_CogResourceLoader );

break;

case 2:

tempCog->LoadMesh( pd3dDevice, Rusty, m_CogResourceLoader );

break;

case 3:

tempCog->LoadMesh( pd3dDevice, Greased, m_CogResourceLoader );

break;

case 4:

tempCog->LoadMesh( pd3dDevice, Spring, m_CogResourceLoader );//Spring cog type = Glow cog

break;

}

//Set the initial position of the cog

position = m_positionFactor * position;

tempCog->setPosition(D3DXVECTOR3(0,position,0));

//Set the scale of the cog

scale = m_scaleFactor * scale;

tempCog->setScale(D3DXVECTOR3(scale,scale,scale));

//Randomly rotate the cog

float amount = static_cast<float>(rand());

tempCog->changeRotation(D3DXVECTOR3(amount, 0, 0));//Amount first

//Determine the position the camera should be in when the player is turning this cog

FLOAT nextCogPosition = m_positionFactor * position;

FLOAT lastCogPosition = lastCog->getPosition();

FLOAT thisCogCameraPosition = nextCogPosition - lastCogPosition + 1;

tempCog->setCameraPosition(D3DXVECTOR3(0,thisCogCameraPosition,0));

//Set up the linking pointer for the last cog

lastCog->nextCog = tempCog;

lastCog = tempCog;

float progress = (float)i/(float)numberOfLevels;

float progressOutOf100 = progress*100;

}

currentCog = firstCog->nextCog;

// Initialize the view matrix

D3DXMATRIX mView;

D3DXMatrixIdentity( &mView );

D3DXVECTOR3 Eye( 0.0f, 0.0f, -2.25f );

D3DXVECTOR3 At( 0.0f, 0.0f, 0.0f );

D3DXVECTOR3 Up( 0.0f, 1.0f, 0.0f );

D3DXMatrixLookAtLH( &mView, &currentCog->getCameraPosition(), &At, &Up );

m_CogResourceLoader->setViewMatrix(mView);

//This variable tracks the camera postion matrix

m_CameraPos = currentCog->getCameraPosition();

return hr;

}

The CogStack class handles puzzle completion. The CogStack tests a cog’s rotation when a cog is “pushed” using the GameManager class. A cog is considered solved if the rotation value for the cog is between 5 degrees and 355 degrees (this can be reduced to increase difficulty). The following code shows the logic used to test whether a cog is positioned correctly in the CogStack class.

bool CogStack::PushCog()

{

//If already animating a cog push, return false

if(animatingStack)

return false;

//Get the current rotation, measured in radians

double currentRotationInRadians = currentCog->getRotation();

//Convert radians to degrees

int currentRotationInDegrees = static_cast<int>(D3DXToDegree(currentRotationInRadians));

//Get the remainder of 360 degrees. This is the amount that the cog is rotated from 0 degrees

int rotation = currentRotationInDegrees % 360;

//Determine whether the cogs line up

if(rotation < 5 || rotation > 355)

{

//The cogs are lined up: lock out user input and begin an animation to drop the stack

//The commented out code below indicates where inertia could be used

//To use this, initialize inertia in createcogstack();

/*

switch(currentCog->m_CogType)

{

case Normal:

g_cInertManip.setInertiaSettings(g_cInertManip.standard);

break;

case Gold:

g_cInertManip.setInertiaSettings(g_cInertManip.standard);

break;

case Rusty:

g_cInertManip.setInertiaSettings(g_cInertManip.rust);

break;

case Greased:

g_cInertManip.setInertiaSettings(g_cInertManip.grease);

break;

case Spring:

g_cInertManip.setInertiaSettings(g_cInertManip.goo);

break;

}

*/

animatingStack = true;

//Rotate the cog to zero

currentCog->setRotation(D3DXVECTOR3(0.0,0.0,0.0));

//Get and store the distance the cog needs to travel

FLOAT firstCogPos = firstCog->getPosition();

FLOAT currentCogPos = currentCog->getPosition();

m_distanceToTravel = currentCogPos - firstCogPos;

//Get and store the distance the camera needs to travel

if(currentCog->nextCog)

m_cameraDistanceToTravel = currentCog->nextCog->getCameraPosition().y - currentCog->getCameraPosition().y;

//Increment the score

m_CurrentCogCount++;

//Set the dropping cog to the current cog for animation

droppingCog = currentCog;

//Set the current cog to the next cog so that it can move while it drops

currentCog = currentCog->nextCog;

return true;

}

return false;

}

When a cog is successfully pushed into the completed stack, the camera position is reset by setting the m_cameraDistanceToTravel member of the CogStack class and setting the CogStack animation state, set in the animatingStack member, to TRUE.

When a level is completed, the CogStack class is destroyed and then a new stack is created for the next level. For performance reasons, textures and geometry are cached.

Windows Touch Integration

Windows Touch integration is one of the simplest components of the game. The CManipulationEventSink class implements the _ManipulationEventSInk interface to enable control of the current cog using Windows Touch. The IntertManipUtil class hooks the CManipulationEventSink implementation to the game window and acts as an intermediary between Windows Touch and the game.

Hooking the Event Sink

The event sink is hooked when the InertManipUtil::Initialize method is called from Main, which happens when the application starts. RegisterTouchWindow is called to the application window and the event sink is notified so that the ManipulationStarted, ManipulationUpdate, and ManipulationCompleted events are registered. WM_TOUCH messages are forwarded to the InertManipUtil class so they can be passed to the manipulation processor. The following code shows how the WM_TOUCH events are passed to the manipulation processor in the InertManipUtil class.

void CALLBACK InertManipUtil::TouchMessageProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ){

UINT cInputs = LOWORD(wParam);

PTOUCHINPUT pInputs = new TOUCHINPUT[cInputs];

if (NULL != pInputs)

{

if (GetTouchInputInfo((HTOUCHINPUT)lParam,

cInputs,

pInputs,

sizeof(TOUCHINPUT)))

{

for (UINT i=0; i<cInputs; i++){

if (pInputs[i].dwFlags & TOUCHEVENTF_DOWN){

m_spIManipProc->ProcessDown(pInputs[i].dwID, (FLOAT)pInputs[i].x, (FLOAT)pInputs[i].y);

}

if (pInputs[i].dwFlags & TOUCHEVENTF_MOVE){

m_spIManipProc->ProcessMove(pInputs[i].dwID, (FLOAT)pInputs[i].x, (FLOAT)pInputs[i].y);

}

if (pInputs[i].dwFlags & TOUCHEVENTF_UP){

m_spIManipProc->ProcessUp(pInputs[i].dwID, (FLOAT)pInputs[i].x, (FLOAT)pInputs[i].y);

}

}

} else {

//GetLastError() and error handling

}

}else {

//Error handling, presumably out of memory

}

if (!CloseTouchInputHandle((HTOUCHINPUT)lParam)) {

//Error handling

}

delete [] pInputs;

}

Spinning and Pushing Cogs

Cogs are spun by using rotation values from ManipulationDelta and then triggering the GameManager::SpinCog method through the InertManipUtil’s reference to the GameManager class. The following code shows how this is done in CManipulationEventSink.

if (rotationDelta != 0){

if(true || m_fSpin){

//Spin the cog

m_pGameManager->SpinCog(rotationDelta);

}else{

}

lastDelta = rotationDelta;

}

//Zoom Gesture

if (expansionDelta > 200){

if (m_pGameManager->PushCog()){

m_pGameManager->AddScore(200);

}

}

//Pinch Gesture - Enable pinch gear insertion

if (expansionDelta < -200 ){

if (m_pGameManager->PushCog()){

m_pGameManager->AddScore(200);

}

}

Pinch and zoom amounts can be changed to alter the sensitivity of the game to gestures.