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

Beginning CSharp Game Programming (2005) [eng]

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

250 Chapter 10 Putting Together a Game

or above 100, in which case more operations must be performed. If the energy went below 0, then the ship is destroyed and the energy is reset to 0 (for cleanliness’ sake). If the energy went above 100, then it is set back to 100.

n o t e

You could find a way to use the extra energy, rather than letting it go to waste. Perhaps you could have a special “reserve tank” add-on to the ship, into which all extra energy would be tossed. The possibilities are really endless. Always think about expansion.

The Shields property is simpler; it just makes sure the energy can’t go below 0 or above 1. The CurrentWeapon property is get only (no set function); it simply goes into the weapons array and returns a reference to the current weapon.

Here’s the rest of the functions in the class, with code removed:

public void NextWeapon() public void PreviousWeapon()

public void AddWeapon( Weapon weapon ) public Projectile[] Fire( float time )

The first two functions change the current weapon to the previous or next weapon. AddWeapon simply adds a new Weapon object to the weapons, and Fire asks the current weapon for an array of projectiles—but only if the ship can fire. If the ship can’t fire, then null is returned. Here’s the code for the function that checks for that:

public Projectile[] Fire( float time )

{

// no weapons

if( weapons.Count == 0 ) return null;

// check the time

if( time >= NextFire )

{

// get the weapon

Weapon w = CurrentWeapon;

//reset the firing delay NextFire = time + w.Delay;

//tell the weapon to fire return w.Fire( this );

}

Generic Space Shooter 3000

251

// can’t fire yet

return null;

}

The most important part of the code is the part where it checks the time. Weapons can only be fired at certain intervals, so if your code tells the ship to fire before it can fire again, then the ship needs to put its foot down and say, “No how, no way!” The weapon class needs a reference to a Spaceship when it fires because it needs certain information, such as current speed and velocity, in order to create its projectiles. Also, projectiles need to know about who fired them so that they can award points to whomever did so when something is destroyed. That’s why a this reference is passed into the firing function.

Projectiles

Projectiles are really simple objects:

public class Projectile : GameObject

{

int damage;

Spaceship owner = null;

<snip>

}

I snipped out two properties, Damage and Owner, which simply get and set the values of damage and owner. Everything else a projectile needs is inherited from GameObject.

Powerups

Powerups are even simpler than projectiles:

abstract public class Powerup : GameObject

{

public Powerup()

{

this.VY = 50;

}

public abstract void DoPowerup( Spaceship s );

}

Powerups set their default Y velocity to 50, meaning they’ll scroll downwards at 50 pixels per second, just slow enough to let the player catch most of them. They also define an abstract DoPowerup function, which performs a powerup on a spaceship. Following are two powerup examples.

252 Chapter 10 Putting Together a Game

Energy Powerups

The first example is an energy powerup, which adds energy to a ship:

public class EnergyPowerup : Powerup

{

public EnergyPowerup()

{

this.Texture = GameObjectLoader.PowerupTextures[0]; this.Scale = 0.5f;

this.Center();

}

public override void DoPowerup( Spaceship s )

{

s.Energy += 20.0f;

}

}

An energy powerup uses powerup texture 0, and is scaled to 50 percent of the size of the texture (the textures are 64×64, so powerups end up being 32×32).

The DoPowerup function simply adds 20 energy to a ship, and that’s it.

Shield powerups and speed powerups are almost identical to energy powerups; they just add shields or speed to a ship.

Weapon Powerups

The second example—and the fourth powerup type—is the WeaponPowerup, which adds a new weapon to a ship:

public class WeaponPowerup : Powerup

{

<snip>

public override void DoPowerup( Spaceship s )

{

// find out how many weapons the ship has switch( s.Weapons )

{

case 1:

s.AddWeapon( new DoubleLaserWeapon() ); break;

case 2:

s.AddWeapon( new PlasmaWeapon() ); break;

Generic Space Shooter 3000

253

case 3:

s.AddWeapon( new DoublePlasmaWeapon() ); break;

case 4:

s.AddWeapon( new MissileWeapon() ); break;

case 5:

s.AddWeapon( new DoubleMissileWeapon() ); break;

case 6:

s.AddWeapon( new LaserSpreadWeapon() ); break;

case 7:

s.AddWeapon( new AnihilatorWeapon() ); break;

}

}

}

I cut out the constructor; it was simply rehashing the same code you’ve seen before. The weapon powerup checks how many weapons a ship already has, and depending on how many weapons there are, adds a new weapon of a different type. So if a ship only has one weapon, then WeaponPowerup adds a double-laser weapon, and if the ship has two weapons, then WeaponPowerup adds a plasma weapon, and so on.

n o t e

