Skip to main content
Email Password   helpLost your password?

Tetris3D - A Screenshot

Introduction

Since DirectX 9.0, a managed version of DirectX is available. Currently, nearly all the samples available focus on C# and Visual Basic and not even Microsoft's SDK provides tutorials or samples using MC++. Still, most serious game programmers tend to use C++ because of the little bit of extra speed they gain. In my opinion, Managed DirectX provides a quite nice and handy structure for the programmer, especially for people who are new to Direct3D. If you already are coding C++, there is no need to change to VB or C# - all the information provided by the MSDN can easily be translated.
This article shows how a 3D game can be done in managed C++ with a quite standard example - Tetris.

Requirements

A graphics adapter which is likely to be fast enough to run a simple 3D game. Graphic modes can be configured. The source code uses DirectX 9.0b, but 9.0a will do as well. Of course, the latest .NET framework is required.

Running the Executable

This will be the most interesting for you at the beginning anyways - and that's the best thing to do, because you'll know what I'm talking about later. So let me start off by briefly explaining how it works. First of all, you need to download the executable, of course. You must download the resources, too, and unzip them into the very same directory as the executable. They include graphic models and sound files.

The controls are pretty straight-forward: Use left/right to move the current tile, Up to rotate, down to drop the tile a little faster and space to drop it instantly. With the + and - keys, you can zoom a little and hitting P enables dynamic texture generation (blitting the contents of pr0.txt onto the frame's texture). For the rules, drop times and scoring, I used the information, which I have found on this site: http://www.colinfahey.com/2003jan_tetris/tetris_standard_specifications.htm.

The menu items are self-explanatory, so I won't waste your time with them here ;)

Looking at the Code

Of course, I cannot describe each line of the code, but there are lots of comments in case you want to re-use some things. The header files are even commented using XML Comments so a documentation can be generated as soon as VS supports that properly.

However, I tried to point out the most important - and in my view most interesting - pieces of code.

Namespaces and Classes

The source code only contains one namespace: Tetris3D. The whole game is structured into the following classes, most of them declared and defined in the equally named files:

Class Name Description
T3D The main class. It contains objects of the other classes and provides the basic application control functions like Initialize, Run and CleanUp.
T3DWindow Derived from System::Windows::Forms::Form - the main window.
T3DGraphics This class contains the main render function and the code to initialize 3d graphics.
T3DSound Contains functions to initialize and play sound buffers.
T3DKeyboard Uses DirectInput to read the keyboard buffer and process controls.
T3DGame Maintains the game field and applies the rules.
T3DBlock Represents a Tetris tile.
T3DMenu Provides the render and execution functionality for the menu.
T3DFrame Loads and renders the frame mesh.
T3DIntro Loads and renders the intro mesh.
T3DScoreBoard Loads and renders the score board.
T3DGameover Renders the game over screen.
T3DHighscore Manages and renders the highscore list.
T3DConfiguration Contains the dialog for the graphic device's configuration.

Each of the classes (except for T3D) contains a private member variable named parent, which is a pointer to the parent T3D object. This allows each class to access any of the objects of the current game instance.

The Main Loop

If you are not new to game programming, you certainly know that nearly every game has a main loop which generally does the following: get input - react on input - output current screen. This main loop can be found inside the T3D::Run () function:

while (m_Window->Created && !bTerminate) {
    // need to call tick function?

    if(Environment::TickCount - nLastTimerCall > 
       T3D_Elapse [m_Game->nCurrentLevel] &&
        m_Game->m_GameState == T3DGame::T3DGameStates::T3DGS_Running) {
        nLastTimerCall = Environment::TickCount;
        m_Game->Tick ();
    }

    // play outstanding falling row sounds

    m_Sound->Tick ();

    // process application events

    Application::DoEvents ();

    // get keys

    if (m_Window->bActive)
        m_Keyboard->ProcessKeys ();

    Render ();
}
        

This main loop does pretty much the same as described above, but additionally it adds a timed call to move the current tile down one unit and to play the line-completed-sound again, if needed (if the player completes more than one row at a time). There is also a call to Application::DoEvents, which processes window messages.

The most interesting function is certainly the Render function, but let's have a look at the game class and the graphics initializaton first.

The Game Class

A Tetris implementation is not very difficult and this is certainly not the best implementation around, but it does its job. The complete implementation can be found in the T3DGame.h / cpp file and consists of the two classes T3DGame and T3DBlock. Basically, a two-dimensional array of the game field is kept (cells). Each cell inside the array is either 0, indicating that there is no block there, or it specifies an index inside the T3DGame::Colors array, which means that there is a block with the given color. The class also contains two T3DBlock objects: m_CurrentBlock and m_NextBlok. I will describe the T3DBlock class in more detail later.

The game state is indicated by the m_GameState member variable. Possible values are enumerated in the T3DGameStates enumeration. CheckLines checks the cells array for complete lines, removes them, and plays a sound effect.

Rows, score and level are stored in the nRows, nScore, and nCurrentLevel member variables. These values are updated by the CheckLines and T3DBlock::Settle functions.

The most important function of the class is the Tick function. It is called in specific intervals depending on the current level and it moves the current block down one unit or settles it on the game field and spawns a new block if no movement is possible.

Blocks

The T3DBlock class provides the functions to validate and execute a block's move. The color member variable contains the block's color as an index in the T3DGame::Colors array and the block's position is held by the x and y member variables. rotation indicates the block's rotation (in 90 degree steps).

The block's look is stored in the T3DGame::Blocks array. It can be accessed like this:

unsigned short data = T3DGame::Blocks[rotation];

The data format is rather complex. You might already have noticed that each tile consists of four squares. Each of these squares has (local) coordinates between 0 and 3, that is, each coordinate can be represented using two bits. Therefore, each square can be represented using four bits, thus each block can be represented using 4 * 4 = 16 bits. Here's an example:

4
2
1
0
0 1 2 3

This gives us the four coordinate pairs: (1,1) (1,2) (2,1) (2,2) - or, in binary format: (01,01) (01,10) (10,01) (10,10). That means that the data entry would be: 0101011010011010 in binary format, or, in decimal format: 22170. I have created those numbers for each of the seven blocks and for each possible rotation. The T3DBlock::ToVirtualCoordinates function extracts those coordinates and maps them to the game field.

When rendering, each of the squares is rendered as a seperate block mesh. The .x model file can be found in "gfx/cube.x".

Validating Moves

Using the extracted coordinates, validating a move is quite easy. First, the move is executed on a temporary copy of the current block and then the coordinates of the temporary block's squares are extracted. If the T3DGame::cells array is non-zero at any of the squares' positions or the positions are out of range (x: smaller than 0, bigger or equal to 10; y: smaller than 0, too large is impossible) that means that the movement is not possible and the block is in its final position.

The T3DBlock::SpawnNew function uses the Random class to create a new block. It also executes T3DBlock::ValidMove to check whether a new tile can be created at all. If the creation of a new tile fails, the game is over and the game over screen is shown. I will not go into any details concerning the T3DGameover class as it is just another example of mesh rendering.

Direct3D

To use managed Direct3D, the Microsoft::DirectX::Direct3D namespace needs to be included. All graphics are rendered using a Direct3D::Device object. This is stored in T3DGraphics::device.

Configuration and Initialization

The device's configuration is done with the T3DConfiguration class. It detects the installed adapters and supported graphic modes. It is done using the Direct3D::Manager object, the code is easily understandable, so I wont comment on it any further right now. The dialog stores the settings that have been selected into the T3DGraphics::m_PresentParams variable. This variable is used to initialize the device, which is done in the T3DGraphics::Initialize function:

try {
    // create device

    Console::Write ("Creating Device\t\t\t\t\t");

    m_PresentParams->AutoDepthStencilFormat = DepthFormat::D16;
    m_PresentParams->BackBufferCount = 1;
    m_PresentParams->BackBufferFormat = Format::X8R8G8B8;
    m_PresentParams->DeviceWindow = parent->m_Window;
    m_PresentParams->DeviceWindowHandle = parent->m_Window->Handle;
    m_PresentParams->EnableAutoDepthStencil = true;
    m_PresentParams->MultiSample = MultiSampleType::None;
    m_PresentParams->MultiSampleQuality = 0;
    m_PresentParams->PresentationInterval = PresentInterval::Default;
    m_PresentParams->PresentFlag = PresentFlag::None;
    m_PresentParams->SwapEffect = SwapEffect::Flip;
    m_PresentParams->Windowed = !parent->m_Configuration->checkBox1->Checked;

    device = new Direct3D::Device (
        m_Adapter,
        devtype,
        parent->m_Window,
        CreateFlags::SoftwareVertexProcessing, m_PresentParams);

    device->DeviceLost += new EventHandler (this, T3DGraphics::OnDeviceLost);
    device->DeviceResizing += new System::ComponentModel::CancelEventHandler
                                   (this, T3DGraphics::EnvironmentResize);
    device->Disposing += new EventHandler (this, T3DGraphics::OnDisposing);
    device->SetCursor (parent->m_Window->Cursor, false);
    device->ShowCursor (false);

    Console::WriteLine ("Ok");
} catch (Exception *e) {
    Console::WriteLine (String::Format ("Failed:\n\t {0}", e->Message));
    return false;
}
        

As you can see, a few settings like the back buffer and depth stencil formats cannot be configured. After the device has been created using the Direct3D::Device call, it is ready to use. A few event handlers are added, they are needed for device lost exceptions (see Lost Devices). Finally, the cursor is hidden.

A Basic Render Function

The basic render function is T3D::Render. Here's the most important excerpt:

// clear device

m_Graphics->device->Clear ((ClearFlags) (ClearFlags::Target | 
    ClearFlags::ZBuffer),
    System::Drawing::Color::Black, 1.0f, 0);

// begin scene

m_Graphics->device->BeginScene ();

// execute render function

CurrentRenderFunction ();

// end scene

m_Graphics->device->EndScene ();

// present scene

try {
    m_Graphics->device->Present ();
} catch (DeviceLostException *) { // device lost?

    m_Graphics->bDeviceLost = true;
    Console::WriteLine ("DeviceLost Exception caught.");
}
        

All the rendering needs to be done between the BeginScene and EndScene calls. Finally, the scene is presented using the Present function. CurrentRenderFunction is a pointer to the current render code, which does mainly render the different meshes. An example for such rendering can be found at "Loading and Using Meshes".

Loading and Using Meshes

All meshes are loaded in the T3DGraphics::OnDeviceReset function. Here is a representive excerpt of the function. The loading of all other meshes is done exactly the same way:

// load mesh (block)

try {
    m_Block = Mesh::FromFile (S"gfx/cube.x", (MeshFlags)0,
        device);
} catch (Exception *) {
    Console::WriteLine ("Error: Cannot load file \"gfx/cube.x\".");
    return;
}

// create clone with normals

Mesh *TempMesh = m_Block->Clone (
    (MeshFlags)(m_Block->Options.Use32Bit ? MeshFlags::Use32Bit : 0),
    (VertexFormats)(m_Block->VertexFormat | VertexFormats::Normal),
    device);
TempMesh->ComputeNormals ();
m_Block->Dispose ();
m_Block = TempMesh;
        

As you can see, the Mesh::FromFile function makes it quite easy to load a .x file into a Mesh object. After the block mesh has been loaded the meshes normals are computed. This is important for lighting the mesh because a triangle's vertexes' normals affect the way light is reflected and without the normals the object would appear in its ambient color and without any shadows. Therefore, the mesh is first copied into a temporary mesh variable and the normal flag is added to the vertex format. After that, the normals are computed. As a reading on vertexes, normals and vertex formats I recommend this site.

The block mesh is rendered in the T3DGraphics::DrawBlock function:

// draw block at given position

void T3DGraphics::DrawBlock (float x, float y, float z, int c)
{
    // setup view

    device->Transform->World = Matrix::Translation (x, y, z);

    // set material

    Material m;

    // make sure that the color is valid

    Debug::Assert (c >= 0 && c < T3DGame::color_count);
    m.Diffuse = Drawing::Color::FromArgb (T3DGame::Colors [c]);
    m.Ambient = m.Diffuse;

    // ...


    device->Material = m;

    // block

    m_Block->DrawSubset (0);
}
        

I left out the setting of the material's emmisive color. This is just an unimportant effect and it would take too much space here. Before drawing the mesh, it needs to be transformed. Therefore, the device->Transform->World variable is changed. For a block, this is just a simple translation. Rotation could be added like this:

// rotate 180 degrees around the x-axis

DirectX::Matrix matWorld = DirectX::Matrix::RorationX ((float)Math::PI); 
matWorld.Multiply (DirectX::Matrix::Translation (x, y, z)); // translate block

device->Transform->World = matWorld;
        

Using the Multiply functions, you can apply as many transformations as you like. After translating the block, a material is assigned and finally the mesh is drawn using the DrawSubset function. If a mesh contains more than one material it is split into several subsets, but this simple block contains only one subset. A more detailed description on using meshes can be found here: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_m/directx/direct3d/tutorials/tutorial6.asp.

Lost Devices

If the user presses ALT-TAB in fullscreen mode the device gets lost. That means that all the rendering functions will fail and Device::Present will throw an exception. If you look at the above T3D::Render code you will notice the try-catch block around this function. It sets the T3DGraphics::bDeviceLost variable to true. If a device is lost, an application should test whether it can gain control over the device again and in that case reset the device again. This is also done in the T3D::Render function:

if (m_Graphics->bDeviceLost) {
    try {
        m_Graphics->device->TestCooperativeLevel ();
    } catch (DeviceLostException *) {
        return;
    } catch (DeviceNotResetException *) {
        Console::WriteLine ("Resetting...");
        m_Graphics->bDeviceLost = false;
        m_Graphics->device->Reset (m_Graphics->m_PresentParams);
    }
    m_Graphics->bDeviceLost = false;
}
            

For further reading on lost devices, I recommend: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_m/directx/direct3d/gettingstarted/devices/lostdevices.asp.

T3DFont - A Special Work-Around

If you have already run the executable, you will have noticed the three-dimensional font inside the scoreboard frame. Initially, I have created a new mesh using the Mesh::TextFromFont function every frame. However, after a few minutes this call somehow failed returning a mesh that contained a few more vertexes, but not showing up at all. This was not a memory error - I free'ed all the meshes properly. I searched the newgroups and found someone else complaining about the same problem, but I did not find a solution. This is why I decided to code a little workaround. The T3DFont class generates a mesh for every graphical character. If text needs to be drawn, it puts together all the generated meshes to form the requested string. No new meshes are created, so the problem with Mesh::TextFromFont won't occur.

Dynamic Texture Generation

This is a little gimmick that I couldn't resist. Hitting P while running the game starts scrolling the contents of pr0.txt on the frame mesh. But besides the gimmick effect, this is also a nice sample for generating textures at runtime. The Texture class contains a FromBitmap function to convert a Bitmap into a texture. At the beginning, I generated a bitmap each frame containing the current graphical data and converted it to a texture using this function. As you can imagine, this causes the framerate to drop horribly. Currently, I use the Texture::GetSurfaceLevel function to retreive a Surface object of the texture so I can modify it directly. Here's the code:

// get graphics from bitmap

Surface *s = m_pr0Texture->GetSurfaceLevel (0);
Graphics *g = s->GetGraphics ();

// execute some GDI+ functions like this:


// clear bitmap

g->FillRegion (new SolidBrush (m_FrameMaterials[0].Material3D.Diffuse),
    new Drawing::Region (Drawing::Rectangle (0, 0, pr0x, pr0y)));
    
// ...


// release graphics

s->ReleaseGraphics ();
s->Dispose ();
        

Of course, there is still a little framerate drop, but it works much better this way.

DirectSound

To use DirectSound, the Microsoft::DirectX::DirectSound namespace needs to be referenced. All DirectSound functions can be found in the T3DSound.h / cpp files.

Initialization

Initializing the sound device is quite easy unless you need some special cooperative mode or something like that. For my purposes, those few lines did the job:

try {
    device = new DirectSound::Device (); // use default device

    device->SetCooperativeLevel (parent->m_Window, 
                                 DirectSound::CooperativeLevel::Normal);
} catch (Exception *) {
    Console::WriteLine ("Failed");
    return false;
}
        

Loading and Playing Sounds

To laod and play sound buffers like the little sound effects, I use the SecondaryBuffer class. A secondary sound buffer can be loaded like this:
// load hit wave

try {
    m_Knock = new SecondaryBuffer ("Sounds/Hit.wav", device);
} catch (Exception *) {
    Console::WriteLine ("Failed: Could not load file \"Sounds/Hit.wav\"");
    parent->bTerminate = true;
    return false;
}
        

Playing the sound buffer is just as easy:

// play knock sound

void T3DSound::PlayKnock ()
{
    if (!bSoundEffects) return;
    m_Knock->Play (0, BufferPlayFlags::Default);
}
        

As further reading on sound buffers I recommend another MSDN page: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_m/directx/sound/playingsounds/playing_sounds_entry.asp.

Unfortunately, the Tetris.mp3 music theme file is a quit big file to fit in a sound buffer. Also, loading mp3 files is not possible with the SecondaryBuffer class. Therefore I use the DirectX::AudioVideoPlayback::Audio class:

// create tetris theme IMediaControler

m_TetrisTheme = new DirectX::AudioVideoPlayback::Audio ("Sounds/Tetris.mp3");
m_TetrisTheme->Play ();
        

That was all about sounds. A simple game like Tetris doesn't require 3D effects or anything alike ;)

DirectInput

The namespace that needs to be referenced to use DirectInput is Microsoft::DirectX::DirectInput.

All DirectInput functions are place in the T3DKeyboard.h / cpp files.

Initialization

Again, initialization is really simple as the game does not require any special input capabilities:

try {
    // create new device

    device = new DirectInput::Device (SystemGuid::Keyboard);
} catch (Exception *e) {
    Console::WriteLine ("Failure");
    Windows::Forms::MessageBox::Show (e->Message);
    return false;
}

// set cooperative level

device->SetCooperativeLevel (parent->m_Window, 
    (DirectInput::CooperativeLevelFlags)
    (DirectInput::CooperativeLevelFlags::Foreground |
    DirectInput::CooperativeLevelFlags::Exclusive));

// set buffersize

try {
    device->Properties->BufferSize = buffer_size;
} catch (Exception *e) {
    Console::WriteLine ("Failure");
    Windows::Forms::MessageBox::Show (e->Message);
    return false;
}

try {
    device->Acquire ();
} catch (Exception *) {
}
        
The Acquire function fails, if the device is lost, just like in graphics mode. Since keys are read in regularly, acquire is called over and over again until it does not fail at some point of time. If the window is not active, the keyboard buffer is not read in anyways.

Reading Buffered Data

The function T3DKeyboard::ProcessKeys reads the buffered keyboard data and passes each key to a key processing function which is assigned via the T3DKeyboard::KeyProcessor member variable. Also, the code maintains a list of currently pressed keys, so key press repetition can be simulated, because the device will not send buffered data again, once a key is held down.
try {
    dataCollection = device->GetBufferedData ();
} catch (InputException *e) {
    // device lost :/

    // clear pressed key list

    m_PressedKeys->Clear ();
    try {
        device->Acquire ();
    } catch (InputLostException *) {
        return;
    } catch (InputException *ie) {
        e = ie;
    }

    if (e) return;
}

// no data caught?

if (dataCollection) {
    for (int i = 0; i < dataCollection->Count; i++) {
        BufferedData *CurrentKey = dataCollection->Item [i];

        if (dataCollection->Item [i]->Data & 0x80) { // key down

            // add key to pressed list

            m_PressedKeys->Add (CurrentKey);
        } else { // key up

            // remove key from list

            for (int j = 0; j < m_PressedKeys->Count; j++) {
                if (__try_cast <BufferedData *> 
                       (m_PressedKeys->Item[j])->Offset == CurrentKey->Offset) {
                        m_PressedKeys->RemoveAt (j--);
                    }
            }
        }

        // execute current key processor

        if (KeyProcessor) {
            KeyProcessor (CurrentKey);
        }
    }
}

// process outdated keys

for (int i = 0; i < m_PressedKeys->Count; i++) {
    BufferedData *CurrentKey = __try_cast <BufferedData *> 
                                                   (m_PressedKeys->Item[i]);
    if (Environment::TickCount - CurrentKey->TimeStamp > KeyboardDelay)
        if (KeyProcessor)
            KeyProcessor (CurrentKey);
}
        

The key processor reacts on the input and moves the current tile, jumps to the next menu item or executes a menu item, for example.

Conclusion

This article should have given you a rough overview of what's to be thought about when writing a simple 3D game. If you consider writing your own little game, you should start reading the pages of the MSDN which I have linked here. Also, the tutorials included in the SDK and found in the internet are quite helpful. As I have already said, there is not much to be found about C++ and Managed DirectX, but I hope I could encourage some readers to try using this very powerful combination. As this is the first article I have ever written, I hope that I didn't miss the point too much and that I could answer all or at least most of your questions. If you have any further questions or comments, feel free to drop me a mail.

History

August, 1st 2003: First version of the article.

Bugs

So far, I have not discovered any bugs, but there certainly are some ;) Also, I have not tested the program on many environments, mostly WinXP with a relatively new graphics adapter and the latest DirectX SDK. So please let me know if you experience any problems.

