Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning CSharp Game Programming (2005) [eng]

.pdf
Скачиваний:
151
Добавлен:
16.08.2013
Размер:
4.97 Mб
Скачать

230Chapter 10 Putting Together a Game

Instead of saying, “All spaceships move at 200 pixels per second,” you can say, “Spaceships can move at 200 pixels per second, but if they get a speed powerup, they can move faster!” In the interest of flexibility, spaceships in GSS3K will contain a speed variable.

Just to spice things up, I’ve added the concept of shields to the spaceships. A shield will diminish or erase damage—say, if the ship gets hit by a laser beam, the damage will be reduced so that it doesn’t hurt the ship so much. Shield values will range from 0.0 to 1.0, where 0 will deflect no damage at all, and 1.0 will deflect all damage. To further spice things up, every time the player’s shields get hit by something, they will be reduced by 0.02 (2 percent), meaning a ship can get hit 50 times before its shields are completely depleted. If the shields are at 0.5 (50 percent), then a 10-damage laserbeam will take off 5 energy points and 2 percent of your shields.

Spaceships will also have an array of weapons that the player can use, a variable containing the next time at which the player can fire his weapons, and a score variable.

Weapons

Weapons are simple objects that describe how projectiles are generated. Weapons simply have a sound that is played when they are fired, a delay time telling the player how much time the weapon needs to recharge itself, and a name. When weapons are fired, they are asked to return an array of projectiles.

Projectiles

Projectiles are the actual objects in the game that inflict damage; they’re extremely simple objects. Projectiles simply store how much damage they inflict, and they have a reference to the spaceship that fired the projectile so that you can award points if the projectile happens to destroy anything.

Powerups

Powerups are the most abstract object in the game; they don’t actually store any special data at all. Powerups simply have an abstract function that they apply to a spaceship whenever the player gets them.

Common Attributes

The three tangible object types in GSS3K—spaceships, projectiles, and powerups—all share some common attributes. First of all, they’re all sprites, so they must have texture information as well as positions, scaling, angles, and so on.

Where have you seen these attributes before? The Sprite class from Chapter 7, of course! That’s right, my custom Sprite class has all of those attributes, so all game objects will inherit from the sprite.

A New Framework 231

Game objects need more than just the attributes provided from the Sprite class, however. For the physics of the game, all game objects will store additional vector data representing the current x and y velocities of the objects, as well as the current x and y accelerations.

Other attributes are the collision rectangle and the destroyed Boolean. The collision rectangle is used to tell whether the object has collided with another object, and the destroyed Boolean is used to tell the game that the object has been killed, and therefore should not be used in any further calculations. You’ll see why this is later on.

That pretty much sums up the objects of GSS3K, so now on to a new framework!

A Short Physics Lesson

Velocity and acceleration are very simple concepts. If you have an object whose velocity is defined as 10 meters per second in the X axis, then the object will be moved forward by 10 meters in the X axis every second. This is a simple calculation:

new position x = old position x + (10 time elapsed)

If 0.3 seconds have elapsed, then three units are added to the object’s x position.

Acceleration is a more advanced concept, but it’s also easy to understand. Acceleration simply defines the speed at which an object’s velocity changes. If you have an acceleration of one meter per second squared, it means that the velocity of an object will be increased by one meter per second every second. This is also a simple calculation:

new velocity x = old velocity x + (1 time elapsed)

If you start off with a velocity of 10, then after one second the velocity will be 11, then 12 the next second, and so on.

A New Framework

The simple frameworks generated by most compilers achieve their purposes: to provide you with a simple place from which to build your program. Such frameworks are not terribly complex, and they’re not really expandable, either—they’re just something quick-and- dirty.

But a framework can be much more advanced and flexible. For example, almost every game out there uses the concept of a game state. A game can be in “Introduction” mode, “Playing” mode, “Show High Scores” mode, or any of a million other modes. The simple frameworks you’ve seen so far haven’t made use of the game state concept.

232Chapter 10 Putting Together a Game

