Skip to content

Platformer Tutorial

boxgaming edited this page Oct 1, 2026 · 13 revisions

For this tutorial we will build this little game below, starting from scratch and building it step-by-step, highlighting the various features of the GX engine along the way.

image

If you want to try it out now press play and use the arrow keys to move and space bar to jump:
Open in QBJS | Download

About the Source

This sample includes all of the code needed for building the game in QB64, so you can download the project zip, export the contents and build it with QB64(PE). Strictly speaking, the contents of the "lib" directory are only needed for building the game in QB64 as QBJS has the GX engine already included. All of the included artwork is from opengameart.org.

Contents

Part 1 - The Basics

If this is your first time making a GX game it is recommended that you take a quick look at the Basic Concepts page to familiarize yourself with some of the key concepts of the GX engine.

A word about coding conventions

All methods and constants in GX library are prefixed with "GX" to prevent naming collisions. Methods are further grouped by concept. For example, all of the methods that can interact with the scene are prefixed with "GXScene" (e.g. GXSceneCreate, GXSceneX, GXSceneMove). A listing of the library methods can be found here (still a work in progress).

Part 2 - Let's Start Coding

Setting the Scene

For any GX project there are a couple of steps that we always need to do in order to use the library. First, we need to include the GX engine code:

'$Include: 'lib/gx.bi'

'$Include: 'lib/gx.bm'

This will include all of the GX game data structures and methods that we will need to build our game. All of our specific game code will be added between these two include statements.

Next, we need to define our game event method. The GX engine will call this method for all events that occur in the game... more on this later.

'$Include: 'lib/gx.bi'

Sub GXOnGameEvent (e As GXEvent)
End Sub
'$Include: 'lib/gx.bm'

Now we need to create our scene with GXSceneCreate. This is usually the first step in the setup of our game. Let's create a new scene that is 256 pixels by 144 pixels.

'$Include: 'lib/gx.bi'

GXSceneCreate 256, 144

Sub GXOnGameEvent (e As GXEvent)
End Sub
'$Include: 'lib/gx.bm'

If you run the program at this point you will just see a small black screen.  So, let's add something there to look at...

Adding the Player

Let's create a new entity which represents our player character with the GXEntityCreate method:

...
GXSceneCreate 256, 144

' Create the player
Dim Shared player As Long
player = GXEntityCreate("img/character.png", 16, 20, 4)
...

The first argument references the spritesheet image which should be used for the entity display.  The next two arguments define the width and height of the entity, respectively, and the last argument indicates how many frames of animation to expect.  Let's look a bit more closely at the character.png to see how GX expects spritesheets to be constructed...
image
As you can see from the image above, this spritesheet contains four different animation sequences with each distinct sequence placed on a separate row in the spritesheet image.  Each animation sequence has four frames of animation.

In order to see what affect this has had we need to tell GX to start handing game events and display.  We do this by calling the GXSceneStart method:

...
' Create the player
Dim Shared player As Long
player = GXEntityCreate("img/character.png", 16, 20, 4)

GXSceneStart
...

If you run the program now you will see our little guy in the upper left corner.  Since we haven't yet specified any position for our player entity it has defaulted to (0, 0).  We can also see that the sprite image has defaulted to the first frame of the first animation sequence.

image

This is way too small though, let's scale it up a bit.  GX has this scaling built in for us.  We can do this by adding a call to GXSceneScale right after we create the scene:

'$Include: 'lib/gx.bi'

GXSceneCreate 256, 144
GXSceneScale 2

...

This will tell the engine to scale the entire scene up to two times its original size and start to give us that retro pixel art look.  While we're in there, let's also reposition the player character.  We can do this with a call to the GXEntityPos method:

...

' Create the player
Dim Shared player As Long
player = GXEntityCreate("img/character.png", 16, 20, 4)
GXEntityPos player, 120, 75

GXSceneStart
...

The first parameter is the handle to the player object that was returned from the call to GXEntityCreate.  This will be the case or all of the other GXEntity* methods.  The next two parameters specify the x and y position of the entity.  This will cause the upper left corner of the entity to be positioned at the indicated coordinates.

image

Ok, this is great and all, but a game needs to be more than a static image, next time we'll start tackling some movement and talk about game events...

Part 3 - Game Events & Movement

Before we start adding movement to our game we need to first understand the basics of the game event model in GX.  And to understand the game event model let's first look at the game loop.

