A GameProgrammer's Journey
Table of contents
Tutorial 4 - How to create a platform game
Here we go again!
Welcome to the next part of the platform series. In this tutorial we are going to finish the work we've started in the previous tutorial.
If you have not finished the previous tutorial, I strongly suggest you finish that one first.
To make sure you have the correct code and images, download the file at the bottom of the page The images in this file are made by me, you are of course allowed to use your own if you want to.
Right, grab yourself a soda, unplug the phone, lock the doors so you can't be disturbed... here we go!
Sound & Images
(Doubleclick on the icon to get the images for this tutorial)
What's the plan
Usually the big idea in platform games is not very spectacular. You've got your basic hero, some coins or items to collect, a few enemies to get in the hero's way and an exit of somesort, usually to go to the next level.
All these ingredients are also going to be in our game. Note that I'm not going to dicuss all parts in great detail. I do not want this tutorial to reach the size of a book, there are still going to be things for you to figure out yourself, in case you're planning to write the next Prince of Persia or something :)
Drawing the Items
We are going to start with something simple: Items. The procedure for creating items and drawing them is fairly easy.
To get the items on our screen we are going to use the same methode as we used for the tiles. Let's have a look at the new fgland.bmp, that was in the zip you've downloaded a few minutes ago. There are two new colors in the image.
One, purple, wil be used in a later chapter, the other, light blue, is meant for the items.
To make this work we need to do four things:
create the items class and variables
adjust the procedure Loadmap a little
write a procedure which loads the actual items, and make sure it gets called when the game starts
draw the items.
Ok, here we go with the first part;
type TDirection = (dLeft, dRight, rNone);
TGameItemType = (banana, apple);
type
TGameItem = class(TImageSprite)
SpriteImg : TDirectDrawSurface;
isDead : boolean;
kind : TGameItemType;
end;
const maxSprites = 10;
maxItems = 10;
var
gameitem : array [0..maxItems] of TGameItem;
Then, the modification to the loadMap procedure:
procedure TForm1.LoadMap(level:byte);
var xpos,ypos : integer;
currentColor : TColor;
Bitmap: TBitmap;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
for xpos:=0 to MAPWIDTH do
for ypos:=0 to MAPHEIGHT do
begin
currentColor := bitmap.Canvas.Pixels[xpos,ypos];
case currentColor of
clfuchsia..clAqua :
begin
if TileInfo[xpos-1,ypos].tilenr = 100 then
TileInfo[xpos,ypos].tilenr:=100
else TileInfo[xpos,ypos].tilenr:=6;
end;
clwhite : TileInfo[xpos,ypos].tilenr:=100;
(..)
And the new procedure called loadItems;
procedure TForm1.LoadItems(level:byte);
var x,y : integer;
currentColor: TColor;
Bitmap: TBitmap;
itemCounter : integer;
itemKind : byte;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
itemCounter := 0;
for x:=0 to MAPWIDTH do
for y:=0 to MAPHEIGHT do
begin
itemKind := random(2);
currentColor := bitmap.Canvas.Pixels[x,y];
if (currentColor = clAqua) and (itemCounter <= maxItems) then
begin
gameItem[itemCounter] := TGameItem.Create(Form1.DXSpriteEngine1.Engine);
gameItem[itemCounter].x := x*32;
gameItem[itemCounter].y := y*32+2;
gameItem[itemCounter].isDead:=false;
gameItem[itemCounter].SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
gameItem[itemCounter].SpriteImg.LoadFromGraphic
(Form1.DXImagelist1.items.items[itemKind+12].picture.graphic);
gameItem[itemCounter].SpriteImg.TransparentColor:=clblack;
case itemKind of
0: begin
gameItem[itemCounter].kind := apple;
gameItem[itemCounter].width := 30;
gameItem[itemCounter].height := 27;
end;
1: begin
gameItem[itemCounter].kind := banana;
gameItem[itemCounter].width := 21;
gameItem[itemCounter].height := 29;
end;
end;
inc(itemCounter);
end;
end;
Bitmap.free;
except
messagedlg('error while loading the map',mtinformation,[mbok],0);
end;
end;
Ok, so what does all this do? The modification is actually a check to see which tile we are. The background of the tile can either be the open-air (tilenumber = 100), or a wall in which case the tilenumber = 6.
The procedure loops through the bitmap and checks for all items. When it has found one it'll create an item.
Of course, we must not forget to actually declare and call the procedure...
{ Private declarations }
public
procedure LoadMap(level:byte);
procedure LoadItems(level:byte);
(..)
wallpaper3 := TDirectDrawsurface.Create(DXDraw1.ddraw);
wallpaper3.LoadFromGraphic(Form1.DXImagelist1.items.items[10].picture.graphic);
Loadmap(0);
LoadItems(0);
(..)
The final part of this chapter, drawing the items is done rather easy now:
for imgCounter:= 0 to maxItems do
begin
if Gameitem[imgCounter].isDead = false then
begin
dxdraw1.Surface.draw(round(Gameitem[imgCounter].x)
+scrollHorizontal,round(Gameitem[imgCounter].y),
Gameitem[imgCounter].SpriteImg.clientrect, Gameitem[imgCounter].SpriteImg,true);
end;
end;
Place the code above in the dxtimer event, right after the part where the other tiles are drawn.
We loop though all items, check if an item isn't dead (picked up) and then, taking the scrolling into consideration, draw the items.
Drawing the npc's
Now that you know how the items are done, putting some other characters in all this isn't hard anymore.
We'll follow the same plan again:
type
TOurSprite = class(TImageSprite)
SpriteImg : TDirectDrawSurface;
Direction : TDirection;
walking : boolean;
isDead : boolean;
Speed : single;
startFrame : integer;
CurrentFrame : integer;
MaxFrame : integer;
function testForWall(spriteXpos,spriteYpos:single;
spriteWidth,spriteHeight:integer):boolean;
procedure ChangeFrame(nr:byte);
end;
var
npc : array [0..maxSprites] of TOurSprite;
(...)
procedure TForm1.LoadEnemyMap(level:byte);
var x,y : integer;
currentColor: TColor;
Bitmap: TBitmap;
spriteCounter : byte;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
spriteCounter := 0;
for x:=0 to MAPWIDTH do
for y:=0 to MAPHEIGHT do
begin
currentColor:= bitmap.Canvas.Pixels[x,y];
if (currentColor = clfuchsia) and (spriteCounter <= maxSprites) then
begin
npc[spriteCounter] := TOurSprite.Create(Form1.DXSpriteEngine1.Engine);
npc[spriteCounter].x := x*32;
npc[spriteCounter].y := y*32-14;
npc[spriteCounter].width := 23;
npc[spriteCounter].height := 45;
npc[spriteCounter].isDead:=false;
npc[spriteCounter].Direction:=dRight;
npc[spriteCounter].speed := 1;
npc[spriteCounter].startFrame := 26;
npc[spriteCounter].CurrentFrame := npc[spriteCounter].startFrame;
npc[spriteCounter].MaxFrame := 9;
npc[spriteCounter].SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
npc[spriteCounter].SpriteImg.LoadFromGraphic
(Form1.DXImagelist1.items.items[26].picture.graphic);
npc[spriteCounter].SpriteImg.TransparentColor:=rgb(125,150,255);
inc(spriteCounter);
end;
end;
Bitmap.free;
except
messagedlg('error while loading the map',mtinformation,[mbok],0);
end;
end;
You should recognize most things from the previous chapter. The class has a few extra's, but nothing fancy. Well, mayby the testForWall function, but that will be explained later on...
First, add this code, to call the procedure when the game is started:
(..)
Loadmap(0);
LoadItems(0);
LoadEnemyMap(0)
Then, it's time to write the drawing routine in the dxtimer event.
Make sure you place the code below the part that draws the tiles, otherwise you won't see the npcs.
for imgCounter := 0 to maxSprites do
begin
//(1) Npc moves right: turn npc to the left when it is close to an edge
if (TileInfo[(round(npc[imgCounter].x)+30) div 32 ,(round(npc[imgCounter].y)+65)
div 32].tilenr in [6,7,100]) or npc[imgCounter].testForWall(npc[imgCounter].x,
npc[imgCounter].y,npc[imgCounter].width+5,
npc[imgCounter].height) then
begin
npc[imgCounter].Direction := dLeft;
npc[imgCounter].startFrame := 16;
npc[imgCounter].CurrentFrame := npc[imgCounter].startFrame;
end;
//(1)Npc moves left: turn nps to the right when it is close to an edge
if (TileInfo[round(npc[imgCounter].x+2) div 32 ,round(npc[imgCounter].y+65)
div 32].tilenr in [6,7,100]) or npc[imgCounter].testForWall(npc[imgCounter].x,
npc[imgCounter].y, -5, npc[imgCounter].height) then
begin
npc[imgCounter].Direction := dRight;
npc[imgCounter].startFrame := 26;
npc[imgCounter].CurrentFrame := npc[imgCounter].startFrame;
end;
//(2)move the npc
if gamecounter mod 2 = 0 then
case npc[imgCounter].Direction of
dLeft : npc[imgCounter].x := npc[imgCounter].x - npc[imgCounter].speed;
dRight : npc[imgCounter].x := npc[imgCounter].x + npc[imgCounter].speed;
end;
//(3)If npc isn't dead then draw it
if npc[imgCounter].isDead = false then
begin
npc[imgCounter].ChangeFrame(0);
dxdraw1.Surface.draw(round(npc[imgCounter].x)+scrollHorizontal,round(npc[imgCounter].y),
npc[imgCounter].SpriteImg.clientrect, npc[imgCounter].SpriteImg,true);
end;
end;
A bit more code than the items had, I know, but then,... npc's move, items don't :) In short what is happening...
The testForWall function is called and returns a true/false value. If it's true the npc turns(1) otherwise it will move on (2).
But what exactly does the testForWall function do? Here's the answer...
function TOurSprite.testForWall(spriteXpos,spriteYpos:single;
spriteWidth,spriteHeight:integer):boolean;
begin
testForWall := false;
//test topside
if not (TileInfo[((round(spriteXpos)+spriteWidth) div 32),
(round(spriteYpos) div 32)].tilenr in [6,7,100])
//test bottomside
or not ((TileInfo[((round(spriteXpos)+spriteWidth) div 32),
((round(spriteYpos)+spriteHeight) div 32)].tilenr in [6,7,100])
and (TileInfo[(round(spriteXpos) div 32),
((round(spriteYpos)+spriteHeight) div 32)].tilenr in [6,7,100]))
then testForWall := true;
end;
The function is testing for four points: topleft, topright, bottomleft and bottomright. If one of those gets inside a tile which has a value of 6,7 or 100, which in this case means air or a background wall then it will return true, meaning the character has hit a wall. Otherwise it will return false, the character is free to go.
And last but not least, the changeFrame animation.
procedure TOurSprite.ChangeFrame(nr:byte);
begin
if nr = 0 then
begin
if GameCounter mod 5 = 0 then
begin
if CurrentFrame < startFrame+MaxFrame-1 then
inc(CurrentFrame)
else
CurrentFrame:= StartFrame;
end;
end
else
CurrentFrame := nr;
spriteImg.LoadFromGraphic(form1.dximagelist1.Items.Items[currentFrame].Picture.Graphic);
end;
Go ahead,.. look what we've done sofar.
The first steps
It's beginning to look pretty cool no? One important thing is still missing, though. Our hero.
Implementing him is not going to be as easy I'm afraid. The thing is, we can't just give him the same 'treatment' as the npcs.
They are 'stuck' on their own platform while our guy has to able to jump around from one platform to another.
Let's start with a new list of things to do:
Modify ourSprite class;
Draw hero at a certain starting position;
create basic walk routines;
add gravity;
make hero jump.
Right. let's start with the first piece of code, a couple of modifications to the class OurSprite.
type
TOurSprite = class(TImageSprite)
SpriteImg : TDirectDrawSurface;
Direction : TDirection;
walking : boolean;
isDead : boolean;
Speed : single;
startFrame : integer;
CurrentFrame : integer;
MaxFrame : integer;
isJumping : boolean;
OnPlatform : boolean;
inAir: boolean;
yVelocity : integer;
function testForWall(spriteXpos,spriteYpos:single;
spriteWidth,spriteHeight:integer):boolean;
procedure ChangeFrame(nr:byte);
//procedure testForCollision;
end;
var player : TOurSprite;
Before we are able to display our hero on to the screen, we have to create him. We'll do this in the oninitialize event.
procedure TForm1.DXDraw1Initialize(Sender: TObject);
(..)
player:= TOurSprite.Create(Form1.DXSpriteEngine1.Engine);
player.SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
player.SpriteImg.LoadFromGraphic(Form1.DXImagelist1.items.items[16].picture.graphic);
player.SpriteImg.TransparentColor:=clblack;
player.x:=6*32;
player.y:=(4*32)-14;
player.Height:= 46;
player.width := 28;
player.Direction:=dRight;
player.startFrame := 26;
player.CurrentFrame := player.startFrame;
player.MaxFrame := 9;
Nothing special to see here, so I'm quickly moving on to point three. The basic walk routines.
In the previous tutorial, I showed you how to make the map scroll. We are now going to use this code to make our guy move. The trick is to let the character move together with the screen behind him. Here's how it goes.
Modify the code which is used to scroll the screen in to the following:
DXInput1.Update;
player.walking :=false;
if not player.testForWall(player.x,player.y,-2,player.height-1) then
begin
if (isleft in DXInput1.States) then
begin
if (scrollHorizontal< 0) and (player.x + scrollHorizontal< 400) then
inc(scrollHorizontal,2);
if player.Direction <> dLeft then
begin
player.startFrame := 16;
player.currentFrame := 16;
end;
player.Direction:=dLeft;
player.walking:=true;
end;
end;
if not player.testForWall(player.x,player.y,player.width,player.height-1) then
begin
if (isRight in DXInput1.States) then
begin
if (player.x > 400) and (abs(scrollHorizontal) < (MAPWIDTH*32)-dxdraw1.Width)
then dec(scrollHorizontal,2);
if player.Direction <> dRight then
begin
player.startFrame := 26;
player.currentFrame := 26;
end;
player.Direction:=dRight;
player.walking:=true
end;
end;
if player.walking then
begin
case player.Direction of
dLeft : player.x := player.x - 2;
dRight : player.x := player.x + 2;
end;
player.ChangeFrame(0);
end
else
begin
case player.Direction of
dLeft : player.ChangeFrame(16);
dRight : player.ChangeFrame(26);
end;
end;
When the left or right button has been pressed and the testForWall function returns false (no hit) the map will start to scroll and our hero will start walking.
Notice the commented lines. When the end of the map has been reached (and the scrolling stops) what do you think will our character do?
Simple, our hero will continue to walk in the choosen direction (as it should be). But, what do think will happen when he starts to move in to the opposite direction? Correct, the screen will immediately start to scroll again. But that is not what we want. We want the screen to start scrolling when the hero is in the center. That is why I added that code. Go ahead and remove the {}, add the code that draws the guy and have a look at the game so far.
dxdraw1.surface.draw(round(player.x)+scrollHorizontal,round(player.y), player.SpriteImg.clientrect,
player.SpriteImg,true);
Adding gravity
When you run the game you'll see that our hero is not obeying the laws of gravity very well. This would be cool if he's superman, but, unfortunately for us, he's not. So we have to add some gravity to the game, which will hopefully give our character some vertical movements too...
When you've been looking around on the Web you might have seen this document . It's a pretty good explanation of how to add gravity to sprites. Excellent starting material for our platform game.
There's one tiny problem though. The article describes that when a certain point has been reached, the character should stop falling. The problem is that in a platform game, you don't know where that point is going to be. It could be 2 tiles below but it could also be 3 or 5 tiles. ie you can never for example say, "if player.y = 100 then falling:=false" or something like that. Because most of the time you'll end up somewhere inside a platform.
What we need to do is to figure out when such a point is, at a given time. There are probably numerous ways to solve this. This is one of them.
procedure TForm1.DXTimer1Timer(Sender: TObject; LagCount: Integer);
var xpos,ypos : integer;
stepsToFloor: integer;
floorFound : boolean;
imgCounter : integer;
begin
DXInput1.Update;
player.walking :=false;
stepsToFloor:=0;
floorFound := false;
player.inAir := true;
while (not floorFound) do
begin
dec(stepsToFloor);
if (player.testforwall(player.x,player.y,player.width-1,abs(stepsToFloor)))
then floorFound:= true;
if stepsToFloor < -100 then floorFound := true;
end;
Ok, what's happening here. It's actually pretty simple. We use a variable stepsToFloor in combination with the testForWall function. when y+stepsToFloor results is true then we have found a floor.
In case stepsToFloor reaches -100 (don't ask me why I used a negative value here) then apparently no floor has been found.
Notice that this piece of code will be called before the vertical movement is actually performed!
Now, when we do find a floor, ie stepsToFloor > -100. Then we continue with this:
if (abs(player.yVelocity) - abs(stepsToFloor+player.height)) >= 0 then
begin
player.y := abs(player.y + abs(stepsToFloor+player.height));
player.isJumping:=false;
player.OnPlatform:=true;
player.inAir:=false;
player.yVelocity:=0;
end
else
player.isJumping:=true;
We test the value of stepsToFloor and use it to calculate the y-velocity.
Have a look at the following example:
current y-velocity = 8;
current y-position = 400;
next platform = 416;
With these values all is well, the character will fall, the code above will not be used.
But at the next loop the y-velocity has increased a bit (see the doc why)
current y-velocity = 10;
current y-position = 408;
next platform = 416;
With the curtent velocity the character will end up inside the floor. This time however the code will be used and the y-velocity will be changed to the remaining value of stepsToFloor and the character will land exactly on top of the platform.
The rest is easy now.
if (player.inAir) then //* Move Joe if we are jumping */
begin
if (player.yVelocity > -40) then // Limit Joe's velocity to -40 */
begin
player.yVelocity := player.yVelocity - 1;
player.y := player.y - player.yvelocity;
end;
end;
Every time our hero is falling we decrease the y position a little.
As you can see, I even used some of Edgar Roman's comments to see what piece of his puzzle goes into mine :)
That's about it. You can run the game now.
The first jumps
When you run the game, you should be able to move the character and walk off of the platform and land a couple of tiles below.
The next thing we are going to do is handling the jumping. Because we do want to see our guy move towards the end of the level.
Fortunately with the code in the previous chapter doing all the hard work, this is only a matter of giving our hero a gentle kick in the butt. :)
if (isbutton1 in Form1.dxinput1.States) and (player.isjumping = false) then
begin
player.yVelocity:=10;
player.y:=player.y-5;
player.inAir:=true;
player.isJumping:=true;
player.OnPlatform:=false;
end;
if (player.inAir) then //* Move Joe if we are jumping */
(..)
There you go. When button 1 (spacebar) is pressed, the y_velocity is initiated, the y-position is placed a few pixels up (the kick in the butt) and a couple of vars are set. That's really all there is to it... it's all sooo easy :)
Beam me up scotty
In platform games it's not uncommen for characters to 'warp' themselves to other places on the map. How is this accomplished?
Well, it's really not that hard.
Let's say we have the following transport device in our game. (You should know by now how you draw and create images, so I'm going a little bit faster here)
var
teleporterStart : TDirectDrawSurface;
teleporterEnd : TDirectDrawSurface;
(..)
teleporterStart := TDirectDrawsurface.Create(Form1.DXDraw1.ddraw);
teleporterStart.LoadFromGraphic(Form1.DXImagelist1.items.items[14].picture.graphic);
teleporterStart.TransparentColor:=clblack;
teleporterEnd := TDirectDrawsurface.Create(Form1.DXDraw1.ddraw);
teleporterEnd.LoadFromGraphic(Form1.DXImagelist1.items.items[15].picture.graphic);
(..)
dxdraw1.Surface.Draw((32*16)+scrollHorizontal,(32*14),
teleporterStart.clientrect, teleporterStart,true);
dxdraw1.Surface.Draw((32*44)+scrollHorizontal,(32*3),
teleporterEnd.clientrect, teleporterEnd,false);
We can then use the following code to actually perform a transport.
Place this code just below the jump code:
if (isup in Form1.dxinput1.States) and ((round(player.x)-400 in [115..130]) and (player.y > 448)) then
begin
player.y:=(1*32)-14;
player.x:=(32*44);
scrollHorizontal := -(32*32);
end;
What's happing here is that I'm checking for the characters coordinates. If they match then a transport will occur. This actually means that the x- and y value of the hero as well as variable that holds the scrolling value are changed.
Note that in case you are going to use this in your own games, make sure you include the start/end coordinates in an editor!
Right...the only thing that is missing now is a fancy animation a la StarTrek, but I'll leave that to you :)
Finishing the items
Our game is coming up rather nicely now. Only a few things are remaining: Collecting the items and freeing the images.
Because of the way I handled the movements, we can't really use the collision detection system found in DelphiX. This isn't such a big problem though. The items in our game don't require a fancy detection system.
procedure TOurSprite.testForCollision;
var counter : byte;
begin
for counter := 0 to maxItems do
begin
if gameitem[counter].isDead = false then
begin
if (x+(width div 2) >= gameItem[counter].X) and
(x+(width div 2) <= gameItem[counter].x+gameItem[counter].width) and
((y+(height div 2) > gameItem[counter].y) and
(y+(height div 2) < gameItem[counter].y+gameItem[counter].height)) then
gameItem[counter].isDead := true;
end;
end;
end;
This is all. We loop through all the items and check for a certain point. If that point corresponds with the position of the player then we mark the item as dead, ie the item nolonger gets drawn.
To make all this work, we must not forget to make the call to this procedure.
procedure TForm1.DXTimer1Timer(Sender: TObject; LagCount: Integer);
var xpos,ypos : integer;
stepsToFloor: integer;
floorFound : boolean;
imgCounter : integer;
begin
(..)
player.testForCollision;
As we are nearing the end now, we must not forget to free all objects and images used in the game.
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var imgCounter: byte;
begin
for imgCounter := 0 to MAXTILES do Tiles[imgCounter].free;
buildings.free;
wallpaper1.free;
wallpaper2.free;
wallpaper3.free;
teleporterStart.free;
teleporterEnd.free;
for imgCounter := 0 to maxItems do
begin
gameitem[imgCounter].SpriteImg.free;
gameitem[imgCounter].Free;
end;
for imgCounter := 0 to maxSprites do
begin
npc[imgcounter].SpriteImg.free;
npc[imgCounter].free;
end;
player.SpriteImg.free;
player.Free;
end;
The End
Well done. The final piece of this tutorial has been layed and I can congratulate you on a job well done. If all is right, then you now have a working platform game.
Of course there are a lot of things that could need more work. A score system, real enemies that actually do something, or perhaps extra levels. But I'll leave those things entirely up to you. With the basics done, you are now ready to make it in to anything you want.
I'm looking forward to your creations.
Until next time, Happy coding
PS. I have added the complete source for this tutorial on the next page!
Full source code
Below the entire source for the game.
unit Tut4Unit;
//*******************************************************/
// Source for the tutorial: How to create a platform game
// Copyright 2002-2003 by Alexander Rosendal
// http://www.gameprogrammer.net
// for questions mail to: traveler@gameprogrammer.net
//*******************************************************/
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, DXDraws, DXSprite, DXInput, DXClass;
type
TTileInfo = record
tilenr : integer;
obstacle : boolean; // can both be wall or floor
item : byte; // bonus, health, etc
end;
type TDirection = (dLeft, dRight, rNone);
TGameItemType = (banana, apple);
type
TGameItem = class(TImageSprite)
SpriteImg : TDirectDrawSurface;
isDead : boolean;
kind : TGameItemType;
end;
type
TOurSprite = class(TImageSprite)
SpriteImg : TDirectDrawSurface;
Direction : TDirection;
walking : boolean;
isDead : boolean;
Speed : single;
startFrame : integer;
CurrentFrame : integer;
MaxFrame : integer;
isJumping : boolean;
OnPlatform : boolean;
inAir: boolean;
yVelocity : integer;
function testForWall(spriteXpos,spriteYpos:single;
spriteWidth,spriteHeight:integer):boolean;
procedure ChangeFrame(nr:byte);
procedure testForCollision;
end;
var player : TOurSprite;
const maxSprites = 10;
maxItems = 10;
var
gameitem : array [0..maxItems] of TGameItem;
npc : array [0..maxSprites] of TOurSprite;
var
teleporterStart : TDirectDrawSurface;
teleporterEnd : TDirectDrawSurface;
type
TForm1 = class(TForm)
DXDraw1: TDXDraw;
DXTimer1: TDXTimer;
DXInput1: TDXInput;
DXSpriteEngine1: TDXSpriteEngine;
DXImageList1: TDXImageList;
procedure DXDraw1Initialize(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure DXTimer1Timer(Sender: TObject; LagCount: Integer);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
procedure LoadMap(level:byte);
procedure LoadItems(level:byte);
procedure LoadenemyMap(level:byte);
{ Public declarations }
end;
const MAPHEIGHT = 18;
const MAPWIDTH = 100;
const MAXTILES = 7;
var
Form1: TForm1;
gameCounter : integer;
TileInfo : array [0..MAPWIDTH, 0..MAPHEIGHT ] of TTileInfo;
Tiles : array[0..MAXTILES] of TDirectDrawSurface;
scrollHorizontal: integer;
buildings : TDirectDrawSurface; // used for the background
wallpaper1 : TDirectDrawSurface;
wallpaper2 : TDirectDrawSurface;
wallpaper3 : TDirectDrawSurface;
implementation
{$R *.DFM}
procedure TForm1.DXDraw1Initialize(Sender: TObject);
var imgCounter : byte;
begin
buildings := TDirectDrawsurface.Create(DXDraw1.ddraw);
buildings.LoadFromGraphic(Form1.DXImagelist1.items.items[11].picture.graphic);
wallpaper1 := TDirectDrawsurface.Create(DXDraw1.ddraw);
wallpaper1.LoadFromGraphic(Form1.DXImagelist1.items.items[8].picture.graphic);
wallpaper2 := TDirectDrawsurface.Create(DXDraw1.ddraw);
wallpaper2.LoadFromGraphic(Form1.DXImagelist1.items.items[9].picture.graphic);
wallpaper3 := TDirectDrawsurface.Create(DXDraw1.ddraw);
wallpaper3.LoadFromGraphic(Form1.DXImagelist1.items.items[10].picture.graphic);
teleporterStart := TDirectDrawsurface.Create(Form1.DXDraw1.ddraw);
teleporterStart.LoadFromGraphic(Form1.DXImagelist1.items.items[14].picture.graphic);
teleporterStart.TransparentColor:=clblack;
teleporterEnd := TDirectDrawsurface.Create(Form1.DXDraw1.ddraw);
teleporterEnd.LoadFromGraphic(Form1.DXImagelist1.items.items[15].picture.graphic);
Loadmap(0);
LoadItems(0);
LoadEnemyMap(0);
player:= TOurSprite.Create(Form1.DXSpriteEngine1.Engine);
player.SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
player.SpriteImg.LoadFromGraphic(Form1.DXImagelist1.items.items[16].picture.graphic);
player.SpriteImg.TransparentColor:=clblack;
player.x:=6*32;
player.y:=(4*32)-14;
player.Height:= 46;
player.width := 28;
player.Direction:=dRight;
player.startFrame := 26;
player.CurrentFrame := player.startFrame;
player.MaxFrame := 9;
for imgCounter := 0 to MAXTILES do
begin
Tiles[imgCounter] := TDirectDrawsurface.Create(DXDraw1.ddraw);
Tiles[imgCounter].LoadFromGraphic(
Form1.DXImagelist1.items.items[imgCounter].picture.graphic);
end;
With dxdraw1.surface.Canvas do
begin
brush.Style := bsclear;
font.color := clred;
font.size := 9;
font.name := 'Arial';
end;
gameCounter := 1;
dxtimer1.Enabled:=true;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Height:=600;
Width:=800;
end;
procedure TForm1.DXTimer1Timer(Sender: TObject; LagCount: Integer);
var xpos,ypos : integer;
stepsToFloor: integer;
floorFound : boolean;
imgCounter : integer;
begin
DXInput1.Update;
player.walking :=false;
stepsToFloor:=0;
floorFound := false;
player.inAir := true;
while (not floorFound) do
begin
dec(stepsToFloor);
if (player.testforwall(player.x,player.y,player.width-1,abs(stepsToFloor)))
then floorFound:= true;
if stepsToFloor < -100 then floorFound := true;
end;
if (abs(player.yVelocity) - abs(stepsToFloor+player.height)) >= 0 then
begin
player.y := abs(player.y + abs(stepsToFloor+player.height));
player.isJumping:=false;
player.OnPlatform:=true;
player.inAir:=false;
player.yVelocity:=0;
end
else
player.isJumping:=true;
if (isbutton1 in Form1.dxinput1.States) and (player.isjumping = false) then
begin
player.yVelocity:=10;
player.y:=player.y-5;
player.inAir:=true;
player.isJumping:=true;
player.OnPlatform:=false;
end;
if (isup in Form1.dxinput1.States) and ((round(player.x)-400 in [115..130])
and (player.y > 448)) then
begin
player.y:=(1*32)-14;
player.x:=(32*44);
scrollHorizontal := -(32*32);
end;
if (player.inAir) then //* Move Joe if we are jumping */
begin
if (player.yVelocity > -40) then // Limit Joe's velocity to -40 */
begin
player.yVelocity := player.yVelocity - 1;
player.y := player.y - player.yvelocity;
end;
end;
if not player.testForWall(player.x,player.y,-2,player.height-1) then
begin
if (isleft in DXInput1.States) then
begin
if (scrollHorizontal< 0) and (player.x + scrollHorizontal< 400) then
inc(scrollHorizontal,2);
if player.Direction <> dLeft then
begin
player.startFrame := 16;
player.currentFrame := 16;
end;
player.Direction:=dLeft;
player.walking:=true;
end;
end;
if not player.testForWall(player.x,player.y,player.width,player.height-1) then
begin
if (isRight in DXInput1.States) then
begin
if (player.x > 400) and (abs(scrollHorizontal) < (MAPWIDTH*32)-dxdraw1.Width)
then dec(scrollHorizontal,2);
if player.Direction <> dRight then
begin
player.startFrame := 26;
player.currentFrame := 26;
end;
player.Direction:=dRight;
player.walking:=true
end;
end;
if player.walking then
begin
case player.Direction of
dLeft : player.x := player.x - 2;
dRight : player.x := player.x + 2;
end;
player.ChangeFrame(0);
end
else
begin
case player.Direction of
dLeft : player.ChangeFrame(16);
dRight : player.ChangeFrame(26);
end;
end;
inc(gameCounter);
player.testForCollision;
for imgCounter := 0 to 2 do
dxdraw1.Surface.Draw(imgCounter*357,0, buildings.clientrect, buildings,false);
for xpos := 0 to (form1.Width) div 32 do
for ypos := 0 to form1.Height div 32 do
dxdraw1.surface.draw((xpos*32)+(scrollHorizontal mod 32),(ypos*32),
tiles[1].clientrect,tiles[TileInfo[xpos+(abs(scrollHorizontal div 32)),ypos].tilenr],false);
dxdraw1.Surface.Draw((32*23)+scrollHorizontal,(32*10), wallpaper1.clientrect, wallpaper1,false);
dxdraw1.Surface.Draw((32*77)+scrollHorizontal,(32*5), wallpaper1.clientrect, wallpaper1,false);
dxdraw1.Surface.Draw((32*88)+scrollHorizontal,(32*13), wallpaper2.clientrect, wallpaper2,false);
dxdraw1.Surface.Draw((32*27)+scrollHorizontal,(32*12), wallpaper2.clientrect, wallpaper2,false);
dxdraw1.Surface.Draw((32*4)+scrollHorizontal,(32*10), wallpaper3.clientrect, wallpaper3,false);
dxdraw1.Surface.Draw((32*16)+scrollHorizontal,(32*14),
teleporterStart.clientrect, teleporterStart,true);
dxdraw1.Surface.Draw((32*44)+scrollHorizontal,(32*3),
teleporterEnd.clientrect, teleporterEnd,false);
for imgCounter:= 0 to maxItems do
begin
if Gameitem[imgCounter].isDead = false then
begin
dxdraw1.Surface.draw(round(Gameitem[imgCounter].x)
+scrollHorizontal,round(Gameitem[imgCounter].y),
Gameitem[imgCounter].SpriteImg.clientrect, Gameitem[imgCounter].SpriteImg,true);
end;
end;
for imgCounter := 0 to maxSprites do
begin
//(1) Npc moves right: turn npc to the left when it is close to an edge
if (TileInfo[(round(npc[imgCounter].x)+30) div 32 ,(round(npc[imgCounter].y)+65)
div 32].tilenr in [6,7,100]) or npc[imgCounter].testForWall(npc[imgCounter].x,
npc[imgCounter].y,npc[imgCounter].width+5,
npc[imgCounter].height) then
begin
npc[imgCounter].Direction := dLeft;
npc[imgCounter].startFrame := 16;
npc[imgCounter].CurrentFrame := npc[imgCounter].startFrame;
end;
//(1)Npc moves left: turn nps to the right when it is close to an edge
if (TileInfo[round(npc[imgCounter].x+2) div 32 ,round(npc[imgCounter].y+65)
div 32].tilenr in [6,7,100]) or npc[imgCounter].testForWall(npc[imgCounter].x,
npc[imgCounter].y, -5, npc[imgCounter].height) then
begin
npc[imgCounter].Direction := dRight;
npc[imgCounter].startFrame := 26;
npc[imgCounter].CurrentFrame := npc[imgCounter].startFrame;
end;
//(2)move the npc
if gamecounter mod 2 = 0 then
case npc[imgCounter].Direction of
dLeft : npc[imgCounter].x := npc[imgCounter].x - npc[imgCounter].speed;
dRight : npc[imgCounter].x := npc[imgCounter].x + npc[imgCounter].speed;
end;
//(3)If npc isn't dead then draw it
if npc[imgCounter].isDead = false then
begin
npc[imgCounter].ChangeFrame(0);
dxdraw1.Surface.draw(round(npc[imgCounter].x)+scrollHorizontal,
round(npc[imgCounter].y),
npc[imgCounter].SpriteImg.clientrect, npc[imgCounter].SpriteImg,true);
end;
end;
dxdraw1.surface.draw(round(player.x)+scrollHorizontal,round(player.y),
player.SpriteImg.clientrect, player.SpriteImg,true);
DXDraw1.flip;
end;
function TOurSprite.testForWall(spriteXpos,spriteYpos:single;
spriteWidth,spriteHeight:integer):boolean;
begin
testForWall := false;
//test topside
if not (TileInfo[((round(spriteXpos)+spriteWidth) div 32),
(round(spriteYpos) div 32)].tilenr in [6,7,100])
//test bottomside
or not ((TileInfo[((round(spriteXpos)+spriteWidth) div 32),
((round(spriteYpos)+spriteHeight) div 32)].tilenr in [6,7,100])
and (TileInfo[(round(spriteXpos) div 32),
((round(spriteYpos)+spriteHeight) div 32)].tilenr in [6,7,100]))
then testForWall := true;
end;
procedure TOurSprite.testForCollision;
var counter : byte;
begin
for counter := 0 to maxItems do
begin
if gameitem[counter].isDead = false then
begin
if (x+(width div 2) >= gameItem[counter].X) and
(x+(width div 2) <= gameItem[counter].x+gameItem[counter].width) and
((y+(height div 2) > gameItem[counter].y) and
(y+(height div 2) < gameItem[counter].y+gameItem[counter].height)) then
gameItem[counter].isDead := true;
end;
end;
end;
procedure TOurSprite.ChangeFrame(nr:byte);
begin
if nr = 0 then
begin
if GameCounter mod 5 = 0 then
begin
if CurrentFrame < startFrame+MaxFrame-1 then
inc(CurrentFrame)
else
CurrentFrame:= StartFrame;
end;
end
else
CurrentFrame := nr;
spriteImg.LoadFromGraphic(
form1.dximagelist1.Items.Items[currentFrame].Picture.Graphic);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var imgCounter: byte;
begin
for imgCounter := 0 to MAXTILES do Tiles[imgCounter].free;
buildings.free;
wallpaper1.free;
wallpaper2.free;
wallpaper3.free;
teleporterStart.free;
teleporterEnd.free;
for imgCounter := 0 to maxItems do
begin
gameitem[imgCounter].SpriteImg.free;
gameitem[imgCounter].Free;
end;
for imgCounter := 0 to maxSprites do
begin
npc[imgcounter].SpriteImg.free;
npc[imgCounter].free;
end;
player.SpriteImg.free;
player.Free;
end;
procedure TForm1.LoadMap(level:byte);
var xpos,ypos : integer;
currentColor : TColor;
Bitmap: TBitmap;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
for xpos:=0 to MAPWIDTH do
for ypos:=0 to MAPHEIGHT do
begin
currentColor := bitmap.Canvas.Pixels[xpos,ypos];
case currentColor of
clfuchsia..clAqua :
begin
if TileInfo[xpos-1,ypos].tilenr = 100 then
TileInfo[xpos,ypos].tilenr:=100
else TileInfo[xpos,ypos].tilenr:=6;
end;
clwhite : TileInfo[xpos,ypos].tilenr:=100;
clblack : TileInfo[xpos,ypos].tilenr:=5;
clred : TileInfo[xpos,ypos].tilenr:=6;
clblue : TileInfo[xpos,ypos].tilenr:=7;
clgreen : TileInfo[xpos,ypos].tilenr:=random(5);
end;
end;
except
messagedlg('error while loading the map',mtinformation,[mbok],0);
end;
Bitmap.free;
end;
procedure TForm1.LoadItems(level:byte);
var x,y : integer;
currentColor: TColor;
Bitmap: TBitmap;
itemCounter : integer;
itemKind : byte;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
itemCounter := 0;
for x:=0 to MAPWIDTH do
for y:=0 to MAPHEIGHT do
begin
itemKind := random(2);
currentColor := bitmap.Canvas.Pixels[x,y];
if (currentColor = clAqua) and (itemCounter <= maxItems) then
begin
gameItem[itemCounter] := TGameItem.Create(Form1.DXSpriteEngine1.Engine);
gameItem[itemCounter].x := x*32;
gameItem[itemCounter].y := y*32+2;
gameItem[itemCounter].isDead:=false;
gameItem[itemCounter].SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
gameItem[itemCounter].SpriteImg.LoadFromGraphic
(Form1.DXImagelist1.items.items[itemKind+12].picture.graphic);
gameItem[itemCounter].SpriteImg.TransparentColor:=clblack;
case itemKind of
0: begin
gameItem[itemCounter].kind := apple;
gameItem[itemCounter].width := 30;
gameItem[itemCounter].height := 27;
end;
1: begin
gameItem[itemCounter].kind := banana;
gameItem[itemCounter].width := 21;
gameItem[itemCounter].height := 29;
end;
end;
inc(itemCounter);
end;
end;
Bitmap.free;
except
messagedlg('error while loading the map',mtinformation,[mbok],0);
end;
end;
procedure TForm1.LoadEnemyMap(level:byte);
var x,y : integer;
currentColor: TColor;
Bitmap: TBitmap;
spriteCounter : byte;
begin
try
Bitmap:= TBitmap.Create;
Bitmap.LoadFromFile(ExtractFilePath(application.ExeName)+'fgland.bmp');
spriteCounter := 0;
for x:=0 to MAPWIDTH do
for y:=0 to MAPHEIGHT do
begin
currentColor:= bitmap.Canvas.Pixels[x,y];
if (currentColor = clfuchsia) and (spriteCounter <= maxSprites) then
begin
npc[spriteCounter] := TOurSprite.Create(Form1.DXSpriteEngine1.Engine);
npc[spriteCounter].x := x*32;
npc[spriteCounter].y := y*32-14;
npc[spriteCounter].width := 23;
npc[spriteCounter].height := 45;
npc[spriteCounter].isDead:=false;
npc[spriteCounter].Direction:=dRight;
npc[spriteCounter].speed := 1;
npc[spriteCounter].startFrame := 26;
npc[spriteCounter].CurrentFrame := npc[spriteCounter].startFrame;
npc[spriteCounter].MaxFrame := 9;
npc[spriteCounter].SpriteImg := TDirectDrawsurface.Create(DXDraw1.ddraw);
npc[spriteCounter].SpriteImg.LoadFromGraphic
(Form1.DXImagelist1.items.items[26].picture.graphic);
npc[spriteCounter].SpriteImg.TransparentColor:=rgb(125,150,255);
inc(spriteCounter);
end;
end;
Bitmap.free;
except
messagedlg('error while loading the map',mtinformation,[mbok],0);
end;
end;
end.
A GameProgrammer's Journey - Tutorial 4
Copyright Alexander Rosendal 2000-2003
Page 3
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!