Credits

Thanks to...

Oliver Sluke for testing the game and giving lots of ideas for improvements.

Dark Nebula for the remix of the tetris theme. Other remixes and sound files can be found on his website: http://www.angelfire.com/darkside/darknebula/

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralResources. Pin
Saksida Bojan
22:17 18 Jul '05  
GeneralCompiler Error 3635 Pin
bollwerj
7:27 20 Dec '04  
GeneralRe: Compiler Error 3635 Pin
edx_fa
10:08 20 Dec '04  
GeneralRe: Compiler Error 3635 Pin
bamoo
4:13 13 Jan '05  
GeneralRe: Compiler Error 3635 Pin
edx_fa
8:04 13 Jan '05  
GeneralRe: Compiler Error 3635 Pin
the_jebus
8:34 18 Oct '06  
GeneralProblem in downloading source files Pin
ycomp_ff
22:52 27 Sep '04  
GeneralDoes this program work? Pin
ShadowP13
15:59 22 Sep '04  
GeneralRe: Does this program work? Pin
ShadowP13
21:51 22 Sep '04  
GeneralRe: Does this program work? Pin
Felix Arends
4:30 23 Sep '04  
GeneralRe: Does this program work? Pin
ShadowP13
4:21 24 Sep '04  
GeneralTetris3d err0r Pin
vampster
2:11 16 Feb '04  
GeneralRe: Tetris3d err0r Pin
edx_fa
8:04 17 Feb '04  
GeneralSuper stark gemacht! Pin
DietmarS
8:41 19 Sep '03  
GeneralRe: Super stark gemacht! Pin
edx_fa
2:05 27 Sep '03  
GeneralRe: Super stark gemacht! Pin
DietmarS
8:29 27 Sep '03  
GeneralRe: Super stark gemacht! Pin
edx_fa
13:41 27 Sep '03  
GeneralRe: Super stark gemacht! Pin
DietmarS
0:44 11 Oct '03  
GeneralRe: Super stark gemacht! Pin
edx_fa
11:54 11 Oct '03  
GeneralRe: Super stark gemacht! Pin
DietmarS
8:23 13 Oct '03  
GeneralAll nice stuff cept... Pin
Lord Jimbo
6:17 17 Aug '03  
GeneralRe: All nice stuff cept... Pin
edx_fa
10:14 17 Aug '03  
GeneralRe: All nice stuff cept... Pin
Anonymous
0:33 15 Mar '04  
GeneralStarkes Stück Arbeit! Pin
count.negative
0:29 16 Aug '03  
GeneralAgreed... Pin
nutcase
21:01 12 Aug '03  


Last Updated 6 Aug 2003 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2009