Top down shooter tutorial with BlitzMax

I was asked to do a little tutorial for BlitzMax.

A suggested game was a simple top down shooter where the player controls a rotating turret in the center of the screen and has to shoot different enemies that approach him from all sides.
The game gets more and more difficult by raising the speed of the enemies, introducing different enemies that take more hits and so on.

This sounded doable to me and it seemed to offer many possibilities to introduce some OO and other nice features of BlitzMax so I decided to give it a go.

The tutorial is a work in progress right now and only contains thoughts and reminders about how to proceed. It will be properly written over the next few weeks or months ;-)

Part 2 of the tutorial

Coding conventions and short introduction

Okay, so you want to code a game with BlitzMax?
Great choice!

BlitzMax is a BASIC like language offering object oriented (OO) extensions. Those will help you organize and simplify your code a lot compared to the "old" procedural versions of BlitzBasic (and other Basics).
BlitzMax offers Types which is the BlitzMax name of a class in other OO languages like Java or C++. It might happen during this tutorial that I mix the usage of type and class as they are identical and class is the more often used phrase to me ;-)

What is a Type now? It's nothing more but a template for new objects. Every object you create with that type has all the fields as the type describes.
A Type in BlitzMax can also have methods which can be called for each object of that type. There are some more features of types which I'll explain later on in the tutorial.

  • A good way to structure your code is to use a separate file in your project for each new type.
  • As already mentioned in my Space Invaders tutorial part 1 you should write constants in capital letters.
  • Keep your naming conventions for types, methods, variables consistent. Use underlines like my_age or the_var or avoid them and use upper/lower case notation like myAge or theVar. I prefer the upper/lower case notation.

Oh, and before I forget: Your first command in any BlitzMax game you code should be


SuperStrict


SuperStrict forces you to declare all variables in your game with their type and also declare the return type of all methods and functions in your game.
Simply said it helps you to get more readable code 8-)

Enough intro, we'll do the rest while we work through the code.

It doesn't matter if you use the standard BlitzMax IDE or the community edition of the BlitzMax IDE or if you use BLIde Plus like I do. But I will not explain how to create a project or create a new file as it's all pretty self explaining. If you don't manage to figure that out on your own you shouldn't yet consider coding your own games ;-)

Simple game design

The idea is to create a simple version of a top down shooter which we extend later on.
So we use a simple single screen game without any scrolling maps or tiled background or such things.
The player controls a turret or a base in the center of the screen. The turret rotates following the mouse pointer and clicking the left mouse button will fire a bullet in the turret's direction.
Enemies will return in random intervals from the screen boundaries and move directly towards the player's turret. They do not fire and might require several hits before they die or explode.
As soon as an enemy touches the turret the game ends.
The enemies attack in phases. After all enemies of a phase have been destroyed the next phase starts with more enemies, harder enemies, faster enemies and so on.

Later on we will extend the game and replace the turret with a moving player sprite, add keyboard control for movement, better room graphics, add several rooms, different weapons and what else I can come up with while coding this tutorial :LOL:

Very important: we need a nice name for the game of course! I chose ComeGetMe!

Some thoughts about the main game loop

Each game you code has some basic main loop looking like this (in pseudo language)


do
    clear screen
    update all game objects and handle game logic
    render all remaining game objects
while (forever)

Unfortunately game developer's life isn't that easy. Because different computers have different speeds (older ones are generally slower than the newest high end quad core machines) this main loop would run at different speeds too.
Your game would become unplayable on faster machines. Too bad.

There are several approaches to solve this problem. One is called delta timing. A game loop using delta timing would look similar to this one:


last_time = currentTimeInMilliSeconds()
do
    now = currentTimeInMilliSeconds()
    delta = now - last_time
    clear screen
    update all game objects and handle game logic and use delta for all calculations
    render all remaining game objects
while (forever)


You have to use the delta, which is the time that passed since the last update and rendering, for all your speed and position change calculations in the update functions of your game objects.
For example you would calculate a new position of your space ship like this:


newpos = oldpos + (speed * delta)


On a fast machine the delta will be very small and so will be the product (speed * delta). So on a fast computer your ship will move in small steps but often because your main loop is executing really quick.
On a slow machine the delta will be comparably bigger and the same for the product (speed * delta). Your ship will move in bigger steps but not as often because the main loop takes a longer time to process.
But on both machines you'll get the same distance over the same amount of time!
So this is a great working method!

But it has two disadvantages: you update a lot on fast machines with only minimal impact on game objects (because the machines are sooo fast) and physics engines don't like it :-(
Physics engines most of the times expect to be called at certain identical time intervals, say every 20 milliseconds. This way you'll avoid too many too complicated calculations and nearly no game player would realize a difference if the physics engines were called faster.

