Developing a simple game in Code :: blocks using Direct3D 9

I want to talk about my first experience in game dev. It’s worth mentioning right away that the article will be purely technical, since my goal was only to gain skills in developing graphic applications using Direct3D without involving high-level game development tools such as Unity. Accordingly, there will also be no talk about the implementation, monetization and promotion of the game. The article is aimed at beginners in programming Direct3D applications, as well as just about people who are interested in the key mechanisms of such applications. Also at the end I give a list of literature on game dev, carefully selected by me from more than a hundred books on game programming and computer graphics.

Introduction


So, in my free time I decided to study the popular graphic API. After reading several books and analyzing a bunch of examples and tutorials (including from the DirectX SDK), I realized that the moment had come when you should try your hand yourself. The main problem was that most of the existing examples simply demonstrate this or that API capability and are implemented procedurally in almost one cpp-file, and even using the DXUT wrapper, and do not give an idea of ​​what structure the final application should have which classes need to be designed and how it should interact with each other so that everything is beautiful, readable and efficiently works. This flaw also applies to Direct3D books: for example, for many newcomers, the fact

Idea


The first thing I needed to decide on the very idea of ​​the game. An old game from 1992 under MS-DOS immediately came to mind, which, I think, is familiar to many. This is the Gamos logic game Lines .

Well, the call has been accepted. Here is what we have:
  • there is a square field of cells;
  • in the cells on the field there are colorful balls;
  • after moving the ball, new balls appear;
  • the player’s goal is to line up one-color balls in a line: accumulating in a single line a certain number of one-color balls leads to their detonation and accrual of points;
  • the main task is to hold out as long as possible until free cells run out on the field.

Now let's look at the point of view of the Direct3D application:
  • we will make the field of cells in the form of a three-dimensional platform with protrusions, each cell will be something like a podium;
  • Three types of ball animation should be implemented:
    1. appearance of a ball: first a small ball appears, which in a short time grows to an adult size;
    2. moving the ball: just a sequential movement through the cells;
    3. jumping ball: when you select a ball with the mouse, it should activate and start jumping in place;
  • a particle system should be implemented that will be used in the animation of the explosion;
  • the output of the text should be implemented: to display the earned points on the screen;
  • virtual camera control should be implemented: rotation and zoom.

In fact, the points listed above are a miserable likeness of a document called a design project. I strongly recommend that before starting the development to paint everything in it to the smallest detail, print and keep before your eyes! Looking ahead, I immediately show a demo video for clarity of the implementation of the items (by the way, the video was recorded using the ezvid program , so do not be alarmed by their splash screen at the beginning):



Development start


So far I have not mentioned which tools were used. First, you need the DirectX software development kit (SDK), which is always available for free download on the Microsoft website: DirectX SDK . If you are going to use a version of Direct3D 9, like me, then after installation it is necessary to open the DirectX Control Panel through the main menu and on the Direct3D 9 tab select which version of the libraries will be used during assembly - retail or debug (this affects whether Direct3D will report to the debugger about the results of his activity):

Debug or retail


Why is Direct3D version 9? Because this is the latest version, where there is still a fixed function pipeline, that is, a fixed graphics pipeline that includes, for example, functions for calculating lighting, processing vertices, blending, and so on. Starting with the 10th version, developers are encouraged to independently implement these functions in shaders, which is an undeniable advantage, but, in my opinion, difficult to understand during the first experiments with Direct3D.

Why Code :: blocks? It was probably foolish to use a cross-platform IDE to develop an application that uses a non-cross-platform API. Just Code :: blocks takes several times less space than Visual Studio, which turned out to be very relevant for my country PC.

Starting development with Direct3D was very simple. In Code :: blocks, I created an empty project, then in build options I had to do two things:

1) On the search directories tab and the compiler sub tab, add the path to the DirectX SDK include directory, for example, like this:
Search directories


2) On the linker tab, add two libraries - d3d9.lib and d3dx9.lib:
Linker


After that, you will need to include the Direct3D header files in the application source code:

#include "d3d9.h"
#include "d3dx9.h"

Application structure


Here I made the first mistake: I began to reflect on which design pattern to choose. I came to the conclusion that MVC (model-view-controller) is best suited: the model is the game class (game), which includes all the logic - calculating the movement paths, the appearance of balls, parsing explosive combinations; the presentation will be the engine class, which is responsible for rendering and interacting with Direct3D; the controller will be the wrapper itself (app) - this includes a message processing cycle, user input processing, and, most importantly, a state manager and ensuring the interaction of game and engine objects. Everything seems to be simple, and you can start writing header files, but it wasn’t there! At this stage, it turned out to be very difficult to navigate and understand what methods these classes should have. It is clear that the complete lack of experience affected“Do not try to write the perfect code from the very beginning, let it be non-optimal and chaotic. Understanding comes with time, and you can do refactoring later. ” As a result, after several iterations of refactoring an already working layout, the definition of the three main classes took the form:

Class tgame
class TGame {
private:
    BOOL gameOver;
    TCell *cells;
    WORD *path;
    WORD pathLen;
    LONG score;
    void ClearField();
    WORD GetSelected();
    WORD GetNeighbours(WORD cellId, WORD *pNeighbours);
    BOOL CheckPipeDetonate(WORD *pPipeCells);
public:
    TGame();
    ~TGame();
    void New();
    BOOL CreateBalls(WORD count);
    void Select(WORD cellId);
    BOOL TryMove(WORD targetCellId);
    BOOL DetonateTest();
    WORD GetNewBallList(TBallInfo **ppNewList);
    WORD GetLastMovePath(WORD **ppMovePath);
    WORD GetDetonateList(WORD **ppDetonateList);
    LONG GetScore();
    BOOL IsGameOver();
};


Class tengine
class TEngine {
private:
    HWND hWindow;
    RECT WinRect;
    D3DXVECTOR3 CameraPos;
    LPDIRECT3D9 pD3d;
    LPDIRECT3DDEVICE9 pDevice;
    LPDIRECT3DTEXTURE9 pTex;
    LPD3DXFONT pFont;
    D3DPRESENT_PARAMETERS settings;
    clock_t currentTime;
    TGeometry *cellGeometry;
    TGeometry *ballGeometry;
    TParticleSystem *psystem;
    TBall *balls;
    TAnimate *jumpAnimation;
    TAnimate *moveAnimation;
    TAnimate *appearAnimation;
    LONG score;
    void InitD3d();
    void InitGeometry();
    void InitAnimation();
    void DrawPlatform();
    void DrawBalls();
    void UpdateView();
public:
    TEngine(HWND hWindow);
    ~TEngine();
    void AppearBalls(TBallInfo *ballInfo, WORD count);
    void MoveBall(WORD *path, WORD pathLen);
    void DetonateBalls(WORD *detonateList, WORD count);
    BOOL IsSelected();
    BOOL IsMoving();
    BOOL IsAppearing();
    BOOL IsDetonating();
    void OnResetGame();
    WORD OnClick(WORD x, WORD y, BOOL *IsCell);
    void OnRotateY(INT offset);
    void OnRotateX(INT offset);
    void OnZoom(INT zoom);
    void OnResize();
    void OnUpdateScore(LONG score);
    void Render();
};


TApplication Class
class TApplication {
private:
    HINSTANCE hInstance;
    HWND hWindow;
    POINT mouseCoords;
    TEngine* engine;
    TGame* game;
    BOOL moveStarted;
    BOOL detonateStarted;
    BOOL appearStarted;
    void RegWindow();
    static LRESULT CALLBACK MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    void ProcessGame();
public:
    TApplication(HINSTANCE hInstance, INT cmdShow);
    ~TApplication();
    TEngine* GetEngine();
    TGame* GetGame();
    INT MainLoop();
};


The TGame class has only 3 methods that the user himself can initiate - New (new game), Select (ball selection) and TryMove (attempt to move the ball). The rest are auxiliary and are called by the controller in special cases. For example, DetonateTest (test for explosive combinations) is called after the appearance of new balls or after trying to move. GetNewBallList, GetLastMovePath, GetDetonateList are called, respectively, after the appearance of balls, after moving, and after an explosion, with one purpose: to get a list of specific balls and pass it to the engine object for processing to draw something. I do not want to dwell on the logic of TGame, because there are source codes with comments . I can only say that the determination of the path of movement of the ball is implemented using the Dijkstra algorithmover an undirected graph with equal weights of all edges.

Let's consider engine and controller classes in more detail.