I’ve gone ahead and designed a brand-new framework for you to use. It’s not that complex, but it involves a fair amount of code, a lot of which I won’t be showing you. All of the code is on the CD in Demo 10.1. The framework uses the new concept of states.

Setting Up

In previous demos, I’ve always just assumed that the resolution 640×480 is available. While there’s a 99.999 percent chance that the resolution is available, that’s still an assumption, and that’s dangerous. I guarantee you that some player won’t have that resolution on his system, and he’ll track you down, demanding to know why your game won’t run. So it’s always a good idea to let the game check capabilities and let the user pick some options as well.

In the file Setup.cs on the CD, there’s a form class that loads user preferences. The form lets the user select the resolution he wants to run in and the input device he wants to use. The code basically goes into Direct3D and DirectInput and asks them to list all the available devices. The setup form takes a DeviceOptions class when it’s created, and fills the class out with the appropriate parameters, depending on what the user wants to use.

Device Options

The DeviceOptions class is simple (in Device.cs):

public class DeviceOptions

{

// graphics

public bool Windowed;

public D3D.DisplayMode GraphicsMode;

// joystick

public bool UseJoystick = false; public Guid Joystick;

public int JoystickDeadzone = 1500;

public DI.InputRange JoystickRange = new DI.InputRange( -10000, 10000 );

}

The options allow you to define some configurable options for each device on the system. The graphics part, for example, determines whether the user wants windowed mode, and what display mode to use. The setup form allows both of these to be filled out.

The joystick part has a bit more information; it determines whether the user wants to use a joystick, and if so, it determines the GUID of the joystick. Remember the demos from Chapter 8? They simply picked off the first attached joystick; that was kind of stupid

A New Framework 233

because a user may have more than one joystick installed. The DeviceOptions class (used in conjunction with the setup form I’ll get to later on) allows the user to pick what he wants.

The deadzone and range are hard-coded to certain values, and the setup form doesn’t allow you to change them. There’s really no reason to, though. This class is simply a convenient place to put the device values.

Device Blocks

I’ve found that it’s helpful to encapsulate all of the game devices into one class. I’ve done so, and I call it the DeviceBlock class.

public class DeviceBlock

 

{

 

public D3D.Device Graphics

= null;

public DS.Device Sound

= null;

public DI.Device Keyboard

= null;

public DI.Device Mouse

= null;

public DI.Device Joystick

= null;

public D3D.Sprite Sprite

= null;

public void Initialize( DeviceOptions options, Game game ) void InitGraphics( DeviceOptions options, Game game ) void InitSound( Game game )

void InitKeyboard( Game game ) void InitMouse( Game game )

void InitJoystick( DeviceOptions options, Game game )

}

I’ve cut out all of the code for the functions because you’ve seen 99 percent of it already, in Chapters 6, 7, 8, and 9. The functions simply initialize the devices using the given options.

n o t e

Feel free to add more device options to the DeviceOptions class in order to make it more flexible. This simple version suits my needs, so I saw no point in going all-out and making the class ultraconfigurable.

Input Checkers

Remember how Demo 8.4 used Booleans to determine exactly when a joystick button was pressed or released, transforming the “polling” input gathering process into a pseudo “event notification” system? That was kind of a pain in the butt, wasn’t it? You had to have a Boolean variable for each button, and you had to check whether the Boolean changed.

234 Chapter 10 Putting Together a Game

I decided to encapsulate that behavior into classes called KeyboardChecker, MouseChecker, and JoystickChecker. Each class is different, and each handles a different kind of device. I’m willing to bet you can figure out which class handles which device. These classes are in

Input.cs.

The code for the classes is long and boring and doesn’t really show you anything that you haven’t seen before, so I’ll skip it and jump right into explaining how the classes work.

Keyboard Checking

The keyboard is a simple device, consisting only of buttons. Whenever a button is pressed or released, a message should be sent to an event handler.

