Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
GX Platformer Tutorial
#1
I've had a couple of requests to do a tutorial for building a 2D platformer in GX

What is GX? 
Well, it's a game engine that I started work on a few years ago, back in the qb64.org days. The first post was back in December of 2021.  In the process of working on several 2d game projects I was noticing that I was needing to write a lot of game "plumbing" for each project.  I wanted to see if it would be possible to create a generic game API and engine for QB64 that could support a number of different types of 2D games (platformers, top down, isometric strategy, etc.).  GX was the result of that effort.

The goal of the project is to create a flexible, event-based game engine. Based on your game requirements you can use as much or as little of it as you need, but the engine will take care of the main tasks of managing the game loop and screen buffering for the display.  The current version has support for:
  • Scene(viewport) management
  • Entity(sprite) management
  • Tiled map creation and management
  • Bitmap font support
  • Collision detection
  • Basic physics/gravity
  • Device input management
  • Keyboard, mouse, and game controller
  • Interactive debugging
  • Export to Web
That last bit, "Export to Web" got the most initial interest and was the starting point for what would eventually become QBJS.

The Tutorial
My idea for the tutorial is to build this little game below over the course of several posts, starting from scratch and building it step-by-step, highlighting the various features of the engine along the way.  If you want to try it out now press play and use the arrow keys to move and space bar to jump:


About the Source
This sample includes all of the code needed for building the game in QB64, so you can also download the attached simple-platformer.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.

I would love to have feedback along the way (positive or negative).  I know there are a lot of experienced game developers here and would value any experiences and suggestions that could make the engine better.


Attached Files
.zip   simple-platformer.zip (Size: 86.85 KB / Downloads: 347)
Reply
#2
@dbox - this is awesome. seriously. your work is unsung.

You should make more tutorials on various other things with GX to get it some momentum.

Here are some good types to make that Construct3 has:
https://www.construct.net/en/make-games/free-trial

I think if you scroll down you'll see what I mean.

Confession: I have always wanted to dig into GX but haven't had the time/motivation/etc. That said you making this tutorial makes it a hell of a lot more likely that I would.

Perhaps you should consider creating/hosting a simple gx website in github pages using SSR tools to showcase what is made, your tutorials etc.

All of the smartest people I know are not marketing / pumping out this kind of thing. I'd point you to just looking at the repo I made for QB64PE web as a good starting point, and would be happy to help you make assets and maintain GX web with you.

Anyway, your work is exemplary, excellent, exciting, and ... i can't think of another positive adjective for the last e. But it is!

Smile

Heart
grymmjack (gj!)
GitHubYouTube | Soundcloud | 16colo.rs
Reply
#3
Thanks @grymmjack!

That's a nice list of starter projects on the Construct3 site and I agree that it would be good to add to the library of GX samples available.

I like the work you did on the QB64PE landing page, simple, nice and clean.  I'm open to any help offered for the project!
Reply
#4
Part 1 - The Basics
Before we start coding, let's look at a few of the main concepts behind the engine...

The World
As GX is designed with 2D games in mind, the world can be imagined as a two-dimensional plane that expands out forever in every direction (north, south, east and west). Positions in this flat expanse are identified by X and Y pixel coordinates. Negative coordinates indicate positions north or east of the universe center (0, 0).
   

The Scene
The scene is the player's viewport into the vast expanse of the underlying world. While the world is infinite, the view into this world is a fixed size and defines the part of the world we see on the screen. So, for example, if we create a new scene that is 320 pixels x 200 pixels in size, we would initially see any content in the world that is between world coordinates (0,0) and (320, 200).

The scene itself has an X and Y position and can be "moved" around the world.  Only the portion of the world within the boundaries of the scene will be rendered.  In our simple side scroller example the X position of the scene is changed as the player moves through the map:
   

Entities
An entity is any object that can be placed in the world. An entity could be a spaceship, an enemy monster, a tree, an extremely athletic plumber... anything that needs to be in a certain place in the world. Entities have X and Y positions that indicate their location in the world. Movement can be indicated by setting an entity's X and/or Y velocity. Entities can be visible or hidden and can be rendered programmatically or have their appearance automatically rendered from a sprite sheet image, like the one we are using in our sample game:
   

We'll go deeper and cover more concepts later, but that's enough to get us started.

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).

Ok, that's enough exposition for now.  Next time we'll fire up a new IDE window and start building our game...
Reply
#5
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:
Code: (Select All)
'$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.
Code: (Select All)
'$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.
Code: (Select All)
'$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:
Code: (Select All)
...
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...
     
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:
Code: (Select All)
...
' 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.
   

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

' 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.
   

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...
Reply
#6
This is pretty cool! I may eventually attempt to use this to port the Commodore 64 version of Lode Runner (with in-game level editor) to QB64PE. This could also be a good platform for porting Atari 2600 Pitfall or Pitfall II: Lost Caverns or Broderbund's Spelunker.
Reply
#7
Thanks @madscijr, I look forward to seeing your creations.
Reply
#8
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:
Code: (Select All)
Initialization
Do
    Input Handling
    Game Logic / Updates
    Physics & Collision Detection
    Rendering
    Timing/Frame Regulation
Loop
  • 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.
Code: (Select All)
...
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:
Code: (Select All)
...
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:
Code: (Select All)
...
'$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.
Code: (Select All)
...
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 GXAnimationStop method.


Attached Files
.zip   part3.zip (Size: 49.91 KB / Downloads: 50)
Reply




Users browsing this thread: 3 Guest(s)