This is all hard-coded in, which, as I’ve said before, is inflexible. A better way to do this would be to have an advancement list for each kind of spaceship, or even have different powerups for each weapon type. For example, a laser powerup could add a double laser weapon, then a triple laser weapon, then a laser spread weapon, and so on. You’re free to do whatever you want.

Weapons

The weapon system of GSS3K is pretty flexible because each weapon has been abstracted into its own class. You can program each class to do anything you want.

Here’s the base Weapon class with the code cut out of it:

public abstract class Weapon

{

// data

float delay = 0; DS.SecondaryBuffer sound;

254 Chapter 10 Putting Together a Game

string name;

//properties public float Delay

public DS.SecondaryBuffer Sound public string Name

//constructor

public Weapon( float delay, string name )

public Projectile[] Fire( Spaceship owner )

protected abstract Projectile[] CustomFire( Spaceship owner ); protected void SetupProjectile(

Projectile p,

float vx, float vy, float ax, float ay, float offsetx,

int damage, Spaceship owner )

}

Each weapon knows how long it takes to recharge, hence the delay variable. Weapons also make a sound when fired—that’s why they have a DirectSound secondary buffer.

The interesting parts are the Fire, CustomFire, and SetupProjectile functions. Fire is a function that takes care of some housekeeping details:

public Projectile[] Fire( Spaceship owner )

{

//play the sound Sound.Stop();

Sound.Play( 0, DS.BufferPlayFlags.Default );

//return a custom firing action

return CustomFire( owner );

}

It plays the sound (stopping it first, in case it’s already playing), and then calls CustomFire to actually return a list of projectiles. This is done so that your CustomFire function (which is abstract here) doesn’t have to remember to play the firing sound. You can make your own weapon objects play additional sounds if you like.

SetupProjectile is a helper function that automatically sets up a bunch of information, such as velocity, acceleration, damage, and so on, for a projectile object. It’s not really that important, but you can look at it on the CD if you want; it’s in the Weapons.cs file.

Generic Space Shooter 3000

255

Your Own Weapons

I’ve defined a bunch of custom weapons for you to use, but feel free to make your own. Here’s an example of the simplest one:

public class LaserWeapon : Weapon

{

// fire every 1/2 second, name “Lasers”, and use firing sound 0. public LaserWeapon()

: base( 0.5f, “Lasers” )

{

Sound = GameObjectLoader.FiringSounds[0].Clone( Game.devices.Sound );

}

protected override Projectile[] CustomFire( Spaceship owner )

{

Projectile[] values = new Projectile[1];

values[0] = (Projectile)GameObjectLoader.Laser.Clone(); SetupProjectile( values[0], 0, 400, 0, 0, 0, 5, owner ); return values;

}

}

This code needs a little explaining. The constructor creates a new sound buffer by cloning the existing FiringSound[0] buffer object; this is done because any given sound buffer can only make one sound at any given time. If you have two weapons using the same sound buffer and firing at almost the same time, then only one of them is going to make a sound. In order to duplicate the sound so that both weapons sound as if they’re firing, the buffer must be cloned.

The CustomFire function creates a new array of projectiles that contains only one projectile, which is cloned from the Laser object. Now that you have a base laser projectile, all you need to do is set up the other values on it; this is done using SetupProjectile. In this particular case, SetupProjectile is making a laser that is traveling 0 pixels per second in the x direction and 400 pixels per second in the y direction, has 0 acceleration for x and y and an 0 x offset (you’ll see what this is used for in a bit), and does 5 damage.

Finally, the projectile array is returned.

Here’s a more complicated example:

public class DoubleLaserWeapon : Weapon

{

<snip>

protected override Projectile[] CustomFire( Spaceship owner )

256 Chapter 10 Putting Together a Game

{

Projectile[] values = new Projectile[2];

values[0] = (Projectile)GameObjectLoader.Laser.Clone(); values[1] = (Projectile)GameObjectLoader.Laser.Clone(); SetupProjectile( values[0], 0, 400, 0, 0, 28, 5, owner ); SetupProjectile( values[1], 0, 400, 0, 0, -28, 5, owner );

return values;

}

}

This creates a “double laser,” which is two projectiles. The main difference is the x offset parameters, which are set to 28 and -28, meaning the first projectile is moved 28 pixels to the right and the second is moved 28 pixels to the left.

There are other weapons defined, such as plasma, double plasma, missiles, double missiles, laser spread, and the annihilator weapon, as well. You can play the game or look at the code to see how these work out.

The States for GSS3K

The game will have four different states in it. The first state is the startup state, which shows the title screen. The next state is the obligatory “cheesy arcade shooter storyline” state, which opens up the game with a very vague background story. The most important state is the actual game state, which manages all the objects and physics and pretty much everything else. A final state takes care of the help menu, showing the user what keys to use to accomplish different tasks.