So people came up with another method of game loop: fixed rate logic plus tweening.
What you do here is calling the update methods with a given fixed rate all the time, for example every 20 milliseconds. Of course it could happen that your game logic needs more time or the Garbage collector interrupts a bit or your game is synchronizing with the vertical monitor sync signal to avoid flickering and you don't exactly match the 20 milliseconds. So the next call to your update would be a few milliseconds too late. You would still call the update for all pending amounts of 20 milliseconds and the remainder (the tweening value) would be passed as input to all drawing methods. The drawing methods would then interpolate between last position and current position of all moving game objects and the distance travelled is dependent on the tweening value.

So instead of having a different value each call in the update routine as in the delta timing approach we now have a different value in the drawing routines.
Visually the output is pretty much identical but the advantage of the fixed rate logic approach is indeed what the name says: your game logic is called with fixed intervals all the time.
A pseudo code game loop using fixed rate logic would look like this:


last_time = currentTimeInMilliSeconds()
do
now = currentTimeInMilliSeconds()
delta = now - last_time
accumulator = accumulator + delta
clear screen
while accumulator > fixed rate
update all game objects and handle game logic and use fixed rate for all calculations
accumulator = accumulator - fixed rate
endwhile
tween = accumulator / fixed rate
render all remaining game objects and use the tween value for interpolation where current drawing pos = newpos * tween + oldpos * ( 1.0 - tween)
while (forever)

As the drawing functions do the interpolation all your moving game elements need to have two positions: the last position on screen and the new position on screen. The new position on screen is calculated in the update phase and in the drawing phase your code will have to interpolate between those two positions based on the size of the tween value. Each drawing or rendering phase will move your game elements closer to their new position that was calculated in the update phase. Finally, when the new position is reached time is up for a new update phase and we'll start all over again.

There is a nice long thread in the BlitzMax forum here and a really good explanation of fixed rate logic in games can be found on Gaffer's Blog.

For this tutorial we will use the fixed rate logic plus tweening approach - mainly just because I want to give it a try :LOL:

First approach to main loop

So let's have some code now, shall we?

I want to mention that some of the code is adopted and modified or simplified from Chroma's public domain BlitzMax framework which you can find on Google code pages here. It seems to be no longer maintained but it gave me a good starting point.

Ok, create an empty directory for your project on your hard disk and fire up your preferred BlitzMax IDE.

First we'll start with the main loop code base. You can just copy and paste it into a file named ComeGetMe.bmx.


SuperStrict

Import "TFRLTimer.bmx"

Const GFX_WIDTH:Int = 640, GFX_HEIGHT:Int = 480

Const UPDATE_FREQUENCY:Float = 100.0, SPIKE_SUPPRESSION:Int = 20

Graphics GFX_WIDTH, GFX_HEIGHT, 0

SetBlend ALPHABLEND

Local gameTime:TFRLTimer = New TFRLTimer.CreateFRL(UPDATE_FREQUENCY, SPIKE_SUPPRESSION) '1st number is logic updates per second and 2nd number is how much spike suppression you want

' the main loop starts here
While Not KeyDown(KEY_ESCAPE) And AppTerminate() = False
Cls

' update part of the main loop with constant speed
Local delta:Float = gameTime.GetDelta()
While gameTime.accumulator > gameTime.logicFPS
doGameLogic(gameTime.LogicFPS) 'Update Game Logic Here
gameTime.accumulator:-gameTime.logicFPS 'update the accumulator
Wend

' rendering with additional tweening
Local tween:Float = gameTime.GetTween() 'calc the tween for smooth graphics
doGameRender(tween) 'Render Game Here

Flip 1 ' synchronize graphics buffer flipping with vsync of monitor
Wend
EndGraphics
End

Function DoGameLogic(fixedRate:Float)
' nothing here yet
End Function

Function DoGameRender(tween:Float)
' nothing here yet
End Function

So what does that code do?
First we switch on "SuperStrict" as I already recommended. The second line imports another source code file named TFRLTimer.bmx which we'll have a look at in a minute.
We then define constants for the screen size and the update frequency which is the fixed frame rate. So we plan to call the update functions 100 times a second which means every 10 milliseconds.
The last constant SPIKE_SUPPRESSION is a cool one as it is another feature of the TFRLTimer type that is defined in the TFRLTimer.bmx file. It allows us to smooth the tweening value a bit to avoid spikes that may arise while other background processes on your PC start or terminate while playing the game. So the game play will appear even more smooth 8-) The number (20 in our example) just tells the type to store the last 20 values and use them for smoothing.

We open the graphics mode with given width and height and stay in windowed mode (the 0 at the end of the Graphics command). We set the active blend mode to ALPHABLEND right at the beginning because that's the blend mode we are mainly going to use in the game and it's not the default blend mode when you start a BlitzMax application so we do it right at the beginning.

Then we create a TFRLTimer object named gameTime. This object will help us to keep track of all this fixed rate logic and the tweening value and all that.

Finally we start our main loop which we'll only leave if the player presses the Escape key or clicks on the upper right close button of our game window.

The loop itself is pretty simple: we clear the screen first. Then we ask the gameTime object how many milliseconds have passed since the last call to GetDelta(). The inner while loop helps us to call all outstanding updates. The gameTime.accumulator stores how many milliseconds are left as we are only interested in multiples of gameTime.LogicFPS which is exactly our fixed time rate (the update frequency, here 60).

