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 Definitions of the GameStarter

image

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

Game initialization: More than just loading a settings file

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!


GameStarter async support

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:

Async control 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.

Loading spinner animation control

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.
5677f50d-213b-4b44-82bd-d49b31961c6c
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.

The onLoadingScreen(task, frame) Callback

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 and frame. The latter starts with 0 and increases by 1 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.

Example onLoadingScreen implementation

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);
}

Example async startup implementation without onLoadingScreen

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
}

Run Custom Async Loops

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.

run_async_loop

/// @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 optional data
  • 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
⚠️ **GitHub.com Fallback** ⚠️