Tengine


  • The class defines fields for storing the handle of the window and its rectangle. They are used in the OnResize method, which the controller calls when the window is resized, to calculate a new projection matrix.
  • The CameraPos field stores observer coordinates in world space. The direction vector of the gaze is not necessary to store, because according to my idea the camera is always directed to the origin, which, by the way, coincides with the center of the platform.
  • There are also pointers to Direct3D interfaces: LPDIRECT3D9, which is needed only to create a device; LPDIRECT3DDEVICE9 - in fact, the Direct3D device itself, the main interface with which you have to work; LPD3DXFONT and LPDIRECT3DTEXTURE9 for working with text and texture.
  • The currentTime field is used to store the current time in milliseconds and is necessary for rendering smooth animation. The fact is that drawing each frame takes a different number of milliseconds, so you have to measure these milliseconds each time and use it as a parameter when interpolating the animation. This method is known as time synchronization and is used everywhere in modern graphics applications.
  • Pointers to objects of the TGeometry class (cellGeometry and ballGeometry) store the geometry of one cell and one ball. The TGeometry object itself, as the name implies, is designed to work with geometry and contains vertex and index buffers, as well as a description of the material (D3DMATERIAL9). When rendering, we can change the world matrix and call the Render method of the TGeometry object, which will lead to the drawing of several cells or balls.
  • TParticleSystem is a particle system class that has methods for initializing multiple particles, updating their positions in space, and, of course, rendering.
  • TBall * balls - an array of balls with information about color and status [bouncing, moving, appearing].
  • Three objects of type TAnimate - for providing animation. The class has a method for initializing key frames, which are world transformation matrices, and methods for calculating the current position of the animation and applying the transformation. In the rendering procedure, the engine object sequentially draws the balls and, if necessary, calls the ApplyTransform method of the desired animation to warp or move the ball.
  • InitD3d, InitGeometry, InitAnimation are called only from the TEngine constructor and are separated into separate methods for clarity. A Direct3D device is created in InitD3d and all necessary render states are installed, including the installation of a point light source with a specular component directly above the center of the platform.
  • The three methods AppearBalls, MoveBall, and DetonateBalls trigger the appearance, movement, and explosion animations, respectively.
  • The IsSelected, IsMoving, IsAppearing, IsDetonating methods are used in the state manager function to track the moment the animation ends.
  • Methods with the On prefix are called by the controller when the corresponding events occur: mouse click, camera rotation, etc.

Consider the main Render method:

TEngine :: Render ()
void TEngine::Render()
{
    //вычисляем, сколько миллисекунд прошло с момента отрисовки предыдущего кадра
    clock_t elapsed=clock(), deltaTime=elapsed-currentTime;
    currentTime=elapsed;
    //обновляем позиции анимаций, если они активны
    if(jumpAnimation->IsActive())
    {
        jumpAnimation->UpdatePosition(deltaTime);
    }
    if(appearAnimation->IsActive())
    {
        appearAnimation->UpdatePosition(deltaTime);
    }
    if(moveAnimation->IsActive())
    {
        moveAnimation->UpdatePosition(deltaTime);
    }
    pDevice->Clear(0,NULL,D3DCLEAR_STENCIL|D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,D3DCOLOR_XRGB(0,0,0),1.0f,0);
    pDevice->BeginScene();
    //рисуем платформу
    DrawPlatform();
    //рисуем шары
    DrawBalls();
    //если активна система частиц, то обновляем положения частиц и рендерим их с текстурой
    if(psystem->IsActive())
    {
        pDevice->SetTexture(0,pTex);
        psystem->Update(deltaTime);
        psystem->Render();
        pDevice->SetTexture(0,0);
    }
    //вывод заработанных очков
    char buf[255]="Score: ",tmp[255];
    itoa(score,tmp,10);
    strcat(buf,tmp);
    RECT fontRect;
    fontRect.left=0;
    fontRect.right=GetSystemMetrics(SM_CXSCREEN);
    fontRect.top=0;
    fontRect.bottom=40;
    pFont->DrawText(NULL,_T(buf),-1,&fontRect,DT_CENTER,D3DCOLOR_XRGB(0,255,255));
    pDevice->EndScene();
    pDevice->Present(NULL,NULL,NULL,NULL);
}


At the very beginning, it is calculated how many milliseconds have passed since the previous call to Render (), then the animation progresses, if they are active, are updated. Buffers are cleared using the Clear method and the platform, balls, and particle system are sequentially drawn, if active. Finally, a line with the current value of points earned is displayed.

TApplication


  • The class has a field for storing mouse coordinates, since we will need to calculate the relative cursor offsets to rotate the camera.
  • The boolean flags appearStarted, moveStarted, and detonateStarted are needed to track the status of the corresponding animations.
  • The code for registering a window class has been moved to the RegWindow method.
  • The MsgProc static method is the so-called window procedure.
  • ProcessGame is a simplified version of the state manager, in which the current state of the game is evaluated and some actions are taken depending on it.
  • MainLoop - message processing loop.

Here is such a lightweight controller. A similar message processing loop can be found in any Direct3D book:

TApplication :: MainLoop ()
INT TApplication::MainLoop()
{
    MSG msg;
    ZeroMemory(&msg,sizeof(MSG));
    while(msg.message!=WM_QUIT)
    {
        if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            //если нет сообщений, то обрабатываем состояние игры и занимаемся рендерингом
            ProcessGame();
            engine->Render();
        }
    }
    return (INT)msg.wParam;
}