We then need to do all drawing but we need to pass the tween value into the draw methods as explained in the previous paragraph.

The main loop finishes with a call to Flip 1 which switches the two drawing buffers of BlitzMax synchronized with the screen's next vertical sync (vsync) to avoid flickering.

If the player presses Escape or clicks into the close box of our window we leave the main loop, call EndGraphics to switch off BlitzMax' graphics mode and terminate the game.

I already added two empty functions DoGameUpdate(fixedRate:Float) and DoGameRender(tween:Float) which will contain all required calls to update and render the game objects.

So that's the first approach of the main loop. Save the file as ComeGetMe.bmx and be prepared to create the second file in a second!

The TFRLTimer type

The name TFRLTimer stands for Fixed Rate Logic Timer and the first leading T just stands for a Type. It's good coding practise to always start your Types with a T and create a new source code file for each type.

Shall we have a look at it?


'==============================================================
'===FIXED RATE LOGIC TWEEN DELTA SPIKE SUPPRESSION (DSS)===
'==============================================================
Type TFRLTimer
Field newTime:Float = MilliSecs()
Field oldTime:Float = MilliSecs()
Field delta:Float

Field dssOn:Int ' do we use delta spike suppression?
Field dssIndex:Int ' index into DSS_Array where next delta value is written
Field dssArray:Float[] ' this array contains the delta values to smooth
Field dssLenArray:Int ' how big is the array of delta values

Field logicFPS:Float
Field accumulator:Float, tween:Float

Function CreateFRL:TFRLTimer(logicCyclesPerSec:Float, numSamples:Int = 0)
Local frl:TFRLTimer = New TFRLTimer
frl.logicFPS = 1.0 / logicCyclesPerSec
If numSamples
frl.dssOn = True
frl.dssArray = New Float[numSamples]
frl.dssLenArray = numSamples
EndIf
Return frl
End Function

Method GetDelta:Float()
Self.newTime = MilliSecs()
Self.delta = Float (Self.newTime - Self.oldTime) * 0.001
Self.oldTime = Self.newTime

If Self.dssOn = True
Self.dssArray[Self.dssIndex] = Self.delta

Local smoothDelta:Float = 0

For Local i:Int = 0 To Self.dssLenArray - 1
smoothDelta:+Self.dssArray[i]
Next
Self.delta = Float smoothDelta / dssLenArray

Self.dssIndex:+1
If Self.dssIndex > dssLenArray - 1 Then Self.dssIndex = 0
EndIf

Self.accumulator:+Self.delta

Return Self.delta
End Method

Method GetTween:Float()
Return Self.accumulator / Self.logicFPS
End Method

Method ShowSpikeSuppression(x:Int, y:Int)
DrawText "Delta Spike Suppressor:", x, y
For Local i:Int = 0 To Self.dssLenArray - 1
DrawText Self.dssArray[i], x, y + (i + 1) * 25
Next
DrawText "Final Delta: " + Self.delta, x, y + (Self.dssLenArray + 1) * 25
End Method

End Type

Explanation of the TFRLTimer class

If the code looks just too complicated and you don't want to delve into it that's fine. Just create the file TFRLTimer.bmx, copy the source code into the file and save it. It will work and you don't have to understand the source code to get a running game.

But if you want to learn how your game system ticks and also want to know about some more BlitzMax features you should read this paragraph ;-)

Every type declaration starts with the keyword Type and the type's name and ends with the keyword End Type. All code between those keywords belongs to the type.

We start with some field declarations. You can imagine fields as properties of objects. For example every car has a color and a number of doors. So color and nrOfDoors would be fields of a type TCar. As I already mentioned every type is mainly a template for objects. So if you create an object of type TXYZ it will have all the fields that are declared in the type. And if you have two or more objects of type TXYZ all of those objects have their unique values inside the fields, just like you can have a red car with three doors and a blue car with five doors.

The fields we define for our type TFRLTimer are newTime and oldTime to store the current time in milliseconds (set when we call GetDelta()) and the last time we called GetDelta(). The delta field should be self explaining.

All fields starting with dss are used for smoothing the delta values. Finally we store the accumulator and tween values as Floats.

Then we meet something new: a Function. I already told you that methods can be called for each instance of a type. But what if you don't have an instance yet? Stuck? No. There are methods that you can send to the type directly without having an instance of that type. Those are called Functions.
And one of the most used kind of function is a Create function which is commonly used in BlitzMax to create instances of a given type.


Function CreateFRL:TFRLTimer(logicCyclesPerSec:Float, numSamples:Int = 0)


This function does exactly that: it will return an instance of TFRLTimer which is specified as the return type of this function. So the syntax for functions is:


Function NameOfYourFunction:ReturnTypeOfFunction(parameterOne:TypeOfParameterOne, ParameterTwo:TypeOfParameterTwo = DefaultValueIfNotGiven)