An important aspect of any game is the game loop.  The pseudo-code for a typical game loop looks something like this:

Initialization
Do
    Input Handling
    Game Logic / Updates
    Physics & Collision Detection
    Rendering
    Timing/Frame Regulation
Loop
Step Description
Initialization This is where the game sets up everything it needs, like loading assets, initializing variables, and preparing game objects.
Input Handling The loop checks for user inputs (keyboard, mouse, controller) and processes them so the game can react accordingly.
Game Logic/Updates Here, the game updates the state of the world based on inputs, AI behavior, physics, and other rules of the game.
Physics & Collision Detection The loop calculates object movements and interactions, ensuring things like gravity, collisions, and object responses are handled correctly.
Rendering The game draws the updated world to the screen. This includes rendering characters, backgrounds, UI elements, and visual effects.
Timing/Frame Regulation To ensure the game runs at a consistent speed, the loop manages time, often using a fixed or variable timestep to control updates and rendering.

You'll notice in the examples shown so far there is no main loop in our actual game code.  That is because the GX engine is taking care of a lot of the heavy lifting here for us.  Instead, the engine sends events to our game at various points so we can implement the desired behavior that is specific to our game.  It does this by calling the GXOnGameEvent method that we added at the beginning of Part 1.

Ok, getting back to our code, if we want to move our little player character when the user presses certain keys we will need to handle the Update event.  Let's add a Select...Case statement to our GXOnGameEvent to set ourselves up for handling multiple events and add a Case statement for the GXEVENT_UPDATE event type.  To keep things organized, let's call a new OnUpdate method when the event is fired.

...
Sub GXOnGameEvent (e As GXEvent)
    Select Case e.event
        Case GXEVENT_UPDATE: OnUpdate e
    End Select
End Sub

Sub OnUpdate (e As GXEvent)
End Sub
...

Now, let's add the logic in our new OnUpdate method to make the player move left or right when the left or right arrow keys are pressed:

...
Sub OnUpdate (e As GXEvent)
    If GXKeyDown(GXKEY_LEFT) Then
        GXEntityVX player, -40

    ElseIf GXKeyDown(GXKEY_RIGHT) Then
        GXEntityVX player, 40

    Else
        GXEntityVX player, 0
    End If
End Sub
...

The GXKeyDown function will return true (-1) if the specified key is currently being pressed and false (0) when it is not.  The GXEntityVX sets the entity's X or horizontal velocity.  A negative value will indicate movement to the left. A positive value will indicate movement to the right.  The value passed for the velocity is in pixels per second.

Ok, we're starting to get somewhere now, we can move our player.  However, he just seems to be sliding around without moving his feet and he doesn't change direction.  So, let's add some animation.  First let's add some constants to define our left and right animation sequences that we looked at in Part 2:

...
'$Include: 'lib/gx.bi'
Const MOVE_RIGHT = 1
Const MOVE_LEFT = 2

GXSceneCreate 256, 144
GXSceneScale 2
...

Then we'll add a call to GXEntityAnimate to show the appropriate animation when the user presses a direction key.  The third parameter to this method indicates the speed of the animation in frames per second.

...
Sub OnUpdate (e As GXEvent)
    If GXKeyDown(GXKEY_LEFT) Then
        GXEntityVX player, -40
        GXEntityAnimate player, MOVE_LEFT, 10

    ElseIf GXKeyDown(GXKEY_RIGHT) Then
        GXEntityVX player, 40
        GXEntityAnimate player, MOVE_RIGHT, 10

    Else
        GXEntityVX player, 0
        GXEntityAnimateStop player
    End If
End Sub
...

Now if you run the program you will see that our player now appears to be walking left or right based on the keys pressed.  When the keys are released he stops and the walk animation is stopped since we called the GXEntityAnimateStop method.
Open in QBJS | Download

Part 4 - Maps

Ok, so far we have just been walking around in the dark.  Let's look at creating an environment for our player to inhabit.  We'll start by creating a map.

GX has pretty robust support for tile-based maps. What are tile-based maps? Well, they are used in many 2D games to be able to construct large worlds from a relatively small set of reusable image tiles called a tileset.  For our game sample we will be using the following tileset image:

image

This tileset image is made up of 16x16 image tiles.  Each tile in the set is assigned a number based on its position:

image

A map, then, is made up of a collection of these tiles arranged in a grid.  If we want to create a map based on this tileset that fills the screen we can determine the dimensions of the map by dividing the screen dimensions by the tileset size.  Our screen size is 256x144. Dividing this by the tileset size 16x16 we get a map that has 16 columns x 9 rows of tiles.

