Friday, 31 January 2014

Beginning Game Development: Part II - Introduction to DirectX

Beginning Game Development: Part II - Introduction to DirectX

Introduction

Welcome to the second article on beginning game development. In this article we are going to cover the basics of DirectX.
DirectX is a multimedia API that provides a standard interface to interact with graphics and sound cardsinput devices and more. Without this standard set of APIs you would have to write different code for each combination of graphics and sound cards and for each type of keyboard, mouse and joystick. DirectX abstracts us from the specific hardware and translates a common set of instructions into the hardware specific commands.
Like all new tools, DirectX has a number of new terms and definitions that you need to understand. In addition to these new terms you also are going to have to brush up on yourmath skills. DirectX and game development in general is pretty math intensive, and it helps if you understand the basics. There is no need to get out the old calculator however, the idea is to understand the goal and the way to get to that goal, the rest we do in code with a number of pre-prepared math libraries.

DirectX Overview

DirectX first appeared in 1995 and was then called the “GameSDK”. In its original form it was targeted at developers using C and C++. Only with the release of the first managed version (9.0) of the API in December 2002 has it been possible to use C# or VB.NET with DirectX (actually you can use any CLR compliant language if you want to).
While much has been said and written about the performance of managed DirectX when compared to the unmanaged version, the fact that commercial games have already been created using managed DirectX should settle that argument once and for all. While certain games with extreme performance needs might need to use unmanaged code, most games can be created with managed code or using a mix of managed and unmanaged code. Writing in managed code makes the developers more productive and thus write more code and produces safer code.
After installing the DirectX SDK you should have a directory at C:\WINDOWS\Microsoft.NET\Managed DirectX with a subdirectory for each version of the SDK that has been installed on your machine. I am on my fourth version on the machine I am using and consequently I have four subdirectories. In each of these subdirectories you should have nine DLL and nine XML files. Since the managed world of .NET allows us to have multiple versions of the same DLL file on the same computer without causing the problems previously referred to as DLL-hell we can have multiple versions of the managed DirectX libraries available to us. This allows you to easily roll back to a previous version after installing a new version.
If you have previous experience with DLL files in Windows you might be worried that having multiple versions of the same file installed on the same computer will cause problems. These versioning issues are no longer an issue since the introduction of side-by-side versioning in .NET.  This means you can use the multiple versions to check for compatibility issues when a new version of the SDK is released without having to commit yourself to an upgrade.
The nine DLL files roughly correspond to the ten namespaces in DirectX. As we create our game we'll use a number of these namespaces to provide support for input devices, sound, network play and of course 3D graphics.
NamespaceDescription
Microsoft.DirectXCommon Classes and math Structures
Microsoft.DirectX.Direct3D3D graphics and helper libraries
Microsoft.DirectX.DirectDrawDirect Draw graphics API. This is a legacy namespace and you should not need to use it.
Microsoft.DirectX.DirectPlayNetworking API for multiplayer games
Microsoft.DirectX.DirectSoundSound support
Microsoft.DirectX.DirectInputInput device support (i.e mouse and joystick)
Microsoft.DirectX.AudioVideoPlaybackPlay Video and Audio (i.e playback a DVD on your PC)
Microsoft.DirectX.DiagnosticsTroubleshooting
Microsoft.DirectX.SecurityAccess security
Microsoft.DirectX.Security.PermissionsAccess security permissions
Figure 1. List of DirectX 9.0 namespaces.
Before we go any further we need to complete some unfinished business from the last article. After adding the FrameworkTimer class we were no longer able to build the project because we were missing the references to DirectX. Let's fix that now and add them.
  • Right-click References in the Solution Explorer and select Add Reference.
  • On the .NET tab scroll down until you find the component named Microsoft.DirectX
  • While pressing the CTRL key select the following components: Microsoft.DirectX, Microsoft.DirectX.Direct3D and click OK.
  • The last step we need to complete before we can finally build the solution is to comment out the portions of the dxmutmisc.cs file we do not need.
  • Open the dxmutmisc.cs file and comment out all of the code other than that in the Native Methods and Timer regions.
  • Now build the solution (press F6). If you did everything right the solution will now build.

 My GPU is Bigger than Yours

