.GAME FRAMEWORK

开始用.NET构建我们梦想中的游戏

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Introduction

This article presents a reusable framework for the development of Pocket PC games.

Among its main features are:

  • 2D graphics
  • Complete set of GUI widgets and event-based controls
  • Optional client/server network architecture
  • Can be used on Pocket PC's as well as desktop PC's

To limit the scope of the article, I will not cover the client/server architecture, although it is fully available in the framework.
Since this is only a framework and not an actual game I will not discuss any game-specific logic either.

Background

No matter which game development message board you visit, you will always find it filled with people who think they are going to create the next big hit. However you will also find a lot of people saying you should start out small and write easy & simple games, like pong and tetris. Although it might seem useless to do this, it will teach you an important lesson:
Virtually all games, no matter how simple or complex have the same basic requirements. They all need to draw bitmaps to the screen, they need a GUI (windows, buttons, labels, ...) and some of them need a network connection to allow multiplayer capabilities.

This article will describe a reusable framework that provides all of this functionality, so you can concentrate on implementing the actual game, instead of the supporting infrastructure.

Framework components

The framework consists of 4 components:

1. TLM.GameFramework.Common

Here we define various interfaces and base classes to be used by the client, the server and the GUI.
It is important to know that this framework can be used on Pocket PC's as well as regular desktop PC's. This means we need to create an abstraction layer for the graphics, sound, input and network code. A desktop game will obviously use DirectDraw for its graphics while the same game would use GapiDraw on the Pocket PC. Our abstraction layer will ensure that we don't need to change our code if we want to run it on either platform.

Even if you're not interested in cross-platform compatibility, the abstraction layer still proofs useful. For example, instead of using GapiDraw, which is a commercial component, you might want to use GDI if you don't want to shell out the cash.

The following interfaces and abstract classes are defined:

  • VideoEngine: load images into the memory, draw images on the screen, draw scaled images, draw translucent images
    • Before you can render an image, you first have to load it into memory using AddImage(string filename), which will return a so-called surface identifier.
      To render the image, use DrawImage(int surfaceId, int destinationSurface,int x, int y, bool useKey). surfaceId is the surface identifier you want to render. destionationSurface is the surface you are rendering to. If you simply want to render to the screen use "(int) VideoSurface.BackBuffer" as destionation surface. x and y define where the surface should be renderd on the destination surface. Finally, useKey should be set when working with images containing a transparancy color key.
  • IAudioEngine: play sound files, adjust the volume
    • To play a sound use PlaySound(string filename).
  • IInputEngine: intercepts stylus clicks as well as keyboard input (an in-game on-screen keyboard is available in the framework)
  • INetworkClientEngine: client-side network component
  • INetworkServerEngine: server-side network component

For more information please refer to the included sample game as it demonstrates the use of all of the above classes.

2. TLM.GameFramework.GUI

The framework provides a complete event-based GUI system. It currently features the following widgets:

  • Windows (modal and regular)
  • Labels
  • Buttons
  • Checkboxes
  • Textboxes
  • Pictures

3. TLM.GameFramework.Client

Client

Games typically run in a continous loop, called a game loop. During this loop the game will process input, simulate the game world and render it to the screen. This keeps on going untill the game is shut down.

In code the game loop looks like this:

while(client.IsRunning)
{
    client.Think();
    client.ProcessInput();
    client.Render();
}

States

Games typically are state-based applications. For exampe: when you first start a game and a menu is shown, that is defined and controlled by a "Menu" state. It is only when the player starts the game that the client changes its state to a "game" state.

This is done because every state reacts differently to user input, has a different gameloop and renders different things to the screen.

To create your own state, you have to subclass the frameworks abstract State class. In a state object's lifetime there are 4 important moments:

  • Constructor
    • When a state object is created, it's constructor code is obviously executed. During this phase all one-time code (like initialization) is done.
  • WindUp()
    • When the client changes from one state to another, the WindUp() function of the new state is called.
  • Think()
    • The Think() method on defined on the Client class will actually call the  Think() method of currently active state.
  • WindDown()
    • Before the client changes to a new state, it will call the WindDown() function of the previous state.

Lastly, each State class must implement the following methods:

  • RenderState()
    • The Render() method on defined on the Client class will actually call the Render() method of currently active state. After the state has rendered itself, the client will render other things on top of it (for example, the frames-per-second counter)
  • ProcessInput()
    • The client will first check if the stylus interacted with the onscreen keyboard. If this isn't the case it will pass the user input to the state using this method.

4. TLM.GameFramework.Server

When creating a multiplayer game with a client/server architecture, the server typically contains most of the gameplay rules. The client's only job is to render to correct scene to the screen and send any user input to the server. Since we cannot trust clients (ie. clients can be hacked or faked), all of the gameplay decisions are made by the server.
Just like it was the case for the client, this framework defines the server as a state-based machine.

