BlitzMax

BlitzMax

  • Downloads
  • Docs
  • API
  • Resources
  • About

›Tutorials

Setup

  • Getting Started
  • Win32
  • Linux
  • macOS
  • Android
  • iOS
  • Raspberry Pi
  • NX (Switch Homebrew)
  • Custom Settings

Tutorials

  • Beginners Guide
  • OOP Tutorial
  • Network Programming
  • TCP Socket Programming

Language

  • Arrays
  • Basic Compatibility
  • Collections
  • Comments
  • Conditional Compiling
  • Constants
  • Data Types
  • Debugging
  • Enums
  • Exceptions
  • Expressions
  • Functions
  • Identifiers
  • Literals
  • Modules
  • Objects
  • Program Flow
  • Slices
  • Strings
  • User Defined Types
  • Variables
  • Advanced Topics

    • Interfacing With C
    • Memory Management
    • Pointers
    • Custom Pre/Post Compilation Scripts
    • Creating DLLs

Tools

  • MaxIDE
  • BlitzMax Make (bmk)
  • BlitzMax Compiler (bcc)
Edit

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, the Increment() Method simply supplies the computer with a method to increment a TCounter object; it makes no difference what counter is being manipulated, where Increment() is being called from, etc. – just as long as Increment() 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) and DrawRobot(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.

← Beginners GuideNetwork Programming →
  • Introduction
  • Custom Types
  • Object-Orient Programming Basics
  • Inheritance
  • Polymorphism
  • Constructors & Destructors
  • Constructor Overloading
  • Static Methods & Fields
  • Encapsulation & Access Modifiers
  • Abstraction & Interfaces
  • Interface
  • Operator Overloading
  • Generics
  • Summary
BlitzMax
Docs
Getting StartedDownloadsAbout
Community
ResourcesSyntaxBomb Forums
More
GitHubStarChat on Discord
Copyright © 2022 Bruce A Henderson