Before we dig into the DirectX API let's take a step back and think about what we are trying to do. To create a fast game we need to use some type of processor that allows us to compute the actual picture that will be shown on the monitor. Since none of us have 3D monitors that image will be 2D. So we need to do some math to compute each and every frame by converting 3D models into 2D images. If we used the CPU of the computer for all of these calculations our game would run slower, since we also have to use the same CPU to compute the AI, check for input, and oh by the way, we still need to run the operating system and all the background processes. If we can pass the computation of the graphics portions to a separate processor we can speed things up.
Modern graphics cards have their own processor called a Graphics Processing Unit or GPU. These GPUs are specialized processors optimized to do the type of calculations we need. In addition each graphics card also has its own memory, in effect making it a separate computer inside our computer. This means that regardless how big and fast your basic computer is, graphics speed is dependent more on the GPU and video memory than anything else.

Adapters and Devices

Most graphics cards allow only one monitor to be connected at a time, but some provide support for multiple monitors. You could also have more than one graphics card in your computer at a time. Regardless of your setup, each graphics card has an Adapter. You can think of this as the physical video card in your computer.  The adapters have “names” in the computer sense, with the first, or default adapter, being “named” 0, the second adapter 1 and so on. In DirectX you do not directly interact with the adapter. Instead you connect to an adapter using a Device.
A device represents a connection to a specific adapter; each adapter can have multiple devices associated with it. DirectX supports three types of devices: Hardware, References and Software. We will use the Hardware type for our game as it provides the speed we need to run our game.
Now its time to create the device we are going to use for our game. Add the following code to the constructor of the GameEngine form after the existing code. We are using the constructor because we can guarantee that any code in it will be run before anything else. This ensures that we always have a valid device object to reference later on.
  • Add the following code to the GameEngine constructor immediately following the this.SetStyle statement
Visual C#
// Get the ordinal for  the default adapter    
int adapterOrdinal =  Manager.Adapters.Default.Adapter;     
// Get our device capabilities so  we can check them to set up the      
// CreateFlags    
Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);
CreateFlags createFlags;     
// Check the capabilities of the  graphcis card is capable of    
// performing the vertex-processing  operations    
// The HardwareVertexProcessing  choice is the best    
if  (caps.DeviceCaps.SupportsHardwareTransformAndLight)    
{
    createFlags = CreateFlags.HardwareVertexProcessing;
}    
else
{
    createFlags = CreateFlags.SoftwareVertexProcessing;
}
// If the graphics card supports  vertex processing check if the device
// can do rasterization, matrix  transformations, and lighting and     
//shading operations    
// This combination provides the  fastest game experience    
if  (caps.DeviceCaps.SupportsPureDevice && createFlags ==  
    CreateFlags.HardwareVertexProcessing)
{
    createFlags |= CreateFlags.PureDevice;
}     

// Set up the PresentParameters  which determine how the device behaves    
PresentParameters presentParams = new PresentParameters();    
presentParams.SwapEffect = SwapEffect.Discard;

// Make sure we are in windowed  mode when we are debugging    
#if DEBUG       
     presentParams.Windowed = true;
#endif
// Now create the device    
device = new  Device(adapterOrdinal,DeviceType.Hardware,this, 
     createFlags,presentParams);
Visual Basic
' Get the ordinal for the  default adapter
Dim adapterOrdinal As Integer = Manager.Adapters.Default.Adapter
' Get our device capabilities so we  can check them to set up the
 ' CreateFlags    
Dim caps As Caps =  Manager.GetDeviceCaps(adapterOrdinal,
    DeviceType.Hardware)    
Dim createFlags As CreateFlags       

 ' Check the capabilities of the  graphcis card is capable of   
 ' performing the vertex-processing  operations   
 ' The HardwareVertexProcessing  choice is the best    
If  caps.DeviceCaps.SupportsHardwareTransformAndLight Then
    createFlags = createFlags.HardwareVertexProcessing    