Only what is inside the else block deserves attention - this is the so-called IdleFunction, which is executed in the absence of messages.

And here is the state manager function:

TApplication :: ProcessGame ()
void TApplication::ProcessGame()
{
    if(moveStarted)
    {
        //ждем до окончания анимации перемещения
        if(!engine->IsMoving())
        {
            //перемещение окончено - тестируем на взрыв
            moveStarted=FALSE;
            if(game->DetonateTest())
            {
                //инициируем взрыв и увеличиваем очки
                WORD *detonateList,
                count=game->GetDetonateList(&detonateList);
                detonateStarted=TRUE;
                engine->DetonateBalls(detonateList,count);
                engine->OnUpdateScore(game->GetScore());
            }
            else
            {
                //иначе пытаемся добавить шары
                if(game->CreateBalls(APPEAR_COUNT))
                {
                    TBallInfo *appearList;
                    WORD count=game->GetNewBallList(&appearList);
                    appearStarted=TRUE;
                    engine->AppearBalls(appearList,count);
                }
                else
                {
                    //game over!
                }
            }
        }
    }
    if(appearStarted)
    {
        //ждем до окончания анимации появления
        if(!engine->IsAppearing())
        {
            appearStarted=FALSE;
            //появление окончено - тестируем на взрыв на всякий случай
            if(game->DetonateTest())
            {
                //инициируем взрыв и увеличиваем очки
                WORD *detonateList,
                count=game->GetDetonateList(&detonateList);
                detonateStarted=TRUE;
                engine->DetonateBalls(detonateList,count);
                engine->OnUpdateScore(game->GetScore());
            }
        }
    }
    if(detonateStarted)
    {
        //ждем до окончания анимации взрыва
        if(!engine->IsDetonating())
        {
            //просто сбрасываем флаг
            detonateStarted=FALSE;
        }
    }
}


Well, perhaps that's all!

Conclusion


This is the place to list the flaws. Of course, there are a lot of places for optimizations in the code. In addition, I did not mention such things as changing the video mode settings (screen resolution, multisampling) and device loss processing (LostDevice). At the expense of the latter there is a detailed discussion on the gamedev.ru website .

I hope my research will benefit someone. By the way, the sources on github .

Thanks for attention!

Promised Literature


1. Frank D. Luna Introduction to 3D game programming with DirectX 9.0 - for understanding the basics;
2. Gornakov S. DirectX9 programming lessons in C ++ are also the basics, but there are chapters on DirectInput, DirectSound and DirectMusic. In examples of programs errors are sometimes encountered;
3. Flenov M. E. DirectX and C ++ the art of programming - a fun presentation style. Basically, the goal of the book is to create animated clips using interesting effects, including with shaders. Judge for yourself by the name of the sections: heart attack, fire dragon;
4. Barron Todd Programming strategic games with DirectX 9 - fully devoted to topics related to strategic games: block graphics, AI, creating maps and landscapes, sprites, special effects with particle systems, as well as developing screen interfaces and working with DirectSound / Music;
5. Bill Fleming 3D Creature WorkShop - a book not on programming, but on the development of three-dimensional character models in LightWave, 3D Studio Max, Animation Master environments;
6. Thorn Alan DirectX 9 User interfaces Design and implementation - a detailed book on the development of graphical interfaces with DirectX. A hierarchical model of components of screen forms, similar to that implemented in Delphi, is considered;
7. Adams Jim Advanced Animation with DirectX - consider types of animation (skeletal, morphing and varieties) and their implementation, as well as work with geometry and animation from X-files;
8. Lamot Andre Programming games for Windows. Professional advice - this book is even more serious: optimization issues, data structure selection for various tasks, multithreading, physical modeling, AI are considered. The final chapter describes the creation of a game about a spaceship and aliens;
9. David H. Eberly 3D Game engine design - a good book for understanding the whole theory of game building: first, the technologies of graphic APIs (transformations, rasterization, shading, blending, multitexturing, fog, etc.) are described, then topics such as the scene graph , object selection, collision detection, character animation, level of detail, landscapes;
10. Daniel Sánchez-Crespo Dalmau Core techniques and algorithms in game programming - describes in detail the algorithms and data structures used in game engineering tasks, such as AI, scripting, rendering in closed and open spaces, clipping algorithms, procedural techniques, methods for implementing shadows , camera implementation, etc .;
11. Lamot Andre Programming Role Playing Games with directX 9 - A thousand-page detailed RPG development guide. It includes both theoretical chapters on programming with Direct3D, DirectInput, DirectSound, DirectPlay, and applied chapters that are directly related to the game engine.

Also popular now: