aGPJ Tutorial 2


A GameProgrammer's Journey

0x08 graphic

Table of contents

Tutorial 2 - How to create a spaceshooter

Before you start

Before we embark on the second tutorial let me point out that some of the code used in this tutorial comes from the first tutorial. If you haven't completed that tutorial, I strongly suggest you follow that one first.

Also, while I do encourage you to make your own graphics/sounds, it's not really necessary to do so. If you've been following tutorial 1 then you already have the graphics otherwise get the graphics and the sounds from the link at the bottom.

Ok, now if you are ready. Here we go...

Images & Sound

0x01 graphic
(Doubleclick on the icon to get the images for this tutorial)

The Game

Like the title says, we're going to make a very cool little space shooter. I'll try to go through all main features and, hopefully, after finishing this tutorial, you'll have the knowlegde to add new, cooler, better and greater things to this game yourself.

Ok, now let me tell you exactly what we're going to make. The player has a spaceship that can move all over the screen. The ship will of course be equipped with a gun. Not just a gun, but a really, really cool gun. One shot usually is enough to blow the enemy in to a billion pieces.

The enemy, who for some weird reason want to destroy the player, are going to present themselves in formations. We know three kinds of formations in this game.

0x01 graphic

0x01 graphic

0x01 graphic

0x01 graphic

In this game, the player will have three lives. When the player gets shot, he will lose a life. Three lives lost means game over. If the players shoots an enemy he receives points and the enemy is destroyed.

That is in short what we are going to make.

To the stars

Let's start with the easy things first: the stars. These are tiny pictures that move in a vertical direction. To make it look better, we'll apply different speedrates to them.

The first thing we have to do is creating a sprite:

type

TStar = class(TImageSprite)

protected

starType: byte;

procedure DoMove(MoveCount: Integer); override;

end;

Then add a new variable, an array of Tstar:

...

SelectorImage : TDirectDrawSurface;

InstructionImage : TDirectDrawSurface;

stars : array [1..75] of TStar;

Given the size of our screen I think 75 stars, should be enough. Like all the other sprites, we are going to create these in the doReset procedure:

procedure doReset;

var numberOfStars : byte;

begin

// put the things before the game starts

Form1.DXDraw1.surface.Canvas.Font.Color:=clwhite;

Form1.DXDraw1.surface.Canvas.Brush.Style:=bsclear;

for numberOfStars := 1 to 75 do

begin

Stars[numberOfStars]:= TStar.Create(Form1.DXSpriteEngine1.Engine);

with Stars[numberOfStars] do

begin

case numberOfStars of

0..25: starType := 1;

26..50: starType := 2;

51..75: starType := 3;

end;

Image := Form1.DXImageList1.Items.Find('star 1');

X := Random(form1.width);

Y := Random(form1.height);

Z := 1;

Width := Image.Width;

Height := Image.Height;

end;

end;

gameCounter := 0;

GameState := gsGame;

...

To actually make the stars visible in the game, we have to add these lines in the doGame procedure:

procedure doGame;

begin

// put the game stuff in here

inc(gameCounter);

Form1.DXDraw1.surface.Fill(0);

Form1.Dxinput1.Update;

Form1.DXSpriteEngine1.Move(0);

Form1.DXSpriteEngine1.Draw;

Form1.DXSpriteEngine1.Dead;

end;

Before you can actually run the game you need to write the doMove procedure of the stars:

procedure TStar.DoMove(MoveCount: Integer);

begin

case starType of

1: y:=y+1;

2: y:=y+2;

3: y:=y+3;

end;

if y > form1.height then

begin

y:=0;

x:=random(form1.width)

end;

end;

As you can see, it's all quite simple. The speed of the star is controlled by the value of starType. When the star has reached the end of the screen, it's placed at the top again, but this time with a different x value.

It's important in any game to release all sprites used in the game. For this purpose we will use the doEndGame procedure:

procedure doEndGame;

var numberOfStars : byte;

begin

// do stuff here like freeing sprites

for numberOfStars := 1 to 75 do

stars[numberOfStars].Free;

gameState := gsMenu;

end;

When all sprites are freed, we return to the menu. Again, freeing sprites is absolutely necessary, so don't forget to do this in you own games.