In order to make creating and maintaining maps a bit easier, GX includes a [Map-Maker](Map Maker) application.  Originally, this was built with InForm and the (source)[https://github.com/boxgaming/gx/tree/main/mapmaker} is included in the GX Github project.  Going forward, there is a web-based version.

We'll come back to this later, but first, let's look at what's involved in creating a map programmatically using the GX API.  Understanding this will be important if we want to load map data from an existing source or if we want to get into more advanced procedurally generated maps.  (An example of this is the [url=https://qb64phoenix.com/forum/showthread.php?tid=2824]Wave Function Collapse POC.)

Picking up where we left off in Part 3, let's create a new method to set up our map:

Sub LoadMap
    GXTilesetCreate "img/tileset.png", 16, 16
    GXMapCreate 16, 9, 1

    GXMapTile 0, 2, 1, 3
    GXMapTile 0, 3, 1, 15
    GXMapTile 0, 4, 1, 15
    GXMapTile 0, 5, 1, 15
End Sub

Before creating our map we need to first load the tileset we described above with the GXTilesetCreate method.  The first parameter should indicate the image file which contains the tileset. The next two parameters indicated the width and height of the tiles in the set. Then we create a new map with the GXMapCreate method. This method expects us to pass in the number of columns, rows, and layers the map should contain. We'll keep it simple for now and start with a single layer.

The next set of calls to GXMapTile will place a tile in the map at the specified column, row and layer.  The last parameter is the tile id which is derived from its position in the tileset image.

Let's call our new method after we initialize the scene:

...

GXSceneCreate 256, 144
GXSceneScale 2

LoadMap

...

If we run the program now, we'll see the beginnings of our map:

image

This would be a bit tedious to add an individual call to GXMapTile for every single tile in the map.  So, let's add some basic map data:

...

mapLayer1:
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
Data 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,13
Data 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,13
Data 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,13
Data 15, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0,13
Data 15, 0, 0, 0, 0, 0, 0,13,15, 0, 0, 0, 0, 0, 0,13
Data 28, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,30

Sub GXOnGameEvent (e As GXEvent)
...

Then let's update our LoadMap method to load the map tiles from our data section:

Sub LoadMap
    GXTilesetCreate "img/tileset.png", 16, 16
    GXMapCreate 16, 9, 1

    Dim As Integer col, row, tile, layer
    For layer = 1 To GXMapLayers
        For row = 0 To GXSceneRows - 1
            For col = 0 To GXSceneColumns - 1
                Read tile
                GXMapTile col, row, layer, tile
            Next col
        Next row
    Next layer
End Sub

That's starting to look more interesting:

image

Let's add some more detail now by adding another layer of tiles.  First add the additional layer data:

...

mapLayer2:
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0,53,54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data  0, 0,65,66, 0, 0, 0, 0, 0, 0, 0, 0,50, 0, 0, 0
Data  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

...

Then adjust the call to GXMapCreate in our LoadMap method to initialize two layers:

...
    GXMapCreate 16, 9, 2
...

Here is the completed exercise:
Open in QBJS | Download

Now we have the beginnings of an interesting environment for our player.  However, there are two issues that become apparent...  He can walk right through walls and seems to be floating in the air.

In part 5 we'll look at addressing this with collision detection.

Part 5 - Collision Detection

So, if we want to prevent our player character from being able to walk through walls, we need to be able to detect when a collision has occurred.  

GX has built-in support for two different types of collision detection. The first, which we'll look at now, handles the scenario when an entity collides with a given tile position on the map.  The second, which we may cover in the future, handles the scenario when an entity collides with another entity.

To implement tile collision detection, we need to handle a new event (GXEVENT_COLLISION_TILE).  So, let's add a new Case statement for this event to our main event function GXOnGameEvent and create a new method for this logic:

...
Sub GXOnGameEvent (e As GXEvent)
    Select Case e.event
        Case GXEVENT_UPDATE: OnUpdate e
        Case GXEVENT_COLLISION_TILE: OnTileCollision e
    End Select
End Sub
...
Sub OnTileCollision(e As GXEvent)
    If GXMapTile(e.collisionTileX, e.collisionTileY, 1) Then
        e.collisionResult = GX_TRUE
    End If
End Sub
...

GX is going to pass us the handle of the entity and information about the tile with which the entity is currently intersecting.  It does this by setting values in the GXEvent object that is passed to our event handler.  So far, we have only been concerned with the event type. For the tile collision event GX will also set the "entity" attribute to the handle of the entity involved in the collision as well as the position of the tile in the "collisionTileX" and "collisionTileY" attributes. The GXEvent object has an additional attribute which we can use to communicate back to the engine whether a collision was encountered, "collisionResult".

Since there is currently only one entity (the player), we need only check the map to see whether there is a tile present at the location by calling the GXMapTile function.

If we run the program now, we'll see that the player will not be able to walk past the walls on either side of the screen.

image

Ok, well that fixed one of our problems, but our player still seems to be walking through the air.  Now that we have our collision detection in place, we can turn on the gravity and our player won't fall through the floor. In GX, gravity is applied at the entity level.  We can apply this to our player by adding a call to the GXEntityApplyGravity method:

...
' Create the player
Dim Shared player As Long
player = GXEntityCreate("img/character.png", 16, 20, 4)
GXEntityPos player, 120, 75
GXEntityApplyGravity player, GX_TRUE
...

This will now cause our player to fall down to the ground tiles:

image

You may also notice that the player is able to walk in front of the tree and barrel tiles.  This is because we are only checking for collisions on layer 1.

Now we can fall, but we're missing a key element of any good platformer... the jump.  We can add this by triggering a sudden upward velocity when the user presses the spacebar by adding the following to our OnUpdate method:

...
    Else
        GXEntityVX player, 0
        GXEntityAnimateStop player
    End If
    
    If GXEntityVY(player) = 0 And GXKeyDown(GXKEY_SPACEBAR) Then
        GXEntityVY player, -120
    End If
End Sub
...

In addition to checking to see if the spacebar is pressed, we also want to make sure that the player's current Y velocity is 0.  We don't want him to be able to be able to start a jump when he is already jumping or falling.

Putting it all together, we can now walk and jump around our little map!
Open in QBJS | Download

Next, we'll look at refining the controls and movement.

Part 6 - Refining the Controls

The feel of player movement and the responsiveness of the controls are two of the most important aspects of a platformer.  Before we get too much further building out the levels of our game we want to dial in our player controls and movement as much as possible.  Otherwise, if we spend a bunch of time now building out our level and then find later that the jump is too low or too high or too "floaty" or we want to change the walk/run speed of our character this could cause us a lot of re-work to make the levels work with the new mechanics.

That being said, depending on the type of game we are making, the player movement that we have at the end of Part 5 might be totally serviceable, it might even be preferred. In this particular example, however, we want to aim for movement that is more like Mario's from Super Mario Bros on the NES.

The first thing we want to do to improve the feel of the player movement is to incorporate some acceleration.  In our current logic, when the user presses left or right, the player character is immediately at full velocity.  When the key is released, the movement immediately stops.  This can leave the movement feeling a bit flat.

To incorporate acceleration, we can change our current implementation so that instead of full velocity being applied immediately when the direction key is applied, we instead increase the velocity a bit more each frame that the button is pressed and decrease the velocity a bit each frame after the key is released.

Let's start by adding some new constants for acceleration and max velocity:

...
'$Include: 'lib/gx.bi'
Const MOVE_RIGHT = 1
Const MOVE_LEFT = 2
Const ACCEL = 7
Const MAX_VELOCITY = 120
...

Let's also add a new global variable to track the current player direction:

...
Dim Shared direction As Integer
direction = MOVE_RIGHT
...

Next, we need to make a number of changes to our logic which handles player movement.  Let's take the opportunity to move this logic from the OnUpdate method into a dedicated method named HandlePlayerEvents:

...
Sub OnUpdate (e As GXEvent)
    HandlePlayerMovement e
End Sub
...
Sub HandlePlayerMovement (e As GXEvent)
    If GXKeyDown(GXKEY_LEFT) Then
        GXEntityVX player, -60
        GXEntityAnimate player, MOVE_LEFT, 10

    ElseIf GXKeyDown(GXKEY_RIGHT) Then
        GXEntityVX player, 60
        GXEntityAnimate player, MOVE_RIGHT, 10

    Else
        GXEntityVX player, 0
        GXEntityAnimateStop player
    End If
    
    If GXEntityVY(player) = 0 And GXKeyDown(GXKEY_SPACEBAR) Then
        GXEntityVY player, -120
    End If
End Sub
...

Now, let's add some variables to use for our velocity adjustments and initialize them to the current player velocity:

...
Sub HandlePlayerMovement (e As GXEvent)
    Dim As Integer vx, vy
    vx = GXEntityVX(player)
    vy = GXEntityVY(player)
...

Then, we'll incorporate the velocity changes, starting with the right movement.  We'll replace our original simple algorithm...

...        
    ElseIf GXKeyDown(GXKEY_RIGHT) Then
      GXEntityVX player, 60
...

...with the following:

...
    ElseIf GXKeyDown(GXKEY_RIGHT) Then
        vx = vx + ACCEL
        If vx > MAX_VELOCITY Then vx = MAX_VELOCITY
        direction = MOVE_RIGHT
...

With this change, we will increase the player's horizontal velocity by our acceleration constant (ACCEL).  We'll add a condition to make sure that the velocity doesn't exceed our MAX_VELOCITY constant.  Then we'll update our variable that is tracking the direction our player is facing.

Next, we need to make the same changes to our left movement logic:

...
    If GXKeyDown(GXKEY_LEFT) Then
        vx = vx - ACCEL
        If vx < MAX_VELOCITY * -1 Then vx = MAX_VELOCITY * -1
        direction = MOVE_LEFT
...

Now we can add the logic to decelerate when the player is no longer pressing the left or right arrow keys. 

...
    ' slow down when a direction key is not pressed
    ElseIf vx > 0 Then
        vx = vx - ACCEL
        If vx < 0 Then vx = 0

    ElseIf vx < 0 Then
        vx = vx + ACCEL
        If vx > 0 Then vx = 0

    Else
...

Finally, after the end of our If..ElseIf..Else block, we can add a call to set the horizontal velocity of our player based on our updated vx variable:

...
    End If
    
    GXEntityVX player, vx
...

If you try out the program now you will see that our player character takes a little bit to get up to full speed and when you release the arrow keys he'll continue moving for a step or two before he comes to a stop.  You can experiment with the acceleration constant to get just the right feel.  For example, if you set it lower to a value of 3, it will feel more like we're trying to run on a slipper surface like ice.  (You can see an example of this type of movement in Sleighless which has Santa running around on the ice and snow.)

Alright, the horizontal movement is looking better, now let's look at adjusting a few more things to improve the feel of the player movement:

  • The jump feels a little too "floaty" for what we are wanting in this game.
  • The player seems to keep walking in the air instead of striking a jumping pose.
  • The player isn't returning to an idle pose when he has stopped moving.

We can address the "floaty-ness" of the jump by increasing the speed of the jump and adding some new logic to juice up the effect of gravity a bit:

...
    If GXEntityVY(player) = 0 And GXKeyDown(GXKEY_SPACEBAR) Then
        GXEntityVY player, -260
    End If

    ' fall faster for a less "floaty" jump
    If vy > 0 Then
        GXEntityVY player, vy + 25
    ElseIf vy < 0 Then
        GXEntityVY player, vy + 10
    End If
...

Finally, let's add some logic to strike a jump pose when our player is in the air and return to an idle pose when he has stopped moving:

...
    ' show jump frame
    If vy <> 0 Then
        GXEntityAnimateStop player
        GXEntityFrameSet player, direction, 2
    End If

    ' idle
    If vy = 0 And vx = 0 Then
        GXEntityAnimateStop player
        GXEntityFrameSet player, direction, 1
    End If
...

If we run the program now, we will see that the controls and movement feel much more responsive.  However, there is one issue that still remains.   If we walk just a little past the edge of the center platform and stop, we seem to be levitating.

image

The reason for this has to do with the way collision detection works in GX.  By default, it uses the entity's height and width to determine the bounding box when checking for collisions.  Our player character has a default collision boundary that looks like this:

image

We can adjust this default bounding box with a call to the GXEntityCollisionOffset method which allows us to tell GX how many pixels in from the left, top, right and bottom sides of the entity we should offset the collision bounding box

...
GXEntityApplyGravity player, GX_TRUE
GXEntityCollisionOffset player, 3, 5, 3, 0

GXSceneStart
...
image

Here now are all of the changes we covered in Part 6: Open in QBJS | Download

Part 7 - Using the Map Maker

In Part 4 we looked at how to create a map programmatically using the GX API.  In this section we'll look at how to create the same simple map as an introduction to using the GX Map Maker.  This tool aims to make it easier to create and maintain the maps used in your game projects.  This will set us up to build out the rest of our level.

To get started, you can launch the GX Map Maker from this URL:
https://boxgaming.github.io/gx-mapmaker

Let's create a new map by selecting File -> New.  This will show us the new map dialog.  We'll initially use the same tile dimensions (16 columns by 9 rows) and start with one layer.  We'll also want to select the same tileset.png image we used in Part 4 and leave the default values of 16 x 16 for the size of the tiles.

image

This will create a new map and load the tileset in our tile palette in the right panel. This tileset is pretty small so we may want to zoom in the tileset and map views using the Tileset -> Zoom In and Map -> Zoom In menu options.

Click a tile in the tileset panel to select it as the active brush.  The selected tile will be framed with a yellow square and the tile's id will be displayed at the bottom of the panel.  Once a tile is selected it can be placed on the map at the desired location by clicking on the map.

image

You can either place a single tile at a time in this manner, or, hold down the left button and move the mouse to paint continuously with the selected tile.

Multiple tiles can be selected as a block. Click and drag the mouse to select a block of tiles. The map cursor will now be resized to the size of the tile selection.

You can also copy tiles from a selection on the map. Hold the Shift key and drag the mouse down and to the right to select the tiles you wish to copy. A yellow border will be displayed around the block of tiles to copy.  The cursor will change to the size of the tile selection. Clicking on the map will paste a copy of the selected tiles at the current cursor location.

image

If you place a block in the wrong place, you can delete it by pressing either the X or Delete key.

Using these basic techniques let's recreate the first layer from Part 4.  Then we can create a new layer by pressing the "+" button on the far right side of the Layers panel.  This will add a "Layer 2" to the list below. To make this the active layer for editing click the on the "Layer 2" label. We can now add our tree and barrel from Part 4 to the map.

Clicking the eye icon on a given row in the Layers panel will toggle the visibility of that layer.  You can also lock a layer for edit so you don't accidently make changes to it when you don't intend to.

Once you've got your masterpiece complete you can save it with the File -> Save menu option.  This will prompt you to download the file.  Let's name it "platformer.gxm".

Loading the Map

Let's look at using this map in our work-in-progress.  First, we'll create a new folder in our Files tab named "map" and then drop in our "platformer.gxm" file.  We can now remove all of our map Data sections as well as our LoadMap function from our code.  We can also remove the tileset.png from our img folder as it is now contained within our map file.

Then we can replace our call to the LoadMap function with the GXMapLoad method:

...
GXSceneCreate 256, 144
GXSceneScale 2

' Create the map
GXMapLoad "map/platformer.gxm"

' Create the player
Dim Shared player As Long
...

Here's the complete set of changes.  This is the first step where we actually have less code than the last:
Open in QBJS | Download

Part 8 - Collision Layer

In Part 5 we implemented our original collision detection.  It is based on simple logic that looks to see if our character is intersecting with any tile in layer 1.  If so, then we have a collision, and the player should not be allowed to move through that tile space.  This method works fine for simple maps with simple collision rules.  However, as you add complexity to your map you may run into scenarios where you want to separate the collision rules from the art placement and/or you may want to support different types of collision.  One common approach is to create a dedicated collision layer in your map.  Let's look at creating that for our game.  

We can open the map we were working on in part 6 from the File -> Open menu in the GX Map Maker.  Let's create a new layer by selecting the "+" button in the right corner of the Layers Panel:

image

This will create a new layer with the label "Layer 3".  Let's select that layer in the list to make it the active layer. 

In the bottom right corner of the tilesheet there is a tile that looks like a square with a dotted outline.  This was included to use in our collision layer.  You could actually use any tile you wanted, but it is useful to create one that allows you to see the tiles underneath.

With that tile selected, let's add collision tiles around the borders of our map like so:

image

Then save the changes with the File -> Save menu item.

Let's incorporate these changes into our game.  Copy the updated platformer.gxm file into map folder of the project.   Let's add a couple of constants:

...
Const COLLISION_LAYER = 3
Const COLLISION_TILE = 72
..

Now let's rework our OnTileCollision method to use the new method:

...
Sub OnTileCollision (e As GXEvent)
    Dim ctile
    ctile = GXMapTile(e.collisionTileX, e.collisionTileY, COLLISION_LAYER)
    If ctile = COLLISION_TILE Then
        e.collisionResult = GX_TRUE
    End If
End Sub
...

If we run our game now, we will see that we can now walk right past the little hill in the center of the screen since we did not add any collision tiles in that area.  Unfortunately, we can see the collision tiles on the screen.  No problem, we can remove this from view with a call to the GXMapLayerVisible method:

...
' Create the map
GXMapLoad "map/platformer.gxm"
GXMapLayerVisible COLLISION_LAYER, GX_FALSE
...

Ok, now let's look at a more advanced scenario for collision detection.  Suppose we want our player to be able to walk in front of the little hill in the center but still allow him to jump on top of it.  Well, first we'll need a way to indicate that we should use a different kind of collision detection in our collision layer.

If we go back to our GX Map Maker, we can see that next to the collision tile we just used is another similar tile in position 71.  Let's select that tile and then, with our map layer (3) selected, place that collision tile at the top of our little hill:

image

Save the changes and copy the map into the game project.

Now, let's add a constant for the new collision tile and name it something wildly creative like "COLLISION_TILE2":

...
Const COLLISION_TILE = 72
Const COLLISION_TILE2 = 71
...

Then we can update our OnTileCollision method with the new logic for this collision type:

...
Sub OnTileCollision (e As GXEvent)
    Dim ctile
    ctile = GXMapTile(e.collisionTileX, e.collisionTileY, COLLISION_LAYER)

    If ctile = COLLISION_TILE Then
        e.collisionResult = GX_TRUE

    ElseIf ctile = COLLISION_TILE2 Then
        If GXEntityVY(e.entity) >= 0 Then
            If GXEntityY(e.entity) + GXEntityHeight(e.entity) < e.collisionTileY * GXTilesetHeight + 1 Then
                e.collisionResult = GX_TRUE
            End If
        End If
    End If
End Sub
...

Our logic here when intersecting with our new collision tile basically checks to see if the player entity is falling from above the tile, then prevent movement, otherwise, let them pass through.

With these changes we have all of the player movement and collision mechanics in place:
Open in QBJS | Download

Part 9 - Side Scrolling

Well at this point we have some pretty decent platforming mechanics, but we're missing one important element - side scrolling.  The first thing we need is a map that is larger than the visible screen.  Let's copy the full map from the original post into our work in progress. 

If we run the game now, well... it's not a lot different.  There's a new platform we can jump on and exit the current screen, but the view doesn't follow our player so we can't see what's happening.

image

We need to scroll the screen.  We could implement the logic ourselves to change the scene position (GXScenePos) as the player moves through the level.  However, GX has a couple of built in methods that can make this easy for us.  First, we can call GXSceneFollowEntity to tell GX to keep our player character centered on the screen.  Then we can call GXSceneConstrain to keep the scene from moving past the edges of our map.

...
GXEntityCollisionOffset player, 3, 5, 3, 0

GXSceneFollowEntity player, GXSCENE_FOLLOW_ENTITY_CENTER
GXSceneConstrain GXSCENE_CONSTRAIN_TO_MAP

GXSceneStart
...

If we run our program again, we will now see the scene stay centered on our player as he moves through the map.

Backgrounds

Up to now we've just been rendering our map on top of a plain black background.  We can make this a bit more interesting.  GX has support for multiple background layers.  Let's start with a static background with a blue sky and some wispy clouds.

...
GXSceneScale 2

' Create the background
Dim As Long bg1
bg1 = GXBackgroundAdd("img/clouds.png", GXBG_STRETCH)

' Create the map
GXMapLoad "map/platformer.gxm"
...

We pass to the GXBackgroundAdd method the path to our background image.  The second parameter indicates the background mode.  In this case we will tell it to just stretch the background image to fill the scene.

image

Ok this is more interesting, but we can do more.  If we want to add more of a feeling of depth, we can add additional layers that scroll at different rates (parallax scrolling).  Let's add our mountain background and use this method:

...
GXSceneScale 2

' Create the backgrounds
Dim As Long bg1, bg2
bg1 = GXBackgroundAdd("img/clouds.png", GXBG_STRETCH)
bg2 = GXBackgroundAdd("img/mountains.png", GXBG_WRAP)
GXBackgroundWrapFactor bg2, .25

' Create the map
GXMapLoad "map/platformer.gxm"
...

This time we have specified the wrap mode.  This tells GX to scroll the background image in the direction the scene is moving and to wrap the image so that when the end of the image is reached it repeats the image from the beginning.  This gives the appearance of a continuous image that doesn't have a start or end.  The GXBackgroundWrapFactor indicates how fast the image will scroll.  The default value of 1 will cause the background to scroll in sync with the scene.  We are passing in a value of .25 which will cause this background to scroll at 1/4th the speed of the scene.

image

Here is the updated project up to this point:
Open in QBJS | Download

And with that we're almost done, just one more part to go...

Part 10 - Game Over

Well, all good things must come to an end, and so it is with our game (and this tutorial).  All that's left from what we set out to cover is to detect when the game has ended and show a "Game Over" message. This message should be shown if the player falls off the screen or when he successfully makes it to the end of the level.

In order to draw text on the screen we need to implement a new event handler for the "Draw Screen" event. So let's add a new Case statement to our GXOnGameEvent and call a new OnDrawScreen method.  For testing purposes let's just have it use the GXDrawText method to display a test message on the screen:

...
Sub GXOnGameEvent (e As GXEvent)
    Select Case e.event
        Case GXEVENT_UPDATE: OnUpdate e
        Case GXEVENT_COLLISION_TILE: OnTileCollision e
        Case GXEVENT_DRAWSCREEN: OnDrawScreen e
    End Select
End Sub
...
Sub OnDrawScreen (e As GXEvent)
    GXDrawText GXFONT_DEFAULT, 1, 1, "TEST"
End Sub
...

The first parameter to the GXDrawText method indicates the bitmap font to use.  There are two default fonts included with the engine GXFONT_DEFAULT and GXFONT_DEFAULT_BLACK. The next two parameters indicate the screen coordinates of where to place the text and the final parameter is the text to display.

If we run the program now, we'll see "TEST" displayed in the upper left corner of the screen.  Let's adjust this to show instead the current x coordinate of our player.  We'll switch to use the default black font as it should show up better against the light sky:

...
Sub OnDrawScreen (e As GXEvent)
    GXDrawText GXFONT_DEFAULT_BLACK, 1, 1, Str$(Fix(GXEntityX(player)))
End Sub
...

Now we can just run to the end of the board and find the x coordinate which should trigger that the player has made it to the end of the level.

image

Let's add a new variable to track our game over state:

...
Const COLLISION_TILE2 = 71

Dim Shared gameOver As Integer
Dim Shared direction As Integer
...

Now, let's add the logic to our OnUpdate method to detect the end of game

...
Sub OnUpdate (e As GXEvent)
    HandlePlayerMovement e
    
    If gameOver Then GXSceneStop
    
    If GXEntityY(player) > GXSceneHeight Or GXEntityX(player) > 1462 Then
        gameOver = GX_TRUE
    End If
End Sub
...

Finally, let's update our OnDrawScreen method to remove our test message and show a "Game Over" message when our gameOver variable has been set to true:

...
Sub OnDrawScreen (e As GXEvent)
    'GXDrawText GXFONT_DEFAULT_BLACK, 1, 1, Str$(Fix(GXEntityX(player)))

    If gameOver Then
        Dim As Integer x, y
        x = GXSceneWidth / 2 - 32
        y = GXSceneHeight / 2 - 8
        GXDrawText GXFONT_DEFAULT, x, y, "GAME OVER"
    End If
End Sub
...

If we run the game now, we'll see our "Game Over" message anytime we fall off the screen or reach the end.  

Bitmap Fonts

However, it might be nice to make the message stand out a bit more.  Let's load a custom bitmap font from the following image:

image

We can create our font with the GXFontCreate method:

...
GXSceneConstrain GXSCENE_CONSTRAIN_TO_MAP

Dim Shared gfont As Long
gfont = GXFontCreate("img/font.png", 8, 9, _
                    "0123456789ABCDEF" + GX_CRLF + _
                    "GHIJKLMNOPQRSTUV" + GX_CRLF + _
                    "WXYZc>-x'!/")
GXSceneStart
...

The first parameter specifies the path to the file containing the font image.  The next two parameters are the character width and height in pixels.  The last parameter is a string which defines the character map.

With the font loaded we can now use it instead of the default font in our OnDrawScreen method:

...
    If gameOver Then
        Dim As Integer x, y
        x = GXSceneWidth / 2 - 32
        y = GXSceneHeight / 2 - 8
        'GXDrawText GXFONT_DEFAULT, x, y, "GAME OVER"
        GXDrawText gfont, x, y, "GAME OVER"
    End If
...

If we run our program again, we'll now see the new font in action:

image

And with that we've come to the end of our platformer tutorial. You can try out the final version here:
Open in QBJS | Download

Clone this wiki locally