Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
GX Platformer Tutorial
#11
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:
Code: (Select All)
...
'$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:
Code: (Select All)
...
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:
Code: (Select All)
...
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:
Code: (Select All)
...
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...
Code: (Select All)
...        
    ElseIf GXKeyDown(GXKEY_RIGHT) Then
      GXEntityVX player, 60
...
...with the following:
Code: (Select All)
...
    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:
Code: (Select All)
...
    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. 
Code: (Select All)
...
    ' 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:
Code: (Select All)
...
    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:
Code: (Select All)
...
    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:
Code: (Select All)
...
    ' 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.
   

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:
   

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
Code: (Select All)
...
GXEntityApplyGravity player, GX_TRUE
GXEntityCollisionOffset player, 3, 5, 3, 0

GXSceneStart
...
   

Here now are all of the changes we covered in Part 6:


Attached Files
.zip   part6.zip (Size: 82.66 KB / Downloads: 210)
Reply
#12
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.
   

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.
   
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.
   
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:
Code: (Select All)
...
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:


Attached Files
.zip   part7.zip (Size: 113.77 KB / Downloads: 183)
Reply
#13
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:
   
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:
   
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:
Code: (Select All)
...
Const COLLISION_LAYER = 3
Const COLLISION_TILE = 72
..
Now let's rework our OnTileCollision method to use the new method:
Code: (Select All)
...
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:
Code: (Select All)
...
' 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:
   
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":
Code: (Select All)
...
Const COLLISION_TILE = 72
Const COLLISION_TILE2 = 71
...

Then we can update our OnTileCollision method with the new logic for this collision type:
Code: (Select All)
...
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:


Attached Files
.zip   part8.zip (Size: 82.79 KB / Downloads: 143)
Reply
#14
Hello there fellow engine designer!

Not bad bro, not bad at all! KUDOS! 

I started building my game engine UnseenGDK in QB64 way back in 2010/2011 and have worked on it on and off ever since, currently I am working on UnseenGDK2, which is both a headache and a pleasure as I am sure you are well aware!

If you ever want to add models to your entity type then let me know and i'll drop you the code...GDK2's entity type only uses an index into the Asset array whos corresponding ID tag defines what type of asset the entity is...this way even if you draw a model or sprite a thousand times, you only ever load it once!

Keep it up!

Unseen
Reply
#15
(03-21-2025, 02:12 AM)Unseen Machine Wrote: Hello there fellow engine designer!

Not bad bro, not bad at all! KUDOS! 

I started building my game engine UnseenGDK in QB64 way back in 2010/2011 and have worked on it on and off ever since, currently I am working on UnseenGDK2, which is both a headache and a pleasure as I am sure you are well aware!
...

Thanks @Unseen Machine!  I'd be curious to learn more about your engine.  Do you have a github repo or documentation page?
Reply
#16
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.
   

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.
Code: (Select All)
...
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.
Code: (Select All)
...
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.
   
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:
Code: (Select All)
...
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.
   

Here is the updated project up to this point:


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


Attached Files
.zip   part9.zip (Size: 86.7 KB / Downloads: 68)
Reply
#17
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:
Code: (Select All)
...
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:
Code: (Select All)
...
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.
   

Let's add a new variable to track our game over state:
Code: (Select All)
...
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
Code: (Select All)
...
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:
Code: (Select All)
...
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:
   
We can create our font with the GXFontCreate method:
Code: (Select All)
...
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:
Code: (Select All)
...
    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:
   

And with that we've come to the end of our platformer tutorial.  We've even made a few enhancements along the way to our original target program:


Miscellaneous
I thought I'd mention a couple of additional items that didn't make it into this tutorial but might be useful to you:
  • Fullscreen Mode
    If you'd like to run your game in full screen mode, simply call the following method after creating your scene:
    Code: (Select All)
    GXFullscreen GX_TRUE
  • Hardware Images 
    While it might not make a huge difference for a game as simple as this one you can enable hardware image support in QB64 by calling:
    Code: (Select All)
    GXHardwareAcceleration GX_TRUE
    If used, his needs to be the first GX method called in your application.



Final Thoughts
Just to reiterate what I said at the outset, I'd be very curious to hear of any feedback on the tutorial or game engine, positive or negative.  Would there be other topics of interest to see (e.g. sound, device input, enemies, projectiles)?  Did anyone even make it this far?  Or was it more of a case of TL;DR and I should have just made a YouTube video?


Attached Files
.zip   part10.zip (Size: 96.52 KB / Downloads: 34)
Reply




Users browsing this thread: 1 Guest(s)