The player

Displaying the player is done in a similar way. First we create a new class:

type

TPlayer = class(TImageSprite)

protected

hits : byte;

startFrame : byte;

CurrentFrame : byte;

maxFrame : byte;

fireRate : byte;

procedure DoMove(MoveCount: Integer); override;

procedure DoCollision(Sprite: TSprite; var Done: Boolean); override;

procedure ChangeFrame;

end;

Next, we declare a new variable, simply called player:

...

InstructionImage : TDirectDrawSurface;

stars : array [1..75] of TStar;

player : TPlayer;

As you can see there are more procedures for this Sprite. DoCollision and DoMove should look familiar, the ChangeFrame procedure, however is new.

I don't use the DelphiX way for animations. I've developed my own way:

procedure TPlayer.ChangeFrame;

begin

if GameCounter mod 4 = 0 then

begin

if CurrentFrame < startFrame+MaxFrame-1 then

inc(CurrentFrame)

else

CurrentFrame:= StartFrame;

end;

image := Form1.DXImagelist1.items.items[CurrentFrame];

end;

This procedure checks the variable gameCounter which will increase everytime doGame is called. It then checks CurrentFrame. If it is less than (startFrame+MaxFrame)-1 then it's save to increment it, otherwise currentFrame has to be startFrame. It basically loops through the images in the DXImagelist.

The player is, just like the stars, created in the doReset procedure. Notice that currentFrame, maxFrame and startFrame are set here too. The variable fireRate will be discussed later.

...

player := TPlayer.Create(Form1.DXSpriteEngine1.Engine);

With player do

begin

Image := Form1.DXImageList1.Items.items[19];

X := (form1.width - 15) div 2;

Y := form1.Height - 50;

Z := 10;

Width := Image.Width;

Height := Image.Height;

hits :=4;

startFrame := 19;

CurrentFrame := startFrame;

maxFrame := 2;

fireRate := 1;

end;

GameState := gsGame;

end;

Next is the movement procedure for the player:

procedure TPlayer.DoMove(MoveCount: Integer);

begin

pixelcheck:=true;

if (x > 3) and (isLeft in form1.DXInput1.states) then x:=x-2;

if (x+image.width+3 < form1.width) and (isright in form1.DXInput1.states) then x:=x+2;

if (Y > 3) and (isup in form1.DXInput1.states) then y:=y-2;

if (Y+image.height+3 < form1.height) and (isdown in form1.DXInput1.states) then

y:=y+2;

ChangeFrame;

collision;

end;

The line 'pixelcheck := true' is a typical DelphiX thing. If it's true, collisions will be calculated at pixellevel. Otherwise a rectangle the size of the image will be used as a reference.

The status of DXInput and the value of x and y are checked, to see if the spaceship is allowed to move in a certain direction. At the end of the procedure, ChangeFrame and collision are called to change the image and to see if collisions have occured.

There are of course collisions in the this game, but I will tell about those later, for now, just add this empty procedure:

procedure TPlayer.DoCollision(Sprite: TSprite; var Done: Boolean);

begin

// put player collisions here

end;

Last but not least, we have to free the sprite in the doEndGame procedure:

...

for numberOfStars := 1 to 75 do

stars[numberOfStars].Free;

player.Free;

gameState := gsMenu;

end;

Enemy formations

Well, what do you think so far? It's looking pretty cool, right? The stars are moving, you can move the spaceship. It is soo cool.... But, before we actually start drooling all over the keyboard, let us continue.

As told in the introduction, we are going to present the enemy in waves. We shall use four kinds of waves in our game. Here are two procedures, that will do all of the hard work:

function CalculateYPos(formation:integer;yPos:double;enemieCounter:integer):double;

begin

case formation of

0: CalculateyPos:=yPos+((enemieCounter-1)*40);

1: begin

if enemieCounter <= 3 then

CalculateyPos:=yPos+((enemieCounter-1)*40)

else

CalculateyPos:=(yPos+160)-((enemieCounter-1)*40);

end;

2: CalculateyPos:=yPos-((enemieCounter-1)*40);

3: CalculateyPos:=-40;

end;

end;

procedure CreateFormation;

var xPos, yPos, enemieCounter, chooseFormation :integer;