A function can have zero to many parameters, not only two as in my example. You can also give default values to parameters which will be used if you omit this parameter in the function call. Those default parameters must be used from right to left in your function declaration - you cannot have a default value for the first parameter. That's because the compiler can't know if you want to use the default value or the first passed in value for the first parameter. So you can only omit parameters starting at the right end of your function declaration.

Anyway, back to the CreateFRL() function. First it creates an empty instance of a TFRLTimer and then it takes the logic cycles per second and calculates the milliseconds per logic cycle and stores it in the field logicFPS.


Local frl:TFRLTimer = New TFRLTimer
frl.LogicFPS = 1.0 / logicCyclesPerSec


There's another thing to mention here: Local. With this keyword you define a local variable which means this variable can only be used inside the function or method where it was defined. Outside of the function CreateFRL() you cannot access the local variable frl but you would get a compiler error!

On with the remaining lines. If numSamples is bigger than zero (so we passed in a value to the CreateFRL() call) we store the information to use samples in dssOn and store the number of entries in dssLenArray and allocate an array for the values in dssArray.
Whoops! Another new thing to learn: Arrays in types are not automatically allocated when you create a new type instance. You'll have to do that on your own, for example in the Create function of your type.

At the end the CreateFRL() function returns the properly initialized instance of type TFRLTimer to the caller.

Next we find a method named GetDelta().


Method GetDelta:Float()
Self.newTime = MilliSecs()
Self.delta = Float (Self.newTime - Self.oldTime) * 0.001
Self.oldTime = Self.newTime

If Self.dssOn = True
Self.dssArray[Self.dssIndex] = Self.delta

Local smoothDelta:Float = 0

For Local i:Int = 0 To Self.dssLenArray - 1
smoothDelta:+Self.dssArray[i]
Next
Self.delta = Float smoothDelta / dssLenArray

Self.dssIndex:+1
If Self.dssIndex > dssLenArray - 1 Then Self.dssIndex = 0
EndIf

Self.accumulator:+Self.delta

Return Self.delta
End Method


We know that a method is bound to an instance of a type. So with every instance of TFRLTimer we can call its GetDelta() method.
The method returns a float value, the delta and the method takes no parameters. Look back into the main loop to see where we call


gameTime.GetDelta()

The method calculates the delta time which is the amount of milliseconds between the last and current calls to GetDelta(). If dssOn is true we store the current delta value in the dssArray and immediately calculate the smoothed delta. The dssArray is a wrapping array where the index restarts at 0 when the array size limit is reached. The accumulator is increased by the delta value so that the accumulator always contains the milliseconds since the last completed update call.

The last method in this class/type is


Method GetTween:Float()
Return Self.accumulator / Self.logicFPS
End Method


Here we simply return the tween value which is the accumulator divided by logicFPS.

First test

Now that you have ComeGetMe.bmx and TFRLTimer.bmx stored in your project folder you should be able to compile the main file ComeGetMe.bmx which will also compile TFRLTimer.bmx and the result should be an executable. If you start it a simple black window should open.
Press the 'Escape' key or use your mouse to close the window.

Great! We now got a working code base.

Already the first refactoring!

After reading the tutorial for several times now (proof reading while adding new content) I detected what was nagging me all the time. I didn't like the implementation of the TFRLTimer class. The main class in it's main loop had constant access to some fields of the TFRLTimer and even modified them!
This breaks many OO rules like encapsulation and information hiding of course. So I just had to refactor the class. And while I was at it I also added some FPS code to show the amount of logic update calls and render calls per second.

Let's look at the modified main class ComeGetMe and the new TFRLTimer class.

ComeGetMe.bmx


SuperStrict

Import "TFRLTimer.bmx"

Const GFX_WIDTH:Int = 640, GFX_HEIGHT:Int = 480

Const UPDATE_FREQUENCY:Float = 100.0, SPIKE_SUPPRESSION:Int = 20

Graphics GFX_WIDTH, GFX_HEIGHT, 0

SetBlend ALPHABLEND

AutoMidHandle(True)

Global gameTime:TFRLTimer = New TFRLTimer.CreateFRL(UPDATE_FREQUENCY, SPIKE_SUPPRESSION) '1st number is logic updates per second and 2nd number is how much spike suppression you want

Global player:TPlayer = TPlayer.Create(TCoordinate.Create(GFX_WIDTH / 2, GFX_HEIGHT / 2, 0))

' the main loop starts here
While Not KeyDown(KEY_ESCAPE) And AppTerminate() = False
Cls

' update part of the main loop with constant speed
Local delta:Float = gameTime.ProcessTime()
While gameTime.LogicUpdateRequired()
DoGameLogic(gameTime.GetLogicFPS()) 'Update Game Logic Here
Wend

' rendering with additional tweening
Local tween:Float = gameTime.GetTween() 'calc the tween for smooth graphics
doGameRender(tween) 'Render Game Here

Flip 0 ' synchronize graphics buffer as soon as possible
Wend
EndGraphics
End

Function DoGameLogic(fixedRate:Float)
' nothing here yet
End Function

Function DoGameRender(tween:Float)
' nothing here yet
gameTime.ShowFPS(0, 0)
End Function

So the interesting part is the modified main loop. Notice that I only call methods of the gameTime object. No field of the TFRLTimer is exposed to the calling class. I also renamed the method GetDelta() because the method name is misleading. We don't only retrieve the delta value but the most important thing is that this method also calculates the passed time and updates a lot of internal stuff inside the TFRLTimer instance. So I decided to rename this method to ProcessTime() which better describes what the method really does. And as a return value we retrieve the delta time that passed since the last call to ProcessTime(). Voila. Much better.
Also I don't access the accumulator field of the timer instance. Instead I encapsulated that in a method named LogicUpdateRequired(). It returns true if enough time passed to at least call the update routines of the game elements once. Also all internal fields like the accumulator are updated. I also added a accessor method GetLogicFPS() which only returns the field logicFPS of the TFRLTimer instance. But this way I can hide all information inside the class. The caller does not know if the logicFPS is calculated for each call or precalculated on instance creation time.

I also added a call to gameTime.ShowFPS() in the function DoGameRender(). When we compile ComeGetMe.bmx we should see a black window with logic and render FPS shown at the top of the screen.

Now finish the refactoring by looking at the refactored TFRLTimer class.

The refactored TFRLTimer class

Let's start with the complete source code for TFRLTimer.bmx:


'==============================================================
'===FIXED RATE LOGIC TWEEN DELTA SPIKE SUPPRESSION (DSS)===
'==============================================================
Type TFRLTimer
Field newTime:Float = MilliSecs()
Field oldTime:Float = MilliSecs()
Field delta:Float

Field dssOn:Int ' do we use delta spike suppression?
Field dssIndex:Int ' index into DSS_Array where next delta value is written
Field dssArray:Float[] ' this array contains the delta values to smooth
Field dssLenArray:Int ' how big is the array of delta values

Field logicFPS:Float
Field accumulator:Float, tween:Float

Field fpsAccumulator:Float
Field updateCount:Int
Field renderCount:Int
Field updatesPerSecond:Int
Field rendersPerSecond:Int

Function CreateFRL:TFRLTimer(logicCyclesPerSec:Float, numSamples:Int = 0)
Local frl:TFRLTimer = New TFRLTimer
frl.logicFPS = 1.0 / logicCyclesPerSec
If numSamples
frl.dssOn = True
frl.dssArray = New Float[numSamples]
frl.dssLenArray = numSamples
EndIf
Return frl
End Function

Method ProcessTime:Float()
Self.newTime = MilliSecs()
Self.delta = Float (Self.newTime - Self.oldTime) * 0.001
Self.oldTime = Self.newTime

If Self.dssOn = True
Self.dssArray[Self.dssIndex] = Self.delta

Local smoothDelta:Float = 0

For Local i:Int = 0 To Self.dssLenArray - 1
smoothDelta:+Self.dssArray[i]
Next
Self.delta = Float smoothDelta / dssLenArray

Self.dssIndex:+1
If Self.dssIndex > dssLenArray - 1 Then Self.dssIndex = 0
EndIf

Self.accumulator:+Self.delta

Self.fpsAccumulator:+Self.delta
If Self.fpsAccumulator > 1.0
Self.fpsAccumulator:-1.0
Self.updatesPerSecond = Self.updateCount
Self.updateCount = 0
Self.rendersPerSecond = Self.renderCount
Self.renderCount = 0
End If

Return Self.delta
End Method


Method LogicUpdateRequired:Int()
If Self.accumulator > Self.logicFPS
Self.updateCount:+1
Self.accumulator:-Self.logicFPS
Return True
End If
Return False
End Method


Method GetLogicFPS:Float()
Return Self.logicFPS
End Method


Method GetTween:Float()
Self.renderCount:+1
Return Self.accumulator / Self.logicFPS
End Method

Method ShowSpikeSuppression(x:Int, y:Int)
DrawText "Delta Spike Suppressor:", x, y
For Local i:Int = 0 To Self.dssLenArray - 1
DrawText Self.dssArray[i], x, y + (i + 1) * 20
Next
DrawText "Final Delta: " + Self.delta, x, y + (Self.dssLenArray + 1) * 20
End Method


Method ShowFPS(x:Int, y:Int, showUpdateFPS:Int = True, showRenderFPS:Int = True)
Local ty = y

If showUpdateFPS
DrawText "Logic FPS: " + updatesPerSecond, x, ty
ty:+20
End If
If showRenderFPS
DrawText "Render FPS: " + rendersPerSecond, x, ty
End If
End Method
End Type

As you can see I added some more fields to do the FPS calculation.


Field fpsAccumulator:Float
Field updateCount:Int
Field renderCount:Int
Field updatesPerSecond:Int
Field rendersPerSecond:Int


The fpsAccumulator is increased every time the method ProcessTime() is called. This way we can detect when one second passed.
As I already mentioned ProcessTime() is just the renamed method GetDelta() with some minor enhancements.
As soon as a second is over the fields updatesPerSecond and rendersPerSecond are set with the current values of updateCount and renderCount. The two counting variables updateCount and renderCount are reset to zero to start again counting the update and render calls for the next second.

updatesPerSecond and rendersPerSecond are shown when you call the new method ShowFPS(). They keep their values for one second and then they are overwritten by the new values of updateCount and renderCount.


Method LogicUpdateRequired:Int()
If Self.accumulator > Self.logicFPS
Self.updateCount:+1
Self.accumulator:-Self.logicFPS
Return True
End If
Return False
End Method

The method LogicUpdateRequired:Int() is new. It is called from the main loop to detect if at least one update call is pending. If that's the case the updateCount is incremented by one and the accumulator is decreased by the update time logicFPS.

The renderCount is incremented in the GetTween() method because this method is called for each render phase to retrieve the tween value that needs to go into every render call to tween the movement of game elements.

That's pretty much all for the important and mentionable changes in the TFRLTimer class.

Compile the two source files and if all went well you should see a screen similar to the one below.

Those two files can be reused as the base for all your games - consider it the beginning of a small library to help you creating your own games ;-)

Introducing the player - not yet!

Before we start with the code for the player we'll need to think a bit about the types we are going to create:

  • we need an old x/y position and a new x/y position for every game element
  • we need old and new angles for every game element
  • we need to copy new to old positions in a simple way
  • we need to calculate distances and angles between positions (for aiming and firing)
  • we do have game elements that share quite some functionality
  • all game elements should be forced to implement an update and a render method
  • we want to share and reuse as much code as possible. Coding by using copy and paste is bad 8-)

So how do we put those requirements into types?

One approach is to have a simple class for x/y positions or coordinates and the angle. It could contain x/y position, the current angle or direction, some methods to copy coordinates and methods for angle and distance calculations.
Let's name this new class/type TCoordinate.

The second class could be called TGameElement and contains old and new TCoordinates and directions and will have all methods that are shared between all different TGameElements.

To force the programmer to implement proper update and render methods we make those methods abstract and also we make the whole type abstract.
That's another OO feature you'll use frequently.

  • If a type is abstract you cannot create instances of that type. So there will never be an instance of TGameElement.
  • You will have to extend or subclass this type and create your own derived type of TGameElement. For example a type TPlayer will be a subclass of TGameElement and automatically inherit all fields and all methods of TGameElement without writing or copying a single line of code!
  • If a method is abstract the programmer is forced to implement this method in his subclass of the abstract class (so you can only have abstract methods in abstract types).

So let's start with the TCoordinate class.

TCoordinate or Where am I?

The TCoordinate type in it's first incarnation looks like this:

TCoordinate.bmx


Type TCoordinate
Field x:Float
Field y:Float
Field angle:Int ' we simplify here and just use an integer for angles between 0 and 359 degrees

Function Create:TCoordinate(x:Float, y:Float, angle:Int)
Local coord:TCoordinate = New TCoordinate
coord.x = x
coord.y = y
coord.angle = angle
Return coord
End Function


Function Copy:TCoordinate(original:TCoordinate)
Local copy:TCoordinate = New TCoordinate
copy.x = original.x
copy.y = original.y
copy.angle = original.angle
Return copy
End Function

Method setPosition(x:Float, y:Float)
Self.x = x
Self.y = y
End Method

Method setAngle(angle:Int)
self.angle = angle
End Method

Method getDistance:Float(otherPos:TCoordinate)
Local l1:Float = Abs(Self.x - otherPos.x)
Local l2:Float = Abs(Self.y - otherPos.y)
Return Sqr((l1 * l1) + (l2 * l2))
End Method

Method getAngle:Int(otherPos:TCoordinate)
Local dx:Float = otherPos.x - Self.x
Local dy:Float = otherPos.y - Self.y
Return Int((ATan2(dy:Float, dx:Float) + 360) Mod 360)
End Method
End Type

Explanation of the TCoordinate type

The TCoordinate type is not very complicated.
First we have three fields: x, y and angle. This means that every instance of TCoordinate has it's own x and y position and it's own angle it is targeting at.

Then we find two functions that return a TCoordinate type and are our two valid creator functions.

The first function Create:TCoordinate(x:Float, y:Float, angle:Int) takes the x, y and angle values we want to have in our new instance as parameters.
The line


Local coord:TCoordinate = New TCoordinate


creates an empty TCoordinate instance and stores it in the local variable coord. We then fill the three fields with the parameter values and return the properly initialized instance by returning coord.

For simplification I added another function that creates a TCoordinate instance. But this function doesn't take x, y and angle but another TCoordinate instance that is simply copied into a new instance.


Function Copy:TCoordinate(original:TCoordinate)
Local copy:TCoordinate = New TCoordinate
copy.x = original.x
copy.y = original.y
copy.angle = original.angle
Return copy
End Function

There are two methods SetPosition(x:Float, y:Float) and SetAngle(angle:Int) which allow to modify the fields of an TCoordinate instance.

And finally there are two methods that calculate the distance between two TCoordinate instances (GetDistance:Float(otherPos:TCoordinate)) and the angle between two TCoordinate instances (GetAngle:Int(otherPos:TCoordinate)).

That's basic math I copied from somewhere of the Internet :LOL:

Now let's move on to the next class.

TGameElement or Who's in the game?

To share common fields and functionality I explained here we'll use the class TGameElement.
It's not big yet so let's look at the source:

TGameElement.bmx


Import "TCoordinate.bmx"

Type TGameElement Abstract
Field pos:TCoordinate
Field oldPos:TCoordinate
Field angle:Int
Field oldAngle:Int

Method Update(fixedRate:Float) Abstract

Method Render(tween:Float) Abstract
End Type

We need to import the file TCoordinate.bmx because we use TCoordinates in our TGameElement instance so we need to tell BlitzMax how a TCoordinate looks like. The Import statement is just some kind of hint for BlitzMax where to look for the definition of a TCoordinate.

The Type declaration line ends with the keyword Abstract which tells BlitzMax two things:

  • There must never be an instance of TGameElement. Only instances of non abstract subclasses are allowed!
  • This type/class may contain abstract methods which force subclasses to implement them exactly with the given parameters and return types.

Now let's look at the fields. Not very much surprising - we have two TCoordinate fields to store the current and the last position of a game element and the same for the angle.

Finally we define two abstract methods, Update(fixedRate:Float) and Render(tween:Float). Again, there is no implementation for these methods required. We will have to implement those methods in the subclasses of TGameElement.

That does make sense because the subclasses know best what to update and what to render.

Let's finish the first part of the tutorial by introducing the TPlayer type, updating the main class ComeGetMe and have the first rotating turret on screen.

The player - our first visible object on screen

Time for the complete source code of the TPlayer type.


Import "TGameElement.bmx"
Import "TCoordinate.bmx"

Incbin "gfx/turret.png"

Type TPlayer Extends TGameElement
Global playerImg:TImage


Function Create:TPlayer(pos:TCoordinate)
Local player:TPlayer = New TPlayer
player.pos = pos
player.oldPos = TCoordinate.Copy(pos)
If playerImg = Null
playerImg = LoadImage("incbin::gfx/turret.png")
End If
Return player
End Function


Method Update(fixedRate:Float)
Local mousePos:TCoordinate
mousePos = TCoordinate.Create(MouseX(), MouseY(), 0)
' let's rotate to always look at the mouse
Self.oldAngle = Self.angle
Self.angle = pos.getAngle(mousePos)
End Method


Method Render(tween:Float)
SetRotation(self.angle)
DrawImage(playerImg, pos.x, pos.y)
SetRotation(0)
End Method
End Type

Because the TPlayer uses a TCoordinate for its position and inherits from (or extends) TGameElement we need to import both files to tell BlitzMax what they are.

The next line


Incbin "gfx/turret.png"


shows a cool feature of BlitzMax. You can tell BlitzMax to include any binary file into your executable with the command Incbin. This means you do not have to release all your valuable assets like images, animation strips or sounds as separate files but you can include all of them into your game executable and distribute just that single file!

Notice that I used a "/" instead of a "\" for the directory path. This is because BlitzMax is a multi platform solution and under MacOS and Linux the directory separator character is the "/". So BlitzMax does some automatic platform dependent conversion and you can simply use the "/" to happily retrieve all of your files on any platform.

To finish the long description of this single command: I told BlitzMax to load the file "gfx/turret.png" and store it into the executable of our game. Of course the file must be existant under the given directory and filename...

Let's continue with the declaration of the Tplayer type, making it an extension of the TGameElement type. That means we need to implement the Update() and Render() methods later on in our type.
But first we define a global


Global playerImg:TImage


A Global is a field that is exactly the same for all instances. In other languages like C++ or Java such a variable is a static variable. It belongs to the type and should only be accessed by using the type prefix (TPlayer.playerImg) from outside of the type itself.

As we reuse the image for the player every time we draw it loading and storing the image once is fine.

Let's have a short look at the Create method which is used to create a TPlayer instance.


Function Create:TPlayer(pos:TCoordinate)
Local player:TPlayer = New TPlayer
player.pos = pos
player.oldPos = TCoordinate.Copy(pos)
If playerImg = Null
playerImg =
End If
Return player
End Function

We want to create the player instance at the center of the screen so we let the Create method take the position as a parameter. A new empty TPlayer instance is created and it's fields pos and oldPos (inherited from TGameElement) are initialized with the parameter position.
The line


If playerImg = Null


checks if we already loaded the image. By default all fields that take a type (like TImage, the BlitzMax type for images) are initialized with Null. So we can compare against it.
If the image is loaded the global playerImg is "pointing" to an image and thus no longer Null.
When loading the image we need to tell BlitzMax that the image was initially incbined and needs to be loaded from inside the executable and not from a directory. So we prefix the file path with