The keyboard checker must be polled; you do this by calling KeyboardChecker.ProcessInput. This function gets an array of the buttons that are pressed down and compares this array with the buttons that were pressed down the last time ProcessInput was called. If any of the buttons changed, then the function sends out the button event.

Set up button events using delegates, like this:

KeyboardChecker checker = new KeyboardChecker( keyboard ); checker.PressEvent = new KeyboardChecker.KeyFunction( KeyDown ); checker.ReleaseEvent = new KeyboardChecker.KeyFunction( KeyUp );

KeyboardChecker.KeyFunction is a delegate for a function that takes one DirectInput.Key structure as its input. The previous code would then call this function whenever a key was pressed down:

public void KeyDown( DirectInput.Key k )

{

// code to handle key press

}

Mouse Checking

Mice are more complicated than keyboards because they have buttons and axes, and you need to handle both. Buttons are handled in a similar manner to keyboard keys, except that instead of DirectInput.Key values, they use ints. For example:

MouseChecker checker = new MouseChecker( mouse ) checker.PressEvent = new MouseChecker.ButtonFunction( MouseDown ); // later:

public void MouseDown( int button )

{

// handle button press

}

Joysticks 235

The checker also checks axis movement:

checker.MoveEventX = new MouseChecker.MovementFunction( MouseMoveX ); checker.MoveEventY = new MouseChecker.MovementFunction( MouseMoveY ); checker.MoveEventZ = new MouseChecker.MovementFunction( MouseMoveZ ); // later:

public void MouseMoveX( int delta )

{

// x moved by “delta” units, handle that here

}

Whenever a mouse moves, the move events are triggered. However, if a mouse doesn’t move, then the delta value is 0, and the events don’t get triggered at all.

As with the keyboard, mouse checkers must be polled using the ProcessInput function:

checker.ProcessInput();

// call any events if they occur

Joysticks

The last class, the JoystickChecker, is very similar to the mouse checker, with one difference in behavior: The Joystick axis events are called every time the ProcessInput function is called. This is because joystick devices use absolute positioning—it’s more useful to know where the joystick is positioned at all times than simply if it moved.

Joysticks have two button delegates and five axis delegates:

public ButtonFunction PressEvent; public ButtonFunction ReleaseEvent; public MovementFunction MoveEventX; public MovementFunction MoveEventY; public MovementFunction MoveEventZ; public MovementFunction MoveEventU; public MovementFunction MoveEventV;

These are used just like the mouse functions.

Game States

Games are state machines, meaning their states, or modes, are always changing. When you start up a game, it shows you the title screen and maybe the name of the company that designed it. This is the Introduction state. When you’re actually playing the game, you’re in the Game state. If you’re playing around with options, you’re in the Configuration state. Some games may even have many different kinds of playing states, as when the player must change user interfaces to go from different modes of gameplay.

236Chapter 10 Putting Together a Game

Each state handles things differently. In the Introduction state, you might want all input to just exit and move on to a Menu state. In the Game state, when a button is pressed a gun is fired, but obviously you don’t want that to happen in any other state. The Help state will show you a help menu explaining the game, so you don’t want all the objects in the game being drawn; you only want to see a Help menu.

Having different states allows you to separate all of this kind of stuff into discrete chunks. A state object will handle input, process events, and draw the game screen, essentially encapsulating the ProcessInput, ProcessFrame, and Render functions from the earlier frameworks.

Let me show you the base GameState class, sans code:

public abstract class GameState