More information about the client/server architecture is outside the scope of this article.

Using the framework

This chapter will explain how to create your own client. The use of states will be shown by creating a "menu" state. In this state we will use the framework's gui system to show buttons and dialog boxes.

First we create our Client class, which is a subclass of the framework's Client class.
The only thing that we need to add to this class is the initial state, in this case it will be a state displaying the main menu, aptly called: MenuState:

public class PWClient : TLM.GameFramework.Client.Client
{
    public PWClient(INetworkClientEngine networkEngine,
               IInputEngine inputEngine,
               IAudioEngine audioEngine,
               VideoEngine videoEngine):base(networkEngine, inputEngine, audioEngine, videoEngine)
    {
        this.state = new States.MenuState(this);
    }

This is all that is needed to implement your own client, since all of the actual logic of the game will be defined in various State classes. The Client class is no more than a manager class that deals with orchestrating the user input, switching between states and so forth.

Next, we will create a state that uses the GUI system to display a menu.

Like all States, MenuState is derived from the framework's base State class.
In the its WindUp() phase we make sure the on-screen keyboard is hidden. Remember that WindUp() is called each time we switch from one state to another. Therefore this is the place to hide to keyboard. Initializing the menu however, should not be done here, but in the constructor, since it only needs to be done once.

public override void WindUp()
{
    client.ShowKeyboard=false;
}

To create a menu we first create a Window and add it to the guiManager. You can add as many windows to the gui manager as you want. If needed windows can also be modal, which is useful for things like dialog boxes.

Window window = guiManager.CreateWindow("mainmenu",
                   (videoEngine.ScreenWidth-windowWidth)/2,
                   (videoEngine.ScreenHeight-windowHeight)/2,
                   windowWidth, windowHeight, "", true, false);
guiManager.AddWindow(window);

Next we create a few buttons and add them to the window we just created. To create a button we specify its location, relative to the parent window's upper left corner, its width and a label.

Button btnStartGame    = guiManager.CreateButton(0,0,buttonWidth,"Start new game");
Button btnOptions        = guiManager.CreateButton(0,30,buttonWidth,"Options");
Button btnExit        = guiManager.CreateButton(0,60,buttonWidth,"Exit");

window.AddWidget(btnStartGame);
window.AddWidget(btnOptions);
window.AddWidget(btnExit);

Finally we create eventhandlers for our button Clicked events.

btnStartGame.Clicked+=new ButtonClicked(btnStartGame_Clicked);
btnOptions.Clicked+=new ButtonClicked(btnOptions_Clicked);
btnExit.Clicked+=new ButtonClicked(btnExit_Clicked);

When we click the "Start new game" button, we want to warn the player in case the game is already running. We do this by checking if client.GameState is already set. If so, we display a dialogbox that let's the player choose to actually start a game or not.

private void btnStartGame_Clicked(object from, EventArgs args)
{
    if (client.GameState == null)
    {
        client.GameState = new GameState(client);
        client.ChangeState(client.GameState);
    }
    else
    {
        DialogBox box = GuiManager.CreateDialogBox("Start new game",
                               "This will end the current game, are you sure?",
                               DialogBoxMode.YesNo);
        box.Clicked+=new DialogBox.DialogBoxClicked(startnewgame_Clicked);
    }
}

private void startnewgame_Clicked(object from, DialogBoxEventArgs args)
{
    if (args.Value==DialogBoxButtons.Yes)
    {
        client.GameState = new GameState(client);
        client.ChangeState(client.GameState);
    }
}

This concludes the creation of the menu state.

Now that we have our client class and a state, we can create an executable, add references to our client and make use of them. The game loop is as follows:

/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main(string[] args)
{
    LoopbackConnector looper = new LoopbackConnector();
    PocketPuzzle.Client.PWClient client = new PocketPuzzle.Client.PWClient(looper,new InputGapiDrawPPC(), new AudioHekkus(), new VideoGapiDrawPPC(VideoMode.FullScreen, 240,320,16));
    client.Start();
    while(client.IsRunning)
    {
         client.ProcessInput();
         client.Think();
         client.Render();
    }
    client.Shutdown();
}

Additional software used

Included in the demo project, is an implementation of the VideoEngine and the AudioEngine.

The VideoEngine uses GapiDraw (http://www.develant.com/) and a .Net wrapper made by Intuitex (http://www.intuitex.com/). If you want to use GapiDraw you will need a commercial license.

The AudioEngine uses a freeware library called Hekkus Sound System (http://www.shlzero.com/) and again, a .Net wrapper made by Intuitex.

tmbamf


Click here to view tmbamf's online profile.
Download demo project - 476 Kb

posted on 2004-05-31 20:37  我们的游戏世界  阅读(984)  评论(0编辑  收藏  举报