Chapter 12. Supporting Systems

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

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.

Image

Figure 12.1 Class diagrams for the GameComponent and DrawableGameComponent classes.

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.

Listing 12.1 The GameComponent.h Header File


#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);
    };
}


Listing 12.2 The GameComponent.cpp File


#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.

Custom Runtime Type Information

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.

Listing 12.3 The RTTI.h Header File


#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

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.

Listing 12.4 The DrawableGameComponent.h Header File


#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);
    };
}


An Updated Game Class

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.

Listing 12.5 Updated Game.h File to Support Game Components (Abbreviated)


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;
    };


Listing 12.6 Updated Game.cpp File to Support Game Components (Abbreviated)


/* ... 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.

A Frame Rate Component

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.

Image

Figure 12.2 Output of the frame rate component.

Listing 12.7 The FpsComponent.h Header File


#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.

SpriteBatch and SpriteFont

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.

Image

Figure 12.3 Invocation of 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.

Listing 12.8 The Library Project’s Post-build Event


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"


Listing 12.9 The Game Project’s Prebuild Event


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.

Listing 12.10 The FpsComponent.cpp File


#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.

Integrating the Component

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.

Listing 12.11 Integration of the FpsComponent Within the RenderingGame Class


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.

Device Input

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.

Keyboard Input

Using DirectInput to query the keyboard translates into the following steps:

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.

Create the DirectInput Object

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);

Image 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.

Image dwVersion: The version number of DirectInput. This is always DIRECTINPUT_VERSION.

Image riidltf: The unique identifier of the requested interface. This is always IID_IDirectInput8.

Image ppvOut: Returns the created DirectInput object.

Image 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.

Listing 12.12 DirectInput Object Creation and Release


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();
}


Create the DirectInput Keyboard Device

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);

Image rguid: The unique identifier of the requested input device. For a keyboard, this is GUID_SysKeyboard.

Image lplpDirectInputDevice: Returns the created DirectInput device.

Image 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.

Set the Device Data Format and Cooperative Level

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:

Image c_dfDIKeyboard

Image c_dfDIMouse

Image c_dfDIMouse2

Image c_dfDIJoystick

Image 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);

Image hwnd: The top-level window handle that belongs to the application. This is created within Game::InitializeWindow() and stored in the Game::mWindowHandle member.

Image dwFlags: Bitwise OR’d flags that describe the cooperative level. Table 12.1 lists the possible flags.

Image

Table 12.1 DirectInput Device Cooperative-Level Flags

Acquire the Device

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.

Listing 12.13 The Keyboard::Initialize() Method


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");
    }
}


Query the Device State

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);

Image cbData: The size (in bytes) of the buffer specified in lpvData.

Image 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.

A Keyboard Component

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.

Listing 12.14 The Keyboard.h Header File


#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];
    };
}


Listing 12.15 The Keyboard.cpp File


#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.

Image

Table 12.2 Common DirectInput Keyboard Scan Codes

Reacquiring the Device

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.

Integrating the Keyboard Component

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.

Listing 12.16 Integration of the Keyboard Component Within the RenderingGame Class


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);
}


Mouse Input

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.

Listing 12.17 The Mouse.h Header File


#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;
    };
}


Listing 12.18 The Mouse.cpp File


#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));
    }
}


Integrating the Mouse Component

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.

Listing 12.19 Integration of the Mouse Component Within the RenderingGame Class


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.

Image

Figure 12.4 Output of the integrated mouse component.

Software Services

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.

A Service Container Class

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.

Listing 12.20 The ServiceContainer.h Header File


#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.

Listing 12.21 The ServiceContainer.cpp File


#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);
    }
}


An Updated Game Class

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.

Listing 12.22 Registering Objects 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.

Summary

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.

Exercises

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.97.109