作者:Geoff Schwab 来源:Microsoft 时间:
Applies to:
Microsoft® .NET Compact Framework 1.0
Microsoft Visual Studio® .NET 2003
Microsoft eMbedded Visual C++® 3.0
Summary: This sample expands upon the GAPI series of articles by creating a playable game demo. (13 printed pages)
Download G-Man Game Sample
Evaluate or Purchase GapiDraw
Download GAPI
Download eMbedded Visual Tools 2002 Edition
Introduction
The Ultimage GMan game demo is the culmination of my series of GAPI wrapper articles. I first started this series as a challenge to myself to develop a graphics library that ran under managed code and both outperformed GDI and implemented specific functionality (alpha blending, destination key transparency, etc.) not available through GDI. Having achieved this goal, I decided to take it even further by developing a level of a playable game demo. To do this, I elicited the help of two extremely talented artists: Douglas Albright III, who provided the art, and Jason Ilano, who provided the animation. Figure 1 shows an introduction screen to the game.
Figure 1. Introduction screenshot from Ultimate GMan.
Due to the size and complexity of the project, I have chosen to keep the article clean of sample code and discuss the components and technical details instead. For a more detailed technical description of gaming in the .NET Compact Framework, see the MSDN article titled "Gaming with the .NET Compact Framework: A Simple Example".
Ultimate GMan consists of a layered, parallax scrolling background with AI controlled objects (planes and evil mechanical rabbits) that attack the player. The player has the ability to duck and crawl to avoid the attacks of these enemies and when the best defense is a good offense, the player can launch fireballs. The trajectory of these fireballs is controlled by the length of time that the player holds down the fire button. Figure 2 shows the game in action.
Figure 2. Screenshot of Ultimate GMan gameplay.
GXInput
The GXInput library provides the application with the ability to track button presses. The functionality of GXInput is described in detail in the following article:
Dancing Particles: Adding Points, Lines, Fonts, and Input to the Managed Graphics Library
GXSound
GXSound is a new library in this series of articles. This sound library provides .wav playback capability to the game and is based on the sample found in the P/Invoke library. That library is described in detail at the following link:
Recording and Playing Sound with the Waveform Audio Interface
The GXSound library provides a wrapper around the WaveOut functionality provided in the above sample and improves upon it by providing a means for preloading wave files and re-using the buffers of pre-loaded sounds rather than releasing the buffers when playback is completed.
The GXSound library maintains a list of sounds and provides the application with ID's for each sound that is loaded. The application, in turn, uses these ID's to perform actions on each sound. These actions consist of functions such as Play, Pause, Stop, etc.
GXGraphics
The GXGraphics library is a shell that exposes high level graphics functionality at an API level. In this demo, the GXGraphics API provides a wrapper around two other graphics API's and an implementation of a third managed code graphics library.
The two wrappers consist of the .NET Compact Framework's GDI API defined in the System.Drawing namespace, and the native commercial product GapiDraw. The managed library utilizes the Pocket PC Game API (GAPI) which provides information about the device's display properties and access to the display memory. Figure 3 represents this relationship with arrows representing the direction of function calls.
Figure 3. GXGraphics API relationship.
Regardless of which graphics engine is used to build an application, the interface to GXGraphics remains the same, thus providing great flexibility during the development process. The various implementations are controlled by compiler switches used to include and exclude the separate implementations via the preprocessor directives: USE_GXLIBS, USE_GDI, and USE_GAPIDRAW. These directives define the use of the GAPI implementation, the GDI wrapper, and the GapiDraw wrapper respectively.
For the purpose of this demo, only the minimum necessary amount of functionality is implemented, thus various features that are supported by the various underlying API's are not necessarily available in GXGraphics.
GX Libraries and GAPINet
The GXGraphics library's managed code graphics implementation is based on GAPI. This implementation is a subset of the functionality provided by the previous graphics articles leading up to this demo. For more information on the GAPI graphics library, refer to the following articles:
Dancing Rectangles: Using GAPI to Create a Managed Graphics Library
Dancing Zombies: Adding Bitmaps to the Managed Graphics Library
Dancing Particles: Adding Points, Lines, Fonts, and Input to the Managed Graphics Library
The GAPINet project is a native DLL which provides a wrapper for GAPI that is safe to be used within the confines of a managed application. This wrapper is provided because some GAPI functions return types that are not supported by the .NET Compact Framework P/Invoke marshaller.
A pre-built release version of the ARM binary of this DLL is linked into the application project. The GAPINet project is provided with the source of the sample so that it can be built for other platforms. This project is located in Code\GAPINet. Microsoft eMbedded Visual C++ is required to build this project.
Note: The binary of the GAPINet project linked to by the game application only supports ARM based processors. The project may be re-built to support others.
The GX library version of GXGraphics is built using the preprocessor directive USE_GXLIBS.
This build also requires that the GAPI binary GX.DLL be available to the application on the target device. GAPI can be downloaded from the link below. Place the DLL in the device's \Windows directory to make it available to other applications (recommended), or place it in the application's root directory.
Note: The game application project links in an ARM based version of GAPI located in Code\bin and GAPINet from its project directory. To build the game for a different platform, change these references.
The emulator does not support the GX library, however, if you are attempting to build the game for the emulator there is sometimes an unsupported x86 build of gx.dll available from the PocketFrog website.
Download (included with the solution):
Supplemental download if attempting to build GAPINet:
Microsoft eMbedded Visual Tools 2002 Edition
GapiDraw and GDWrap
GapiDraw is a commercial product offered at the link at the end of this section. A build of GXGraphics that utilizes this library is included in the sample because it provides a flexible, well-performing graphics engine and is the most widely used Pocket PC graphics library available.
Note: It is only necessary to download GapiDraw to build the GapiDraw configurations of the project. To build other configurations, simply select a configuration that does not utilize GapiDraw, such as GDI or the GX Libraries. These configurations are provided with the projects.
The GDWrap project provides a wrapper to GapiDraw that implements the functionality needed by GXGraphics. This is in no way a complete wrapper of the GapiDraw library. A pre-built release version of the ARM binary of this DLL is linked into the application project. The GDWrap project is provided with the source of the sample so that it can be built for other platforms. This project is located in Code\GDWrap.
Note: The binary of the GDWrap project linked to by the game application only supports ARM based processors. The project may be re-built to support others.
The GapiDraw library version of GXGraphics is built using the preprocessor directive USE_GAPIDRAW.
This build also requires that the GapiDraw binary GD300.DLL (at the time of this article) be available to the application on the target device. GapiDraw can be downloaded from the link below. Place the DLL in the device's \Windows directory to make it available to other applications (recommended), or place it in the application's root directory.
Note: The game application project links in an ARM based version of GapiDraw which, for legal reasons, is not included with the sample. To build the game with GapiDraw, download the GapiDraw library and change this reference.
For evaluation and licensing of GapiDraw, see the following link:
Supplemental download if attempting to build GDWrap:
eMbedded Visual Tools 2002 Edition
Technical Design
Much thought must be given to the design and architecture of the systems and tools used to develop a game. Because there are so many cross-disciplinary dependencies, it is imperative that a game be as data-driven as possible. In addition, thought must be given to developing technology for multiple platforms and projects. Whether or not you believe your code will be used on another project or ported to another platform, you should always design it as if it will be. This will ease the transition to other projects and provide a more easily maintainable codebase that can be reused and updated instead of scrapped and forgotten.
The GXGraphics library is an example of a system designed to be cross platform. It provides a consistent interface that is utilized by the game, while the underlying implementation varies depending upon which low-level graphics library is used. These libraries could be implemented on various platforms and because the top-level interface stays the same, the game code is not affected. This particular implementation utilizes #ifdef statements which is not particularly eloquent but is the only method I am familiar with for developing this type of architecture in managed C#. In C++, my strongest language, I would have implemented these as different source implementations of a class definition. The class header would have an associated common source file, and one for each platform that contains the platform specific implementations. The latter file(s) would only be linked into the projects that target their corresponding platform.
In order to keep the game as data-driven as possible I chose to implement the data as XML DataSets. In a commercial gaming environment, these XML data files would most likely act as intermediate data formats that would be converted in the datapipe to binary load-in-place data. This would provide great flexibility for writing editing tools and allow the data to be built separately for various platforms. For example, the data ultimately used in a build for Pocket PC may be different from that of a Smartphone build but the intermediate XML files are the same.
In this demo, the game data is basically laid out in the same manner as the code. In fact, I generally design the data format first and then build a class around it. To be truly efficient in the load process, the data should be laid out in memory just as it is in the class and loaded in place. Loading in place is a method of loading memory directly to a layout in code. To be specific, in C++, one could allocate memory for a class instance and then load a file directly into the pointer to that instance. In managed code, this could be achieved by defining the data of a class as a byte array and accessing its members with properties. This also makes it easier to serialize data formats and eases tool development. Tools can share the class definitions so that the data will always stay synchronized with the game.
The game data layout consists of three primary data holders: Levels, Intros, and UI. I must add the caveat that because this is a single level demo, there is some data that would be global to a multiple level game and should be separated from the level data.
Levels define everything that is required by the game to play a level of the game. Levels contain player information, enemy information, AI implementations, and animations. UI defines the user interface data defined by the game. Finally, Intros define an introduction sequence that is played before each level starts. In this demo, while introductions are playing, the level is being loaded on a separate thread.
Game Data: XML and DataSets
The game data in Ultimate GMan is defined by DataSets. Because the .NET Compact Framework does not support strongly typed DataSets, the implementation has the potential to contain incomplete or erroneous data. The game code, however, is relatively clean of asserts and bounds checks on the data. As mentioned earlier, there would typically be a datapipe in a game project which would be responsible for building all of the game data. It is in this datapipe that data validation should occur, as the data is converted to the native format. Checking and validating all of the data used in this sample was simply beyond the scope of this demo.
The data formats are designed to share memory whenever possible and, therefore, several XML data files contain lists that are used within the game. For example, the file animations.xml contains a list of animations that are used by the objects in worldobjects.xml. Multiple instances of world objects may use the same animation by creating an instance of an animation that is based on the animations in animations.xml.
Intros
An introductory sequence is defined by intro.xml as a sequence of screens displayed for a specified amount of time. Each of these screens is defined as a Page row within the XML DataSet. There is a corresponding Intro class which internally defines a Page class. The code only keeps the currently displayed image in memory and stores the information needed to load any page upon request.
UI
The UI data is defined in ui.xml as a list of images and some general information. The images represent UI panels and the general information defines font and text location information, as well as the player's shot charge bar parameters. This data file has a corresponding UI class which defines a UIPanel class that corresponds to the panels defined in ui.xml.
Level Construct
The level class encapsulates everything needed to actually play the game. This includes the player, world objects, animations, audio, and level information. The level itself is not terribly complex, however it maintains lists that are used by objects within the world and is responsible for acting as the top-level game object manager. In other words, the level controls updating and drawing of all objects that make up the level, even though the actual implementation of the updating and drawing is defined by the objects themselves. The level is also responsible for instigating collision detection between all world objects since it is responsible for maintaining their lists.
The level class is directly responsible for maintaining some layers which make up the parallax scrolling effect that comprises the actual game level. The level also directly defines some general game information such as the gravitational constant and display information.
In order to achieve the parallax scrolling effect, the level defines some layers. Each layer is a panorama of images that scroll at a specified rate. By putting together multiple layers with transparency that scroll at varying rates, the illusion of distance is achieved through parallax.
Layers
Layers are defined in the level.xml DataSet and by their corresponding Layer class. A layer defines a sequence of image indices. These indices correspond to a list of images maintained by the level.
The Layer class is responsible for updating and drawing a single layer of the background. Each layer is tiled, in that it is made up of several bitmaps. This implementation was chosen to increase the playable level size by providing the ability to repeat the same image within a scrolling sequence.
Each layer keeps track of the pixel location at which it is currently scrolled and displays the appropriate image sections. This location is the location of the left edge of the visible screen within the layer. The second layer (index 1) is considered to be the player's layer and the layer which defines the game's world coordinates. If the player is at world coordinate 790,200 then his center pixel is located at layer 1's pixel location 790,200. If the layer's images are 400 pixels in width then he would be near the end of the second image in the sequence.
World Objects
World objects are objects that appear within the game world. These objects can be static or animated and can be controlled by AI. World objects are defined in the worldobjects.xml DataSet and WorldObject class. World objects define their role in the world by a large number of parameters and their implementation is probably the most complicated of all game code.
World objects define several animation cycles (walk, duck, attack, etc), associated animation and AI IDs, shot information, draw mode, collidable state, and an activation distance.
Animation Cycle: An animation cycle describes information about which animation to use and how to play it for a given animation state.
Animation ID: Identifies the index into the level's animation list that defines the world object's visual presence.
AI ID: Defines the AI instance used to control the object. Static objects will set this to -1 but all other objects will use this as an index into the level's AI instance list to determine which AI type controls it.
Shot Information: The world object can define information regarding shots. If the object has an attack then this information defines the information needed to fire a shot.
Draw Mode: The world object specifies whether or not it is drawn with transparency.
Collidable State: This parameter specifies whether the object is collidable with other objects in the world. Some objects, such as a mailbox or a tree, may be added for ambience and not be part of the gameplay mechanic.
Activation Distance: This parameter defines the distance at which the object should start updating and drawing. All objects defined in the worldobjects.xml DataSet start their lives as inactive and are only activated when they come within the activation distance of the player.
Each animation cycle that is defined for a world object maintains its own Bounds class instance. This class defines the bounds of the object while that animation is playing. These bounds are then used to check collisions against other objects in the world. In this demo, bounds are simply defined by a radius and a collision is detected when the distance between two objects (from their center points) is less than the sum of their radii. If more detailed collision detection were required, objects could be made up of multiple spheres or bounding boxes. Bounding boxes slightly complicate the collision detection code and for the sake of this demo, radii work well.
When a world object fires a shot, whether it is the player or an AI controlled object, the shot is created in the world as a dynamic world object using the parameters specified by its parent's shot information. These shots keep track of their parent in order to help with collision detection since some shots may start in a position in which they are colliding with their parent. For example, the player can only collide with a shot he has fired if the shot is falling. This prevents the detection of collisions with shots he has just fired. When a dynamic world object reaches as specified distance from the player, it is removed from the world object list and disposed.
Animation, Characters, and the Player
Animations are defined by the animations.xml DataSet and the GXAnimation class which is part of the GXGraphicsLibrary project. The DataSet provides a list of animation definitions which define the basic parameters of an animation but nothing about the sequences within it. The sequences are defined by world objects as animation cycles which define the start and end frames, and the playback rate.
The level maintains a list of animations that can be instanced by objects in the world. Instancing is a memory sharing technique that allows objects to create an instance of GXAnimation that shares the data from an animation definition, including the bitmap, the most significant portion of data. An instance simply defines its set of the data that is required to create a unique playback of an animation sequence.
Characters, or enemies, in the game are defined as world objects and also share the animations defined in animation.xml. The player is a special case of a world object and the Player class is derived from the WorldObject class.
AI and the AI Handler
AI is implemented in a unique way in this sample. The abstract base AI class defines a number of types that can be derived from it. In turn, the AIHandler class is responsible for creating an instance of the proper derived class based on the type and returning that instance to the level. The level keeps an array of AI class instances which it can access without regard to the type due to the virtual nature of the update methods.
The meanings of the parameters defined by each AI definition in ai.xml vary according to the AI type. The ai.xml DataSet can define several instances of the same AI type so that objects can use different parameters with the same behaviors. For example, two aircraft can share the same AI type, meaning that their AI instance is derived from the same class, but provide different velocities and altitudes. To accomplish this, two different AI instances would be defined in ai.xml that are of the same type but have different parameters. The world objects that represent the aircraft in the level would then each use a different AI ID that referred to whichever of the two AI instances they wanted to utilize. Figure 4 shows this relationship.
Figure 4. AI Relationship.
As can be seen in Figure 4, two world objects are controlled by AI that is derived from the same type but have different parameters defining them. To map this to code, the definitions are instances of AI (polymorphed references) with different data parameters and the WorldObject instances reference those AI instances.
Test Application
The test application for the Ultimate GMan game consists of two projects (one for Pocket PC and one for Smarthphone) that maintain game data and link in some shared source files that are common to both projects.
Note: The Smarphone project does not contain signed certificates and therefore requires the user to confirm that the all DLL's and executables are accepted.
The entry point to the game code from the Form is the GameMain class. This is the class responsible for initializing, updating, and drawing the high-level interface to the game.
There are actually 12 different configurations maintained by the code solution:
Pocket PC build with GapiDraw graphics library
Pocket PC build with GX managed code graphics library
Pocket PC build with GDI graphics
Smartphone build with GapiDraw graphics library
Smartphone build with GX managed code graphics library
Smartphone build with GDI graphics
The six configurations listed above are implemented in both Release and Debug versions, thus comprising the 12 configurations. Each configuration defines one compilation constant from each of the following groups:
Debug/Release group:
DEBUG
RELEASE
Graphics engine group:
USE_GXLIBS
USE_GAPIDRAW
USE_GDI
The target platform is defined by which project is built in the configuration. Smartphone projects skip the Pocket PC project and Pocket PC configurations skip the Smartphone project.
The Smartphone and Pocket PC projects share the same source code and differ only in their target platform and data. Because the Smartphone screen resolution is 176x220 compared with 240x320 for Pocket PC, a separate set of reduced assets is maintained by the Smartphone project.
The GameMain Class
The GameMain class is the entry point to the game code. This class maintains all of the libraries used by the game, as well as executing the game loop. The game loop calls various update methods depending upon the current game mode. The game mode is defined by the current update delegate that is called to update the game. When the game is required to switch to a different update mode, e.g., going from showing the intro to the level, the update method sets a variable signaling a change request and the main loop executes a switch on this variable and sets the new update delegate. This mode switch is not handled by the various update delegates directly because there is a chance a delegate could destroy its parent and cause inconsistent behavior from within the game.
GameMain is also responsible for monitoring asynchronous file loads of data and updating the game modes when specific sets of data have finished loading. Multithreading is utilized to support loading of the intro while the splash screen is displayed, as well as loading of the level while the intro is displayed.
Conclusion
This article is intended as a high-level description of the code and process utilized to develop this game. For detailed information, refer to the comments within the code itself. All member comments are well detailed and were built with the automated XML help tags so that documentation can be generated.