begin

chooseFormation:=random(4);

case chooseFormation of

0..1: yPos:=-240;

2..3: yPos:=-40;

end;

xPos := 50;

for enemieCounter := 1 to 5 do

with TEnemy.Create(Form1.DXSpriteEngine1.Engine) do

begin

Image := Form1.DXImageList1.Items.Find('enemie img1');

X := xPos*enemieCounter;

Y := CalculateyPos(chooseFormation, yPos, enemieCounter);

Z := 3;

Width := Image.Width;

Height := Image.Height;

startFrame := 22;

CurrentFrame := startFrame;

maxFrame := 2;

speed := 2.5;

end;

end;

The CreateFormation procedure first calculates a random number. This number determines the formation that is choosen. Then, 5 enemies are created. Each enemy in the formation is assigned a different x-value, based on the number of enemieCounter. The y-value is calculated with the CalculateYPos function.

The CalculateYPos function, calculates the y-values of the enemy fighters. Formations 0 and 2 are quite simple. Every new fighter is placed above or below the previous fighter. Formation 3 needs little words, the y-values stays the same. Formation 1 however, is a bit harder. It's a combination of 0 and 2. It starts with 0. If enemieCounter is equal to 4 then it changes to formation 2.

The only thing we have to do now, is to decide when the enemy appears. We are going to use the gameCounter variable for this. Add the following line in the doGame procedure:

...

Form1.DXSpriteEngine1.Draw;

Form1.DXSpriteEngine1.Dead;

if gameCounter mod 250 = 0 then createFormation;

This line is all we need. Note that this is not the best method. Formations will always appear after the same 'pause' and the player is able to respond to it after while. It would probably be better to use somekind of timetable. The same may apply for the random formations.

Before you start the application again to test it all, you must add the enemy class and the two procedures. Make sure you paste the ChangeFrame and doMove procedures after the CalculateYPos and CreateFormation procedures or else the game won't compile!

type

TEnemy = class(TImageSprite)

protected

firstShot : byte;

secondShot : byte;

shot1Fired : boolean;

shot2Fired : boolean;

startFrame : byte;

CurrentFrame : byte;

maxFrame : byte;

isShot : boolean;

speed : double;

procedure DoMove(MoveCount: Integer); override;

procedure ChangeFrame;

end;

(....)

procedure TEnemy.ChangeFrame;

begin

if GameCounter mod 4 = 0 then

begin

if CurrentFrame < startFrame+MaxFrame-1 then

inc(CurrentFrame)

else

CurrentFrame:= StartFrame;

end;

image := Form1.DXImagelist1.items.items[CurrentFrame];

end;

procedure TEnemy.DoMove(MoveCount: Integer);

begin

Y:=Y+speed;

if (y > form1.height+100) or (GameState = gsEndGame) then dead;

ChangeFrame;

collision;

end;

The ChangeFrame procedure is equal to that of the player.

In the enemy doMove procedure, the position of the enemy is updated and checked for it's position. If it's past the formheight+100 then the enemy is dead (deleted). He will also be deleted if gameState is gsEndGame, but I'll discuss that later.

Shooting the player

The enemy in our game has nearly the same weapon as the player. It's an slightly improved version, but only a experienced eye call tell the difference :)

An enemy is allowed to shoot two times. This will increase the difficulty a bit; we don't want the game to be too simple, right?

In order to make the shooting to work we have to add these lines:

with TEnemy.Create(Form1.DXSpriteEngine1.Engine) do

begin

Image := Form1.DXImageList1.Items.Find('enemie img1');

X := xpos*enemieCounter;

Y := CalculateYPos(chooseFormation, ypos, enemieCounter);

Z := 3;

Width := Image.Width;

Height := Image.Height;

firstShot := Random(form1.height-200);

secondShot := Random(form1.height-200);

shot1Fired :=false;

shot2Fired :=false;

...

When the enemy is created the variables Firstshot and secondShot each get a random number assigned. This number will be used to determine the firing position.

Next, we add a few things in the doMove procedure of the enemy:

procedure TEnemy.DoMove(MoveCount: Integer);

var xpos,ypos : double;

begin

Y:=Y+2.5;

if (y > form1.height+100) or (GameState = gsEndGame) then dead;

