This chapter takes a brief detour from 3D rendering to create some scaffolding to support the rendering engine. You implement classes for keyboard and mouse input, and you establish a reusable component system for adding functionality to your applications. You also learn about text rendering and create a service container for housing commonly accessed software modules.
Game components provide a modular approach to adding functionality to your applications and are supported through two classes, GameComponent
and DrawableGameComponent
. Figure 12.1 show their class diagrams.
You create a game component by deriving from one of these classes—from GameComponent
if your functionality requires no rendering, and from DrawableGameComponent
otherwise. All game components have Initialize()
and Update()
methods, while drawable game components include a Draw()
method. These methods are typically invoked by the general-purpose Game
class methods of the same names. The Game
class stores a std::vector
of game components and you register new components by pushing them onto this vector.
Listings 12.1 and 12.2 present the header file and implementation for the GameComponent
class.
#pragma once
#include "Common.h"
namespace Library
{
class Game;
class GameTime;
class GameComponent : public RTTI
{
RTTI_DECLARATIONS(GameComponent, RTTI)
public:
GameComponent();
GameComponent(Game& game);
virtual ~GameComponent();
Game* GetGame();
void SetGame(Game& game);
bool Enabled() const;
void SetEnabled(bool enabled);
virtual void Initialize();
virtual void Update(const GameTime& gameTime);
protected:
Game* mGame;
bool mEnabled;
private:
GameComponent(const GameComponent& rhs);
GameComponent& operator=(const GameComponent& rhs);
};
}
#include "GameComponent.h"
#include "GameTime.h"
namespace Library
{
RTTI_DEFINITIONS(GameComponent)
GameComponent::GameComponent()
: mGame(nullptr), mEnabled(true)
{
}
GameComponent::GameComponent(Game& game)
: mGame(&game), mEnabled(true)
{
}
GameComponent::~GameComponent()
{
}
Game* GameComponent::GetGame()
{
return mGame;
}
void GameComponent::SetGame(Game& game)
{
mGame = &game;
}
bool GameComponent::Enabled() const
{
return mEnabled;
}
void GameComponent::SetEnabled(bool enabled)
{
mEnabled = enabled;
}
void GameComponent::Initialize()
{
}
void GameComponent::Update(const GameTime& gameTime)
{
}
}
As you can see, this is a fairly simple class. It stores a pointer to the associated Game
instance and a flag denoting the enabled/disabled status of the component. And although the Initialize()
and Update()
methods aren’t purely virtual, their implementations are empty. Your derived classes are intended to override these methods but aren’t explicitly required to do so. This is useful for drawable game components, which are specialized game components that need to draw but not update, or for components that require no initialization.
From the two code listings and the class diagrams, you might have noticed the references to the type RTTI
. This is a custom implementation of Runtime Type Information (RTTI). RTTI refers to type introspection, the program’s capability to examine a type at runtime. At a minimum, this allows a program to identify the specific data type of an object, although the object can be referenced through a generic pointer. Some languages also include the capability to query properties of an interface. Do not confuse RTTI with reflection, which provides the capability to query and manipulate members of an object.
C++ supports RTTI through the typeid
operator, the type_info
class, and the dynamic_cast
operator. However, when facilitated at the language level, RTTI applies either to all polymorphic classes or to none; you cannot select which classes to generate type information for. Furthermore, the expense of RTTI is implementation specific. Its cost might be acceptable, or even negligible, on one platform and not on another. You can remove any doubt by disabling language-level RTTI support and writing your own RTTI implementation. That’s the approach taken here.
To disable RTTI within Visual Studio, open the Property Pages of your projects and navigate to Configuration Properties, C/C++, Language. Set the Enable Run-Time Type Information field to No (/GR-). Listing 12.3 presents the code for a custom RTTI implementation.
#pragma once
#include <string>
namespace Library
{
class RTTI
{
public:
virtual const unsigned int& TypeIdInstance() const = 0;
virtual RTTI* QueryInterface(const unsigned id) const
{
return nullptr;
}
virtual bool Is(const unsigned int id) const
{
return false;
}
virtual bool Is(const std::string& name) const
{
return false;
}
template <typename T>
T* As() const
{
if (Is(T::TypeIdClass()))
{
return (T*)this;
}
return nullptr;
}
};
#define RTTI_DECLARATIONS(Type, ParentType)
public:
typedef ParentType Parent;
static std::string TypeName() { return std::string
(#Type); }
virtual const unsigned int& TypeIdInstance() const { return
Type::TypeIdClass(); }
static const unsigned int& TypeIdClass() { return
sRunTimeTypeId; }
virtual Library::RTTI* QueryInterface( const unsigned int
id ) const
{
if (id == sRunTimeTypeId)
{ return (RTTI*)this; }
else
{ return Parent::QueryInterface(id); }
}
virtual bool Is(const unsigned int id) const
{
if (id == sRunTimeTypeId)
{ return true; }
else
{ return Parent::Is(id); }
}
virtual bool Is(const std::string& name) const
{
if (name == TypeName())
{ return true; }
else
{ return Parent::Is(name); }
}
private:
static unsigned int sRunTimeTypeId;
#define RTTI_DEFINITIONS(Type) unsigned int Type::sRunTimeTypeId =
(unsigned int)& Type::sRunTimeTypeId;
}
As you can see, the entire implementation of the RTTI
class is contained within the header file. Be sure to include this header file within Common.h
. To use the interface, derive a class from the RTTI
type and include the RTTI_DECLARATIONS
macro somewhere within the class declaration. The first argument of the RTTI_DECLARATIONS
macro is the derived type, and the second argument is its parent (this implementation provides no explicit support for multiple inheritance). Use the RTTI_DEFINITIONS
macro in the class implementation. That macro’s only argument is the associated type. Listings 12.1 and 12.2 demonstrate this usage.
When a type is enabled with this RTTI system, it can be queried through the Is()
, As()
, and QueryInterface()
methods. A type’s identification is stored as an unsigned integer and is guaranteed to be unique because it’s assigned as the address of the static sRunTimeTypeId
member. This identifier is exposed through the TypeIdClass()
and TypeIdInstance()
methods, which can be used as arguments to the Is()
and QueryInterface()
methods. The name of the class (stored as a std::string
) can also be used in an Is()
query. If the queried instance is not the specified type, the Is()
methods walk the class hierarchy looking for the type identifier. The QueryInterface()
method has the same behavior but returns an RTTI
pointer instead of a Boolean. The templated As()
method returns a pointer of the specified type or NULL
if the interface is not of the queried type. You see an application of this system shortly.
Drawable game components are extensions of regular game components and add Draw()
and Visible()
methods. They also include the concept of a camera, but we defer that topic for a few sections. Instead of listing the entire implementation, Listing 12.4 presents only the DrawableGameComponent.h
header file. You can deduce the simple implementation or download it from the companion website.
#pragma once
#include "GameComponent.h"
namespace Library
{
class Camera;
class DrawableGameComponent : public GameComponent
{
RTTI_DECLARATIONS(DrawableGameComponent, GameComponent)
public:
DrawableGameComponent();
DrawableGameComponent(Game& game);
DrawableGameComponent(Game& game, Camera& camera);
virtual ~DrawableGameComponent();
bool Visible() const;
void SetVisible(bool visible);
Camera* GetCamera();
void SetCamera(Camera* camera);
virtual void Draw(const GameTime& gameTime);
protected:
bool mVisible;
Camera* mCamera;
private:
DrawableGameComponent(const DrawableGameComponent& rhs);
DrawableGameComponent& operator=(const DrawableGameComponent&
rhs);
};
}
You must update the Library::Game
class to support game components. The class declaration will now include a member for the list of components, and the implementations of Game::Initialize()
, Game::Update()
, and Game::Draw()
will act on these components. Listing 12.5 presents the minor updates to the Game.h
header file. Listing 12.6 shows the implementations for the Initialize()
, Update()
, and Draw()
methods.
class Game
{
public:
/* ... Previously presented members removed for brevity ... */
const std::vector<GameComponent*>& Components() const;
protected:
/* ... Previously presented members removed for brevity ... */
std::vector<GameComponent*> mComponents;
};
/* ... Previously presented members removed for brevity ... */
void Game::Initialize()
{
for (GameComponent* component : mComponents)
{
component->Initialize();
}
}
void Game::Update(const GameTime& gameTime)
{
for (GameComponent* component : mComponents)
{
if (component->Enabled())
{
component->Update(gameTime);
}
}
}
void Game::Draw(const GameTime& gameTime)
{
for (GameComponent* component : mComponents)
{
DrawableGameComponent* drawableGameComponent =
component->As<DrawableGameComponent>();
if (drawableGameComponent != nullptr &&
drawableGameComponent->Visible())
{
drawableGameComponent->Draw(gameTime);
}
}
}
Note the use of C++ 11 range-based for loops in Listing 12.5. These statements are analogous to traditional STL iterator usage, just with happier syntax. Also note the Enabled()
and Visible()
checks for game and drawable game components within the Update()
and Draw()
methods, and the RTTI::As()
call to verify that the component is, in fact, a Drawable-GameComponent
. Alternately, you could consider storing separate vectors for GameComponent
and DrawableGameComponent
objects to eliminate this particular need for runtime type checking.
To demonstrate the component system, you next create a component to display the frame rate of the application in frames per second (FPS). This example also introduces a bitmapped font system for rendering text to the screen. Figure 12.2 shows the desired output of the component. It displays the frame rate and the total elapsed time of the application in a white font toward the top of the screen. Listing 12.7 presents the header file for the FpsComponent
class.
#pragma once
#include "DrawableGameComponent.h"
namespace DirectX
{
class SpriteBatch;
class SpriteFont;
}
namespace Library
{
class FpsComponent : public DrawableGameComponent
{
RTTI_DECLARATIONS(FpsComponent, DrawableGameComponent)
public:
FpsComponent(Game& game);
~FpsComponent();
XMFLOAT2& TextPosition();
int FrameRate() const;
virtual void Initialize() override;
virtual void Update(const GameTime& gameTime) override;
virtual void Draw(const GameTime& gameTime) override;
private:
FpsComponent();
FpsComponent(const FpsComponent& rhs);
FpsComponent& operator=(const FpsComponent& rhs);
SpriteBatch* mSpriteBatch;
SpriteFont* mSpriteFont;
XMFLOAT2 mTextPosition;
int mFrameCount;
int mFrameRate;
double mLastTotalElapsedTime;
};
}
The FpsComponent
derives from the DrawableGameComponent
class; provides implementations for the Initialize()
, Update()
, and Draw()
methods; and stores class members specific to its task. This is the typical pattern for all drawable game components.
Note the SpriteBatch
and SpriteFont
members in the FpsComponent
class. These types come from the DirectX Tool Kit (DirectXTK), discussed in Chapter 3, “Tools of the Trade.” If you haven’t already linked this library, review the related material in Chapter 3 and Chapter 10, or visit the companion website for references to the library.
The SpriteBatch
class is used to render sprites. A sprite is just a texture rendered to the 2D screen surface instead of being mapped to an object in 3D space. As such, sprites do not require a 3D camera. Sprites are commonly used for user interfaces (scores, buttons, or mini-maps) and 2D platformers (games such as Super Mario Bros.). Drawing sprites with the SpriteBatch
class follows this pattern:
mSpriteBatch->Begin();
mSpriteBatch->Draw(texture, position);
mSpriteBatch->End();
Note that more than one sprite draw call can be executed between the calls to SpriteBatch::Begin()
and SpriteBatch::End()
.
For the frame rate component, you use the SpriteBatch
class to render strings representing the frame rate and total elapsed time. Drawing text in Direct3D isn’t done like it is with a Windows or Windows Console application. In Direct3D, text is rendered as a sprite, and the output characters come from a texture that contains the desired character set (such as ASCII) in a particular font.
The SpriteFont
class represents a TrueType font that has been converted to a bitmap using the MakeSpriteFont tool (included with the DirectXTK package). This tool outputs a binary file with an extension of .spritefont
, that is used to instantiate a SpriteFont
object. The following command creates a file named Arial_14_Regular.spritefont
using the Arial font family and a point size of 14:
MakeSpriteFont.exe "Arial" Arial_14_Regular.spritefont /FontSize:14
Figure 12.3 shows the invocation of this command on the DOS command prompt. Visit the DirectXTK website for more options available with the MakeSpriteFont tool.
To use the resulting .spritefont
file within your application, it should reside in a directory that’s relative to your executable. One approach for such data is to create a folder named content
under the sourceLibrary
directory. Its location denotes its association with functionality that resides within the Library project. You then augment the Library project’s post-build event to copy any files within the content directory to $(SolutionDir)..content
. You create the opposite command for the Game project’s prebuild event to copy the files from $(SolutionDir)..content
to the output directory of the game. Listing 12.8 presents the updated post-build event for the Library project, and Listing 12.9 presents the prebuild event for the Game project. Be sure to apply these changes to both debug and release configurations.
mkdir "$(SolutionDir)..lib"
copy "$(TargetPath)" "$(SolutionDir)..lib"
mkdir "$(SolutionDir)..content"
IF EXIST "$(ProjectDir)Content" xcopy /E /Y "$(ProjectDir)Content"
"$(SolutionDir)..content"
IF EXIST "$(TargetDir)Content" xcopy /E /Y "$(TargetDir)Content"
"$(SolutionDir)..content"
mkdir "$(OutDir)Content"
IF EXIST "$(SolutionDir)..content" xcopy /E /Y "$(SolutionDir)..
content" "$(OutDir)Content"
IF EXIST "$(ProjectDir)content" xcopy /E /Y "$(ProjectDir)Content"
"$(OutDir)Content"
Notice that the Game project’s prebuild event adopts the same $(ProjectDir)content
directory structure for game-specific content. Furthermore, the Library project also copies content from a $(TargetDir)content
directory. That directory will be used for shaders that are compiled as part of the Visual Studio build process. Chapter 14 discusses shader compilation.
With a .spritefont
file created and copied to a folder accessible by the executable, it can be used to instantiate a SpriteFont
object with a call such as this:
mSpriteFont = new SpriteFont(mGame->Direct3DDevice(),
L"Content\Fonts\Arial_14_Regular.spritefont");
The SpriteFont
class has a DrawString()
method with parameters for the sprite batch, the string to output, and the 2D screen location. Listing 12.10 shows the full listing of the FpsComponent.cpp
file, including the SpriteBatch
and SpriteFont
usage.
#include "FpsComponent.h"
#include <sstream>
#include <iomanip>
#include <SpriteBatch.h>
#include <SpriteFont.h>
#include "Game.h"
#include "Utility.h"
namespace Library
{
RTTI_DEFINITIONS(FpsComponent)
FpsComponent::FpsComponent(Game& game)
: DrawableGameComponent(game), mSpriteBatch(nullptr),
mSpriteFont(nullptr), mTextPosition(0.0f, 60.0f),
mFrameCount(0), mFrameRate(0), mLastTotalElapsedTime(0.0)
{
}
FpsComponent::~FpsComponent()
{
DeleteObject(mSpriteFont);
DeleteObject(mSpriteBatch);
}
XMFLOAT2& FpsComponent::TextPosition()
{
return mTextPosition;
}
int FpsComponent::FrameRate() const
{
return mFrameCount;
}
void FpsComponent::Initialize()
{
SetCurrentDirectory(Utility::ExecutableDirectory().c_str());
mSpriteBatch = new SpriteBatch(mGame->Direct3DDeviceContext());
mSpriteFont = new SpriteFont(mGame->Direct3DDevice(),
L"Content\Fonts\Arial_14_Regular.spritefont");
}
void FpsComponent::Update(const GameTime& gameTime)
{
if (gameTime.TotalGameTime() - mLastTotalElapsedTime >= 1)
{
mLastTotalElapsedTime = gameTime.TotalGameTime();
mFrameRate = mFrameCount;
mFrameCount = 0;
}
mFrameCount++;
}
void FpsComponent::Draw(const GameTime& gameTime)
{
mSpriteBatch->Begin();
std::wostringstream fpsLabel;
fpsLabel << std::setprecision(4) << L"Frame Rate: " <<
mFrameRate << " Total Elapsed Time: " << gameTime.TotalGameTime();
mSpriteFont->DrawString(mSpriteBatch, fpsLabel.str().c_str(),
mTextPosition);
mSpriteBatch->End();
}
}
The FpsComponent::Initialize()
method first sets the current working directory to the executable directory. This is so that access to the ContentFonts
directory is performed from the correct location. The associated Utility
class contains a variety of useful methods; you can find it on the book’s companion website. Next, the Initialize()
method instantiates the SpriteBatch
and SpriteFont
objects. These objects are released in the component’s destructor.
The FpsComponent::Update()
method increments the mFrameCount
member with each call and resets it after a second of time has passed. The FpsComponent::Draw()
method builds a string and renders it through the mSpriteBatch
and mSpriteFont
members.
The FpsComponent
should reside in the Library project, but you’ll integrate the component in the RenderingGame
class of your Game project. To do this, add an FpsComponent
member within the RenderingGame
class and update the RenderingGame::Initialize()
and RenderingGame::Shutdown()
methods to match Listing 12.11.
void RenderingGame::Initialize()
{
mFpsComponent = new FpsComponent(*this);
mComponents.push_back(mFpsComponent);
Game::Initialize();
}
void RenderingGame::Shutdown()
{
DeleteObject(mFpsComponent);
Game::Shutdown();
}
The component is registered by adding it to the Game::mComponents
vector. Note that you do not need to explicitly call the component’s Initialize()
, Update()
, or Draw()
methods because the base Game
class invokes them. Run the application, and you should see output similar to Figure 12.2. The patterns set in this example are common for all the components you create with this framework.
Another supporting system for interactive rendering is the capability to accept device input. On the PC, this is commonly mouse, keyboard, and gamepad input. This section discusses mouse and keyboard input and you’ll develop corresponding game components.
Two general approaches are useful in collecting device input: You can either poll the device periodically or wait for the device to inform you that its state has changed. The approach you choose should take into account the frequency of input changes, the cost to poll the device, and the cost to process an event. If the device is expected to change every frame, polling the device might make more sense than processing a flood of events. Conversely, if the device is mostly idle and changes state only periodically, you might choose an event-based input system. You need not employ a one-size-fits-all approach; you can poll one device and accept events for another.
Multiple APIs can be used for device input. The Windows API, for example, supports keyboard and mouse events through the windows procedure (the WndProc()
method you wrote for the Game
class) or with polling methods such as GetAsyncKeyState()
. DirectX 11 includes the XInput system for querying Xbox 360 game controllers, although it lacks support for mouse and keyboard input. The DirectX 11 installation also includes the older DirectInput library. DirectInput supports mouse, keyboard, gamepad, and joystick input, but it hasn’t been updated since DirectX 8. Microsoft recommends the use of XInput for “next-generation” game controllers, but XInput also hasn’t seen a major update since DirectX 9.
For the components in this book, you use the DirectInput library for polling the keyboard and mouse.
1. Create the DirectInput object.
2. Create the DirectInput keyboard device.
3. Set the device data format and cooperative level.
4. Acquire the device.
5. Query the device state.
The next few sections cover each of these steps.
The IDirectInput8
interface enumerates, creates, and queries DirectInput devices. You must create an object of this type before performing any of these actions. You accomplish this through the DirectInput8Create()
method; the prototype and parameters are listed next. The dinput.h
header file declares this method and all DirectInput-related functionality.
HRESULT DirectInput8Create(
HINSTANCE hinst, DWORD dwVersion, REFIID riidltf,
LPVOID *ppvOut, LPUNKNOWN punkOuter);
hinst: Specifies the handle to the application that is creating the DirectInput object. This is the same handle you used to instantiate a window, and it’s stored in the mInstance
member of the Game
class.
dwVersion: The version number of DirectInput. This is always DIRECTINPUT_VERSION
.
riidltf: The unique identifier of the requested interface. This is always IID_IDirectInput8
.
ppvOut: Returns the created DirectInput object.
punkOuter: Used for COM aggregation. This is always NULL
.
The keyboard and mouse components use the created DirectInput object and release it after those components have been freed. Thus, you should store this object as a member in your specialized RenderingGame
class, to be instantiated in the Initialize()
method and released in Shutdown()
. Listing 12.12 presents an example call to DirectInput8Create()
and its cleanup within the RenderingGame
implementation.
void RenderingGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION,
IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
/* ... Previously presented statements removed for brevity ... */
}
void RenderingGame::Shutdown()
{
/* ... Previously presented statements removed for brevity ... */
ReleaseObject(mDirectInput);
Game::Shutdown();
}
With the DirectInput object instantiated, you can now create the specific device. To do this, add a class called Keyboard
to the Library project. This class derives from GameComponent
and encapsulates the functionality for creating, configuring, acquiring, releasing, and querying the keyboard. Add a class member with the IDirectInputDevice8
interface, which stores the created device. You create the device with a call to IDirectInput8::CreateDevice()
; the prototype and parameters are listed next:
HRESULT CreateDevice(
REFGUID rguid,
LPDIRECTINPUTDEVICE * lplpDirectInputDevice,
LPUNKNOWN pUnkOuter);
rguid: The unique identifier of the requested input device. For a keyboard, this is GUID_SysKeyboard
.
lplpDirectInputDevice: Returns the created DirectInput device.
pUnkOuter: Used for COM aggregation. This is always NULL
.
Creating, configuring, and acquiring an input device is typically done in the same code block. Thus, we defer an example call until we cover these topics.
After the input device is created, you must specify the format of the data the device should return. This is done through the IDirectInputDevice8::SetDataFormat()
method, which has only one parameter: a DIDATAFORMAT
structure. You can configure a custom DIDATAFORMAT
structure or use one of the following predefined instances:
c_dfDIKeyboard
c_dfDIMouse
c_dfDIMouse2
c_dfDIJoystick2
For the keyboard component, you specify the c_dfDIKeyboard
structure.
Next, you must set the cooperative level, which determines how the device interacts with other instances of the same device and with the rest of the operating system. This is accomplished with a call to IDirectInputDevice8::SetCooperativeLevel()
, which has the following prototype:
HRESULT SetCooperativeLevel(
HWND hwnd,
DWORD dwFlags);
hwnd: The top-level window handle that belongs to the application. This is created within Game::InitializeWindow()
and stored in the Game::mWindowHandle
member.
dwFlags: Bitwise OR’d flags that describe the cooperative level. Table 12.1 lists the possible flags.
After setting the data format and cooperative level, you are ready to acquire the device. A successful device acquisition means that you have access to the device and can query its state. Device acquisition is done through the IDirectInputDevice8::Acquire()
method, which has no input parameters. Listing 12.13 presents the implementation of the Keyboard::Initialize()
method, which demonstrates this call and the ones described in the last few sections.
void Keyboard::Initialize()
{
if (FAILED(mDirectInput->CreateDevice(GUID_SysKeyboard, &mDevice,
nullptr)))
{
throw GameException("IDIRECTINPUT8::CreateDevice() failed");
}
if (FAILED(mDevice->SetDataFormat(&c_dfDIKeyboard)))
{
throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat()
failed");
}
if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(),
DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
{
throw GameException("IDIRECTINPUTDEVICE8::SetCooperativeLevel()
failed");
}
if (FAILED(mDevice->Acquire()))
{
throw GameException("IDIRECTINPUTDEVICE8::Acquire() failed");
}
}
Now that the device has been initialized and acquired, you can query its state. This is done with a call to IDirectInputDevice8::GetDeviceState()
, which has the following prototype and parameters:
HRESULT GetDeviceState(
DWORD cbData,
LPVOID lpvData);
cbData: The size (in bytes) of the buffer specified in lpvData.
lpvData: The buffer to write the device state to. The structure of this buffer is defined by the format specified through IDirectInputDevice8::SetDataFormat()
. When using the predefined c_dfDIKeyboard
data format structure, this buffer is simply an array of 256 bytes.
Instead of just presenting a call to IDirectInputDevice8::GetDeviceState()
, the next two listings present the complete declaration (Listing 12.14) and implementation (Listing 12.15) of the Keyboard
class. You can find the IDirectInputDevice8::GetDeviceState()
call within the Keyboard::Update()
method.
#pragma once
#include "GameComponent.h"
namespace Library
{
class Keyboard : public GameComponent
{
RTTI_DECLARATIONS(Keyboard, GameComponent)
public:
Keyboard(Game& game, LPDIRECTINPUT8 directInput);
~Keyboard();
const byte* const CurrentState() const;
const byte* const LastState() const;
virtual void Initialize() override;
virtual void Update(const GameTime& gameTime) override;
bool IsKeyUp(byte key) const;
bool IsKeyDown(byte key) const;
bool WasKeyUp(byte key) const;
bool WasKeyDown(byte key) const;
bool WasKeyPressedThisFrame(byte key) const;
bool WasKeyReleasedThisFrame(byte key) const;
bool IsKeyHeldDown(byte key) const;
private:
Keyboard();
static const int KeyCount = 256;
Keyboard(const Keyboard& rhs);
LPDIRECTINPUT8 mDirectInput;
LPDIRECTINPUTDEVICE8 mDevice;
byte mCurrentState[KeyCount];
byte mLastState[KeyCount];
};
}
#include "Keyboard.h"
#include "Game.h"
#include "GameTime.h"
#include "GameException.h"
namespace Library
{
RTTI_DEFINITIONS(Keyboard)
Keyboard::Keyboard(Game& game, LPDIRECTINPUT8 directInput)
: GameComponent(game), mDirectInput(directInput),
mDevice(nullptr)
{
assert(mDirectInput != nullptr);
ZeroMemory(mCurrentState, sizeof(mCurrentState));
ZeroMemory(mLastState, sizeof(mLastState));
}
Keyboard::~Keyboard()
{
if (mDevice != nullptr)
{
mDevice->Unacquire();
mDevice->Release();
mDevice = nullptr;
}
}
const byte* const Keyboard::CurrentState() const
{
return mCurrentState;
}
const byte* const Keyboard::LastState() const
{
return mLastState;
}
void Keyboard::Initialize()
{
if (FAILED(mDirectInput->CreateDevice(GUID_SysKeyboard,
&mDevice, nullptr)))
{
throw GameException("IDIRECTINPUT8::CreateDevice()
failed");
}
if (FAILED(mDevice->SetDataFormat(&c_dfDIKeyboard)))
{
throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat()
failed");
}
if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(),
DISCL_FOREGROUND| DISCL_NONEXCLUSIVE)))
{
throw GameException("IDIRECTINPUTDEVICE8::
SetCooperativeLevel() failed");
}
if (FAILED(mDevice->Acquire()))
{
throw GameException("IDIRECTINPUTDEVICE8::Acquire()
failed");
}
}
void Keyboard::Update(const GameTime& gameTime)
{
if (mDevice != nullptr)
{
memcpy(mLastState, mCurrentState, sizeof(mCurrentState));
if (FAILED(mDevice->GetDeviceState(sizeof(mCurrentState),
(LPVOID)mCurrentState)))
{
// Try to reaqcuire the device
if (SUCCEEDED(mDevice->Acquire()))
{
mDevice->GetDeviceState(sizeof(mCurrentState),
(LPVOID)mCurrentState);
}
}
}
}
bool Keyboard::IsKeyUp(byte key) const
{
return ((mCurrentState[key] & 0x80) == 0);
}
bool Keyboard::IsKeyDown(byte key) const
{
return ((mCurrentState[key] & 0x80) != 0);
}
bool Keyboard::WasKeyUp(byte key) const
{
return ((mLastState[key] & 0x80) == 0);
}
bool Keyboard::WasKeyDown(byte key) const
{
return ((mLastState[key] & 0x80) != 0);
}
bool Keyboard::WasKeyPressedThisFrame(byte key) const
{
return (IsKeyDown(key) && WasKeyUp(key));
}
bool Keyboard::WasKeyReleasedThisFrame(byte key) const
{
return (IsKeyUp(key) && WasKeyDown(key));
}
bool Keyboard::IsKeyHeldDown(byte key) const
{
return (IsKeyDown(key) && WasKeyDown(key));
}
}
This implementation follows the previously established game component pattern. It creates, configures, and acquires the input device within the Initialize()
method and then queries the device state with each call to Update()
. Notice the two class members mCurrentState
and mLastState
, which store the current state of the device and the state from the previous frame. This allows you to ask questions such as WasKeyPressedThisFrame()
, WasKeyReleasedThisFrame()
, or IsKeyHeldDown()
. The parameter to such methods is the index to the state byte array. The DirectInput library includes a set of definitions to use with these queries. You can find these in the dinput.h
header file; they all begin with the DIK_
prefix. Table 12.2 lists a few of these scan codes.
Notice the additional IDirectInputDevice8::Acquire()
call within the Keyboard::Update()
method in Listing 12.15. Throughout the execution of your application, you might lose access to the device; if so, you must reacquire access before the device can be read. This can happen, for example, if another application requests exclusive access to the device. For the previous implementation, if the state retrieval fails, a single attempt is made (per frame) to reacquire the device. No attempt is made to error out or warn the user if the device can’t be reacquired. You might want to add such a feature to your own implementation.
As with the frame rate component, you integrate your keyboard component in the RenderingGame
class of your Game project. Add a Keyboard
class member, and update your RenderingGame::Initialize()
and RenderingGame::Shutdown()
methods to match Listing 12.16. This listing also includes a modification to the RenderingGame::Update()
method that exits the application when the Escape key is pressed.
void RenderingGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION,
IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
mKeyboard = new Keyboard(*this, mDirectInput);
mComponents.push_back(mKeyboard);
mFpsComponent = new FpsComponent(*this);
mComponents.push_back(mFpsComponent);
Game::Initialize();
}
void RenderingGame::Shutdown()
{
DeleteObject(mKeyboard);
DeleteObject(mFpsComponent);
ReleaseObject(mDirectInput);
Game::Shutdown();
}
void RenderingGame::Update(const GameTime &gameTime)
{
if (mKeyboard->WasKeyPressedThisFrame(DIK_ESCAPE))
{
Exit();
}
Game::Update(gameTime);
}
To access the mouse with DirectInput, you use the very same interfaces and methods you did for the keyboard. The chief difference is the data you query. Instead of byte arrays, you store mCurrentState
and mLastState
members whose types are of the DIMOUSESTATE
structure. This structure has the following definition:
typedef struct _DIMOUSESTATE {
LONG lX;
LONG lY;
LONG lZ;
BYTE rgbButtons[4];
} DIMOUSESTATE;
The lX
and lY
members of this structure store the X and Y positions of the mouse. The lZ
member stores the position of the mouse wheel. These positions are relative to their previous values. A negative X value indicates that the mouse has moved to the left, and a positive Y value indicates that the mouse has moved down (toward the bottom of the screen). The higher the magnitude of the value, the more the mouse has moved from its previously polled position.
The rgbButtons
array supports a four-button mouse, and the elements of the array are identified by an enumeration such as this:
enum MouseButtons
{
MouseButtonsLeft = 0,
MouseButtonsRight = 1,
MouseButtonsMiddle = 2,
MouseButtonsX1 = 3
};
To configure the proper data format, you specify c_dfDIMouse
in the call to IDirectInput-Device8::SetDataFormat()
. You also specify GUID_SysMouse
instead of GUID_SysKeyboard
as the first argument to IDirectInputDevice8::CreateDevice()
.
Listings 12.17 and 12.18 present the declaration and implementation of the Mouse
class.
#pragma once
#include "GameComponent.h"
namespace Library
{
class GameTime;
enum MouseButtons
{
MouseButtonsLeft = 0,
MouseButtonsRight = 1,
MouseButtonsMiddle = 2,
MouseButtonsX1 = 3
};
class Mouse : public GameComponent
{
RTTI_DECLARATIONS(Mouse, GameComponent)
public:
Mouse(Game& game, LPDIRECTINPUT8 directInput);
~Mouse();
LPDIMOUSESTATE CurrentState();
LPDIMOUSESTATE LastState();
virtual void Initialize() override;
virtual void Update(const GameTime& gameTime) override;
long X() const;
long Y() const;
long Wheel() const;
bool IsButtonUp(MouseButtons button) const;
bool IsButtonDown(MouseButtons button) const;
bool WasButtonUp(MouseButtons button) const;
bool WasButtonDown(MouseButtons button) const;
bool WasButtonPressedThisFrame(MouseButtons button) const;
bool WasButtonReleasedThisFrame(MouseButtons button) const;
bool IsButtonHeldDown(MouseButtons button) const;
private:
Mouse();
LPDIRECTINPUT8 mDirectInput;
LPDIRECTINPUTDEVICE8 mDevice;
DIMOUSESTATE mCurrentState;
DIMOUSESTATE mLastState;
long mX;
long mY;
long mWheel;
};
}
#include "Mouse.h"
#include "Game.h"
#include "GameTime.h"
#include "GameException.h"
namespace Library
{
RTTI_DEFINITIONS(Mouse)
Mouse::Mouse(Game& game, LPDIRECTINPUT8 directInput)
: GameComponent(game), mDirectInput(directInput),
mDevice(nullptr), mX(0), mY(0), mWheel(0)
{
assert(mDirectInput != nullptr);
ZeroMemory(&mCurrentState, sizeof(mCurrentState));
ZeroMemory(&mLastState, sizeof(mLastState));
}
Mouse::~Mouse()
{
if (mDevice != nullptr)
{
mDevice->Unacquire();
mDevice->Release();
mDevice = nullptr;
}
}
LPDIMOUSESTATE Mouse::CurrentState()
{
return &mCurrentState;
}
LPDIMOUSESTATE Mouse::LastState()
{
return &mLastState;
}
long Mouse::X() const
{
return mX;
}
long Mouse::Y() const
{
return mY;
}
long Mouse::Wheel() const
{
return mWheel;
}
void Mouse::Initialize()
{
if (FAILED(mDirectInput->CreateDevice(GUID_SysMouse, &mDevice,
nullptr)))
{
throw GameException("IDIRECTINPUT8::CreateDevice()
failed");
}
if (FAILED(mDevice->SetDataFormat(&c_dfDIMouse)))
{
throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat()
failed");
}
if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(),
DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
{
throw GameException("IDIRECTINPUTDEVICE8::
SetCooperativeLevel() failed");
}
if (FAILED(mDevice->Acquire()))
{
throw GameException("IDIRECTINPUTDEVICE8::Acquire()
failed");
}
}
void Mouse::Update(const GameTime& gameTime)
{
if (mDevice != nullptr)
{
memcpy(&mLastState, &mCurrentState, sizeof(mCurrentState));
if (FAILED(mDevice->GetDeviceState(sizeof(mCurrentState),
&mCurrentState)))
{
// Try to reaqcuire the device
if (SUCCEEDED(mDevice->Acquire()))
{
if (FAILED(mDevice->GetDeviceState(sizeof
(mCurrentState), &mCurrentState)))
{
return;
}
}
}
// Accumulate positions
mX += mCurrentState.lX;
mY += mCurrentState.lY;
mWheel += mCurrentState.lZ;
}
}
bool Mouse::IsButtonUp(MouseButtons button) const
{
return ((mCurrentState.rgbButtons[button] & 0x80) == 0);
}
bool Mouse::IsButtonDown(MouseButtons button) const
{
return ((mCurrentState.rgbButtons[button] & 0x80) != 0);
}
bool Mouse::WasButtonUp(MouseButtons button) const
{
return ((mLastState.rgbButtons[button] & 0x80) == 0);
}
bool Mouse::WasButtonDown(MouseButtons button) const
{
return ((mLastState.rgbButtons[button] & 0x80) != 0);
}
bool Mouse::WasButtonPressedThisFrame(MouseButtons button) const
{
return (IsButtonDown(button) && WasButtonUp(button));
}
bool Mouse::WasButtonReleasedThisFrame(MouseButtons button) const
{
return (IsButtonUp(button) && WasButtonDown(button));
}
bool Mouse::IsButtonHeldDown(MouseButtons button) const
{
return (IsButtonDown(button) && WasButtonDown(button));
}
}
Listing 12.19 presents the code to integrate the Mouse
component into your RenderingGame
class. This listing includes a modification to the RenderingGame::Draw()
method that outputs the mouse position and wheel value through the SpriteBatch
/SpriteFont
system. Note that the mouse position values are accumulated from the relative values returned by IDirectInputDevice8::GetDeviceState()
, and no facility has been created to establish a position origin or to limit the values to the application window or screen; it’s left as an exercise for you to add this functionality.
void RenderingGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION,
IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
mKeyboard = new Keyboard(*this, mDirectInput);
mComponents.push_back(mKeyboard);
mMouse = new Mouse(*this, mDirectInput);
mComponents.push_back(mMouse);
mFpsComponent = new FpsComponent(*this);
mComponents.push_back(mFpsComponent);
SetCurrentDirectory(Utility::ExecutableDirectory().c_str());
mSpriteBatch = new SpriteBatch(mDirect3DDeviceContext);
mSpriteFont = new SpriteFont(mDirect3DDevice,
L"Content\Fonts\Arial_14_Regular.spritefont");
Game::Initialize();
}
void RenderingGame::Shutdown()
{
DeleteObject(mKeyboard);
DeleteObject(mMouse);
DeleteObject(mFpsComponent);
DeleteObject(mSpriteFont);
DeleteObject(mSpriteBatch);
ReleaseObject(mDirectInput);
Game::Shutdown();
}
void RenderingGame::Draw(const GameTime &gameTime)
{
mDirect3DDeviceContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&BackgroundColor));
mDirect3DDeviceContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
Game::Draw(gameTime);
mSpriteBatch->Begin();
std::wostringstream mouseLabel;
mouseLabel << L"Mouse Position: " << mMouse->X() << ", " <<
mMouse->Y() << " Mouse Wheel: " << mMouse->Wheel();
mSpriteFont->DrawString(mSpriteBatch, mouseLabel.str().c_str(),
mMouseTextPosition);
mSpriteBatch->End();
HRESULT hr = mSwapChain->Present(0, 0);
if (FAILED(hr))
{
throw GameException("IDXGISwapChain::Present() failed.", hr);
}
}
Figure 12.4 shows the output of the integrated mouse component.
One final piece of scaffolding deserves discussion before you refocus on 3D rendering: software services. A service is just an object (any kind of object) that is useful to other software systems. For example, you might use the keyboard and mouse components throughout your application, and they would be good candidates as services. You could pass such objects as arguments to constructors or through public mutators, but that increases coupling between classes (the degree to which software modules depend on other modules). Good programming practices encourage low coupling, and software services can help. Instead of publicly prescribing necessary software systems, a component can internally query a service container to find the modules it needs. Furthermore, a component’s implementation can change, requiring more or fewer services, without impacting its public interface. The demos in the coming chapters make extensive use of services.
Listing 12.20 presents the declaration of the ServiceContainer
class, a thin wrapper around an std::map
that associates objects with unsigned integers. Any unsigned integer can work as the object’s key, but the system is intended for use with the RTTI system’s type identifier. Thus, you register an object into the service container and use its type for subsequent lookup.
#pragma once
#include "Common.h"
namespace Library
{
class ServiceContainer
{
public:
ServiceContainer();
void AddService(UINT typeID, void* service);
void RemoveService(UINT typeID);
void* GetService(UINT typeID) const;
private:
ServiceContainer(const ServiceContainer& rhs);
ServiceContainer& operator=(const ServiceContainer& rhs);
std::map<UINT, void*> mServices;
};
}
Listing 12.21 shows the simple implementation of the ServiceContainer
class.
#include "ServiceContainer.h"
namespace Library
{
ServiceContainer::ServiceContainer()
: mServices()
{
}
void ServiceContainer::AddService(UINT typeID, void* service)
{
mServices.insert(std::pair<UINT, void*>(typeID, service));
}
void ServiceContainer::RemoveService(UINT typeID)
{
mServices.erase(typeID);
}
void* ServiceContainer::GetService(UINT typeID) const
{
std::map<UINT, void*>::const_iterator it = mServices.
find(typeID);
return (it != mServices.end() ? it->second : nullptr);
}
}
Because the Game
class is central to the rendering engine and accessible by all game components, it works well as the host for the service container. Add a ServiceContainer
member to the Library::Game
class, and expose it through a public accessor. The companion website has a full implementation.
Listing 12.22 presents a version of the RenderingGame::Initialize()
method that registers the mouse and keyboard components with the service container.
void RenderingGame::Initialize()
{
if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION, IID_
IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
{
throw GameException("DirectInput8Create() failed");
}
mKeyboard = new Keyboard(*this, mDirectInput);
mComponents.push_back(mKeyboard);
mServices.AddService(Keyboard::TypeIdClass(), mKeyboard);
mMouse = new Mouse(*this, mDirectInput);
mComponents.push_back(mMouse);
mServices.AddService(Mouse::TypeIdClass(), mMouse);
Game::Initialize();
}
You can retrieve these services with a call to ServiceContainer::GetService()
. For example, a component might look up the Keyboard
service and store it in a class member with a call such as this:
mKeyboard = (Keyboard*)mGame->Services().GetService(Keyboard::TypeIdClass());
Of course, the service container isn’t guaranteed to contain the queried service, and your components should take this into consideration. You can always assert if you don’t find a service that is truly required. Furthermore, you can register only a single object with a type identifier. But nothing prevents you from storing a vector of objects with a given ID. You simply need to know how an object was put into the container so that you can correctly take it out.
In this chapter, you built a lot of infrastructure for your rendering engine. You learned about game components, text rendering, device input, and software services. You implemented components for collecting keyboard and mouse input, as well as a component for displaying the application’s frame rate. You will use these systems in practically all the upcoming demos.
In the next chapter, you write a reusable camera component for viewing objects in your 3D scenes.
1. Experiment with the SpriteBatch
/SpriteFont
system. Create a variety of .spritefont
objects, and render them in different colors. Hint: The SpriteFont::Draw()
method has an overload that accepts a color.
2. Extend the mouse component to limit the range of XY positions. The mouse positions should not continue to increase or decrease after the mouse has reached the edges of the screen. If the application is in windowed mode, the (0, 0)
position should represent the upper-left corner of the client area, and the values should not be allowed to go negative, stopping at the upper-left corner of the screen. Likewise, the values should not be allowed to extend past the application resolution to the lower-right corner of the screen. If the application is in full-screen mode, (0, 0)
marks the minimum position and (width - 1, height - 1)
marks the maximum.
3.145.97.109