![]() |
GX Platformer Tutorial - Printable Version +- QB64 Phoenix Edition (https://qb64phoenix.com/forum) +-- Forum: QB64 Rising (https://qb64phoenix.com/forum/forumdisplay.php?fid=1) +--- Forum: Code and Stuff (https://qb64phoenix.com/forum/forumdisplay.php?fid=3) +---- Forum: Works in Progress (https://qb64phoenix.com/forum/forumdisplay.php?fid=9) +---- Thread: GX Platformer Tutorial (/showthread.php?tid=3514) Pages:
1
2
|
GX Platformer Tutorial - dbox - 03-05-2025 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:
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. Contents
RE: GX Platformer Tutorial - grymmjack - 03-05-2025 @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! ![]() ![]() RE: GX Platformer Tutorial - dbox - 03-05-2025 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! RE: GX Platformer Tutorial - dbox - 03-06-2025 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... RE: GX Platformer Tutorial - dbox - 03-07-2025 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)
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)
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)
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)
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)
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)
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)
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... RE: GX Platformer Tutorial - madscijr - 03-08-2025 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. RE: GX Platformer Tutorial - dbox - 03-08-2025 Thanks @madscijr, I look forward to seeing your creations. RE: GX Platformer Tutorial - dbox - 03-08-2025 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
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)
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)
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)
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)
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. RE: GX Platformer Tutorial - dbox - 03-14-2025 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: This tileset image is made up of 16x16 image tiles. Each tile in the set is assigned a number based on its position: 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 application. Originally, this was built with InForm and the source 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 Wave Function Collapse POC that I hope to get back to at some point.) Picking up where we left off in Part 3, let's create a new method to set up our map: Code: (Select All)
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: Code: (Select All)
If we run the program now we'll see the beginnings of our map: 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: Code: (Select All)
Then let's update our LoadMap method to load the map tiles from our data section: Code: (Select All)
That's starting to look more interesting: Let's add some more detail now by adding another layer of tiles. First add the additional layer data: Code: (Select All)
Then adjust the call to GXMapCreate in our LoadMap method to initialize two layers: Code: (Select All)
Here is the completed exercise: 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. RE: GX Platformer Tutorial - dbox - 03-14-2025 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: Code: (Select All)
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. 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: Code: (Select All)
This will now cause our player to fall down to the ground tiles: 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: Code: (Select All)
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! Next time we'll look at refining the controls and movement. |