{

protected bool graphicslost = false; protected KeyboardChecker keychecker = null; protected MouseChecker mousechecker = null;

protected JoystickChecker joystickchecker = null;

public GameState()

public virtual void ProcessInput()

protected

virtual

void

KeyboardDown( DI.Key key )

{}

protected

virtual

void

KeyboardUp( DI.Key key )

{}

protected virtual void MouseButtonDown( int button ){} protected virtual void MouseButtonUp( int button ) {} protected virtual void MouseMoveX( int delta ) {} protected virtual void MouseMoveY( int delta ) {} protected virtual void MouseMoveZ( int delta ) {}

protected virtual void JoyButtonDown( int button ) {} protected virtual void JoyButtonUp( int button ) {} protected virtual void JoyMoveX( int delta ) {} protected virtual void JoyMoveY( int delta ) {} protected virtual void JoyMoveZ( int delta ) {} protected virtual void JoyMoveU( int delta ) {} protected virtual void JoyMoveV( int delta ) {}

public virtual void LostFocus() {} public virtual void GotFocus() {}

Joysticks 237

public abstract GameStateChange ProcessFrame();

protected abstract void CustomRender(); public void Render()

}

There is, obviously, a lot more to the class than just those three functions. Input processing is a major component of this class—it defines three input checkers and the functions to use with those input checkers. You’ll note that all the input event functions are empty and virtual, allowing you to just override whichever ones are important to you later on and not have to code any of the others. If you don’t care to read the V axis of a joystick (really, who does?), then you don’t have to declare your own custom JoyMoveV function because the base GameState class already has one that does nothing.

Rendering is a little streamlined; there are two rendering functions: Render and CustomRender. Render is implemented in this class; it takes care of losing the device and all that other cool stuff so that you don’t have to. Render calls the CustomRender function, which is up to you to define.

State Changes

The state system for this framework is stack-based, meaning that all states are stored on a stack. This is quite handy, and let me explain why. First, imagine a non-stack system: You start off the game with an introduction state—it shows the title screen and maybe an introduction movie or something—then the game switches to the Menu state, which allows you to start a new game, after which the game switches to the Game state. Figure 10.1 shows this.

When the Menu state is exited, the game destroys it and creates a Game state.

Figure 10.1 A simple state system

238Chapter 10 Putting Together a Game

Now what happens when the user quits the game? In a non-stack-based system, the Game state is destroyed and the Menu state is created again. That seems like kind of a waste; the Menu state was already created at one time, so why destroy the Menu state if the game is just going to go back to it later on?

This is where a stack system comes into play: Instead of destroying the Menu state, you can keep it and put a new state on top of it—the Game state. Once the Game state is done, pop it off the stack and go back to the Menu state. Figure 10.2 shows this new method in a theoretical game.

Figure 10.2

A stack-based state system

The stack allows you to suspend a state and then return to it at a later time without destroying anything.

The ProcessFrame function returns a GameStateChange class, which tells the game how the state should change. It has two variables:

public class GameStateChange

{

public bool Terminated = false; public GameState NextState = null;

}

Joysticks 239

The Terminated Boolean tells the game whether the current state has been terminated or not. If so, the state is popped off the stack. The NextState variable holds a reference to the next state; if it’s null, then the game should fall back to the state that is before it on the stack. Table 10.1 lists the behaviors.

 

Table 10.1

State Change Behaviors

 

Terminated

NextState

Behavior

 

 

false

null

Essentially does nothing because the state isn’t terminated

 

 

 

and there’s no new state. There’s no reason to use these

 

 

 

parameters.

 

true

null

Current state is terminated, so the game should go back to

 

 

 

the previous state. See transitions 3 to 4, 5 to 6, and 8 to 9

 

 

 

from Figure 10.2.

 

false

non-null

Suspend the current state and add a new state on top of the

 

 

 

existing state. See transitions 2 to 3, 4 to 5, 6 to 7, and 7 to 8

 

 

 

from Figure 10.2.

 

true

non-null

Terminate the current state and go to a new state. See

 

 

 

transition 1 to 2 from Figure 10.2.

 

 

 

 

 

A Sample State

I’ve provided a sample state in the State.cs file for you to examine. Here it is:

public class SampleState : GameState

{

bool done = false; bool focused = true;

public override GameStateChange ProcessFrame()

{

if( done == true )

return new GameStateChange( true, null );

if( focused )

{

// do processing here

}

else

{

// else sleep the thread to prevent eating processor cycles System.Threading.Thread.Sleep( 1 );

}