All of the states are stored within the GSS3KStates.cs file.

The Startup State

The startup state is very simple; all it does is load a texture from disk and display it for a few seconds. The state should also quit out if the user presses a certain key—you don’t want to lock the user into looking at a title screen for longer than he wants to. (I just hate it when a game does that to me!)

The state will need a timer, a Boolean denoting whether the user wants to quit out, a texture, and a few vertices used to display the texture:

public class GSS3KStartup : GameState

{

Timer timer;

bool done = false;

D3D.Texture loadscreen;

Generic Space Shooter 3000

257

D3D.CustomVertex.TransformedTextured[] vertexes = null;

<snip>

}

As you can see, this inherits right from the GameState class, gaining all of its wonderful built-in utilities. The only things this class needs to define are the following:

public GSS3KStartup()

public override GameStateChange ProcessFrame() protected override void CustomRender()

protected override void KeyboardUp( DI.Key key ) protected override void MouseButtonUp( int button ) protected override void JoyButtonUp( int button )

I cut out the code for the functions. The constructor, GSS3KStartup, doesn’t do anything spectacular; it simply loads a texture from disk and sets up the vertices to show it on the screen.

ProcessFrame is quite simple:

public override GameStateChange ProcessFrame()

{

//return after 10 seconds or key pressed if( timer.Time() >= 10.0f || done )

return new GameStateChange( true, new GSS3KIntro() );

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

return null;

}

Basically, ProcessFrame just waits for the timer to pass 10 seconds or until the player presses a key (which sets done to true, as you’ll see in a moment). If either of those conditions apply, then a new game state is returned, telling the game to destroy this state and switch to the GSS3KIntro state.

You don’t really need to see the CustomRender function—all it does is draw the texture on the screen the way you saw it done in Demo 7.4.

The input functions are quite simple:

protected override void KeyboardUp( DI.Key key )

{

done = true;

}

protected override void MouseButtonUp( int button )

258 Chapter 10 Putting Together a Game

{

done = true;

}

protected override void JoyButtonUp( int button )

{

done = true;

}

Whenever a keyboard button, mouse button, or joystick button is released, the done Boolean is set to true so the state knows that it needs to exit when it gets to the ProcessFrame function. Wasn’t that simple?

n o t e

When you’re changing states, it’s generally a better idea to check if a button is released than if it’s pressed. Whenever you switch to a brand-new state, new input handler objects are created. They’re going to think that you just pressed down a key as well, as they have no previous state data to compare to, and the state will change much faster than you can release the key. The only way around this is to make the input checkers transfer key states from one state to the next; the framework doesn’t handle this in its current state.

The Introduction State

I’m not going to spend much time going over the Introduction state, as it’s just something really cheesy and simple I threw together to give the game an authentic 1980s arcade machine feel. It simply prints out a text string onto the screen, asking the player for assistance in defending a space outpost against space raiders.

Here’s the data:

public class GSS3KIntro : GameState

{

Timer timer; float next;

bool done = false;

D3D.Font typefont = null;

DS.SecondaryBuffer beep = null;

string fullmessage = @”... INCOMING TRANSMISSION ...\n

... CAPTAIN TYRAZIEL... PLEASE COME IN...\n

... OUTPOST GSK53-ALPHA IS UNDER ATTACK ... \n

... WE NEED YOU ...”;

Generic Space Shooter 3000

259

string message = “”;

int stage = 0;

<snip>

}

The fullmessage contains the whole message, and message will contain a partial string. The idea is that you’re receiving a text transmission over subspace communications (or something, it doesn’t matter); you receive the transmission character by character, so it’s printed out as you get the message. That’s what stage is for—it keeps track of where the message is in terms of being received. If stage is 0, then you haven’t received any part of the message yet, and if it’s 10, then you’ve received “... INCOM,” so that’s what’s going to be inside of message. The beep buffer contains the beeping sound, played for each character printed out.

n o t e

The @ symbol in the code may be new to you. It simply states that everything in between the double quotation marks following the @ sign should be read in literally from the file. This is an easy way to allow you to split up long strings onto multiple lines.

Here’s the ProcessFrame function:

public override GameStateChange ProcessFrame()

{

//exit when user presses a key, go to game if( done == true )

return new GameStateChange( true, new GSS3KGame() );

//add a new character if it’s time and there are characters

//left to add

if( timer.Time() >= next && stage < fullmessage.Length )

{

stage++;

message = fullmessage.Substring( 0, stage ); next += 0.10f;

beep.Stop();

beep.Play( 0, DS.BufferPlayFlags.Default );

}

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

return null;

}