Object-Oriented Programming in BlitzMax
by John Judnich & Bruce A Henderson
Introduction
At first, object-oriented programming (OOP) may appear confusing or unnecessary to those accustomed to procedural programming methods; however, once you are familiar with OOP techniques and their benefits, you may never want to go back to procedural programming again. As the name implies, object-oriented programming basically provides a way to associate variables and functions around virtual objects. By the end of this tutorial, you should have a good understanding of the use and benefits of OOP techniques.
Custom Types
If you are already familiar with the use of custom Types with Fields, you can skip this section.
A custom Type definition is a handy way to define your own custom variable types. A custom Type is basically a group of variables all bundled into one. For example:
SuperStrict
Type TCar
Field image:TImage
Field name:String
Field speed:Float, rotation:Float
End Type
Local a:Int, b:Float, car:TCar
Try running the above example. You may be surprised that it actually works. Yes - ''TCar'' is now a valid variable type! You can pass it to functions, set its value, etc. just like any other variable. The Field lines supply variables which TCar variables include internally; these internal variables are technically called "members" of TCar. A custom Type is really just a handy way to group variables.
The T prefix is just a way of indicating that it is a Type, and is a standard convention used in BlitzMax. You are free to name your Types in any way you like, with or without a T prefix. It's entirely up to you!
Accessing a member of a TCar variable is very easy. Simply use the dot (.) operator like this:
SuperStrict
Type TCar
Field image:TImage
Field name:String
Field speed:Float, rotation:Float
End Type
Local a:Int, b:Float, car:TCar
car = New TCar
car.name = "70s Chevy"
Print car.name
As you can see, car.name
is accessing the name member of the TCar variable car. As in the example, the
name can be modified and read from, just like any other variable.
You may be wondering what the car = New TCar
line does. This aspect of custom Types is generally hard
to understand at first. Think of it this way: a custom type variable (unlike a normal Float, Int, etc. variable) is
not actually a variable – imagine the TCar variable in this example as a handle which you can stick onto any TCar
to get a grip on it. Through this handle (the car variable), you can access any of the TCar's data (such as
car.name
). However, a handle on its own is worthless; it must first be attached to something. To attach this
handle (the car variable) to something, simply assign it to a real object. And, to create a real object, you use the
New keyword. The car = New TCar
line in the example above creates a new TCar object and assigns it to car. Without
this line, the car.name
code below will cause a runtime error since it is trying to access nothingness.
When a custom type variable is attached to nothing, its value is called Null. If you want to detach a handle (custom type variable) from an object, simply set the variable's value to Null (such as
car = Null
). You should always detach variables from real objects when you no longer need access to it.
A real object (created with New) will always remain in memory until all "handles" have been detached from it. Take a look at this example:
SuperStrict
Type TCar
Field image:TImage
Field name:String
Field speed:Float, rotation:Float
End Type
Local a:Int, b:Float, car:TCar, dup:TCar
car = New TCar
dup = car
car.name = "70s Chevy"
Print dup.name
First, a new car is created, and assigned to car. Then car is assigned to dup (another TCar "handle").
When a custom Type variable is assigned to another custom Type variable like this, dup is attached to the same
object that car is attached to. So now both car and dup should be attached to the same TCar.
As you can see below, car.name
sets the TCar's name to "70s Chevy". Then, dup.name
(this is dup now,
not car) displays "70s Chevy" on the screen. Since both car and dup are attached to the same TCar object, this is
the correct behavior.
You may be wondering what all this is good for. The real advantage to custom type variables being only "handles" and not "real objects" is when you begin to represent actual real-world objects in your game. This way, you have complete control over the creation/deletion of objects, and you can easily pass an existing object into a function. For example:
SuperStrict
Type TCar
Field image:TImage
Field name:String
Field speed:Float, rotation:Float
End Type
Local a:Int, b:Float, car:TCar
car = New TCar ' Create a New Car object and assign it to c
InitCar(car)
' Demonstrate that the InitCar() function has actually modified car's TCar object
Print car.speed
Print car.rotation
car = Null ' Car is no longer needed
Function InitCar(obj:TCar)
obj.speed = 0
obj.rotation = 90
End Function
Since TCar variables are only "handles" to the real data, the InitCar()
function is able to perform
operations on an existing object. In InitCar()
, the TCar parameter (obj:TCar
) gets attached to whatever TCar is
specified. In this case, it is the car which car is attached to. The function then modifies the variables of this car,
and returns.
Before you move on in this tutorial, you may want to take a while to experiment with custom Types. To understand object-oriented techniques, you must first be completely comfortable working with custom Types.
Object-Orient Programming Basics
First, take a look at the following example, which is an extremely simple demonstration of a traditional (procedural) implementation of a "counter" (it simply increments a number every time a function is called):
SuperStrict
Type TCounter
Field value:Int
End Type
Function IncrementCounter(counter:TCounter)
counter.value :+ 1
End Function
' Create a test counter
Local test:TCounter = New TCounter
' Display its value
Print "Counter value is " + test.value
' Increment it
IncrementCounter(test)
Print "Counter has been incremented"
' Now display it's value (which should now be 1+ the old value)
Print "Counter value is " + test.value
The above example simply creates an extremely simple type which contains a single integer field (called value). The function called
IncrementCounter()
will add 1 to the value of any TCounter object. The rest of the code below simply
demonstrates the operation of the function and type.
Now, to convert this example into a more object-oriented program (OOP), only a few small changes would be
made. Since OOP allows you to insert procedures within a Type definition (just as you are allowed to insert
variables within them), IncrementCounter()
can be moved inside the type definition (however, this Function
will now be called a Method). For example:
SuperStrict
Type TCounter
Field value:Int
Method Increment()
Self.value :+ 1
End Method
End Type
Now, the Increment() method can now be called like this:
' Create a test counter
Local test:TCounter = New TCounter
' Set the counter value
test.value = 5
' Increment it
test.Increment()
As you can see, the Increment() method is accessed the same way the Value variable is. The object
(test) is followed by a dot (.), then followed by the name of the member to access. In this case, test's value is
set to 5 (test.value = 5
). Then, test's Increment()
method is executed (test.Increment()
).
You may have noticed that the Increment()
Method definition has a few differences from the
IncrementCounter()
function in the other example; the keyword Self is used in place of the counter:TCounter
parameter. Because Increment()
is a member of TCounter, you can use Self to refer to whichever object is
being manipulated. For example:
test.Increment()
When this is executed, the Increment() method is called:
Method Increment()
Self.value :+ 1
End Method
Now, since test.Increment()
was just called, Self is actually referring to test, so Self.value :+ 1
is effectively
performing this operation: test.Value :+ 1
.
The Self keyword is completely optional, and most programmers omit it entirely. For example, the Method
Increment()
could be written like this:Method Increment()
value :+ 1
End Method
In this case, BlitzMax will automatically assume you are referring to the TCounter's value variable. This allows the programmer to more easily focus on the object's perspective, rather than a global perspective. For example, theIncrement()
Method simply supplies the computer with a method to increment a TCounter object; it makes no difference what counter is being manipulated, whereIncrement()
is being called from, etc. – just as long asIncrement()
does its job properly, everything will work seamlessly.
Now that you understand the basic concept of Methods, here is a more practical example which demonstrates the usefulness of OOP techniques:
SuperStrict
Type TRobot
Field name:String
Field x:Float, y:Float
Field health:Int
Method SetName(newName:String)
name = newName
End Method
Method Move(x:Float, y:Float)
Self.x :+ x
Self.y :+ y
End Method
Method Draw()
SetColor 255, 255, 255
DrawText name, x - (TextWidth(name)/2), y - 20
SetColor 0, 0, 255
DrawRect x - 5, y - 5, 10, 10
SetColor 255, 0, 0
DrawRect x - 4, y - 4, 8, 8
End Method
End Type
This TRobot Type has these members: name, x, y, health, SetName()
, Move()
and Draw()
. The Fields (name,
x, and y) store information about the robot, while the Methods (SetName()
, Move()
, and Draw()
) provide actions
which can be performed on any TRobot object. For example:
Graphics 640, 480
Local robot:TRobot = New TRobot
robot.SetName("XB-75b")
While Not KeyHit(KEY_ESCAPE)
Cls
robot.Move(1, 1)
robot.Draw()
Flip
Wend
First, this example creates a New TRobot object (called robot). robot.SetName("XB-75b")
executes the
TRobot SetName()
Method (the SetName()
Method is a little redundant here, since you could just as easily say
robot.name = "XB-75b"
; However, it can be useful, and we will look into access modifiers later which will hopefully explain why).
The main While loop simply calls robot.Move(1, 1)
and robot.Draw()
each frame. The Move()
method
moves the robot 1 unit to the right, and 1 unit down. The Draw()
method draws the "robot" to the screen.
The equivalent of this in traditional/procedural programming methods would be something like:
MoveRobot(robot, 1, 1)
andDrawRobot(robot)
. The object-oriented style is not only more structured and intuitive, but provides advanced features such as Inheritance and Polymorphism which makes programming complex object relationships extremely easy.
As you can see, using OOP techniques enforces a modular design in your programs, taking your mind
off the complex inner workings of the system, and rather focusing on the higher level manipulation of objects.
When writing a Draw()
Method for an object, for example, the programmer only needs to focus on one thing:
instructing the computer to draw an object correctly. Once that task is complete, you will never need to worry
about the internal operations of that Method again; whenever an object needs to be drawn, it's as simple as
calling object.Draw()
.
Inheritance
Inheritance in OOP is really very simple once you understand it. Inheritance is the term for appending one Type onto another one. This may seem slightly confusing at first, and is best taught by example. If you're making a game with BlitzMax, it's most likely that you'll have many different types of game objects (for example, you might have a TPlayer, TRobot, and TBuilding). Here's a short example:
SuperStrict
Type TPlayer
Field x:Float, y:Float
Field health:Int
End Type
Type TRobot
Field x:Float, y:Float
Field health:Int
End Type
Type TBuilding
Field x:Float, y:Float
Field enterable:Int
End Type
Local obj:TPlayer = New TPlayer
obj.x = 1
obj.y = 2
As you can see, all three types have a lot in common; they all have x and y variables, and both TPlayer and TRobot have health. Inheritance provides a way to make a sort of template Type which others can build on. For example:
SuperStrict
Type TEntity
Field x:Float, y:Float
End Type
Type TPlayer Extends TEntity
Field health:Int
End Type
Type TRobot Extends TEntity
Field health:Int
End Type
Type TBuilding Extends TEntity
Field enterable:Int
End Type
Local obj:TPlayer = New TPlayer
obj.x = 1
obj.y = 2
In this example, the Extends keyword after TPlayer, for example, tells BlitzMax that TPlayer not only
contains Field health:Int
, but everything else that TEntity has as well. The result is that any TPlayer objects
will now automatically have x and y variables (notice that obj.x = 1
works just fine, even though the
TPlayer Type doesn't specifically include x).
This may at first seem like only a way to save typing, but it gets amazingly useful when you get into polymorphism. However, you should first learn more about Inheritance's benefits which appear when your types include Methods.
First, here is the same example with a Method added to TEntity:
SuperStrict
Type TEntity
Field x:Float, y:Float
Method Draw()
SetColor 255, 255, 255
Plot x, y
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
End Type
Type TRobot Extends TEntity
Field health:Int
End Type
Type TBuilding Extends TEntity
Field enterable:Int
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer
obj.x = 1
obj.y = 2
obj.Draw()
Flip
WaitKey
The Draw()
method of TEntity simply plots a dot at the entity's location. Now, since TPlayer, TRobot,
and TBuilding all inherit TEntity's properties, this means that they now have a Draw()
Method (as you can see if
you run the example).
Now, drawing a dot might be OK if you want to mark the position of an entity, but TBuilding's, TRobot's,
and TPlayer's should all look unique. This can be done by overriding the Draw()
method. Doing this is really just
as simple as adding a Draw()
method to TBuilding, for example, and BlitzMax will use that one instead. For
example:
SuperStrict
Type TEntity
Field x:Float, y:Float
Method Draw()
SetColor 255, 255, 255
Plot x, y
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
obj.Draw()
Flip
WaitKey
When BlitzMax executes obj.Draw()
, it uses the most appropriate Method. In this case, it is TPlayer's
Draw()
Method (not TEntity's, because it is more abstract).
If you do not want TEntity to provide a default draw method (as it is now, it just draws a dot if no
specific Draw()
Method is provided), you can delete TEntity's Draw()
Method, and obj.Draw
will still work,
since TPlayer contains a Draw()
Method. However, a better way to do this is to add an Abstract Draw()
Method to TEntity. Making a Method Abstract is really just a way of saying that this Method is blank, and sub-
Types (such as TBuilding and TPlayer) must provide one (or else you'll get a compile error). For example:
SuperStrict
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
obj.Draw
Flip
WaitKey
You may notice that Method Draw() Abstract
in TEntity has no End Method
statement. This is
because an Abstract is always blank, and contains no data. This abstract method definition is really just
saying "all Types which inherit TEntity must have a Draw() Method". Try deleting TPlayer's Draw()
Method, and
run the program. A compile error occurs, enforcing the rule that inheritors of TEntity must have a Draw()
Method.
This may at first seem only useful for error checking (to make sure that your entities can be drawn, or whatever actions you require), but it comes in handy when using polymorphism.
Polymorphism
Polymorphism is really just a fancy word for the process of handling your objects in a generalized way. Polymorphism might be easier to understand if you think about it like this: in OOP, a TApple Type would Extend the TFruit Type (since an apple is a fruit, of course). Now you can do this, for example:
Local apple:TApple = New TApple
Local orange:TOrange = New TOrange
Local fruit:TFruit
fruit = apple
fruit = orange
The fruit:TFruit
variable, has the capability to store apples, oranges, or any other Type which Extends
TFruit. In other words, with a generalized variable (a TFruit variable or TEntity variable, for example), you can
store any sub-Type object. This means that a TPlayer, TRobot, or TBuilding object can be stored in a TEntity
variable. This is called polymorphism. For example, you could do this:
Graphics 640, 480
Local player:TPlayer = New TPlayer
player.x = 5
player.y = 7
Local entity:TEntity
entity = player
entity.Draw()
As you can see, the TEntity variable is used to reference a TPlayer Type. The entity.Draw()
line tells
the computer to draw the TEntity. In this case, this TEntity is a actually a TPlayer, so TPlayer's Draw()
Method is
used (not TEntity's). Whether the entity was a TPlayer, TRobot, TBuilding, etc. makes no difference; the
appropriate Draw()
Method will automatically be executed.
However, when accessing a TPlayer, TRobot, etc. through a TEntity variable like this, you will only be allowed access to fields and methods included in the TEntity definition; the only members you can be certain the object will contain are TEntity members – anything else may vary (depending on whether it's a TPlayer, TRobot, TBuilding, etc.)
You might be wondering what it could be useful for. The main advantages of polymorphism become
apparent when you want to do something to all objects of a general type. For example, what if you wanted to
execute Draw()
for every TPlayer, TRobot, and TBuilding? Normally, you would have to keep a list of all players,
all houses, and all robots separately (which would get very messy, especially if you later decide to add more
TEntity-inherited Types). The solution is to keep a list of them as TEntity's, since a TEntity variable has the
polymorphic ability to store TPlayer's, TRobot's, TBuilding's, etc. You can then Draw()
them whenever you need,
since Draw()
is a Method common to all TEntity's. For example:
Graphics 640, 480
Local entityList:TList = New TList
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
entityList.AddLast(obj)
Local obj2:TBuilding = New TBuilding
obj2.x = 15
obj2.y = 3
entityList.AddLast(obj2)
' Draw all entities
For Local ent:TEntity = EachIn entityList
ent.Draw()
Next
Flip
WaitKey
In case you are not familiar with BlitzMax's TList Module, it simply provides an easy way to manage a list of objects, similarly to storing them in an array.
Polymorphism is useful in any case where generalization would benefit. For example, if you have a general-purpose function which accepts a TEntity as a parameter, this means that your players, houses, and robots will all work seamlessly with that function! For example:
Graphics 640, 480
Local player:TPlayer = New TPlayer
player.x = 5
player.y = 7
Local house:TBuilding = New TBuilding
house.x = 15
house.y = 3
While Not KeyHit(KEY_ESCAPE)
Cls
DrawAndMove(player)
DrawAndMove(house)
Flip
Wend
WaitKey
Function DrawAndMove(entity:TEntity)
entity.Draw()
entity.x :+ 1
entity.y :+ 1
End Function
Constructors & Destructors
First, take a look at the following example (specifically, the section where obj and obj2 is added to EntityList):
SuperStrict
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
Draw Oval x, y, 5, 5
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local entityList:TList = New TList
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
entityList.AddLast(obj)
Local obj2:TBuilding = New TBuilding
obj2.x = 15
obj2.y = 3
entityList.AddLast(obj2)
' Draw all entities
For Local ent:TEntity = EachIn entityList
ent.Draw()
Next
Flip
WaitKey
There is one thing which might be considered messy or tedious in the above example: the need to call
entityList.AddLast()
for each new TEntity-derived object (if it wasn't added to the list, it wouldn't be rendered
later on). Wouldn't it be a lot easier if could be done automatically? For example:
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
Local obj2:TBuilding = New TBuilding
obj2.x = 15
obj2.y = 3
It would be much easier if these objects could automatically add themselves to a list somewhere.
Fortunately, this can be done in OOP! Any Type can have a special "constructor" Method which is automatically
called just as the object is created. The constructor can do anything you like, just as any other Method can (in
this case, it will be adding the TEntity to a list). To add a constructor to a Type, simply add a Method named
New()
. The New()
Method will be executed whenever an object of that type is created.
In the example above, the best place to put the constructor is in TEntity; this way, TPlayer, TBuilding, and all the other sub-Types will inherit the constructor as well. And if necessary, some of the sub-Types can override the constructor Method. However, overriding a constructor is not the same as overriding a normal Method; rather than the new Method being performed instead of the original Method, the new constructor method is done in addition to the original Method. The reason for this should be clear in this example:
SuperStrict
Global entityList:TList = New TList
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
Method New()
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New()
health = 100
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New()
health = 100
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer
obj.x = 5
obj.y = 7
Local obj2:TBuilding = New TBuilding
obj2.x = 15
obj2.y = 3
' Draw all entities
For Local ent:TEntity = EachIn EntityList
ent.Draw()
Next
Flip
WaitKey
When the TPlayer object is created, first the inherited TEntity constructor is called, which properly adds the TPlayer to the EntityList. Then, the TPlayer constructor is called additionally (remember – constructors cannot be overridden like normal Methods), which sets the player's health to 100%. Next, the TBuilding is created. With the TBuilding, only the inherited TEntity constructor is called, since no specific constructor code is provided.
After the TBuilding is added to the EntityList through the TEntity constructor, all the items in the EntityList are drawn to demonstrate that both TEntity's have been added to the list correctly.
Constructor Overloading
We can even go one step further in our example above. Rather than having to set all the Type variables individually each
time after the creation of our objects, we can specify some parameters on the New()
method and pass some initial values
into it. For example:
SuperStrict
Global entityList:TList = New TList
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer(5, 7)
Local obj2:TBuilding = New TBuilding(15, 3)
' Draw all entities
For Local ent:TEntity = EachIn entityList
ent.Draw()
Next
Flip
WaitKey
As you can see above, the constructor for TEntity now looks like New(x:Float, y:Float)
. This allows us to remove
lines like obj.x = 5
and instead call New()
by adding those values as arguments to the constructor New TPlayer(5, 7)
.
Techniques such as this can greatly reduce the amount of code you end up having to write, and certainly makes your program
a little tidier.
In the above example, you may have noticed a new line in the TRobot and TPlayer constructors (Super.New(x, y)
).
What this does is call the New()
method of the inherited Type, which in this case is TEntity's New()
method. You
could of course have added lines such as Self.x = x
, but that defeats the elegance of having polymorphism in the
first place.
Of course, the use of a constructor here is only a small example of what you can do with constructors. Basically, any initialization code you want to be performed to new objects can be automated with constructors.
In addition to constructors, there are also destructors. A destructor is done exactly like a constructor, except Delete is used instead of New as the Method's name. Destructors in BlitzMax can be used to perform some final steps before an object is deleted.
Since BlitzMax uses a garbage collection system to delete objects, don't rely on a destructor being called at any specific time; the
Delete()
Method (destructor) will be called whenever the garbage collector gets around to it.
Static Methods & Fields
As you know, a Type is a group of Methods and Fields. The value of Fields, and the operation of
Methods depends entirely on which object you are working with. For example, car1.x
may be different than
car2.x
. In OOP, you can also include what is called static methods and variables. A static variable is shared by
all objects of that type. Static variables in Type's are really no different from a standard Global variable, but with the
added benefit of keeping your code more object-oriented. For example, look at the example from the last
section:
SuperStrict
Global entityList:TList = New TList
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer(5, 7)
Local obj2:TBuilding = New TBuilding(15, 3)
' Draw all entities
For Local ent:TEntity = EachIn entityList
ent.Draw()
Next
Flip
WaitKey
Now, the Global entityList:TList = New Tlist
line can be moved inside the TEntity Type:
SuperStrict
Type TEntity
Field x:Float, y:Float
Global entityList:TList = New TList
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float)
Super.New(x, y)
health = 100
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Graphics 640, 480
Local obj:TPlayer = New TPlayer(5, 7)
Local obj2:TBuilding = New TBuilding(15, 3)
' Draw all entities
For Local ent:TEntity = EachIn TEntity.entityList
ent.Draw()
Next
Flip
WaitKey
This is how static fields work in BlitzMax; instead of using the Field keyword to define the variable,
Global is used. Below, you may notice the use of TEntity.entityList
below. Since the static field entityList is
shared by all TEntity's, you can even use "TEntity." to access it. This comes in handy especially when you don't
know if there are any TEntity objects existing (yes - static fields can be accessed, even when there are no
objects of their type in existence).
Static fields are really just a nice way to categorize your Global variables, keeping everything object oriented. BlitzMax also supports static methods. Just as static fields are simply Global variables associated with a Type, static methods are simply Functions associated with a Type. Static method can be used for a variety of purposes, although the most common is a form of initialization. For example, you could make the functions CreatePlayer(), CreateRobot(), and CreateBuilding() to make it easier to initialize certain properties of objects when creating them (since the functions would allow you to include parameters, such as x, y, etc.):
SuperStrict
Type TEntity
Field x:Float, y:Float
Global entityList:TList = New TList
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
End Type
Function CreatePlayer:TPlayer(x:Float, y:Float, health:Int)
Local ent:TPlayer = New TPlayer(x, y, health)
Return ent
End Function
Function CreateRobot:TRobot(x:Float, y:Float, health:Int)
Local ent: TRobot = New TRobot(x, y, health)
Return ent
End Function
Function CreateBuilding:TBuilding(x:Float, y:Float, enterable:Int)
Local ent:TBuilding = New TBuilding(x, y, enterable)
Return ent
End Function
Graphics 640, 480
Local obj:TPlayer = CreatePlayer(5, 7, 100)
Local obj2:TBuilding = CreateBuilding(15, 3, False)
' Draw all entities
For Local ent:TEntity = EachIn TEntity.entityList
ent.Draw()
Next
Flip
WaitKey
As you can see, using Functions to initialize object has advantages, but doing it this way is not very
object-oriented. Just like the Global entityList
was moved into the Type definition, functions can be move in
also. This way, all player-related code is now associated with the TPlayer object, all building-related code is now
associated with the TBuilding object, etc.:
SuperStrict
Type TEntity
Field x:Float, y:Float
Global entityList:TList = New TList
Method Draw() Abstract
Method New()
entityList.AddLast(Self)
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
Function Create:TPlayer(x:Float, y:Float, health:Int)
Local ent:TPlayer = New TPlayer(x, y, health)
Return ent
End Function
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
Function Create:TRobot(x:Float, y:Float, health:Int)
Local ent: TRobot = New TRobot(x, y, health)
Return ent
End Function
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
Function Create:TBuilding(x:Float, y:Float, Enterable:Int)
Local ent:TBuilding = New TBuilding(x, y, enterable)
ent.enterable = enterable
Return ent
End Function
End Type
Graphics 640, 480
Local obj:TPlayer = TPlayer.Create(5, 7, 100)
Local obj2:TBuilding = TBuilding.Create(15, 3, False)
' Draw all entities
For Local ent:TEntity = EachIn TEntity.entityList
ent.Draw()
Next
Flip
WaitKey
Generally, static methods and fields are useful when you want to perform a Type-related operation
without regard to any specific object. Static fields provide globally applicable data regarding your custom Type,
while static methods provide globally applicable code regarding your custom Type. The Create()
functions in the
previous example is a good demonstration of the most common use of static functions. However, there are
many other uses. For example, the code which draws all entities in the example above could be made into a
static method:
SuperStrict
Type TEntity
Field x:Float, y:Float
Global entityList:TList = New TList
Method Draw() Abstract
Method New()
entityList.AddLast(Self)
End Method
Function DrawAll()
For Local ent:TEntity = EachIn entityList
ent.Draw()
Next
End Function
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Self.New(x, y)
health = 100
End Method
Function Create:TPlayer(x:Float, y:Float, health:Int)
Local ent:TPlayer = New TPlayer(x, y, health)
Return ent
End Function
End Type
Type TRobot Extends TEntity
Field health:Int
Method Draw()
SetColor 255, 0, 0
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
Function Create:TRobot(x:Float, y:Float, health:Int)
Local ent: TRobot = New TRobot(x, y, health)
Return ent
End Function
End Type
Type TBuilding Extends TEntity
Field enterable:Int
Method Draw()
SetColor 255, 255, 255
DrawRect x - 5, y - 5, 10, 10
End Method
Function Create:TBuilding(x:Float, y:Float, enterable:Int)
Local ent:TBuilding = New TBuilding(x, y)
ent.enterable = enterable
Return ent
End Function
End Type
Graphics 640, 480
Local obj:TPlayer = TPlayer.Create(5, 7, 100)
Local obj2:TBuilding = TBuilding.Create(15, 3, False)
' Draw all entities
TEntity.DrawAll()
Flip
WaitKey
Encapsulation & Access Modifiers
By default, all of a Type's Fields and Methods are visible (or accessible) from everywhere. This is all great, but sometimes it would be nice if there were a way to "hide" specific implementation details of your Type, and only allow access to them through the Object's own methods. This is called "Encapsulation". By using access modifiers, you can do just that - hide functionality so that it is only visible from inside your Type.
Let's take a look at our earlier TCounter example:
SuperStrict
Type TCounter
Field value:Int
Method Increment()
value :+ 1
End Method
End Type
As it stands, if you were to use TCounter in your program, you could manipulate value in any way you wished:
Local counter:TCounter = New TCounter
counter.Increment()
counter.value = 50
Here, we've overriden the current value of our counter, and set it to 50. To prevent this, we can tell BlitzMax to change the level of access to different parts of the Type, by utilising the Private, Protected and Public keywords. Let's now lock down our TCounter Type so that we can arbitrarily change its value:
SuperStrict
Type TCounter
Private
Field value:Int
Public
Method Increment()
value :+ 1
End Method
End Type
With this minor change to the code, our previous attempt to modify the value (counter.value = 50
) will no longer compile,
warning us that value is not visible from that part of the program.
All Type members declared after an access modifier inherit that level of privacy, which gives you a fair amount of flexibility
in designing your Types with the levels of access that you desire. For example, if we neglected to add Public before
Method Increment()
then BlitzMax would assume that you wanted the Increment()
method to be Private too. In which case,
a call to counter.Increment()
would not compile either for the same reasons as above.
When considering how to apply access modifiers to Types using inheritance, you will find the Protected modifier useful. Protected allows access within a Type hierarchy but not outside of it. This means that a Protected member variable is only visible to its Type and any sub-Types, and not to your program in general. Let's have a look at an example of the different levels of accessibility we can play with:
SuperStrict
Type TEntity
Field x:Float, y:Float
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
End Method
End Type
Type TPlayer Extends TEntity
Field health:Int
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
End Type
In the code above, all Type members are publicly accessible to all parts of your program. We'll now prevent direct access to the member variables:
SuperStrict
Type TEntity
Private
Field x:Float, y:Float
Public
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
End Method
End Type
Type TPlayer Extends TEntity
Private
Field health:Int
Public
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
End Type
That looks great; access to x, y and health are now Private. But wait. If you attempt to compile this, BlitzMax
will complain about the line DrawOval x, y, 5, 5
, because TPlayer no longer has access to the Private members of TEntity.
We can fix this by changing access to those members to Protected:
SuperStrict
Type TEntity
Protected
Field x:Float, y:Float
Public
Method Draw() Abstract
Method New(x:Float, y:Float)
Self.x = x
Self.y = y
End Method
End Type
Type TPlayer Extends TEntity
Private
Field health:Int
Public
Method Draw()
SetColor 0, 0, 255
DrawOval x, y, 5, 5
End Method
Method New(x:Float, y:Float, health:Int)
Super.New(x, y)
Self.health = health
End Method
End Type
Now, Type members are only directly accessible from within the Types themselves.
By encapsulating your Types, you can protect their integrity by limiting the access your program has to the internal data. This, in theory, can help to prevent subtle bugs by guaranteeing that data can only be changed through the "official" methods, and not by some old code directly modifying it from a shady part of your program.
Abstraction & Interfaces
Abstraction and Encapsulation are connected in terms hiding away the implementation details of your custom types. BlitzMax supports the idea of abstract types and methods. An abstract method is a method that is declared, but contains no implementation or body:
Method Sleep(time:Int) Abstract
An abstract type is one which is either explicitly declared abstract using the Abstract keyword, or is a type that has an abstract method.
You cannot create an instance of an abstract type, but you can Extend it with another custom type and implement any abstract methods as required. For example :
SuperStrict
Local obj:TGraphicObject = New TGraphicObject
Type TGraphicObject Abstract
Field x:Float
Field y:Float
Method Draw() Abstract
End Type
This example will fail to compile with the error "Cannot create instance of an abstract class.", because the method Draw has not been implemented. We need to extend TGraphicObject with a new type that will implement the Draw method :
SuperStrict
Local obj:TGraphicObject = New TRectangle
obj.Draw()
Type TGraphicObject Abstract
Field x:Float
Field y:Float
Method Draw() Abstract
End Type
Type TRectangle Extends TGraphicObject
Field w:Float
Field h:Float
Method Draw()
Print "Drawing a rectangle"
End Method
End Type
Here, a non-abstract TRectangle type extends from TGraphicObject and implements the Draw method. We can now
create an instance of TRectangle, and when we run the program it outputs "Drawing a rectangle
".
Interface
An interface contains definitions for a group of related methods that a type can implement. This is similar in some respect to an abstract type, except that an abstract type may implement as for or as many methods as it needs.
By using interfaces, you may include behaviour from multiple sources in your custom type. This is useful, because BlitzMax doesn't support multiple inheritance of types (i.e. a type can only Extend a single type, but it can Implement many interfaces).
You define an interface with the Interface keyword :
Interface ISerializable
Method Serialize:String()
End Interface
Any type that implements ISerializable must contain a definition for the Serialize method that matches the parameters and return type. As a result, you guarantee that any type that implements ISerializable will contain an Serialize method.
Returning to the graphic object example above, let's say that rectangles should also be serializable. We can redefine the TRectangle type like this :
Type TRectangle Extends TGraphicObject Implements ISerializable
Which states that a TRectangle is contracted to implement both the abstract methods declared by TGraphicObject and those of ISerializable. This amount of flexibility with our custom types allows us to use a specific type in completely different ways. For example, TGraphicObject has no concept of serialization, but our subclass of it can be serialized because it implements the appropriate interface :
SuperStrict
Local obj:TRectangle = New TRectangle
Draw(obj)
Serialize(obj)
' draw an object
Function Draw(obj:TGraphicObject)
obj.Draw()
End Function
' serialize an object
Function Serialize(obj:ISerializable)
Print obj.Serialize()
End Function
Type TGraphicObject Abstract
Field x:Float
Field y:Float
Method Draw() Abstract
End Type
Type TRectangle Extends TGraphicObject Implements ISerializable
Field w:Float
Field h:Float
Method Draw()
Print "Drawing a rectangle"
End Method
Method Serialize:String()
Return x + ", " + y + ", " + w + ", " + h
End Method
End Type
Interface ISerializable
Method Serialize:String()
End Interface
The ideas behind abstraction and interfaces may appear complicated at first, but can be very powerful tools when applied correctly.
Operator Overloading
It is possible to add to and change the way operators (like +
and -
) work for custom types. Normally, for example,
you cannot add two object together:
SuperStrict
Local a:TCalc = New TCalc(10)
Local b:TCalc = New TCalc(15)
' add two objects together
Local c:TCalc = a + b
Type TCalc
Field value:Int
Method New(value:Int)
Self.value = value
End Method
End Type
It will generate a compiler error, "Operator + cannot be used with Objects."
Previously, in order to achieve the addition of two objects, you would need to have implemented your own "Add" method, and used in the following way:
SuperStrict
Local a:TCalc = New TCalc(10)
Local b:TCalc = New TCalc(15)
' add two objects together
Local c:TCalc = a.Add(b)
Print c.value
Type TCalc
Field value:Int
Method New(value:Int)
Self.value = value
End Method
Method Add:TCalc(other:TCalc)
Return New TCalc(value + other.value)
End Method
End Type
Of course, there is nothing wrong with your program filled with code like a.Add(b)
, but it may not seem as clear or
easy to read as it does when adding two Ints together, for example n + j
With operator overloading you now have the ability to write your code as "a + b".
Although you still need to write the equivalent of the "Add" method above, the main advantage is that you can use it in an arguably cleaner way in your program. Operator overloading is "syntactic sugar", in that it can make your code easier to read, perhaps therefore easier maintain and spot bugs.
To overload an operator, we use a specially named method, called Operator, followed by the operator symbol we are
overloading (for example, +
), and then the rest of the method as normal.
To add an addition overload for our example above, we could write the following method :
Method Operator+:TCalc(other:TCalc)
Return New TCalc(value + other.value)
End Method
As you can see, apart from the method name, Operator+
, the rest of the method is written in exactly the same way as
the Add method in the previous example. The method declares a return type of TCalc (:TCalc
) and a parameter of
the type TCalc (other:TCalc
).
Now we are free to write a + b
to add the two TCalc objects together, just as we attempted to do in the first
example:
SuperStrict
Local a:TCalc = New TCalc(10)
Local b:TCalc = New TCalc(15)
' add two objects together
Local c:TCalc = a + b
Print c.value
Type TCalc
Field value:Int
Method New(value:Int)
Self.value = value
End Method
Method Operator+:TCalc(other:TCalc)
Return New TCalc(value + other.value)
End Method
End Type
When you run it this time, the result of c.value
should be output as 25
, which of course is 10 + 15
.
The supported operators for overloading are *
/
+
-
&
|
~
:*
:/
:+
:-
:&
:|
:~
<
>
<=
>=
=
<>
mod
shl
shr
:mod
:shl
:shr
,
where =
is the equality symbol rather than the assignment symbol, eg. If a = b Then
.
Now, you aren't just limited to object/object expressions, you can basically have operator overloads for object/anything, including primitive types like Int and Double. In our above example, we want to add an overload for multiplying by an Int, as well as one for determining if our object value is equal to another :
Method Operator:*(value:Int)
Self.value :* value
End Method
Here we implemented the overloaded operator as :*
which is the symbol for multiplication assignment. You'll notice we don't
declare a return type, because generally :*
is used as a statement rather than an expression.
For equality, we'll create the following overload :
Method Operator=:Int(other:TCalc)
Return value = other.value
End Method
This time we declare a return type of Int (for True or False), and inside the method we perform the actual comparison test.
Here's our program with the new methods added:
SuperStrict
Local a:TCalc = New TCalc(10)
Local b:TCalc = New TCalc(15)
' add two objects together
Local c:TCalc = a + b
' multiply by 3
c :* 3
Print c.value
' 1 if True, 0 if False
Print c = b
Type TCalc
Field value:Int
Method New(value:Int)
Self.value = value
End Method
Method Operator+:TCalc(other:TCalc)
Return New TCalc(value + other.value)
End Method
Method Operator:*(value:Int)
Self.value :* value
End Method
Method Operator=:Int(other:TCalc)
Return value = other.value
End Method
End Type
As you can see, using overloaded operators with your custom types can certainly make the code less wordy, and hopefully easier for you to read.
Operator overloading allows you to define how the operator works in any way you want, but you need to be careful with that freedom.
Let's take another look at the addition overload from before :
Method Operator+:TCalc(other:TCalc)
Return New TCalc(value + other.value)
End Method
It's pretty clear that this
+
overload adds the two object values together. However, we could change it to use the following code instead, where we replace the addition with a subtraction :
Method Operator+:TCalc(other:TCalc)
Return New TCalc(value - other.value)
End Method
Although the code is technically correct, the outcome of the result is confusing, rather difficult to understand and probably hard to debug.
It is your responsibility to use operator overloading properly and in a consistent way, otherwise you are lining yourself up for no end of problems in the future (when you've completely forgotten that your addition actually subtracts!)
And remember, that operator overloading only works for custom types. It does not work for BlitzMax's built in primitive
types like Int or Double. That is, you can't change the way 3 + 5
works, but you can change the way MyObject + 5
works.
Generics
Generics adds improved type-safety to your BlitzMax applications by ensuring that a your code is working on the right kind of data, and throwing a compile-time error if it isn't. A compile-time error is a much better time for your program to fail than when it's busy running.
Summary
Hopefully, by now you will have a good understanding of object-orient programming techniques, and how to use them. Object-oriented programming is an extremely useful tool which makes even the most complex programs easier to make than ever. The entire purpose of OOP is to allow you take your mind off the inner working of your program (the parts you already completed), and lets you focus on more high-level tasks, as you continue to create your program.