PlayBasic Space Invaders tutorial part 1

In this part you will learn about the basics of game development with PlayBasic.

It is not an introduction to BASIC or programming. I assume you have a basic knowledge of programming in any language.

I will introduce you to types, images, sprites, the game loop and so on.
At the end of this tutorial we will have a simple version of Space Invaders running!

Part 2 of the tutorial

Update of the Space Invaders tutorial part 1!

Monday, February 18, 2008, 11:40 PM

Just wanted to let you all know that this tutorial is now updated for PlayBasic 1.60 and above, where PlayBasic 1.63 is the current stable release from Underware Design.

The tutorial offers now inherited types, pixel perfect collisions, native linked list support and uses collision classes. Read on to learn what great features PlayBasic offers even for this simple tutorial!

Have fun,
Tommy

Coding conventions or style

Ok, let's get started.
First we need to define some coding standards or conventions.
PlayBasic does not force you to follow given standards but you can or should define your own.

To help you ease your life here are mine which will be used in this tutorial:

  • The line Explicit True should be put at the beginning of your program. It will help you detect typos in your variable names because it will take care that you define all global, local or static variables before you use them. This is highly recommended and will save you hours of debugging. Promised.
  • Constants are values that never change. You should write them in capital letters (SCREEN_WIDTH, FPS_RATE,...).
  • Globals are values that are valid for the whole program and can be accessed everywhere. To identify them quickly I let all globals start with a capital letter (Score, LifesLeft, IntroRunning). You can also assign new values to Globals, they are not constant.
  • Locals are values that are only valid inside a Function or a PSub which is some special kind of function. They should start with a lowercase letter (index, name$, rate#).
  • PlayBasic knows three base types: Integers, Strings and Floats.
  • Integer identifiers have no trailing character or the '%' sign (lifes, health%).
  • String identifiers must end with the trailing character '$' (name$, HighScoreName$).
  • Float identifiers must end with the trailing character '#' (angle#, speed#).
  • Comments in your source code start with a ';' (semicolon). You can also use the keyword 'Rem' or C style comments like '//' or '/*' and '*/' for multiline comments.

Game Design

Before we start coding it's good to spend some time to think about what we are going to program.

The game is Space Invaders. It's simple and everybody knows it. So we already know what will be required:

  • a windowed or fullscreen game window to play in,
  • a ship at the bottom of the game window controlled by the player,
  • alien ships that move from left to right and right to left and whenever they reach the right or left boundary they come down towards the player,
  • bullets shot by the player or the alien ships,
  • all "game entities" like ship, aliens and bullets will be represented by sprites, which are based on images that we will load on startup.

That's pretty much it for the start.

In our first approach we will start directly with the game (no intro, no menu) and pressing 'Esc' will terminate the game immediately. Sound and music are also tasks for later tutorial parts.

A first glance of code

Let's start coding with a simple "framework" for a game and work through it line by line (or sometimes in bigger steps if possible).

Basic code template

%NOWIKI%

  1. IF PlayBasicVersion<160

#Abort "This Program Requires PlayBasic V1.60 or above"

  1. ENDIF

Explicit True

;
; CONSTANTS
;
Constant SCREEN_WIDTH = 640
Constant SCREEN_HEIGHT = 480
Constant SCREEN_DEPTH = 16
Constant WINDOWED = 1
Constant FULLSCREEN = 2
Constant FPS_RATE = 60

;
; TYPES
;

;
; GLOBALS
;
Global GameCamera
Global RealFPS

;
; THE GAME STARTS HERE
;
SetFPS FPS_RATE
OpenScreen SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, WINDOWED

; Activate Vsync only when in full screen mode. (it's ignored in windowed modes)
if GetScreenType() = FULLSCREEN then ScreenVsync True

; Create a new camera. This camera will auto attach itself to the screen, since the screen is the current surface we're drawing to
GameCamera = NewCamera()

;
; THE MAIN LOOP STARTS HERE
;
Do
RealFPS = FPS()
CaptureToScene
ClsScene
UpdateGame()
DrawCamera GameCamera
Sync
Loop

; THE GAME ENDS HERE, NOW ONLY FUNCTIONS AND PSUBS SHOULD FOLLOW

Psub UpdateGame()
Text 100,100,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub

Explanation of the Basic code template

The template begins with some version checking code. The commands all start with a "#" sign which means they are Compiler directives. So the source code won't even compile if your PlayBasic version is less than 1.60 in our example.
This is good behavior to guarantee your source code snippets will work on other users' PlayBasic as long as they have the minimum required version of PlayBasic installed.

Afterwards we start "the real code" by defining a few constants, for the screen size and color depth and for the FPS rate. Also we define two constants to specify windowed mode or fullscreen mode.

Then we define two global variables. One for the camera we use (read the section about cameras in PlayBasic's online help) and one global to store the current real frame rate.

After the comment "THE GAME STARTS HERE" the main code begins.

We set the game to a fixed frame rate of 60 (FPS_RATE) and create a game window of 640 x 480 with a color depth of 16 in windowed mode.

Setting the ScreenVSync to true will synchronize the screen refresh with the monitor refreshs which gives a smooth refreshing. This only takes effect in fullscreen mode.

We then create a camera.

And then we're already in the main loop. The main loop will change later on while we progress in game coding.

PlayBasic uses the 'Esc' key as the default break key which terminates your game immediately. So we don't need to add special code right now to leave this endless main loop.

In the main loop we retrieve the current frame rate and store it in our global variable RealFPS.
Then we direct all following drawing commands to the scene which will be displayed later on by the camera.
We clear the screen each frame.
Then we call UpdateGame() which will contain the whole game code when we're done with this tutorial. So it will contain update logic and drawing code.
After UpdateGame() is done we draw all that can be seen by our GameCamera and sync.
Sync means we flush the output and wait the necessary time to keep our set frame rate of 60.
And then the loop is done.

Adding the player

Now let's enter the player code.

We start with intoducing a type.

A type is nothing more but a structure to group data that belongs together.
You can allocate a single type or an array of types (which we will do later for aliens and bullets).

In PlayBasic you can even use type inheritance. This is a proper way to extend base types with additional fields and get specialized types.
In our Space Invaders game for example all elements (player, aliens, bullets) have an x and y position, they have a field to store the sprite which is drawn to represent them and they have a status (are they alive or dead).
So we will start with a base type TBaseObj and let the further types expand on that type.
All inheriting (or child) types can directly use all the fields of their base (or parent) type.


Type TBaseObj
x# ; xpos of sprite as float
y# ; ypos of sprite as float
status ; alive or not
spriteNo ; number of sprite that is used to display game element
EndType

In our player type we will store a flag if the player can shoot now.

So our player sprite would look like this:


Type TPlayer as TBaseObj
canShoot ; boolean which is true or false
EndType


The comments should explain the purpose of the fields.
Put both types into your code template in the Types section.
Types cannot be allocated by using Global or Local. Types must be dim'ed. So go to the GLOBALS section and create a player variable like this:


Dim Player As TPlayer ; create a player variable

Before I forget: add the two constants for the status at the bottom of the Constants section.


Constant DEAD = 0
Constant ALIVE = 1

Initializing the player

Let's initialize the player. We need to load an image, create a sprite and fill the player structure with proper values.

We stuff all that code into a nice little PSub we call InitPlayer().

Add a line (and a comment line) after creating the camera in the GAME STARTS HERE section.


; do some initialization stuff
InitPlayer()

And now to the end of the file to add the new PSub to initialize the player.


Psub InitPlayer()
Local img, sprite
; load the image and create the sprite, fill player type
img = GetFreeImage()
LoadImage "ship.png", img
Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(img)

sprite = NewSprite(Player.x#, Player.y#,img)
SpriteDrawMode sprite, 2
AutoCenterSpriteHandle sprite,True
PositionSpriteZ sprite, 10

Player.spriteNo = sprite
Player.status = ALIVE

EndPsub

Let's look at the PSub:

First we get a free handle for an image. Then we load the image (the zip file at the end of the tutorial contains all sources and images).
You can reuse the image to create more than one sprite of it (we will do so when we create the aliens).

We center the ship at the bottom of the screen.

We create a new Sprite with the ship's image and already place it at the proper position.
Sprites are the PlayBasic objects that can be moved, scaled, rotated and can collide with other sprites with collision detection available.

We set the drawing mode of the sprite to Rotation(2) and set it's auto centering. This means that the center of the sprite (used for drawing or rotating) is right at the center of the image.
We also set a Z position for the sprite which specifies the "depth" at which the player sprite is drawn. Sprites with lower Z positions are drawn below sprites with a higher Z position. Z positions must not be less than zero.

Now we store the sprite handle in the Player variable and set the status field to ALIVE.

Updating the player

To finally see the player ship on screen we need to add an UpdatePlayer() PSub which will later on also deal with keyboard input and currently only positions the player sprite.

Place it after the InitPlayer() Psub.


Psub UpdatePlayer()
PositionSprite Player.spriteNo, Player.x, Player.y
EndPsub

Now we need to modify the UpdateGame() PSub which will first call UpdatePlayer() and then assure that all sprites are drawn on the screen. We use DrawOrderedSprites here to assure that the sprites' Z position is considered.

Replace the new code with the old UpdateGame() PSub.


Psub UpdateGame()
UpdatePlayer()
DrawOrderedSprites
Text 100,100,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub

If you now start the game you should see your ship sprite at the bottom of the screen.
Congrats so far!

I like to move it, move it

A fixed ship is no fun in a shooter. Let's add some code to get it moving.

First we will extend the player type. To check if we collide with the boundaries we need to know the width of the player's ship (or more precisely only half the width).


Type TPlayer as TBaseObj
canShoot ; boolean which is true or false
wHalf ; half the width of player sprite as int
EndType

Additionally we need to give the player a moving speed. To allow easy adjustment of the speed while we playtest later on we will make the speed a constant (place it at the end of the other constants).


Constant PLAYER_SPEED# = 2.0


Because the speed is a float constant the name of the constant ends with an "#".
I think these simple changes don't need more detailed explanation.

We need to initialize the player's variable to store half the width of the image.
This will happen in the PSub InitPlayer().
Add the line


Player.wHalf = GetImageWidth(img) / 2


after setting the player's x and y position.

But the following code, the modified UpdatePlayer() routine might need some words to help you understand it.
Here we go:


Psub UpdatePlayer()
Local dx#, dy#

dx# = Player.x#
dy# = Player.y#
If LeftKey()
dx# = dx# - PLAYER_SPEED#
EndIf
If RightKey()
dx# = dx# + PLAYER_SPEED#
EndIf

If dx# < Player.wHalf
dx# = Player.wHalf
EndIf
If dx# > SCREEN_WIDTH - Player.wHalf
dx# = SCREEN_WIDTH - Player.wHalf
EndIf
Player.x# = dx#
Player.y# = dy#
PositionSprite Player.spriteNo, Player.x#, Player.y#
EndPsub


Replace your old PSub with this version.

Explanation of UpdatePlayer()

This PSub isn't too complicated.
We declare two local variables dx# and dy#. Their names tell us that they are float variables and the keyword Local tells us that those two variables are only valid inside this PSub. Outside they are simply nonexistant.

We store the original player coordinates in them.
Then we check if the left key is pressed. If yes we subtract the speed of the stored player position to move the player a bit to the left.
For the right key we do similar stuff except that we add the speed to move the player to the right.
Life would be easy if boundaries were not existant. But they are and so we need to check them.
We don't want to let the player move off at the left border of the screen. The left most position where we can draw the player ship is Player.wHalf, because we draw the player sprite centered. So we need to check our new calculated position (stored position minus speed) against Player.wHalf. If we are less than Player.wHalf and would leave the visible area we reset the calculated position to Player.wHalf.
We also don't want to let the player move off at the right side. The right most position to draw the player's ship is the right screen border (SCREEN_WIDTH) minus half the width of the ship image (remember we use the center of the image for drawing!).
So we check dx# against that value and if it is bigger we set it to the right most position.
Finally, when we are sure that our calculated position is fine, we assign them back to the player variables Player.x# and Player.y#.
The last line places the sprite of the player at the proper position.

Remark: You might have noticed that we didn't modify the y position of the player. That's fine for Space Invaders because the player's ship is not able to move up or down.

To end this part of the tutorial I show you all of the current code in one snippet:

%NOWIKI%
; PROJECT : SpaceInvaders
; AUTHOR : Tommy Haaks
; CREATED : 01.02.2008
; EDITED : 17.02.2008

  1. IF PlayBasicVersion<160

#Abort "This Program Requires PlayBasic V1.60 or above"

  1. ENDIF

Explicit True

;
; CONSTANTS
;
Constant SCREEN_WIDTH = 640
Constant SCREEN_HEIGHT = 480
Constant SCREEN_DEPTH = 16
Constant WINDOWED = 1
Constant FULLSCREEN = 2
Constant FPS_RATE = 60

Constant DEAD = 0
Constant ALIVE = 1

Constant PLAYER_SPEED# = 2.0

;
; TYPES
;
Type TBaseObj
x# ; xpos of sprite as float
y# ; ypos of sprite as float
status ; alive or not
spriteNo ; number of sprite that is used to display game element
EndType

Type TPlayer as TBaseObj
canShoot ; boolean which is true or false
wHalf ; half the width of player sprite as int
EndType

;
; GLOBALS
;
Global GameCamera
Global RealFPS

Dim Player As TPlayer ; create a player variable

;
; THE GAME STARTS HERE
;
SetFPS FPS_RATE
OpenScreen SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, WINDOWED

; Activate Vsync only when in full screen mode. (it's ignored in windowed modes)
If GetScreenType() = FULLSCREEN Then ScreenVsync True

; Create a new camera. This camera will auto attach itself to the screen, since the screen is the current surface we're drawing to
GameCamera = NewCamera()

; do some initialization stuff
InitPlayer()

;
; THE MAIN LOOP STARTS HERE
;
Do
RealFPS = FPS()
CaptureToScene
ClsScene
UpdateGame()
DrawCamera GameCamera
Sync
Loop

; THE GAME ENDS HERE, NOW ONLY FUNCTIONS AND PSUBS SHOULD FOLLOW

Psub UpdateGame()
UpdatePlayer()
DrawOrderedSprites
Text 100,100,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub

Psub InitPlayer()
Local img, sprite
; load the image and create the sprite, fill player type
img = GetFreeImage()
LoadImage "ship.png", img
Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(img)
Player.wHalf = GetImageWidth(img) / 2

sprite = NewSprite(Player.x#, Player.y#,img)
SpriteDrawMode sprite, 2
AutoCenterSpriteHandle sprite,True
PositionSpriteZ sprite, 10

Player.spriteNo = sprite
Player.status = ALIVE
EndPsub

Psub UpdatePlayer()
Local dx#, dy#

dx# = Player.x#
dy# = Player.y#
If LeftKey()
dx# = dx# - PLAYER_SPEED#
EndIf
If RightKey()
dx# = dx# + PLAYER_SPEED#
EndIf

If dx# < Player.wHalf
dx# = Player.wHalf
EndIf
If dx# > SCREEN_WIDTH - Player.wHalf
dx# = SCREEN_WIDTH - Player.wHalf
EndIf
Player.x# = dx#
Player.y# = dy#
PositionSprite Player.spriteNo, Player.x#, Player.y#
EndPsub

Now give it a run and see if your ship moves left and right.
Enjoy!

...loving the aliens...

For the aliens we create a new type. It will look similar to the TPlayer type and will also inherit from type TBaseObj. Additionally we will use PlayBasic's native list support to define a list of aliens.


Type TAlien as TBaseObj
w ; width of alien sprite as int
h ; height of alien sprite as int
EndType


Add the type below the TPlayer type just in front of the globals.

We will also add two constants. One is to specify the horizontal border which will be used when we have to decide if the aliens move down a bit towards the player. The other one defines the collision mode we use in the game, pixel perfect collision mode.
Add it at the end of the other constants.


Constant HORIZ_BORDER = 20

Constant PIXEL_COLLISION = 6


And we will define the global list to store all our aliens. Add the following line at the end of the Globals section.


Dim Aliens As TAlien List


This declares Aliens as a global reference to a list where each element is of type TAlien.

Now all we need to code is some initialization stuff to create aliens and some update code to move them (and later on let them shoot and do collision detection).

We have to load the image of the alien and later on the image of the bullet too so we better create some globals for all images and add a function to load all images on startup.
Place the globals in the globals section.


Global PlayerImage, AlienImage, BulletImage, AlienBulletImage

Now let's code the function to load the images on startup.


Psub InitImages()
PlayerImage = LoadNewImage("ship.png")
PrepareFXimage PlayerImage
AlienImage = LoadNewImage("alien.png")
PrepareFXimage AlienImage
BulletImage =LoadNewImage("shot.png")
PrepareFXimage BulletImage

; the alien bullets are just a mirrored version of the bullet image
AlienBulletImage = GetFreeImage()
CopyImage BulletImage, AlienBulletImage
MirrorImage AlienBulletImage, 0, 1
PrepareFXimage AlienBulletImage
EndPsub


Place this PSub right after the main loop after the comment "THE GAME ENDS HERE, ...".

Finally we need to call this function to load the images. Do this before the call of InitPlayer().


; do some initialization stuff
InitImages()
InitPlayer()


One last thing is left to do: we need to fix the InitPlayer() PSub to use our new global PlayerImage variable. It should now look like this:


Psub InitPlayer()
Local sprite

Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(PlayerImage)
Player.wHalf = GetImageWidth(PlayerImage) / 2

sprite = NewSprite(Player.x#, Player.y#,PlayerImage)
SpriteDrawMode sprite, 2
AutoCenterSpriteHandle sprite,True
PositionSpriteZ sprite, 10

Player.spriteNo = sprite
Player.status = ALIVE
EndPsub


Oh, and it's time for the alien image (see it at the end of this paragraph). Again it's in the zip at the end of this tutorial.

Let's code the initialization PSubs that will place the aliens on screen.

Place the following functions at the end of the file.

%NOWIKI%
; ALIEN FUNCTIONS AND PSUBS
Psub InitAliens()
Local x, y
; let's have 5 rows of 8 aliens each
For y = 1 To 5
For x = 1 To 8
AddAlien(Aliens(), x, y, AlienImage)
Next x
Next y
EndPsub

Psub AddAlien(lst.TAlien, x, y, img)
Local sprite, width, height

lst = New TAlien
width = GetImageWidth(img)
height = GetImageHeight(img)
lst.x# = HORIZ_BORDER + (x-1) * (width + 10)
lst.y# = 10 + (y-1) * (height + 10)
lst.w = width
lst.h = height

sprite = NewSprite(lst.x#, lst.y#, img)
SpriteCollision sprite, True
SpriteCollisionMode sprite, PIXEL_COLLISION
PositionSpriteZ sprite, 5
#IF PBDebug=True
SpriteCollisionDebug sprite, 1
#ENDIF
lst.spriteNo = sprite
EndPsub

Now for the explanation:
InitAliens() just uses two nested loops to create 5 rows of 8 aliens.
Inside the loops we call AddAlien() to add aliens to the list. The parameters are the global reference to the Aliens list, the x and y indices of the loop and the image to use for the alien.

In the PSub AddAlien we do just that: we create a new TAlien object which will be automatically added to the list. We use the name of the list parameter lst to allocate a new TAlien and add it to the list. lst is also used to reference the newly created TAlien object when we want to set the type fields.

After creating (and automatically adding) the alien object in PrepareAlien() we first retrieve the width and height of the alien image which we are going to store in each alien.
Then we calculate the x and y coordinates on screen for the alien, based on the loop values we got passed in. After storing width and height of the image we create a new sprite for the given image and store the handle of the sprite in our Alien's spriteNo and position the sprite at it's starting position.

Additionally we activate the collision mode and set a Z depth for aliens. The value 5 assures that aliens would be drawn above the player ship.

Let's add the call to InitAliens() below InitImages() and InitPlayer().


; do some initialization stuff
InitImages()
InitPlayer()
InitAliens()

Next thing will be the update routine for the aliens to make them move.

The chances of anything coming from Mars are a million to one...

...but still they come!

As usual first the source code and then the explanation:


Psub UpdateAliens()
Local dirChange
dirChange = DirectionChangeForAliensRequired()
If dirChange
AlienMoveDir# = - AlienMoveDir#
EndIf

For Each Aliens()
Aliens.x# = Aliens.x# + AlienMoveDir#
If dirChange
Aliens.y# = Aliens.y# + ALIEN_Y_STEP
If Aliens.y# + Aliens.h > SCREEN_HEIGHT
GameOver = True
EndIf
EndIf
PositionSprite Aliens.spriteNo, Aliens.x#, Aliens.y#
Next
EndPsub

Function DirectionChangeForAliensRequired()
For Each Aliens()
If (Aliens.x# + AlienMoveDir#) < HORIZ_BORDER Or (Aliens.x# + AlienMoveDir# + Aliens.w) > (SCREEN_WIDTH - HORIZ_BORDER)
; we need to change direction and go down a bit
Exitfunction True
EndIf
Next
EndFunction False

I wonder how they do it...

Okay - two functions to explain. But first let's introduce some more constants and globals.


Constant ALIEN_SPEED# = 1.0
Constant ALIEN_Y_STEP = 20


ALIEN_SPEED contains the speed the aliens move each update. With a speed of 1 and a frame rate of 60 the aliens will move 60 pixel per second.
ALIEN_Y_STEP is the y offset that all aliens will move down towards the player ship as soon as one alien touches the border.

And the globals:


Global GameOver = False
Global AlienMoveDir# = ALIEN_SPEED#


The second variable AlienMoveDir# contains the current direction and speed of the aliens. If they touch a border we multiply this global with -1 and this way reverse the direction. That's simple.
The first variable GameOver describes just what it says: the game is over or not. It's initialized with false.

I quickly update the function UpdateGame() to query the GameOver variable and introduce a short PSub UpdateGameOver() that just displays a "game over" text. You'll certainly get that without further explanation.


Psub UpdateGame()
If Not GameOver
UpdatePlayer()
UpdateAliens()
DrawOrderedSprites
Else
UpdateGameOver()
EndIf
Text 0,0,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub

Psub UpdateGameOver()
Local msg$, w
msg$ = "G A M E O V E R"
w = GetTextWidth(msg$)
CenterText SCREEN_WIDTH/2 - (w/2), SCREEN_HEIGHT/2, msg$
EndPsub

Okay, but let's have the UpdateAliens() PSub explanation now.
First we call a function which will return true if the aliens need to scroll down a bit and change direction and false otherwise. We store the result in dirChange.
If we need to change direction we just reverse it (stored in the global variable AlienMoveDir#).
Then we loop through all aliens using the marvelous For Each loop to iterate over a list.
The name of the current alien is the same as the list name, here Aliens.
For the current alien we change it's x position by adding the AlienMoveDir#.
If a direction change is required we also increase the y position of our alien.
If we detect that the current alien reaches the bottom of the screen we set the global GameOver to true.
Finally we call PositionSprite for the current alien with it's updated position to get it drawn at the proper place.

The second function DirectionChangeForAliensRequired() isn't difficult.
Again we loop through all aliens using the For Each loop. As soon as we find an alien where it's x coordinate leaves the border to the left or the right of the screen we return true.
If we don't find an alien that crosses the horizontal borders we return false to tell the calling function that no direction change is required.

All code in one go

Before we start to add bullets and collisions I give you all the code in one piece. Just in case you failed to follow my explanations in germish (mixture of english words with german grammar...).
%NOWIKI%


; PROJECT : SpaceInvaders
; AUTHOR : Tommy Haaks
; CREATED : 01.02.2008
; EDITED : 17.02.2008

  1. IF PlayBasicVersion<160

#Abort "This Program Requires PlayBasic V1.60 or above"

  1. ENDIF

Explicit True

;
; CONSTANTS
;
Constant SCREEN_WIDTH = 640
Constant SCREEN_HEIGHT = 480
Constant SCREEN_DEPTH = 16
Constant WINDOWED = 1
Constant FULLSCREEN = 2
Constant FPS_RATE = 60

Constant DEAD = 0
Constant ALIVE = 1

Constant PLAYER_SPEED# = 2.0

Constant HORIZ_BORDER = 20

Constant PIXEL_COLLISION = 6
Constant ALIEN_SPEED# = 1.0
Constant ALIEN_Y_STEP = 20

;
; TYPES
;
Type TBaseObj
x# ; xpos of sprite as float
y# ; ypos of sprite as float
status ; alive or not
spriteNo ; number of sprite that is used to display game element
EndType

Type TPlayer As TBaseObj
canShoot ; boolean which is true or false
wHalf ; half the width of player sprite as int
EndType

Type TAlien As TBaseObj
w ; width of alien sprite as int
h ; height of alien sprite as int
EndType

;
; GLOBALS
;
Global GameCamera
Global RealFPS
Global PlayerImage, AlienImage, BulletImage, AlienBulletImage

Dim Player As TPlayer ; create a player variable

Dim Aliens As TAlien List
Global GameOver = False
Global AlienMoveDir# = ALIEN_SPEED#

;
; THE GAME STARTS HERE
;
SetFPS FPS_RATE
OpenScreen SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_DEPTH, WINDOWED

; Activate Vsync only when in full screen mode. (it's ignored in windowed modes)
If GetScreenType() = FULLSCREEN Then ScreenVsync True

; Create a new camera. This camera will auto attach itself to the screen, since the screen is the current surface we're drawing to
GameCamera = NewCamera()

; do some initialization stuff
InitImages()
InitPlayer()
InitAliens()

;
; THE MAIN LOOP STARTS HERE
;
Do
RealFPS = FPS()
CaptureToScene
ClsScene
UpdateGame()
DrawCamera GameCamera
Sync
Loop

; THE GAME ENDS HERE, NOW ONLY FUNCTIONS AND PSUBS SHOULD FOLLOW

Psub InitImages()
PlayerImage = LoadNewImage("ship.png")
PrepareFXImage PlayerImage
AlienImage = LoadNewImage("alien.png")
PrepareFXImage AlienImage
BulletImage =LoadNewImage("shot.png")
PrepareFXImage BulletImage

; the alien bullets are just a mirrored version of the bullet image
AlienBulletImage = GetFreeImage()
CopyImage BulletImage, AlienBulletImage
MirrorImage AlienBulletImage, 0, 1
PrepareFXImage AlienBulletImage
EndPsub

Psub UpdateGame()
If Not GameOver
UpdatePlayer()
UpdateAliens()
DrawOrderedSprites
Else
UpdateGameOver()
EndIf
Text 0,0,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub

Psub UpdateGameOver()
Local msg$, w
msg$ = "G A M E O V E R"
w = GetTextWidth(msg$)
CenterText SCREEN_WIDTH/2 - (w/2), SCREEN_HEIGHT/2, msg$
EndPsub

Psub InitPlayer()
Local sprite

Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(PlayerImage)
Player.wHalf = GetImageWidth(PlayerImage) / 2

sprite = NewSprite(Player.x#, Player.y#,PlayerImage)
SpriteDrawMode sprite, 2
AutoCenterSpriteHandle sprite,True
PositionSpriteZ sprite, 10

Player.spriteNo = sprite
Player.status = ALIVE
EndPsub

Psub UpdatePlayer()
Local dx#, dy#

dx# = Player.x#
dy# = Player.y#
If LeftKey()
dx# = dx# - PLAYER_SPEED#
EndIf
If RightKey()
dx# = dx# + PLAYER_SPEED#
EndIf

If dx# < Player.wHalf
dx# = Player.wHalf
EndIf
If dx# > SCREEN_WIDTH - Player.wHalf
dx# = SCREEN_WIDTH - Player.wHalf
EndIf
Player.x# = dx#
Player.y# = dy#
PositionSprite Player.spriteNo, Player.x#, Player.y#
EndPsub

; ALIEN FUNCTIONS AND PSUBS
Psub InitAliens()
Local x, y
; let's have 5 rows of 8 aliens each
For y = 1 To 5
For x = 1 To 8
AddAlien(Aliens(), x, y, AlienImage)
Next x
Next y
EndPsub

Psub AddAlien(lst.TAlien, x, y, img)
Local sprite, width, height

lst = New TAlien
width = GetImageWidth(img)
height = GetImageHeight(img)
lst.x# = HORIZ_BORDER + (x-1) * (width + 10)
lst.y# = 10 + (y-1) * (height + 10)
lst.w = width
lst.h = height

sprite = NewSprite(lst.x#, lst.y#, img)
SpriteCollision sprite, True
SpriteCollisionMode sprite, PIXEL_COLLISION
PositionSpriteZ sprite, 5
#IF PBDebug=True
SpriteCollisionDebug sprite, 1
#ENDIF
lst.spriteNo = sprite
EndPsub

Psub UpdateAliens()
Local dirChange
dirChange = DirectionChangeForAliensRequired()
If dirChange
AlienMoveDir# = - AlienMoveDir#
EndIf

For Each Aliens()
Aliens.x# = Aliens.x# + AlienMoveDir#
If dirChange
Aliens.y# = Aliens.y# + ALIEN_Y_STEP
If Aliens.y# + Aliens.h > SCREEN_HEIGHT
GameOver = True
EndIf
EndIf
PositionSprite Aliens.spriteNo, Aliens.x#, Aliens.y#
Next
EndPsub

Function DirectionChangeForAliensRequired()
For Each Aliens()
If (Aliens.x# + AlienMoveDir#) < HORIZ_BORDER Or (Aliens.x# + AlienMoveDir# + Aliens.w) > (SCREEN_WIDTH - HORIZ_BORDER)
; we need to change direction and go down a bit
Exitfunction True
EndIf
Next
EndFunction False

This code should be fully functional. If you let it run you can move the ship left and right and the aliens should approach you. If the aliens reach the bottom of the screen the game should display "G A M E O V E R" in the center of the screen.
Terminate the game at any time with the "Esc" key.

Bite the bullet, baby!

Time to shoot some stuff, eh?

Let's add a constant for the bullet speed (at the end of the Constants section).


Constant BULLET_SPEED# = 4.0

We add a bullet type in the Types section after the TAlien Type.


Type TBullet As TBaseObj
h ; height of bullet sprite as int
dy# ; movement direction, up or down
playerShot ; this bullet was shot by a player
EndType


Nothing new here except the field dy# which we will use to implement bullets that can move up (dy# is negative) or down (dy# is positive). The field playerShot is a boolean (true or false) to separate between shots of the player and the aliens.

And finally a global list to store all our bullets that are on the screen.


Dim Bullets As TBullet List


This declares Bullets as a global reference to a list where each element is of type TBullet.

We need some code to add bullets if the player or aliens fire. So let's create a PSub for it.


; Bullet functionality

Psub FireBullet(lst.TBullet, x, y, playerShot)
Local sprite, height, width, img, dy#

lst = New TBullet
If playerShot
img = BulletImage
dy# = -BULLET_SPEED#
Else
img = AlienBulletImage
dy# = BULLET_SPEED#
EndIf
width = GetImageWidth(img)
height = GetImageHeight(img)

lst.x# = x - (width/2)
lst.y# = y
lst.h = height
lst.playerShot = playerShot
lst.dy# = dy#

sprite = NewSprite(lst.x#, lst.y#, img)
SpriteCollision sprite, True
SpriteCollisionMode sprite, PIXEL_COLLISION
PositionSpriteZ sprite, 15 ; z order: draw bullets below aliens and player ship
lst.spriteNo = sprite
PositionSprite lst.spriteNo, lst.x#, lst.y#
lst.status = ALIVE
EndPsub


So what happens in FireBullet()?
Input parameters are the global bullet list, x and y position where the bullet should start, also a boolean to indicate if this bullet is fired by the player or an alien.
We create some local variables first. Then we create a new bullet.
We set the image, depending who shot and set the speed in local variable dy#. The speed is negative for the player because we want the bullet to move up.
We then retrieve width and height for the image, similar to the alien's creation routine.
We calculate the x# position of the bullet to be exactly in the middle of the passed in parameter x position. We store the height of the image in the bullet. This is used to calculate later on if the bullet left the screen.
And again we create a sprite, set collision mode and store it in the Bullet type.
We set a Z depth of 15. This value is higher than the player and alien Z depth so the bullets are drawn below the player and alien sprites.

Finally we position the bullet on the calculated x# and y# coordinates and set the bullet's status to ALIVE.

Of course we need to destroy bullets later on when they leave the screen or when they collide with an alien or the player.


Psub DestroyObject(me.TBaseObj)
; release the sprite
If me.spriteNo Then DeleteSprite me.spriteNo
; delete the object itself from the list
me = NULL
EndPsub


Explanation:
The PSub DestroyObject() can be used for Bullets and Aliens because we use the parent type TBaseObj as parameter.
We need to release the sprite to avoid ressource leaks. And we assign NULL to our bullet (or alien) to free up the memory and remove it from the native list.
That was easy. So let's proceed with the UpdateBullets() PSub.


Psub UpdateBullets()
For Each Bullets()
Bullets.y# = Bullets.y# + Bullets.dy
If (Bullets.y# > SCREEN_HEIGHT) Or (Bullets.y# + Bullets.h) < 0
Bullets.status = DEAD
Else
PositionSprite Bullets.spriteNo, Bullets.x#, Bullets.y#
EndIf
Next
EndPsub


What happens here? Simple, we loop through all of the Bullets, the current bullet is accessible by the name of the list, here Bullets.
We move the current bullet in its given direction (either up or down).
If the bullet has left the screen we set it's status to DEAD. We don't delete it directly here, because removing elements from a list we are just traversing is dangerous and can lead to unexpected results...
If the bullet is still visible we position it on its new x# and y# position.

To "get rid" of DEAD objects we'll add another PSub to delete all DEAD elements.


Psub ClearDeadObjects()
; start with bullets
For Each Bullets()
If Bullets.status=DEAD
If Bullets.playerShot
; allow the player to shoot again
Player.canShoot = True
EndIf
DestroyObject(Bullets()); remove this bullet
EndIf
Next
EndPsub


When we delete a player bullet we reset Player.canShoot to allow the player to fire his next bullet.

Now if we could just fire a bullet and see it all work...
First we need to add the call of UpdateBullets() and ClearDeadObjects() to the UpdateGame() PSub. Do it like this:


Psub UpdateGame()
If Not GameOver
UpdatePlayer()
UpdateAliens()
UpdateBullets()
DrawOrderedSprites
Else
UpdateGameOver()
EndIf
ClearDeadObjects()
Text 0,0,"Main Loop with " + Str$(RealFPS) + " FPS"
EndPsub


Now bullets would be drawn and deleted if they were on screen.
So let's give the player the chance to fire a shot.
If the player presses one of the Ctrl keys he can fire a single shot.


Psub UpdatePlayer()
Local dx#, dy#

dx# = Player.x#
dy# = Player.y#
If LeftKey()
dx# = dx# - PLAYER_SPEED#
EndIf
If RightKey()
dx# = dx# + PLAYER_SPEED#
EndIf
If CtrlKeys(0) And Player.canShoot
FireBullet(Bullets(), Player.x#, Player.y#, True)
Player.canShoot = False
EndIf

If dx# < Player.wHalf
dx# = Player.wHalf
EndIf
If dx# > SCREEN_WIDTH - Player.wHalf
dx# = SCREEN_WIDTH - Player.wHalf
EndIf
Player.x# = dx#
Player.y# = dy#
PositionSprite Player.spriteNo, Player.x#, Player.y#
EndPsub


Replace this UpdatePlayer() PSub with the old one.

We also need to initialize the player to be able to shoot! So we need to add the line


Player.canShoot = True


at the end of the PSub InitPlayer().

If you now run the game you should be able to shoot single shots. Try it now!

Next thing to come will be collisions - hang on!

Don't shoot us, we shoot you!

Of course it wouldn't be Space Invaders if the player was the only one being able to shoot.
So let's teach the aliens!

Let's just think about a simple rule of shooting and limitations: for a start there shouldn't be more than 3 bullets from aliens on their way at the same time. They should shoot at random times and any alien should be able to shoot.

Let's try to code that.

First we need a PSub that counts how many alien bullets are underway.


Psub NumberAlienBullets()
Local alienBullets
alienBullets = 0
For Each Bullets()
If Bullets.status = ALIVE And Bullets.playerShot = False
Inc alienBullets
EndIf
Next
EndPsub alienBullets


Put this code at the end of the source code. Explanation is pretty easy. We loop through all bullets and search for bullets which are alive and shot by an alien (playerShot = False). We increment alienBullets for each matching bullet.
This value is returned as the result of the PSub.

Now we need to add some code that randomly decides to add another alien shot if the number of active alien bullets is less than our maximum.
So first the maximum:


Constant MAX_ALIEN_BULLETS = 3


Put it behind the other two alien constants, just to keep all alien related constants next to each other.

And now let's modify the UpdateAliens() PSub to add the alien's shooting.
The new UpdateAliens() PSub looks like this:


Psub UpdateAliens()
Local dirChange
dirChange = DirectionChangeForAliensRequired()
If dirChange
AlienMoveDir# = - AlienMoveDir#
EndIf

For Each Aliens()
Aliens.x# = Aliens.x# + AlienMoveDir#
If dirChange
Aliens.y# = Aliens.y# + ALIEN_Y_STEP
If Aliens.y# + Aliens.h > SCREEN_HEIGHT
GameOver = True
EndIf
EndIf
PositionSprite Aliens.spriteNo, Aliens.x#, Aliens.y#
If Rnd(100) > 95 ; a 5 percent chance to fire a bullet
If NumberAlienBullets() < MAX_ALIEN_BULLETS
; let this alien fire a shoot
FireBullet(Bullets(), Aliens.x# + (Aliens.w / 2), Aliens.y# + Aliens.h, False)
EndIf
EndIf
Next
EndPsub


The code changes were added after the PositionSprite line.
These are the inserted lines:


If Rnd(100) > 95 ; a 5 percent chance to fire a bullet
If NumberAlienBullets() < MAX_ALIEN_BULLETS
; let this alien fire a shoot
FireBullet(Bullets(), Aliens.x# + (Aliens.w / 2), Aliens.y# + Aliens.h, False)
EndIf
EndIf


So for each active alien we check if a random value between 0 and 100 is bigger than 95. If the number of active alien bullets is less than MAX_ALIEN_BULLETS (3 in our case) we let the alien fire a bullet. The bullet starts in the middle of the alien sprite (which is at x position Aliens.x# + (Aliens.w / 2) and at its bottom (which is at y position Aliens.y# + Aliens.h) .
Et voila, shooting aliens.
But don't be afraid, they can't harm you right now. But we will change this in a minute.
For now start your game and look at them shooting at you!

Prepare to bump, baby!

%NOWIKI% Collision detection in PlayBasic is simple.
Basically there are 4 different collision modes for sprites that you can use:

  • circles
  • rectangles
  • shapes
  • pixel perfect

There are some more collision modes but we can ignore them for now.

I will use just one of them - the pixel perfect collision mode.
Many times you will get along with this collision mode for your games.

We already added the constant for the pixel perfect collision mode.


Constant PIXEL_COLLISION = 6


You need to set the collision mode for every sprite you'll use in the game. The collision mode needs to be activated for the sprite and afterwards you need to tell PlayBasic which collision mode is to use for this sprite.
That means you can have a different collision mode for each sprite! And you can even have sprites that completely ignore collisions with other sprites and aren't checked against collisions at all!

We already told PlayBasic to use pixel perfect collision mode for the aliens and the bullets.
Let's quickly add it also for the player in PSub InitPlayer().


Psub InitPlayer()
Local sprite

Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(PlayerImage)

Player.wHalf = GetImageWidth(PlayerImage) / 2

sprite = NewSprite(Player.x#, Player.y#,PlayerImage)
SpriteDrawMode sprite, 2
AutoCenterSpriteHandle sprite,True
SpriteCollision sprite, True
SpriteCollisionMode sprite, PIXEL_COLLISION
#IF PBDebug=True
SpriteCollisionDebug sprite, 1
#ENDIF

Player.spriteNo = sprite
Player.status = ALIVE
Player.canShoot = True
EndPsub


So what did we add?
We switched on sprite collision detection for the sprite and set the mode to pixel perfect.
The last added 3 lines


#IF PBDebug=True
SpriteCollisionDebug sprite, 1
#ENDIF


are a very cool feature of PlayBasic. With #IF and #ENDIF you can add code based on compile time expressions. So if at compile time the variable PBDebug is true the following line SpriteCollisionDebug sprite, 1 will be added to the code. If PBDebug is false the line will not be added.
And even cooler: If you run the game in debug mode (by hitting 'F7' in the IDE) this boolean variable PBDebug will be set to true automatically. If you run the game in normal mode (by hitting 'F5') the variable PBDebug will be false.
How cool is that?
And SpriteCollisionDebug enables the drawing of the collision area for the sprite. So you can easily verify if your collision areas are positioned and sized properly.

Anything left before bumping?

What's left? Oh yes, at the beginning I promised to show you the usage of collision classes which is a very effective way to check collisions by calling the PlayBasic function SpriteHit().
There is another way to check for sprite collisions by calling SpritesOverlap() but this implies some brute force coding on your side by checking each sprite against all the others. Pretty time consuming and slow compared to SpriteHit() which additionally can take advantage of only checking sprites with involved collision classes.

So let's setup some collision classes which must be powers of two.


Constant PLAYER_COLLISION_CLASS = 1
Constant ALIEN_COLLISION_CLASS = 2
Constant PLAYER_BULLETS_COLLISION_CLASS = 4
Constant ALIEN_BULLETS_COLLISION_CLASS = 8

Now let's add the collision classes to the sprites where they are created.

Add the following line to PSub InitPlayer() behind the line where we set the pixel perfect collision mode.


SpriteCollisionClass sprite, PLAYER_COLLISION_CLASS

Do the similar thing for the PSub AddAlien(). Place the following line behind the line where we set the pixel perfect collision mode for the alien sprites.


SpriteCollisionClass sprite, ALIEN_COLLISION_CLASS

In the PSub FireBullet() add the following lines behind the line where the collision mode is set for the bullet sprites. We need to separate if the bullet is a player bullet or an alien bullet.


If (playerShot)
SpriteCollisionClass sprite, PLAYER_BULLETS_COLLISION_CLASS
Else
SpriteCollisionClass sprite, ALIEN_BULLETS_COLLISION_CLASS
EndIf

We are nearly done. We got only one problem left:

Where's my sprite? Where's my type? Am I alive?

When you've followed the tutorial carefully, you will have realized one problem:

  • Our aliens and bullets are stored in types,
  • the types have a field spriteNo to store "their" sprite,
  • collisions are setup and checked for sprites,
  • when two sprites collide I need to do something with their appropriate types like updating or deleting them,
  • but there's no way back from sprite to type!

But have no fear - the solution is near!

Luckily PlayBasic offers a feature called SpriteLocals. This allows us to store additional user data in a memory area attached to the sprite.
We will use this to store the type pointer into each sprite's "personal" memory area.

We need to do this for the bullets and the aliens, so let's go!

Looking at the PSubs InitPlayer(), AddAlien() and FireBullet() you will detect that much of the code to setup and prepare the sprites is identical.
So let's extract or refactor that into a new PSub MakeSprite(). The PSub already contains the code to set the SpriteLocal and to apply the depth to the sprites.

%NOWIKI%
Function MakeSprite(lst.TBaseObj, x#, y#, depth, img, collisionClass)
Local sprite = NewSprite(x#, y#, img)
PositionSpriteZ sprite, depth
SpriteCollision sprite, True
SpriteCollisionMode sprite, PIXEL_COLLISION
SpriteCollisionClass sprite, collisionClass
#IF PBDebug=True
SpriteCollisionDebug sprite, True
#ENDIF
CreateSpriteLocals sprite, 4
SpriteLocalInt sprite, 0, GetObjectPtr(lst())
lst.spriteNo = sprite
EndFunction

GetObjectPtr() is a nice little function that will return the pointer to the type as an int and it looks like this:


Function GetObjectPtr(lst().TBaseObj)
result=Int(lst(0).TBaseObj)
EndFunction result

And because we based the alien and bullet types on the same TBaseObj we can reuse the function GetObjectPtr() for both lists.

We do not need to take care of the allocated memory when we destroy a sprite because that happens automagically inside PlayBasic. That's great, isn't it?!

The refactored PSubs InitPlayer(), AddAlien() and FireBullet() now look like this:


Psub InitPlayer()
Player.x = SCREEN_WIDTH / 2
Player.y = SCREEN_HEIGHT - GetImageHeight(PlayerImage)
Player.wHalf = GetImageWidth(PlayerImage) / 2

MakeSprite(Player(), Player.x#, Player.y#, 10, PlayerImage, PLAYER_COLLISION_CLASS)
AutoCenterSpriteHandle Player.spriteNo, True
Player.status = ALIVE
Player.canShoot = True
EndPsub



Psub AddAlien(lst.TAlien, x, y, img)
Local width, height

lst = New TAlien
width = GetImageWidth(img)
height = GetImageHeight(img)
lst.x# = HORIZ_BORDER + (x-1) * (width + 10)
lst.y# = 10 + (y-1) * (height + 10)
lst.w = width
lst.h = height
MakeSprite(lst(), lst.x#, lst.y#, 5, img, ALIEN_COLLISION_CLASS)
lst.status = ALIVE
EndPsub



Psub FireBullet(lst.TBullet, x, y, playerShot)
Local height, width, img, dy#, collisionClass

lst = New TBullet
If playerShot
img = BulletImage
dy# = -BULLET_SPEED#
Else
img = AlienBulletImage
dy# = BULLET_SPEED#
EndIf
width = GetImageWidth(img)
height = GetImageHeight(img)

lst.x# = x - (width/2)
lst.y# = y
lst.h = height
lst.playerShot = playerShot
lst.dy# = dy#

collisionClass = ALIEN_BULLETS_COLLISION_CLASS
If (playerShot) Then collisionClass = PLAYER_BULLETS_COLLISION_CLASS
MakeSprite(lst(), lst.x#, lst.y#, 15, img, collisionClass)
lst.status = ALIVE
EndPsub

We also added a line to all PSubs to set the status of player, aliens and bullets to ALIVE.
When we check for collisions later on it is easier to just mark bullets and aliens as destroyed/DEAD while we walk through the lists instead of deleting them immediately.
As mentioned before it can lead to pretty ugly results when you delete elements from a list that you are currently traversing...

Did you bump into me?

Now that we prepared collision detection we should use it. Are you ready? Let's go!

We want to detect collisions between

  • player bullet and aliens,
  • alien bullets and player,
  • alien ships and player.

Sounds doable ;-)

First let's start with the player and alien ships or alien bullets.
We'll check that in the UpdatePlayer() as we only need one call to SpriteHit() in this case because we can combine collision classes and check for collisions with any alien or alien bullet.


Psub UpdatePlayer()
Local dx#, dy#, collisionSprite

dx# = Player.x#
dy# = Player.y#
If LeftKey()
dx# = dx# - PLAYER_SPEED#
EndIf
If RightKey()
dx# = dx# + PLAYER_SPEED#
EndIf
If CtrlKeys(0) And Player.canShoot
FireBullet(Bullets(), Player.x#, Player.y#, True)
Player.canShoot = False
EndIf

If dx# < Player.wHalf
dx# = Player.wHalf
EndIf
If dx# > SCREEN_WIDTH - Player.wHalf
dx# = SCREEN_WIDTH - Player.wHalf
EndIf
Player.x# = dx#
Player.y# = dy#
PositionSprite Player.spriteNo, Player.x#, Player.y#
; check for collision with any alien or alien bullet
collisionSprite = SpriteHit(Player.spriteNo, GetFirstSprite(), ALIEN_COLLISION_CLASS + ALIEN_BULLETS_COLLISION_CLASS)
If collisionSprite > 0
; we have a collision with an alien or a alien bullet -> Game Over for now
GameOver = True
EndIf
EndPsub


I added the collision check at the end of the PSub. As soon as the player sprite hits any alien sprite or alien bullet sprite we set GameOver to true. That's pretty simple and straightforward, right?

The collision code is just these six lines of code:


; check for collision with any alien or alien bullet
collisionSprite = SpriteHit(Player.spriteNo, GetFirstSprite(), ALIEN_COLLISION_CLASS + ALIEN_BULLETS_COLLISION_CLASS)
If collisionSprite > 0
; we have a collision with an alien or a alien bullet -> Game Over for now
GameOver = True
EndIf

Now for the player bullets: We need to check collisions of player bullets and aliens (leading to the death of the aliens).
We put the code into the UpdateBullets() Psub cause that is the place where we mainly deal with the bullets.


Psub UpdateBullets()
Local alienSprite
Dim hitAlien As TAlien Pointer

For Each Bullets()
If Bullets.status = ALIVE
Bullets.y# = Bullets.y# + Bullets.dy
If (Bullets.y# > SCREEN_HEIGHT) Or (Bullets.y# + Bullets.h) < 0
Bullets.status = DEAD
Else
; check for player bullet collisions with aliens
If Bullets.playerShot = True
; this is a player bullet, check for alien collisions
alienSprite = SpriteHit(Bullets.spriteNo, GetFirstSprite(), ALIEN_COLLISION_CLASS)
If alienSprite > 0
; we hit an alien, destroy the alien and the bullet
hitAlien = GetSpriteLocalInt(alienSprite, 0)
hitAlien.status = DEAD
Bullets.status = DEAD
Continue
EndIf
EndIf
PositionSprite Bullets.spriteNo, Bullets.x#, Bullets.y#
EndIf
EndIf
Next
EndPsub


I think some little code explanation is recommended.
First I changed the condition inside the For Each loop: we only look at bullets that are ALIVE. DEAD bullets are not treated here.
The new code starts in the Else section where the comment mentions bullet collisions.
We need to check if the bullet is a player bullet and collided with an alien. For that we need to call SpriteHit() and use the ALIEN_COLLISION_CLASS. If we hit an alien (alienSprite > 0) we retrieve the pointer to the alien type and set the status of the alien and the bullet to DEAD. We continue the loop as we don't want to deal with this dead bullet anymore.
Finally we assure that the bullet is drawn if it is still alive.

So it's not too complicated but a bit tricky ;-)

Oh, and of course we need to call the Psub DestroyObject() to remove the dead aliens from screen. We will do this in the PSub ClearDeadObjects(). Here's the updated code of ClearDeadObjects().


Psub ClearDeadObjects()
For Each Aliens()
If Aliens.status=DEAD
DestroyObject(Aliens()); remove this Alien
EndIf
Next
For Each Bullets()
If Bullets.status=DEAD
If Bullets.playerShot
; allow the player to shoot again
Player.canShoot = True
EndIf
DestroyObject(Bullets()); remove this bullet
EndIf
Next
EndPsub

And finally I modified the PSub UpdateAliens() to only deal with alive aliens.


Psub UpdateAliens()
Local dirChange
dirChange = DirectionChangeForAliensRequired()
If dirChange
AlienMoveDir# = - AlienMoveDir#
EndIf

For Each Aliens()
If (Aliens.status = ALIVE)
Aliens.x# = Aliens.x# + AlienMoveDir#
If dirChange
Aliens.y# = Aliens.y# + ALIEN_Y_STEP
If Aliens.y# + Aliens.h > SCREEN_HEIGHT
GameOver = True
EndIf
EndIf
PositionSprite Aliens.spriteNo, Aliens.x#, Aliens.y#
If Rnd(100) > 95 ; a 5 percent chance to fire a bullet
If NumberAlienBullets() < MAX_ALIEN_BULLETS
; let this alien fire a shoot
FireBullet(Bullets(), Aliens.x# + (Aliens.w / 2), Aliens.y# + Aliens.h, False)
EndIf
EndIf
EndIf
Next
EndPsub

And that's it. Have a play for a while and don't get shot.

Final touches

Did you play your game? Was it too fast?
I think so. Let's decrease the speed of the aliens.


Constant ALIEN_SPEED# = 0.5


Aah, better. And see how smart it was to store the alien speed in a constant and use that through all the code? One line changed - big effect achieved.

What next? If you shot all the aliens the game does not end but there's nothing left to shoot.
Let's add some levels and difficulty.
Whenever we kill all aliens we increase the level and restart with the starting number of aliens. Only that they can shoot more ;-)
We code that in a minute (or a bit more).
We add a global Level and set it to 0.
Place the line just before the GameOver global.


Global Level = 0

Now when all aliens are gone we want to recreate them and increase the level. We do that in UpdateAliens().
To get some increase of difficulty we change the game so that the number of alien bullets in game is equal to 3 plus Level. So the higher the level the more bullets the aliens can shoot. This also happens in UpdateAliens().


Psub UpdateAliens()
Local dirChange, activeAliens
dirChange = DirectionChangeForAliensRequired()
If dirChange
AlienMoveDir# = - AlienMoveDir#
EndIf

activeAliens = 0
For Each Aliens()
If (Aliens.status = ALIVE)
Inc activeAliens
Aliens.x# = Aliens.x# + AlienMoveDir#
If dirChange
Aliens.y# = Aliens.y# + ALIEN_Y_STEP
If Aliens.y# + Aliens.h > SCREEN_HEIGHT
GameOver = True
EndIf
EndIf
PositionSprite Aliens.spriteNo, Aliens.x#, Aliens.y#
If Rnd(100) > 95 ; a 5 percent chance to fire a bullet
If NumberAlienBullets() < (MAX_ALIEN_BULLETS + Level)
; let this alien fire a shoot
FireBullet(Bullets(), Aliens.x# + (Aliens.w / 2), Aliens.y# + Aliens.h, False)
EndIf
EndIf
EndIf
Next
If activeAliens = 0
; let's start the next level
Inc Level
; and let's recreate the aliens
InitAliens()
EndIf
EndPsub


The changes are minimal. We introduce a new local variable activeAliens. During our update loop we just count the active aliens. If the count is zero the player killed all aliens. So we only need to increase the level and call InitAliens() again to recreate them.
Do you see how valuable it is to code Psubs and Functions? If you code them smart you can reuse them many times!
For the difficulty increase we had to change just one line.


If NumberAlienBullets() < (MAX_ALIEN_BULLETS + Level)


Now the higher the level the more they shoot.

Finally let's add some scoring. We introduce two more globals, a score and a high score.


Global Score = 0
Global HiScore = 0


Let's also reset the level and score to 0 if we initialize the player.
Place the line


Score = 0
Level = 0


at the end of Psub InitPlayer().

Now for each destroyed alien let's increase the score by some value.
The standard alien killer score will be 100 and of course become a constant.


Constant ALIEN_KILL_SCORE = 100


Put this line at the end of the other alien constants.
We increase the score when we know that the alien was killed by a player bullet in UpdateBullets().
Change the collision code for player bullets to this:


If alienSprite > 0
; we hit an alien, destroy the alien and the bullet
hitAlien = GetSpriteLocalInt(alienSprite, 0)
hitAlien.status = DEAD
Bullets.status = DEAD
Score = Score + ALIEN_KILL_SCORE
Continue
EndIf


So simple, isn't it.

Now let's update the UpdateGame() Psub to see the score so far.


Psub UpdateGame()
If Not GameOver
UpdatePlayer()
UpdateAliens()
UpdateBullets()
DrawOrderedSprites
Else
UpdateGameOver()
EndIf
ClearDeadObjects()
Text 0,0,Str$(RealFPS) + " FPS"
Text 100, 0, "Score: " + Str$(Score)
Text 250, 0, "Hiscore: " + Str$(HiScore)
Text 400, 0, "Level: " + Str$(Level)
EndPsub


So just some text printing added. Simple.

Finally we want to check if we got a new high score and restart the game if necessary.
UpdateGameOver() is our friend here.


Psub UpdateGameOver()
DrawCenteredText(200, "G A M E O V E R")
If Score >= HiScore
DrawCenteredText(300, "Y O U M A D E A N E W H I G H S C O R E !")
HiScore = Score
EndIf
DrawCenteredText(400, "'S P A C E' T O R E S T A R T")
If SpaceKey()
GameOver = False
; clean up the mess
DestroyPlayer()
HideAll(Aliens())
HideAll(Bullets())
; and restart
InitPlayer()
InitAliens()
EndIf
EndPsub

Psub DrawCenteredText(ypos, msg$)
Local w
w = GetTextWidth(msg$)
CenterText SCREEN_WIDTH/2 - (w/2), ypos, msg$
EndPsub


So we simply added a little text output and setting an updated highscore if required.

I added a simple PSub DrawCenteredText() to simplify placing some text horizontally centered on screen as we need multiple text lines now.

We also added a restart option if the space key is pressed.
To enable that it was necessary to add some cleanup functions which mainly hide the sprites that are still on screen and set the status to DEAD. Cleanup happens in the ClearDeadObjects() PSub.
Afterwards it's just calling InitPlayer() and InitAliens() again to have some more fun.
Here comes the code for the hiding Psubs.
No big deal there so I just dump the code.


Psub DestroyPlayer()
DeleteSprite Player.spriteNo
EndPsub

Psub HideMe(me.TBaseObj)
me.status=dead; Tag object as DEAD
SpriteVisible me.SpriteNo,False ; Hide the sprite associated with this object
EndPsub

Psub HideAll(me.TBaseObj)
For Each me()
HideMe(me.TBaseObj)
Next
EndPsub

That was it. A simple Space Invaders game in one go.

I hope you enjoyed the read!

The updated sourcecode for the first part of the tutorial

It's done so get the updated source code of it here!

Feedback via email or in the Underware Design thread of the tutorial here.

SpaceInvadersUpdate.zip