playerImg =


At the end of the Create function the initialized player object is returned.

The Update() method is not very complicated to understand. Let's look at it:


Method Update(fixedRate:Float)
Local mousePos:TCoordinate
mousePos = TCoordinate.Create(MouseX(), MouseY(), 0)
' let's rotate to always look at the mouse
Self.oldAngle = Self.angle
Self.angle = pos.getAngle(mousePos)
End Method


We declare a local variable mousePos which is a TCoordinate. In the next line we create a new TCoordinate that is placed on the current mouse pointer position (retrieved by calling MouseX() and MouseY()). We store the last angle in oldAngle and calculate the new angle by using the TCoordinate method getAngle() between the turret's position and the mouse pointer position.

The Render() method is even simpler:


Method Render(tween:Float)
SetRotation(self.angle)
DrawImage(playerImg, pos.x, pos.y)
SetRotation(0)
End Method


With the BlitzMax SetRotation() function we assure that all following drawing commands will be rotated by the given angle (self.angle is the angle of the player instance).
Then we draw the image playerImg at the turret's position and finally switch back to a rotation of zero to assure that all following drawing commands are not affected by our rotated drawing of the player image.

And that was about it. We finish the first part of the tutorial with a look at the updated main loop code.

The updated main file ComeGetMe.bmx


SuperStrict

Import "TFRLTimer.bmx"
Import "TPlayer.bmx"

Const GFX_WIDTH:Int = 640, GFX_HEIGHT:Int = 480

Const UPDATE_FREQUENCY:Float = 100.0, SPIKE_SUPPRESSION:Int = 20

Graphics GFX_WIDTH, GFX_HEIGHT, 0

SetBlend ALPHABLEND

AutoMidHandle(True)

Global gameTime:TFRLTimer = New TFRLTimer.CreateFRL(UPDATE_FREQUENCY, SPIKE_SUPPRESSION) '1st number is logic updates per second and 2nd number is how much spike suppression you want

Global player:TPlayer = TPlayer.Create(TCoordinate.Create(GFX_WIDTH / 2, GFX_HEIGHT / 2, 0))

' the main loop starts here
While Not KeyDown(KEY_ESCAPE) And AppTerminate() = False
Cls

' update part of the main loop with constant speed
Local delta:Float = gameTime.ProcessTime()
While gameTime.LogicUpdateRequired()
DoGameLogic(gameTime.GetLogicFPS()) 'Update Game Logic Here
Wend

' rendering with additional tweening
Local tween:Float = gameTime.GetTween() 'calc the tween for smooth graphics
doGameRender(tween) 'Render Game Here

Flip 0 ' synchronize graphics buffer as soon as possible
Wend
EndGraphics
End

Function DoGameLogic(fixedRate:Float)
player.Update(fixedRate)
End Function

Function DoGameRender(tween:Float)
player.Render(tween)
gameTime.ShowFPS(0, 0)
End Function


What's new here?
We added the import of the TPlayer type at the beginning of the file.


Import "TPlayer.bmx"


A few lines below we added the line


AutoMidHandle(True)

This line places the hot spot of any image to it's center. The default hot spot of an image is the top left corner. The hot spot is used for drawing commands. When you draw an image with the DrawImage command the hot pot is placed at the position you pass as the parameters of the DrawImage command. Also rotating an image uses the hot spot as the rotation position. So placing it at the center of the image helps us to properly place our player at the center of the screen and smoothly rotate the player turret around it's center and not around it's top left corner.

To access the player everywhere in our main file (and also in all other source code files) we declare a global variable for the player. It's name is player and it is created by calling the TPlayer.Create() function. As you can see we place the player in the middle of the screen.


Global player:TPlayer = TPlayer.Create(TCoordinate.Create(GFX_WIDTH / 2, GFX_HEIGHT / 2, 0))


The TPlayer.Create() function expects a TCoordinate instance as the only parameter. Instead of creating a temporary variable for a TCoordinate we just do some nested call by creating a new TCoordinate instance inside the TPlayer.Create() call.
BlitzMax first creates the TCoordinate instance (which is the result of the TCoordinate.Create() function) and passes that into the TPlayer.Create() function call.
This is a bit tricky to understand at first but it's a very common way in OO world and helps you to avoid unneccessary declaration of local variables in methods and functions.

The last thing we need to do is add the player.Update() and player.render() calls to the two functions DoGameLogic() and DoGameRender(). See the code below.


Function DoGameLogic(fixedRate:Float)
player.Update(fixedRate)
End Function

Function DoGameRender(tween:Float)
player.Render(tween)
gameTime.ShowFPS(0, 0)
End Function

And that's about it for the first part. Find the download of the source code below. And also the link to the second part where we will add bullets and enemies and some sound.

Stay tuned!

Download part one

You can grab the first part of the tutorial including the graphics and all the source code if you click on the button below.

Have fun with it :LOL:

Part 2 of the tutorial

Come Get Me part one