Creating Your Device
For the rest of the development of this game, you should still
be working with the project you started in the last chapter. One of the first
things you want to do now is set up the project to actually work with the sample
framework. In the Main method you created in the last chapter, add the
code in Listing 4.1 immediately
following the creation of the GameEngine class.
Listing 4.1. Hooking Events and Callbacks
// Set the callback functions. These functions allow the sample framework // to notify the application about device changes, user input, and Windows // messages. The callbacks are optional so you need only set callbacks for // events you're interested in. However, if you don't handle the device // reset/lost callbacks, then the sample framework won't be able to reset // your device since the application must first release all device resources // before resetting. Likewise, if you don't handle the device created/destroyed // callbacks, then the sample framework won't be able to re-create your device // resources. sampleFramework.Disposing += new EventHandler(blockersEngine.OnDestroyDevice); sampleFramework.DeviceLost += new EventHandler(blockersEngine.OnLostDevice); sampleFramework.DeviceCreated += new DeviceEventHandler(blockersEngine.OnCreateDevice); sampleFramework.DeviceReset += new DeviceEventHandler(blockersEngine.OnResetDevice); sampleFramework.SetKeyboardCallback(new KeyboardCallback( blockersEngine.OnKeyEvent)); // Catch mouse move events sampleFramework.IsNotifiedOnMouseMove = true; sampleFramework.SetMouseCallback(new MouseCallback(blockersEngine.OnMouseEvent)); sampleFramework.SetCallbackInterface(blockersEngine);
A lot of things are happening in this small section of code. A
total of four events are being hooked to let you know when the rendering device
has been created, lost, reset, and destroyed. You'll need to add the
implementations for these handlers in a moment from Listing 4.2. After that, you'll notice that you're
hooking two callbacks the sample framework has for user input, namely the
keyboard and mouse (Listing 4.3).
Finally, you call the SetCallbackInterface method passing in the game
engine instance; however, you might notice that the instance doesn't implement
the correct interface. You'll need to fix that as well.
Listing 4.2. Framework Event Handlers
/// <summary>
/// This event will be fired immediately after the Direct3D device has been
/// created, which will happen during application initialization and
/// windowed/full screen toggles. This is the best location to create
/// Pool.Managed resources since these resources need to be reloaded whenever
/// the device is destroyed. Resources created here should be released
/// in the Disposing event.
/// </summary>
private void OnCreateDevice(object sender, DeviceEventArgs e)
{
SurfaceDescription desc = e.BackBufferDescription;
}
/// <summary>
/// This event will be fired immediately after the Direct3D device has been
/// reset, which will happen after a lost device scenario. This is the best
/// location to create Pool.Default resources since these resources need to
/// be reloaded whenever the device is lost. Resources created here should
/// be released in the OnLostDevice event.
/// </summary>
private void OnResetDevice(object sender, DeviceEventArgs e)
{
SurfaceDescription desc = e.BackBufferDescription;
}
/// <summary>
/// This event function will be called fired after the Direct3D device has
/// entered a lost state and before Device.Reset() is called. Resources created
/// in the OnResetDevice callback should be released here, which generally
/// includes all Pool.Default resources. See the "Lost Devices" section of the
/// documentation for information about lost devices.
/// </summary>
private void OnLostDevice(object sender, EventArgs e)
{
}
/// <summary>
/// This callback function will be called immediately after the Direct3D device
/// has been destroyed, which generally happens as a result of application
/// termination or windowed/full screen toggles. Resources created in the
/// OnCreateDevice callback should be released here, which generally includes
/// all Pool.Managed resources.
/// </summary>
private void OnDestroyDevice(object sender, EventArgs e)
{
}
Listing 4.3. User Input Handlers
/// <summary>Hook the mouse events</summary>
private void OnMouseEvent(bool leftDown, bool rightDown, bool middleDown,
bool side1Down, bool side2Down, int wheel, int x, int y)
{
}
/// <summary>Handle keyboard strokes</summary>
private void OnKeyEvent(System.Windows.Forms.Keys key, bool keyDown,
bool altDown)
{
}
Now, the
SetCallbackInterface method you called earlier expects a variable of
type IFrameworkCallback, and you passed in the game engine class, which
does not implement this type. You can fix this easily by changing the game
engine class declaration:
public class GameEngine : IFrameworkCallback, IDeviceCreation
Of course, now you need to add the implementation of the two
methods this interface defines (Listing
4.4).
Listing 4.4. Framework Callback Interface
/// <summary>
/// This callback function will be called once at the beginning of every frame.
/// This is the best location for your application to handle updates to the
/// scene but is not intended to contain actual rendering calls, which should
/// instead be placed in the OnFrameRender callback.
/// </summary>
public void OnFrameMove(Device device, double appTime, float elapsedTime)
{
}
/// <summary>
/// This callback function will be called at the end of every frame to perform
/// all the rendering calls for the scene, and it will also be called if the
/// window needs to be repainted. After this function has returned, the sample
/// framework will call Device.Present to display the contents of the next
/// buffer in the swap chain
/// </summary>
public void OnFrameRender(Device device, double appTime, float elapsedTime)
{
}
With that boilerplate code out of
the way, you're ready to start doing something interesting now. First, you
should tell the sample framework that you're ready to render your application
and to start the game engine. Go back to the Main method, and add the
code in Listing 4.5 immediately after
your SetCallbackInterface call.
Listing 4.5. Starting the Game
try
{
#if (!DEBUG)
// In retail mode, force the app to fullscreen mode
sampleFramework.IsOverridingFullScreen = true;
#endif
// Show the cursor and clip it when in full screen
sampleFramework.SetCursorSettings(true, true);
// Initialize the sample framework and create the desired window and
// Direct3D device for the application. Calling each of these functions
// is optional, but they allow you to set several options that control
// the behavior of the sampleFramework.
sampleFramework.Initialize( false, false, true );
sampleFramework.CreateWindow("Blockers - The Game");
sampleFramework.CreateDevice( 0, true, Framework.DefaultSizeWidth,
Framework.DefaultSizeHeight, blockersEngine);
// Pass control to the sample framework for handling the message pump and
// dispatching render calls. The sample framework will call your FrameMove
// and FrameRender callback when there is idle time between handling
// window messages.
sampleFramework.MainLoop();
}
#if(DEBUG)
catch (Exception e)
{
// In debug mode show this error (maybe - depending on settings)
sampleFramework.DisplayErrorMessage(e);
#else
catch
{
// In release mode fail silently
#endif
// Ignore any exceptions here, they would have been handled by other areas
return (sampleFramework.ExitCode == 0) ? 1 : sampleFramework.ExitCode;
// Return an error code here
}
What is going on here? The first thing
to notice is that the entire code section is wrapped in a
TRy/catch block, and the catch block varies depending
on whether you're compiling in debug or release mode. In debug mode, any errors
are displayed, and then the application exits. In release mode, all errors are
ignored, and the application exits. The first thing that happens in the block is
that the sample framework is told to render in full-screen mode if you are not
in debug mode. This step ensures that while you're debugging, the game runs in a
window, allowing easy debugging, but when it's complete, it runs in full-screen
mode, as most games do.
The next call might seem a little strange, but the basic goal
of the call is to determine the behavior of the cursor while in full-screen
mode. The first parameter determines whether the cursor is displayed in
full-screen mode, and the second determines whether the cursor should be
clipped. Clipping the cursor simply ensures that the cursor cannot leave the
area of the game being rendered. In a single monitor scenario, it isn't a big
deal either way, but in a multimon scenario, you wouldn't want the user to move
the cursor to the other monitor where you weren't rendering.
The Initialize call sets up some internal variables
for the sample framework. The three parameters to the call determine whether the
command line should be parsed (no), whether the default hotkeys should be
handled (no again), and whether message boxes should be shown (yes). You don't
want the game to parse the command line or handle the default hotkeys because
they are normally reserved for samples that ship with the DirectX SDK. The
CreateWindow call is relatively self-explanatory; it creates the window
where the rendering occurs with the title listed as the parameter.
Finally, the CreateDevice call is created. Notice that
this is where you pass in the instance of the game engine class for the
IDeviceCreation interface. Before the device is created, every
combination is enumerated on your system, and the IsDeviceAcceptable
method that you wrote in the last chapter is called to determine whether the
device is acceptable to you. After the list is created, the
ModifyDevice method is called to allow you to modify any settings right
before the device is created. The constructor that you use for the device
creation looks like this:
public Device ( System.Int32 adapter , Microsoft.DirectX.Direct3D.DeviceType deviceType , System.IntPtr renderWindowHandle , Microsoft.DirectX.Direct3D.CreateFlags behaviorFlags , params Microsoft.DirectX.Direct3D.PresentParameters[] presentationParameters )
The adapter parameter
is the ordinal of the adapter you enumerated earlier. In the majority of cases,
it is the default parameter, which is 0. The deviceType parameter can be one of the
following values:
-
DeviceType.Reference A device using the reference rasterizer. The reference rasterizer performs all calculations for rendering in software mode. Although this device type supports every feature of Direct3D, it does so very, very slowly. You should also note that the reference rasterizer only ships with the DirectX SDK. End users will most likely not have this rasterizer installed.
Obviously, because you are dealing with graphics, you need
someplace to actually show the rendered image. In this overload of the device
constructor, the sample framework uses the window it has created, but you could
pass in any control from the System.Windows.Forms library that comes
with the .NET framework. Although it won't be used in this game, you could use
any valid control, such as a picture box.
The next parameter is the behavior of the device. You might
remember from the last chapter when you determined the best set of flags,
including whether the device supported transforming and lighting in hardware and
whether the device can be pure. You find the possible value of this parameter by
combining the values in Table 4.1.
|
Used for multimon-capable adapters. Specifies that a single
device will control each of the adapters on the system.
| |
|
Tells Direct3D to handle the resource management rather than
the driver. In most cases, you will not want to specify this flag.
| |
|
Tells Direct3D that a combination of hardware and software
vertex processing will be used. This flag cannot be combined with either the
software or hardware vertex processing flags.
| |
|
Tells Direct3D that all vertex processing will occur in
hardware. This flag cannot be combined with the software or mixed vertex
processing flags.
| |
|
Tells Direct3D that all vertex processing will occur in
software. This flag cannot be combined with the hardware or mixed vertex
processing flags.
| |
|
Specifies that this device will be a pure device.
| |
|
Specifies that this device may be accessed for more than one
thread simultaneously. Because the garbage collector runs on a separate thread,
this option is turned on by default in Managed DirectX. Note that there is a
slight performance penalty for using this flag.
| |
For this game, you should stick with either software or
hardware vertex processing only, which is what the enumeration code picked
during the last chapter. The final parameter of the device constructor is a
parameter array of the PresentParameters class. You only need more than
one of these objects if you are using the
CreateFlags.AdapterGroupDevice flag mentioned earlier, and then you
need one for each adapter in the group.
Construction Cue
|
Before the device is actually created by the framework, you
might notice that it turns off the event-handling mechanism for the Managed
Direct3D libraries.
|
Before you go on, it's important to understand why turning off
the event-handling model is a good idea. The default implementation of the
Managed DirectX classes hooks certain events on the Direct3D device for every
resource that is created. At a minimum, each resource (such as textures or a
vertex buffer) hooks the Disposing event and more likely also hooks
other events, such as the DeviceLost and DeviceReset events.
This step happens for maintaining object lifetimes. Why wouldn't you want this
great benefit in your application?
The main reason is that this benefit comes at a cost, and that
cost could potentially be quite large. To understand this point, you must first
have a sense of what is going on behind the scenes. You can look at this simple
case, written here in pseudo-code:
SomeResource res = new SomeResource(device); device.Render(res);
As you can see, this code looks harmless enough. You simply
create a resource and render it. The object is obviously never used again, so
the garbage collector should be smart enough to clean up the object. This
thought is common, but this thought is incorrect. When the new resource is
created, it hooks a minimum of one event on the device to allow it to clean up
correctly. This hooking of the event is a double-edged sword.
One, there is an allocation of the EventHandler class
when doing the actual hook. Granted, the allocation is small, but as you will
see in a moment, even small allocations can add up quickly. Second, after the
event is hooked, the resource has a hard link to the device. In the eyes of the
garbage collector, this object is still in use and will remain in use for the
lifetime of the device or until the events are unhooked. In the pseudo-code
earlier, imagine if this code were run once every frame to render something.
Imagine that your game was pushing around a thousand frames per second, and
imagine it was running for two minutes. You've just created 120,000 objects that
will not be collected while the device is around, plus another 120,000 event
handlers. All these created objects can cause memory consumption to rise
quickly, as well as extra garbage collections to be performed, which can hurt
performance. If your resources are in video memory, you can be assured you will
run out quickly.
This scenario doesn't even consider what happens when the
device is finally disposed. In the preceding example, when the device is
disposed, it fires the Disposing event, which has been hooked by
120,000 listeners. You can imagine that this cascading list of event handlers
which must be called will take some time, and you'd be correct. It could take
literally minutes and cause people to think the application has locked up.
You only want to use the event handling that is built in to
Managed Direct3D in the simplest of cases. At any point where you care about
memory consumption or performance (for example, in games), you want to avoid
this process, as you've done in this example (or at the very least ensure that
you are disposing of objects properly). The sample framework gives you the
opportunity to do so.
You'll notice that last method you called in the Main
method is the MainLoop method from the sample framework. This point is
where you are telling the sample framework that you're ready to run your
application now. This method will run and process Windows messages as well as
call your rendering methods constantly until the application exits. This method
will not return until it happens. From now on, all interaction with the sample
framework comes from the events it fires and the callbacks it calls into.
A few times throughout the course of the code, you might need
to know the name of the game. You can simply add this constant to your game
engine class:
public const string GameName = "Blockers";
No comments:
Post a Comment