if ((y > firstShot) and (shot1Fired = FALSE)) then

begin

xpos:=x;

ypos:=y;

Form1.DXWaveList1.Items.Items[2].Play(False);

with TBullet.Create(form1.DXSpriteEngine1.Engine) do

begin

Image := form1.DXImageList1.Items.Find('bullet');

X := xpos+27;

Y := ypos+5;

Z := 2;

Width := Image.Width;

Height := Image.Height;

Parent := FALSE; // bullet is from enemy

end;

shot1Fired:=TRUE;

end;

if ((y > secondShot) and (shot2Fired = FALSE)) then

begin

xpos:=x;

ypos:=y;

Form1.DXWaveList1.Items.Items[2].Play(False);

with TBullet.Create(form1.DXSpriteEngine1.Engine) do

begin

Image := form1.DXImageList1.Items.Find('bullet');

X := xpos+27;

Y := ypos+5;

Z := 2;

Width := Image.Width;

Height := Image.Height;

Parent := FALSE; // bullet is from enemy

end;

shot2Fired:=TRUE;

end;

ChangeFrame;

collision;

end;

When the y-value of the enemy is at a position past firstShot or secondShot and shot1Fired or shot2Fired are still false, then a bullet is created at the position of the enemy.

This is the class of the bullet you need to add:

type

TBullet = class(TImageSprite)

protected

Parent : boolean;

procedure DoMove(MoveCount: Integer); override;

procedure DoCollision(Sprite: TSprite; var Done: Boolean); override;

end;

Notice the variable Parent. I wanted a single sprite for both the enemybullet and the playersbullet. For that purpose I'm using the variable Parent.

The doMove procedure:

procedure TBullet.DoMove(MoveCount: Integer);

begin

pixelcheck:=true;

if Parent = TRUE then y:=y-3 else y:=y+6;

if (y > form1.height) or (y < 0) or (GameState = gsEndGame) then dead;

collision;

end;

If the bullet belongs to the player (parent = true) then it has to go up. When it exits the screen, the bullet has to be deleted. Note that the speed of the enemy bullet is the speed of the players bullet times two!

Again, collisions will be discussed in a later chapter, I'll suffice with this.

procedure TBullet.DoCollision(Sprite: TSprite; var Done: Boolean);

begin

// collisions here

end;

Shooting the enemy

The firing code for the player looks much the same.

The following lines should go in the doMove procedure of the player:

var xpos, ypos : double;

begin

...

if (Y+image.height+3 < form1.height) and (isdown in form1.DXInput1.states) then

y:=y+2;

if fireRate > 1 then dec(fireRate);

if (isbutton1 in form1.DXInput1.States) and (fireRate = 1) then

begin

xpos:=x;

ypos:=y;

Form1.DXWaveList1.Items.Items[1].Play(False);

with TBullet.Create(form1.DXSpriteEngine1.Engine) do

begin

Image := form1.DXImageList1.Items.Find('bullet');

X := xpos+12;

Y := ypos;

Z := 2;

Width := Image.Width;

Height := Image.Height;

Parent := true; // bullet belongs to player

FireRate := 20;

end;

end;

...

Notice the 'fireRate = 1'. I use it to stop the weapon from continues fire. Have a look at the picture to see what happens when you don't do this.

0x01 graphic

With this neat little trick, the interval between bullets is much longer.

Almost done

A few more things left before this tutorial is over. Fixing some small things here and there and it's done. First the points. I'll keep that one easy. Just add these lines in the doGame procedure.

...

Form1.DXSpriteEngine1.Draw;

Form1.DXSpriteEngine1.Dead;

if gameCounter mod 250 = 0 then createFormation;

Form1.DXDraw1.surface.Canvas.TextOut(10,5,'Score: '+inttostr(points));

Form1.DXDraw1.surface.Canvas.release;

end;

Furthermore, in the DoReset procedure, we must not forget to initiate the points to zero.

...

GameState := gsGame;

points:=0;

end;

Next are the lives. We'll use an image with three small versions of the players ship.

Again we create a new variable for this, and then load the image in the doReset procedure.

...

stars : array [1..75] of TStar;

player : TPlayer;

LivesImage : TDirectDrawSurface;

