GameStarter - Grisgram/gml-raptor GitHub Wiki
Two of the most important moments in a game are the "start" and the "end" of the game.
While the former creates your globals and prepares everything your game needs, the latter is responsible for cleaning up and maybe disconnecting from any external resources or servers.
This is, what the GameStarter
is designed for.
It generalizes the moments of the game start by abstracting the initialization of the first room (rmStartup
), loading your settings and invokes two callbacks: onGameStart
and onGameEnd
.
In addition to that, a simulated async operation can be performed through the onLoadingScreen(task, frame)
callback. More on that, see below.
These functions are defined in the Game_Configuration
script of the _GAME_SETUP_
folder of the project, thus taking away the need of fiddling code into any events of objects and instead provide a clean function where you can do your initialization tasks.
But the GameStarter
does much more for your game. Let's start with some variables.
Variable | Description |
---|---|
goto_room_after_init |
This is the room that will be entered after the onGameStart callback (and any async operations, more on that below) are finished |
fade_in_frames_first_room |
The GameStarter uses the Room Transitions of the RoomController to allow a soft fade-in of the first user-visible room in the game. The default value 0 disables fading and just shows the room |
Tip
To change these variables, you can either edit the settings of the GameStarter
in rmStartup
,
or you can use these macros, which are available directly in the Game_Configuration script, so you don't need the room editor or the object editor:
#macro ROOM_AFTER_STARTER rmMain
#macro STARTER_ASYNC_MIN_WAIT_TIME 90
#macro STARTER_FIRST_ROOM_FADE_IN 45
Most modern games communicate with a backend, be it Steam or Firebase or your own server, maybe you are even hosting your own api. These operations are asynchronous and work through callbacks, which means, your may not simply leave the rmStartup
while connect requests or other async operations are still running.
GameStarter
fully supports any async startup tasks, but as those are normally made in code, I did not create variable definitions for the room designer for them. Instead, they are simply variables in the GameStarter that you can modify as needed in your async code.
Remember: You can access the (persistent) GameStarter
from everywhere through the GAMESTARTER
macro!
Before we dive into the details it is worth noting, that by default (if you don't change any of the variables mentioned here), GameStarter
will behave purely synchronous. So, if you don't have a backend or other async things to do on startup, you don't need to do anything and it "will just work".
Overview of available variables:
Variable | Default | Description |
---|---|---|
wait_for_async_tasks |
false |
This is the master switch. If you set this to true , GameStarter will not leave the room until either:- the value is false again,- OR the async_wait_timeout is greater than zero and has been reached |
async_wait_timeout |
-1 |
By default, there's no timeout, so GameStarter will wait infinitely, which normally means "until the http timeout aborts the request".However, you may find, that 30 seconds is too long to wait and just set your timeout (in frames) here. |
async_min_wait_time |
90 |
It is psychologically important, that visible text stays on the screen long enough for the user to recognize and understand it. It's a bad behavior if you show something like "Connecting..." only for 0.1 seconds. The user could get the impression that he "missed" something because the time was too short. So, even if your async connect may run way faster, this variable takes care, that the message stays on screen for (by default) at least 1.5 seconds. This is not a random time, it's composed by the average reaction time of humans (~0.5 seconds) plus one second to find and interpret a short connect message. This is enough time to avoid the "I have missed something" impression on your players. Feel free to adapt this minimum wait time as you like, but don't make it too short. |
Variable | Default | Description |
---|---|---|
spinner_sprite |
sprLoadingSpinner |
There is a default spinner animation sprite included with raptor . It can be found in the _gml_raptor/UI/default_sprites folder of the project template and its name is sprLoadingSpinner .It looks like this and rotates slowly clockwise, like a standard loading animation. |
spinner_font |
undefined |
You must set a font to be used to render the text next to the spinner animation. If you don't set a font, no text will be drawn. |
GAMESTARTER.show_loading_text(_LG_string) |
"Connecting to server..." |
Assign any text here and it will be shown on screen. The default message is just an english generic connect message, you may want to change that with localized strings from the LG Localization during your async operations. |
This function will be invoked every step by the GAMESTARTER
and is defined (empty) in your Game_Configuration script.
Here are some basic strategy tipps, how to work with this one:
- You receive two arguments:
task
andframe
. The latter starts with0
and increases by1
each frame. - About
task
: Store your state and all other things you need to continue next frame in this struct. It will be sent to you each frame. - Return as fast as possible, so the game can advance to next frame (and the spinner keeps rotating).
- Design your code in a way, it can continue next frame, where it left off in the previous one. Store everything for that in
task
. - Example: Instead of a "repeat (100000)" during startup, maybe do only 1000 ... things ... at a time and store your counter in
task
, in the next frame, you do the next 1000 things, and so on. Split your actions as much as possible. - The better you split your actions, the smoother the loading experience will be for your players.
This is a part of the code I used in a game jam to load my data files before entering the main menu. It was necessary, because itch.io stored the files somewhere in the google cloud and it took like 0.5 seconds for each file to be loaded in HTML.
function onLoadingScreen(task, frame) {
// pre-load and cache the level files
if (frame == 0) {
// Set up our todo-list in the first frame
task.levels = {
name: "levels",
i: 0, // "i" like in "for (var i = 0;...)"
list: file_read_text_file_lines($"levels/levels.dat")
};
}
var nextthing = (task.levels.i > -1) ? task.levels : undefined;
if (nextthing != undefined) {
var loadstring = LG($"startup/loading_{nextthing.name}");
var counter = $"{(nextthing.i+1)}/{array_length(nextthing.list)}";
ilog($"Loading {nextthing.name} {counter}");
// shows something like "1/5 Loading Levels"
GAMESTARTER.show_startup_text($"{counter} {loadstring}");
// Read only one level per frame
var line = nextthing.list[@nextthing.i];
file_read_struct($"{nextthing.name}/{line}", FILE_CRYPT_KEY, true);
nextthing.i++;
// If we reach the end of the array, set the counter to -1
if (nextthing.i == array_length(nextthing.list)) nextthing.i = -1;
}
// returns true (=continue) as long as we don't reach -1
return (task.levels.i > -1);
}
Here is a short example of the implementation I use for game jams when connecting to my api for cloud based high scores. It's reduced to the important parts that show the manipulation of the GameStarter
variables through async startup.
I do not need onLoadingScreen
here, as the http requests are implemented very nicely in GameMaker and the connection truly runs in a background thread, which gets synced into the game loop, when something happens.
This exmaple here shall show you, how to manipulate the GAMESTARTER
variables.
function connectToApi() {
if (USE_API) {
GAMESTARTER.show_loading_text("Connecting to server...");
GAMESTARTER.wait_for_async_tasks = true;
GAMESTARTER.async_wait_timeout = 300; // 5 seconds
GAMESTARTER.spinner_font = fntTitle;
API.connect(api_connected, api_failed);
} else {
// My Jam template CAN work offline, so no wait time if no api required
GAMESTARTER.async_min_wait_time = -1;
}
}
function api_connected() {
GAMESTARTER.show_loading_text("Loading Highscores...");
HIGHSCORES = API.get_highscores(highscores_loaded, api_failed); // just the next call to the api
}
function api_failed() {
GAMESTARTER.show_loading_text("Server connect failed!");
GAMESTARTER.wait_for_async_tasks = false; // <-- proceed to next room
log("Api connection for highscores failed!");
}
function highscores_loaded() {
GAMESTARTER.wait_for_async_tasks = false; // <-- proceed to next room
}
The system used by raptor, when the onLoadingScreen
callback is invoked every frame, is referred to as an "async loop" internally.
GAMESTARTER
offers you a function to run any function as an async loop, not only onLoadingScreen
.
One implementation of this is the LG_hotswap function from LG localization.
/// @func run_async_loop(
/// _looper = undefined,
/// _finished_callback = undefined,
/// _looper_data = undefined,
/// _target_room_after = undefined,
/// _transition = undefined)
/// @desc Runs the specified (_looper) async loading loop, like the one at game start (onLoadingScreen).
/// If no _looper is specified, this function fires the entire async-startup chain
/// (onLoadingScreen) again, but NOT the onGameStart callback.
/// Use this for language swaps or larger async loading done in the
/// onLoadingScreen callback.
/// Specify any data struct as _looper_data to have it sent to the looper function as
/// third argument. This way you can transport any information to the looper function.
/// If you supply any room, then this function will return to rmStartup, to show
/// the initial loading screen (black with spinner), before returning to the desired
/// room. If you specify nothing, the game stays in the current room and the spinner
/// is just shown as overlay while loading.
Basically you simply set a callback function (_looper
) to be invoked every frame, which follows the same rules, onLoadingScreen
does:
- Gets invoked every frame
- Receives up to three arguments:
task
,frame
and optionaldata
- Returns true while it wants to continue its tasks
In addition to that, run_async_loop offers
- A
_finished_callback
to get informed, when the loop has exited - An optional
_looper_data
struct, which is sent as third argument to the looper function and as argument to the_finished_callback
- You may choose freely, whether the game should return to the black startup-loading screen with the spinner, while the loop is running
- You may specify a target room and/or a transition to a target room to have the room changed for you, when the loop is done