Writing the Game

You can jump right into Visual Studio and get the project started. Create a new C# Windows Application project called "Dodger". The default name of the form that has been created for you is Form1. Replace each instance of Form1 with DodgerGame, which is the name of the class that the code in this chapter will represent. You'll want to add references to the three Managed DirectX assemblies you've been using in your projects thus far, and include a using statement for them in your code file. You should set this project up much like you did most of the others. You will have a private Direct3D device variable and should modify your constructor as follows:

 public DodgerGame() {     this.Size = new Size(800,600);     this.Text = "Dodger Game";     this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true); } 

This will set the window size to 800x600 (which should be plenty good enough), and establish the window title and the style, so the rendering code will work normally. Then you should modify the entry point for the application by replacing the main method with the one found in Listing 6.1.

Listing 6.1 Main Entry Point for Your Game
 static void Main() {     using (DodgerGame frm = new DodgerGame())     {         // Show our form and initialize our graphics engine         frm.Show();         frm.InitializeGraphics();         Application.Run(frm);     } } 

This should be familiar; it's essentially the same code you've used to start all of the examples thus far. You create the windows form, show it, initialize the graphics engine, and then run the form, and thus the application. Inside the InitializeGraphics function, though, is where you will start to make some changes. Add the method found in Listing 6.2 to your application.

Listing 6.2 Initializing Your Graphics Components
 /// <summary> /// We will initialize our graphics device here /// </summary> public void InitializeGraphics() {     // Set our presentation parameters     PresentParameters presentParams = new PresentParameters();     presentParams.Windowed = true;     presentParams.SwapEffect = SwapEffect.Discard;     presentParams.AutoDepthStencilFormat = DepthFormat.D16;     presentParams.EnableAutoDepthStencil = true;     // Store the default adapter     int adapterOrdinal = Manager.Adapters.Default.Adapter;     CreateFlags flags = CreateFlags.SoftwareVertexProcessing;     // Check to see if we can use a pure hardware device     Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);     // Do we support hardware vertex processing?     if (caps.DeviceCaps.SupportsHardwareTransformAndLight)         // Replace the software vertex processing         flags = CreateFlags.HardwareVertexProcessing;     // Do we support a pure device?     if (caps.DeviceCaps.SupportsPureDevice)         flags |= CreateFlags.PureDevice;     // Create our device     device = new Device(adapterOrdinal, DeviceType.Hardware, this, flags, presentParams);     // Hook the device reset event     device.DeviceReset += new EventHandler(this.OnDeviceReset);     this.OnDeviceReset(device, null); } 

At first you create the presentation parameters structure much like you've done before, ensuring that you have a depth buffer. However, what's next is new. First, you store the adapter's ordinal, which you get from the default adapter. The creation flags that will be used when creating the device are also stored, and you default to software vertex processing much like all of our examples have used thus far.

However, most modern-day graphics cards can support vertex processing on the actual graphics hardware. Why would you want to spend valuable CPU time doing something the graphics card can do much faster anyway? The easiest answer is that you don't; however, you don't currently know whether or not the adapter being used supports this feature. This brings us to the next section of code.

You will now get and store the capabilities (Caps for short) of the device before you actually create it, so you can determine the flags you want to use for creating the device. Since you will be creating a hardware device, these Caps are the only set you will store. You may remember back from Chapter 2, "Choosing the Correct Device," when we displayed the entire list of capabilities of the device, that the structure is huge and broken up into many different sections. The section of interest right now is the DeviceCaps section, which stores the Caps specific to a driver.

When you want to check to see whether a particular feature is supported, you can simply check the Boolean value that maps to this feature: If it is true, the feature is supported; otherwise, it is not. You first check to see whether hardware transform and lighting is supported on this device. If it is, you can create the device with the hardware vertex processing flag, so we assign this to our stored flags instead. You then check to see whether you can create a pure device (which can only be created if hardware vertex processing is enabled), and if you can, you do a bitwise OR of the flags, adding this feature as well. The most efficient type of device you can create is a pure hardware device, so if these options are available to you, you should use them.

You then create your device using the flags you've stored, so depending on your graphics cards capabilities, you may have a pure hardware device, or some other variation. If you remember when you were using our vertex buffers and needed to hook the created event for when the device was reset, there is a similar situation here. Whenever the device is reset, you will want to set all of the default state of the device. You should hook the device's reset event, and then call it the initial time to set the state. The event handler method can be found in Listing 6.3. Add this method to your application.

Listing 6.3 Setting Default Device State
 private void OnDeviceReset(object sender, EventArgs e) {     device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,         this.Width / this.Height, 1.0f, 1000.0f);     device.Transform.View = Matrix.LookAtLH(new Vector3(0.0f, 9.5f, 17.0f), new Vector3(),          new Vector3(0,1,0));     // Do we have enough support for lights?     if ((device.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights) &&         ((unit)device.DeviceCaps.MaxActiveLights > 1))     {         // First light         device.Lights[0].Type = LightType.Directional;         device.Lights[0].Diffuse = Color.White;         device.Lights[0].Direction = new Vector3(1, -1, -1);         device.Lights[0].Commit();         device.Lights[0].Enabled = true;         // Second light         device.Lights[1].Type = LightType.Directional;         device.Lights[1].Diffuse = Color.White;         device.Lights[1].Direction = new Vector3(-1, 1, -1);         device.Lights[1].Commit();         device.Lights[1].Enabled = true;     }     else     {         // Hmm.. no light support, let's just use         // ambient light         device.RenderState.Ambient = Color.White;     } } 

Once again, the beginning of this function is pretty similar to things you've done before. You set up the camera by setting the view and projection transforms on your device. For this game, there will be a static non-moving camera, so this will only need to be done once after each device reset (all device-specific state is lost during a reset).

Using ambient light isn't preferred since we've already seen that the ambient light isn't overly realistic, so a directional light would be better. However, you can't be sure if the device actually supports these lights. Once the device has been created, you will no longer need to use the Caps structure that was stored before, since the device maintains this information for you. If the device can support directional lights, and can support more than one of them, you will use them; otherwise, you can default to an ambient light. It may not be all that realistic, but it's better than an all black scene.

Finally, you need to override the OnPaint function to enable your rendering, much like you've done before. You don't need to do much yet; just get something rendered. Add the following function:

USING ONLY THE LIGHTS YOU NEED

Rather than the all-or-nothing approach used in the code here, you could instead perform a "layered" check of the light support. In this scenario, you would first detect if at least one light was supported, and if so, turn that light on. Then you could check if a second light was supported, and turn that one on if so. This enables you to have a fallback case (one light) even for devices that don't have the support you want (two lights). This example wouldn't look very good with only one directional light, so the layered check wasn't done. If it was, though, it would look something like this:

[View full width]

// Do we have enough support for lights? if ((device.DeviceCaps.VertexProcessingCaps graphics/ccc.gif.SupportsDirectionalLights) && ((unit)device.DeviceCaps.MaxActiveLights > 0)) { // First light device.Lights[0].Type = LightType.Directional; device.Lights[0].Diffuse = Color.White; device.Lights[0].Direction = new Vector3(1, -1, -1); device.Lights[0].Commit(); device.Lights[0].Enabled = true; if ((unit)device.DeviceCaps.MaxActiveLights > 1)) { // Second light device.Lights[1].Type = LightType. Directional; device.Lights[1].Diffuse = Color.White; device.Lights[1].Direction = new Vector3(-1, 1, -1); device.Lights[1].Commit(); device.Lights[1].Enabled = true; } }

 protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) {     device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Black, 1.0f, 0);     device.BeginScene();     device.EndScene();     device.Present();     this.Invalidate(); } 

