Windows Touch Game Programming in DirectX 10
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, ¤tCog->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.