Else
    createFlags = createFlags.SoftwareVertexProcessing
End If
' If the graphics card supports  vertex processing check if the device
' can    
' do rasterization, matrix  transformations, and lighting and shading
 ' operations   
 ' This combination provides the  fastest game experience   
If  caps.DeviceCaps.SupportsPureDevice AndAlso createFlags = _
        createFlags.HardwareVertexProcessing Then
    createFlags = createFlags Or createFlags.PureDevice
End If
' Set up the PresentParameters  which determine how the device behaves
Dim presentParams As New PresentParameters()
presentParams.SwapEffect = SwapEffect.Discard

' Make sure we are in windowed mode  when we are debugging
#If DEBUG Then
    presentParams.Windowed = True
#End If
' Now create the device
device = New  Device(adapterOrdinal, DeviceType.Hardware, Me, _
    createFlags, presentParams)
  • At the end of the form, right after the declaration of the deltaTime variable, add the following code:
Visual C#
private Device device;
Visual Basic
Private device As Device
This is a lot of code just to get a Device configured, but this approach to setting up the device is the safest way of ensuring that we maximize the performance of our game based on the graphics card's hardware. The easiest way to understand this block of code is to break it into four distinct pieces.
  1. The first line of code simply gets the name of the default adapter which is usually 0. Instead of betting that it is zero it is safer to use the Manager class to get the name of the default adapter. This way things don't go south if for some reason the default adapter name is really 2.
  2. The next section of code is used to determine the settings of the CreateFlags enumeration that we pass to the Device constructor and which governs the behavior of the device after creation. Again we use the Manager to get a listing of the capabilities (called Caps for short) for the default adapter. We then use this listing of capabilities to determine whether to perform vertex processing in hardware (which is faster) or in software (which is slower but guaranteed to always work). This is actually a misnomer, as the SoftwareVertexProcessing really means that we use the CPU while the HardwareVertexProcessing uses the GPU. We then perform another check to see if our adapter can support a pure device, meaning the graphics card can do rasterization, matrix transformations and lighting and shading calculations. If the device can and the previous check determined that we can use hardware vertex processing we add the PureDevice setting to the CreateFlags enumeration. The combination of HardwareVertexProcessing and PureDevice provides us with the best possible performance, so we want to use it if we can.
  3. The final parameter required to create the Device is the PresentParameters object. This object determines how the device presents its data to the screen, hence the name. First we set the SwapEffect enumeration, which determines how the buffer and the device relate to each other. By selecting the Discard option we are choosing to simply discard the back buffer and write directly to the front buffer. Inside the If statement we determine if the application is running in Debug mode. If we are in debug mode we do not want to run in full screen mode, which is the default, because it makes debugging very difficult. Using this method to determine the configuration is better than hard coding it and then forgetting to switch it when releasing the game.
  4. The final step is to actually create the device. We pass in the ordinal of the default adapter, the window we want to bind the device to, the device type, and then pass in the CreateFlags and PresentParameters objects we created previously.
The net effect of all of this code is that we have a valid device we can use to draw to the screen with. To actually use the device we need to add two lines of code inside the render loop.
  • Add the following code in the OnPaint method immediately following the FrameworkTimer.Start() statement.
Visual C#
device.Clear(ClearFlags.Target, Color.DarkBlue, 1.0f, 0);
device.Present();
Visual Basic
device.Clear(ClearFlags.Target, Color.DarkBlue, 1.0F, 0)
device.Present()
The first line clears the window with the color indicated in the second parameter (you can use any of the predefined windows colors in the Color enumeration). The last two parameters of the Clear method describe the z-depth and stencil values and are not important at this time.
The Present method of the device causes the device to display the contents of the back buffer to the screen. The screen is also called the front buffer and how these buffers interact is the determined by the SwapEffect enumeration you set earlier.
Now we run the solution and viola – we have a blue screen. While this is not very impressive and it seems like a lot of code to get a simple blue screen we have now successfully integrated DirectX into our GameEngine.

