Introduction.
In part one of this guide, we prepared GB Studio for engine editing. In part two, we will look through the structure of one of the state files, adventure.c, to get familiar with how state files work.
The state files.
As mentioned in part one of this guide, the state files control the behaviour of the player, camera and other things for each of the different GB Studio scene types. The state files are found in the folder Your Project/assets/engine/src/states/. Each one contains the following in this order:
- #pragma bank 255 to enable autobanking.
- #include directives to import variables and functions from other files in the engine.
- Variable definitions.
- The init function for the scene type. This runs once at the start of the scene, before the actor and scene on init scripts run.
- The update function for the scene type. This runs once every frame while you are in a scene. This is where most of the functionality lies, including:
- Checking what controls the player is pressing.
- Player movement and collision.
- Player animation.
- Camera movement.
To modify the behaviour of a scene type, you would primarily edit the variable definitions, init function and update function.
Taking a look at the adventure.c state file.
Now let’s go through the entirety of the adventure.c state file from the GB Studio 4.0 engine and break down what each section does. The overall structure is the same as the 3.0 engine, just with some minor syntax and logic revisions. I’ll also add a bunch of related information and tips along the way. It's a little unconventional, but I hope this helps you understand the engine a bit better. If you understand what the engine does, you can modify it.
#pragma bank 255
Starting from the start, this snippet enables autobanking. If you want to know more about banking for some reason, check this great article out, but you don’t need to know about banking for basic engine edits.
#include "data/states_defines.h" #include "states/adventure.h" #include "actor.h" #include "camera.h" #include "collision.h" #include "game_time.h" #include "input.h" #include "scroll.h" #include "trigger.h" #include "data_manager.h" #include "rand.h" #include "vm.h" #include "math.h" #ifndef ADVENTURE_CAMERA_DEADZONE #define ADVENTURE_CAMERA_DEADZONE 8 #endif
This section includes variables and functions from the listed .h files in the engine. This is how adventure.c gets access to variables like camera_offset_x and camera_offset_y, which we are about to see in use.
void adventure_init(void) BANKED { // Set camera to follow player camera_offset_x = 0; camera_offset_y = 0; camera_deadzone_x = ADVENTURE_CAMERA_DEADZONE; camera_deadzone_y = ADVENTURE_CAMERA_DEADZONE; }
adventure_init() is the init function for the Adventure scene type. This runs once when loading into an Adventure scene, before the actor and scene on init scripts. All it does is initialise some variables so that the camera is centered on the player with a camera deadzone of 8 by 8 pixels. If you add variables that need to be initialised at the start of each scene, this is where you would do that.
void adventure_update(void) BANKED { actor_t *hit_actor; UBYTE tile_start, tile_end; UBYTE angle = 0; direction_e new_dir = DIR_NONE; player_moving = FALSE;
adventure_update() is the update function for the Adventure scene type. As mentioned above, this runs once every frame while you are in an Adventure scene. At the start of the function, we have some variable definitions. Because they are defined in the update function, they will be reset to their default value at the start of every frame. You’ll see where each variable is used soon.
if (INPUT_RECENT_LEFT) { new_dir = DIR_LEFT; } else if (INPUT_RECENT_RIGHT) { new_dir = DIR_RIGHT; } else if (INPUT_RECENT_UP) { new_dir = DIR_UP; } else if (INPUT_RECENT_DOWN) { new_dir = DIR_DOWN; }
Continuing with the update function, there is a small block of logic that saves the most recent direction pressed by the player in the variable new_dir. For each of INPUT_RECENT_UP, INPUT_RECENT_DOWN, INPUT_RECENT_LEFT and INPUT_RECENT_RIGHT, the variable is true if that direction is the most recent direction being held, and false otherwise. By the way, these input variables are defined in input.h, which I was able to find with a quick search of the engine files. If you look at input.h, there are helpful comments that explain all of the input variables. It goes without saying, but when you come across an unfamiliar variable or function, this is the best way to try and figure out what it does.
if (INPUT_LEFT) { player_moving = TRUE; if (INPUT_UP) { angle = ANGLE_315DEG; } else if (INPUT_DOWN) { angle = ANGLE_225DEG; } else { angle = ANGLE_270DEG; } } else if (INPUT_RIGHT) { player_moving = TRUE; if (INPUT_UP) { angle = ANGLE_45DEG; } else if (INPUT_DOWN) { angle = ANGLE_135DEG; } else { angle = ANGLE_90DEG; } } else if (INPUT_UP) { player_moving = TRUE; angle = ANGLE_0DEG; } else if (INPUT_DOWN) { player_moving = TRUE; angle = ANGLE_180DEG; }
This block of logic sets the variable angle based on which directions are currently held. For each of INPUT_UP, INPUT_DOWN, INPUT_LEFT and INPUT_RIGHT, the variable is true if that direction is currently held, and false otherwise. Note the difference with the RECENT variables- for the RECENT variables, only one is true at a time, but for these variables any combination can be true. The variable player_moving is also set to true if any direction is held at all.
if (player_moving) { upoint16_t new_pos; new_pos.x = PLAYER.pos.x; new_pos.y = PLAYER.pos.y; point_translate_angle(&new_pos, angle, PLAYER.move_speed); // Step X tile_start = (((PLAYER.pos.y >> 4) + PLAYER.bounds.top) >> 3); tile_end = (((PLAYER.pos.y >> 4) + PLAYER.bounds.bottom) >> 3) + 1; if (angle < ANGLE_180DEG) { UBYTE tile_x = ((new_pos.x >> 4) + PLAYER.bounds.right) >> 3; while (tile_start != tile_end) { if (tile_at(tile_x, tile_start) & COLLISION_LEFT) { new_pos.x = (((tile_x << 3) - PLAYER.bounds.right) << 4) - 1; break; } tile_start++; } PLAYER.pos.x = MIN((image_width - PLAYER.bounds.right - 1) << 4, new_pos.x); } else { UBYTE tile_x = ((new_pos.x >> 4) + PLAYER.bounds.left) >> 3; while (tile_start != tile_end) { if (tile_at(tile_x, tile_start) & COLLISION_RIGHT) { new_pos.x = ((((tile_x + 1) << 3) - PLAYER.bounds.left) << 4) + 1; break; } tile_start++; } PLAYER.pos.x = MAX(0, (WORD)new_pos.x); } // Step Y tile_start = (((PLAYER.pos.x >> 4) + PLAYER.bounds.left) >> 3); tile_end = (((PLAYER.pos.x >> 4) + PLAYER.bounds.right) >> 3) + 1; if (angle > ANGLE_90DEG && angle < ANGLE_270DEG) { UBYTE tile_y = ((new_pos.y >> 4) + PLAYER.bounds.bottom) >> 3; while (tile_start != tile_end) { if (tile_at(tile_start, tile_y) & COLLISION_TOP) { new_pos.y = ((((tile_y) << 3) - PLAYER.bounds.bottom) << 4) - 1; break; } tile_start++; } PLAYER.pos.y = new_pos.y; } else { UBYTE tile_y = (((new_pos.y >> 4) + PLAYER.bounds.top) >> 3); while (tile_start != tile_end) { if (tile_at(tile_start, tile_y) & COLLISION_BOTTOM) { new_pos.y = ((((UBYTE)(tile_y + 1) << 3) - PLAYER.bounds.top) << 4) + 1; break; } tile_start++; } PLAYER.pos.y = new_pos.y; } }
This long section moves the player based on the inputs that were checked above, and is also responsible for collision with the background during movement. I won’t cover the logic in depth due to its length, but there are a few key takeaways from this section.
The tile_at() function is used to check background tiles for collision. Masking the returned value with a constant like COLLISION_TOP from collision.h allows us to check for that specific collision type.
After collision is checked and the new position of the player new_pos is calculated, the position of the player PLAYER.pos is updated to match. By the way, in the GB Studio engine PLAYER.pos.x and PLAYER.pos.y are stored in subpixels, of which there are 16 for every pixel. Bit shifting using >> and << converts between tile, pixel and subpixel values and is important to learn for performant math in the engine. To learn more about subpixels and bit shifting, read Larold's guide on the topic.
if (new_dir != DIR_NONE) { actor_set_dir(&PLAYER, new_dir, player_moving); } else { actor_set_anim_idle(&PLAYER); }
This section sets the direction and animation of the player based on new_dir. It uses the very handy actor_set_dir() and actor_set_anim_idle() functions which are defined in actor.c along with a lot of other useful functions. You probably already noticed, but in the engine the player is treated as an actor and most actor related functions can be used with the player.
hit_actor = NULL; if (IS_FRAME_ODD) { // Check for trigger collisions if (trigger_activate_at_intersection(&PLAYER.bounds, &PLAYER.pos, FALSE)) { // Landed on a trigger return; } // Check for actor collisions hit_actor = actor_overlapping_player(FALSE); if (hit_actor != NULL && hit_actor->collision_group) { player_register_collision_with(hit_actor); } }
This section checks for collisions with triggers and other actors. Note that it only runs on odd frames for performance reasons, which is something I’ve removed in previous engine edits for higher collision accuracy. Also note that you can only have one hit_actor per frame, which means collision with additional actors on the same frame is ignored.
if (INPUT_A_PRESSED) { if (!hit_actor) { hit_actor = actor_in_front_of_player(8, TRUE); } if (hit_actor && !hit_actor->collision_group && hit_actor->script.bank) { script_execute(hit_actor->script.bank, hit_actor->script.ptr, 0, 1, 0); } }
This final section controls the player’s interactions with actors. The code checks if INPUT_A_PRESSED, or in other words if A was pressed down this frame, and then attempts to interact with any actor within 8 pixels of the front of the player. If an actor is present and its collision group is set to None, its interact script is executed.
Next steps.
That completes our examination of adventure.c. The other state files are very similarly structured, so take a look through them and see what you can learn. In part three of this guide, we will look at an actual example of editing the engine for a specific purpose.
Guide by Shin. Last updated 5/9/2024.