Nothing new here, unless you consider the background color being black new or exciting. Now you can get ready to put in the first game play object, the road. The source code on the CD includes a .X file that will hold the road mesh data, so naturally we will need to declare the variables for the road mesh:

 // Game board mesh information private Mesh roadMesh = null; private Material[] roadMaterials = null; private Texture[] roadTextures = null; 

You will also use a variation of the load mesh function you wrote in the previous chapter. The major differences here are that it will be static, since it will need to be called from more than one class, and you will pass in all the material and texture information rather than relying on the class level variables that were used before. Add the method from Listing 6.4 to your code.

Listing 6.4 Generic Mesh Loading Routine
 public static Mesh LoadMesh(Device device, string file, ref Material[] meshMaterials,     ref Texture[] meshTextures) {     ExtendedMaterial[] mtrl;     // Load our mesh     Mesh tempMesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl);     // If we have any materials, store them     if ((mtrl != null) && (mtrl.Length > 0))     {         meshMaterials = new Material[mtrl.Length];         meshTextures = new Texture[mtrl.Length];         // Store each material and texture         for (int i = 0; i < mtrl.Length; i++)         {             meshMaterials[i] = mtrl[i].Material3D;             if ((mtrl[i].TextureFilename != null) && (mtrl[i].TextureFilename !=                 string.Empty))             {                 // We have a texture, try to load it                 meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" +                     mtrl[i].TextureFilename);             }         }     }     return tempMesh; } 

This function has already been discussed in depth before, so there's no need to go into it once more. You will use this function to load the road's mesh, and you should do this in the device reset event function. At the end of that function, add the following code:

 // Create our road mesh roadMesh = LoadMesh(device, @"..\..\road.x", ref roadMaterials, ref roadTextures); 

Make sure you have copied the road mesh and texture file to your source code location. This code will load our road mesh, including the textures, and store the textures, materials, and mesh. Now, you will want to render this road mesh more than once per frame; you should create a function to do the rendering. Add the following function to your code:

 private void DrawRoad(float x, float y, float z) {     device.Transform.World = Matrix.Translation(x, y, z);     for (int i = 0; i < roadMaterials.Length; i++)     {         device.Material = roadMaterials[i];         device.SetTexture(0, roadTextures[i]);         roadMesh.DrawSubset(i);     } } 

You should recognize this function, since it's remarkably similar to the one used to render the mesh before. You translate the mesh into the correct position and render each subset. The plan for rendering the road is to render two sections at a time: the section the car is currently driving on and the section immediately after that. In all actuality, the car will not be moving at all; we will instead move the road.

The reasoning behind doing it this way is two-fold. First, if the car was moved every frame, you would also need to move the camera every frame to keep up with it. That's just extra calculations you don't need. Another reason is precision: If you let the car move forward, and the player was good, eventually the car would be so far into the "world" that you could lose floating point precision, or worse overflow the variable. Since the "world" will not have any boundaries (there won't be any way to "win" the game), you keep the car in the same relative position, and move the road below it.

Naturally, you will need to add some variables for controlling the road. Add the following class level variables and constants:

 // Constant values for the locations public const float RoadLocationLeft = 2.5f; public const float RoadLocationRight = -2.5f; private const float RoadSize = 100.0f; private const float MaximumRoadSpeed = 250.0f; private const float RoadSpeedIncrement = 0.5f; // Depth locations of the two 'road' meshes we will draw private float RoadDepth0 = 0.0f; private float RoadDepth1 = -100.0f; private float RoadSpeed = 30.0f; 

The mesh being used for the road is a known commodity. It is exactly 100 units long and 10 units wide. The size constant reflects the actual length of the road, while the two location constants mark the center of the lanes on either side of the road. The last two constants are designed to control the actual gameplay (once it is implemented). The maximum road speed you want to allow the game to get to is 250 units per second, and every time you increment the speed, you will want to increment it one half of a unit.

Finally, you need to maintain the depth location of the two road sections. You should initialize the first section at zero, with the second section initialized directly at the end of the first (notice that this is the same as the road size variable). With the basic variables and constants needed to draw (and move) the road, let's add the calls to do the actual drawing. You will want to draw the road first, so immediately after your BeginScene call in your rendering function, add the two DrawRoad calls:

 // Draw the two cycling roads DrawRoad(0.0f, 0.0f, RoadDepth0); DrawRoad(0.0f, 0.0f, RoadDepth1); 

Running the application now, you can see that the road is definitely being drawn; however, the "asphalt" of the road looks extremely pixilated. The cause of this pixilation is the way Direct3D determines the color of a pixel in the rendered scene. When one texel happens to cover more than one pixel on the screen, the pixels are run through a magnify filter to compensate. When there can be multiple texels covering a single pixel, they are run through a minifying filter. The default filter for both minification and magnification is a Point filter, which simply uses the nearest texel as the color for that pixel. This is the cause of our pixilation.

Now, there are multiple different ways to filter our textures; however, the device may or may not support them. What you really want is a filter that can interpolate between the texels to give a more smooth look to the road texture. In the OnDeviceReset function, add the code found in Listing 6.5.

Listing 6.5 Implementing Texture Filtering
 // Try to set up a texture minify filter, pick anisotropic first if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyAnisotropic) {     device.SamplerState[0].MinFilter = TextureFilter.Anisotropic; } else if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyLinear) {     device.SamplerState[0].MinFilter = TextureFilter.Linear; } // Do the same thing for magnify filter if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyAnisotropic) {     device.SamplerState[0].MagFilter = TextureFilter.Anisotropic; } else if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyLinear) {     device.SamplerState[0].MagFilter = TextureFilter.Linear; } 

As you can see here, you first detect whether your device can support anisotropic filtering for minification or magnification. If it can, you use that filter for both filters. If it cannot, you next see whether the device supports a linear filter for both, and use that if it's available. If neither of these is available, you should do nothing and live with the pixilation of the road. Assuming your card supports one of these filtering methods, running the application now will show a much less-pixilated road.

Now the road is in the middle of the screen, but it's not yet moving. You will need a new method that will be used to update the game state, and do nifty things like moving the road and detecting when the car has collided with an obstacle. You should call this function as the first thing you do in your OnPaint method (before the Clear method):

 // Before this render, we should update any state OnFrameUpdate(); 

You will also need to add this method to your application. Add the method from Listing 6.6 to your code.

Listing 6.6 Per Frame Updates
 private void OnFrameUpdate() {     // First, get the elapsed time     elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime);     RoadDepth0 += (RoadSpeed * elapsedTime);     RoadDepth1 += (RoadSpeed * elapsedTime);     // Check to see if we need to cycle the road     if (RoadDepth0 > 75.0f)     {         RoadDepth0 = RoadDepth1 - 100.0f;     }     if (RoadDepth1 > 75.0f)     {         RoadDepth1 = RoadDepth0 - 100.0f;     } } 

This function will get much larger before the game is finished, but for now, all you really want it to do is move the road. Ignoring the elapsed time (which I'll get to briefly), the only thing this function currently does is move the road and then remove road sections you've already passed, and place them at the end of the current road section. When determining the amount of movement the road should have, we use the current road speed (measured in units per second), and multiply that by the amount of elapsed time (measured in seconds), to get the "fraction" of movement this frame should have. You will also need to include the reference to the elapsedTime variable in your declaration section:

 private float elapsedTime = 0.0f; 

SHOP TALK: USING REAL-TIME TO MOVE OBJECTS

Why do we need the time? For the sake of argument, let's say you decided to just increment the road position a constant amount for every frame. On your computer, it runs just perfectly, so why wouldn't it run just as well on other systems? So you try it out on your buddy's system, which happens to be quite a bit slower than yours. What's this? The road seems to be moving amazingly slow. Trying it out on a machine faster than yours shows the road moves much faster than it should as well.

The reason for this is that you are doing your calculations based on frame rate. For example, let's say on your system, you run at a near constant 60 frames per second. So when doing your calculations, you base everything off of this static frame rate. However, machines that only run at 40 frames per second, or faster ones that run at say 80 frames per second, will naturally provide different results. Since it should be a goal that your game runs identically on every system (as far as speed is concerned), using frame rate to do your calculations should always be avoided.

A better way to deal with this problem is to define your movements and calculations per some unit of time. For example, our maximum road speed constant is defined as 250 units per second. Our first goal would be to retrieve the amount of time that has passed since our last "update." The .NET Runtime has a built-in property (tick count) that can be used to determine the current tick count of the system, but this has its problems; mainly the low resolution of the timer. This property will only update approximately every 15 milliseconds, so on a system where you are running a high frame rate (above 60 frames per second), the movement will appear choppy, since the times used won't be smooth.

The DirectX SDK includes a class called DirectXTimer that uses a high-resolution timer (usually 1 millisecond) if it's available on your machine. If this timer isn't available on your machine, it will revert back to the tick count. The examples in this book will use this timer as the mechanism for timing. It already includes the code for a high-precision timer, so why should we reinvent the wheel?



Managed DirectX 9 Graphics and Game Programming, Kick Start
Managed DirectX 9 Kick Start: Graphics and Game Programming
ISBN: B003D7JUW6
EAN: N/A
Year: 2002
Pages: 180
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net