3D Graphics Terminology

Before we continue and render the terrain and units we need to step back for a minute and cover some of the principles and definitions used in three-dimensional graphics programming. We are not doing this so we can sound smart among our friends, but because these principles and terms provide the foundation for programming with three-dimensional graphics.
The game I am currently playing is called Brothers in Arms: Road to Hill 30 (www.brothersinarmsgame.com). It is a first person shooter game set in Normandy France in 1942. All of the terrain, buildings, roads, rivers, etc. in this game are exact replicas of the original terrain in Normandy in 1942. The game creators used aerial photographs and maps to recreate the terrain and traveled to France to survey the terrain themselves. They used this information to recreate the original terrain for the game. When we need to describe where in the world something is located, such as the Space Needle in Seattle, we commonly use a coordinate system called the geographic coordinate system (http://en.wikipedia.org/wiki/Geographic_coordinate_system). In this system we express each point as a unique Latitude/Longitude pair (the Space Needle is located at: Lat: 47.62117, Long: -122.34923).
When the creators of the game transferred the terrain to the computer they could not just provide the Latitude/Longitude coordinates to DirectX because the computer has no idea how to represent geographic coordinates. To be able to place objects into a three-dimensional world we have to come up with a coordinate system the computer does understand and transform the coordinates from one system to another. The coordinate system used in DirectX is the left-handed Cartesian coordinate system.
Cartesian Coordinate system
To properly place objects into a three-dimensional world we need to know where to place them and how to define each point unambiguously. We accomplish this using the Cartesian coordinate system. This Cartesian coordinate system is comprised of three axes at right angles to each other with the point of intersection being called the origin(the one you are probably more familiar with is the two-dimensional version with only the x and y axes used in High School geometry). Two additional properties are needed to fully define this three-dimensional coordinate system: handedness and orientation.
Handedness
To make life just a little bit more challenging, there are actually two ways to represent a three-dimensional Cartesian coordinate system: Left-handed and right-handed. If you follow this link (http://en.wikipedia.org/wiki/Cartesian_coordinate_system) you can find out more behind the reason for this, but the main thing to remember is that DirectX uses the left-handed coordinate system. The net effect of using the left-handed coordinate system is that the greater the Z value the greater the distance, while in the right-handed system the values get smaller as the distance increases. You need to know the handedness of the coordinate space to compare two objects using their Z values and to know which one is further away from you.
Orientation
The only other thing of note about the three-dimensional Cartesian coordinate system is that it can have different orientations depending on how the z-axis is drawn. If it is drawn vertically as pictured below on the left then it is called the world coordinates orientation if it is drawn as on the right it is called the local or body coordinates orientation.
Regardless of the orientation you use to reference your points to, the coordinates must be transformed to screen coordinates (two-dimensional screen space) in order to be drawn on the screen.
Figure 2: 3D Cartesian Coordinate systems
Vector
Each point in the three-dimensional coordinate system is defined by three values, the X, Y and Z values. To make life a little easier DirectX provides us with a structure called a Vector3 that lets us store these coordinates. This is purely for our convenience though, as a vector is defined as an object that denotes both direction and velocity and not a location. The methods of the vector class are used towards that end, but for now it's a convenient structure to store the three values in. Depending on your need can also use the Vector2 or Vector4 structures.
Vertex
In the code we added to connect to our device you may have noticed that we needed to determine if we should do vertex processing in software or hardware. So what is vertex processing and what is a vertex? A vertex is a point that is part of a three-dimensional object. A vertex consists of a vector and additional information concerning texture mapping.
Texture
A texture is simply a 2D bitmap that is applied to a 3D object to provide it with some type of look (texture) such as grass, concrete etc.
Mesh
We are not going to cover using meshes in this article; the definition of a mesh is tied to that of a vertex, so let's get it out of the way now. A mesh is the data that describes a three-dimensional shape. This data includes the list of vertexes for the shape as well as information describing how the vertexes are connected and information concerning the textures that cover them.
As you can see, a Mesh is made up of vertices, which in turn contain a vector. Each level simply adds some information concerning how the separate pieces are related to each other. From now on when you hear vector think point, when you hear vertex think point and data, and when you hear mesh think many points and more data.
Triangles
Now that you know that a vertex is really just a point in 3D space, we need to cover how the points are combined to form the objects. Every object in DirectX is composed of one or more triangles. While this may seem awkward at first, it is actually possible to represent any 2D or 3D shape using triangles. The reason for this is simple: triangles are the simplest polygon that is coplanar (all the points of the triangle are on the same plane). This simplifies the math needed to perform all calculations.
While all of this seems to be a lot of new terms to learn, it is important to understand these basic ideas before moving on to matrices. We are going to cover them and the principle behind a camera (in the DirectX sense of the word) and transformations in the next article. We are also going to explain what meshes are and how the texture mapping information of a vertex is used.
A Frame Rate Counter
As I mentioned in the first article, we are going to add a frame rate counter to our game. Knowing the frame rate of your game is important because it determines the speed of your game. Additionally, knowing what the frame rate is now before we add any code to the render loop will show us how each addition affects the speed of the game.
  • Add a new class to the project and name it FrameRate.
  • Add the following code inside the class declaration.
Visual C#
public static int CalculateFrameRate()
{
    if  (System.Environment.TickCount - lastTick >= 1000)
    {
        lastFrameRate = frameRate;
        frameRate = 0;
        lastTick = System.Environment.TickCount;
    }
    frameRate++;
    return lastFrameRate;
}

private static int lastTick;
private static int lastFrameRate;
private static int frameRate;
Visual Basic
Public Shared Function CalculateFrameRate() As Integer
    If  System.Environment.TickCount - lastTick >= 1000 Then
        lastFrameRate = frameRate
        frameRate = 0
        lastTick = System.Environment.TickCount
    End If     frameRate += 1
    Return lastFrameRate
End Function 'CalculateFrameRate 

Private Shared lastTick As Integer
Private Shared lastFrameRate As Integer
Private Shared frameRate As Integer
  • In the GameEngine OnPaint method add the following line of code immediately after the assignment of deltaTime.
Visual C#
this.Text = string.Format("The framerate is {0}", 
    FrameRate.CalculateFrameRate());
Visual Basic

Me.Text = String.Format("The framerate is {0}", _
    FrameRate.CalculateFrameRate())
This frame rate counter uses the TickCount property of the System class which is a wrapper to the GetTickCount method of the WIN32 API and has a precision of approximately 15 milliseconds. While the FrameworkTimer class is more accurate, this precision is good enough to compute the frame rate.

Code Housekeeping

Before we finish up there are two code changes I made to the original code. First I removed the Application.EnablRTLMirroring() statement from the Program class. This method has been deprecated in the Beta2 version of the .NET Framework. The other change I made is to wrap the creation of the GameEngine class into a using statement. This ensures that regardless of what happens in the GameEngine class it will always be properly disposed when we close the application.

Summary

At this point you might be wondering if we are ever going to start working on our game. One of the challenges with learning game development is that you have to create a good foundation at the beginning so the more advanced ideas make sense later on. Once you understand the 3D graphics terms and principles you are free to concentrate on the game creation.
In this article we introduced DirectX which is the API we are going to use for 3D graphics, input device control and sound in BattleTank 2005. Then we discussed what a GPU is and why it is so important for today's games. We also covered what an Adapter and Device are and how to configure them in DirectX. Then we entered the world of terms and definitions that we need to understand moving forward. Finally we added a frame rate counter to the game to track game performance.
In the next article we'll cover matrices and transforms, how to place a camera into the 3D world and what clipping and culling are. In that article we are also going to add the landscape for our game.
I hope that the first foray into the world of DirectX has not discouraged you too much. Because some definitions and principles are a little hard to understand just by reading them I suggest you write some code and play with the various settings to see what happens. The DirectX SDK also includes a great number of samples and tutorials that cover this same subject matter and let you experiment with the settings.

0 comments:

Post a Comment