(...)

procedure doReset;

...

LivesImage:=TDirectDrawSurface.Create(Form1.DXDraw1.DDraw);

LivesImage.LoadFromGraphic(Form1.DXImageList1.Items.Items[8].picture.Graphic);

points:=0;

GameState := gsGame;

end;

Then, in the doGame procedure, add this line:

...

Form1.DXDraw1.surface.Draw(10,25,rect(0,0,(player.hits-1)*25,21),livesImage,true);

Form1.DXDraw1.surface.Canvas.TextOut(10,5,'Score: '+inttostr(points));

Form1.DXDraw1.surface.Canvas.release;

...

Using the function Rect, we can show parts of images. In our case the picture is 75 pixels in width. Each ship uses 25 pixels. Using the hits variable, we can 'cut' pieces of the image. Making it look as if we lost a life.

But, in case we actually do lose all of our lives. We want to display a Game Over message.

Declare a new variable gameOverImage that we'll use for displaying the image.

...

LivesImage : TDirectDrawSurface;

gameOverImage : TDirectDrawSurface;

Next create the gameover image in the doReset procedure.

procedure DoReset

...

LivesImage:=TDirectDrawSurface.Create(Form1.DXDraw1.DDraw);

LivesImage.LoadFromGraphic(Form1.DXImageList1.Items.Items[8].picture.Graphic);

gameOverImage:=TDirectDrawSurface.Create(Form1.DXDraw1.DDraw);

gameOverImage.LoadFromGraphic(Form1.DXImageList1.Items.Items[9].picture.Graphic);

points:=0;

GameState := gsGame;

end;

And, in the doGameOver procedure we display the image again.

procedure doGameOver;

begin

Form1.DXSpriteEngine1.Move(0);

Form1.DXDraw1.surface.Fill(0);

Form1.DXDraw1.surface.Draw(0,0,gameOverImage.clientrect,gameOverImage,true);

end;

Notice the Form1.DXSpriteEngine1.Move(0) call. This is for the remaining enemy fighters. I don't want these fighters to remain in the game when a new game is started. They are automatically deleted in the domove procedure if they reach a certain position (see bullet and enemy movement procedures) or if they have a certain frame number (explosion).

The very last thing we have to do is freeing the images (lives, gameover) used in the game.

procedure doEndGame;

var numberOfStars : byte;

begin

// do stuff here like freeing sprites

Form1.DXSpriteEngine1.Move(0);

for numberOfStars := 1 to 75 do

stars[numberOfStars].Free;

player.Free;

gameoverImage.Free;

livesImage.Free;

GameState:=gsmenu;

end;

The End

Congratulations! You have completed this tutorial. If everything is well, then you should have a fully functional game. Perhaps not the best game ever, but it's a game nonetheless. More important it's made by you.

If you have any questions or comments regarding this tutorial, please don't hesitate to contact me.

If somebody wants to continue the game, add new enemies, weapons and bonusitems, then by all means go ahead. But please let me know about it. I'm very curious about what you come up with.

The End.

A GameProgrammer's Journey - Tutorial 2

Copyright Alexander Rosendal 2000-2003

Page 15

http://www.gameprogrammer.net/

Disclaimer: The tutorial below, including all images, is copyrighted by Alexander Rosendal. You are allowed to use the Delphi code and images for your own purposes. You are not allowed to copy (parts of) these tutorials without my prior written permission!

Give credit where credit is due. Thank you!
 



Wyszukiwarka

Podobne podstrony:
aGPJ Tutorial 4
bugzilla tutorial[1]
freeRadius AD tutorial
Alignmaster tutorial by PAV1007 Nieznany
free sap tutorial on goods reciept
ms excel tutorial 2013
Joomla Template Tutorial
ALGORYTM, Tutoriale, Programowanie
8051 Tutorial uart
B tutorial
Labview Tutorial
Obraz partycji (ghost2003) Tutorial
[LAB5]Tutorial do kartkówki
M2H Networking Tutorial Original
ABAQUS Tutorial belka z utwierdzeniem id 50029 (2)
eagle tutorial
c language tutorial
P J Ashenden VHDL tutorial
Anime drawing tutorials [ENG]

więcej podobnych podstron