Simulation Entry (writing gameplay code in C)

This walkthrough will show you how to create a simulation entry and what a simulation entry is.

If you wish to program gameplay using C code, then you need some way for this code to execute. You can either make lots of entity components that do inter-component communication, but if you want a more classic monolithic approach, then you can use a simulation entry.

In order for your code to execute using a simulation entry you need two things. Firstly you need an implementation of the Simulation Entry interface, tm_simulation_entry_i (see simulation_entry.h) and secondly you need a Simulation Entry Component attached to an entity.

Define a tm_simulation_entry_i in a plugin like this:

static tm_simulation_entry_i simulation_entry_i = {
    .id = TM_STATIC_HASH("tm_my_game_simulation_entry", 0x2d5f7dad50097045ULL),
    .display_name = TM_LOCALIZE_LATER("My Game Simulate Entry"),
    .start = start,
    .stop = stop,
    .tick = tick,
    .hot_reload = hot_reload,
};

Where start, stop and tick are functions that are run when the simulation starts, stop and each frame respectively. Make sure that id is a unique identifier.

Note: There is also a plugin template available that does this, see File -> New Plugin -> Simulation Entry with The Machinery Editor.

Note: to generate the TM_STATIC_HASH you need to run hash.exe or tmbuild.exe --gen-hash for more info open the hash.exe guide

When your plugin loads (each plugin has a tm_load_plugin function), make sure to register this implementation of tm_simulation_entry_i on the tm_simulation_entry_i interface name, like so:

tm_add_or_remove_implementation(reg, load, tm_simulation_entry_i, &simulation_entry_i);

When this is done and your plugin is loaded, you can add a Simulation Entry Component to any entity and select your registered implementation. Now, whenever you run a simulation (using Simulate Tab or from a Published build) where this entity is present, your code will run.

The same Simulation Entry interface can be used from multiple Simulation Entry Components and their state will not be shared between them.

Note: For more in-depth examples, we refer to the gameplay samples, they all use Simulation Entry.

What happens under the hood?

When the Simulation Entry Component is loaded within the Simulate Tab or Runner, it will set up an entity system. This system will run your start, stop and tick functions. You may then ask, what is the difference between using a Simulation Entry and just registering a system from your plugin? The answer is the lifetime of the code. If you register a system from your plugin, then that system will run no matter what entity is spawned whereas the Simulation Entry Component will add and remove the system that runs your code when the entity is spawned and despawned.

Example: Source Code

static struct tm_localizer_api *tm_localizer_api;
static struct tm_allocator_api *tm_allocator_api;
// beginning of the source file
#include <foundation/api_registry.h>
#include <foundation/localizer.h>
#include <foundation/allocator.h>

#include <plugins/simulation/simulation_entry.h>

struct tm_simulation_state_o
{
    tm_allocator_i *allocator;
    //..
};

// Starts a new simulation session. Called just before `tick` is called for the first
// time. The return value will later be fed to later calls to `stop` and `update`.
tm_simulation_state_o *start(tm_simulation_start_args_t *args)
{
    tm_simulation_state_o *state = tm_alloc(args->allocator, sizeof(*state));
    *state = (tm_simulation_state_o){
        .allocator = args->allocator,
        //...
    };
    //...
    return state;
}

// Called when the entity containing the Simulation Entry Component is destroyed.
void stop(tm_simulation_state_o *state, struct tm_entity_commands_o *commands)
{
    //...
    tm_allocator_i a = *state->allocator;
    tm_free(&a, state, sizeof(*state));
}

// Called each frame. Implement logic such as gameplay here. See `args` for useful
// stuff like duration of the frame etc.
void tick(tm_simulation_state_o *state, tm_simulation_frame_args_t *args)
{
    //...
}

// Called whenever a code hot reload has occurred. Note that the start, tick and
// stop functions will be updated to any new version automatically, this  callback is for other
// hot reload related tasks such as updating function pointers within the simulation code.
void hot_reload(tm_simulation_state_o *state, struct tm_entity_commands_o *commands)
{
    //...
}

static tm_simulation_entry_i simulation_entry_i = {
    .id = TM_STATIC_HASH("tm_my_game_simulation_entry", 0x2d5f7dad50097045ULL),
    .display_name = TM_LOCALIZE_LATER("My Game Simulate Entry"),
    .start = start,
    .stop = stop,
    .tick = tick,
    .hot_reload = hot_reload,
};

TM_DLL_EXPORT void tm_load_plugin(struct tm_api_registry_api *reg, bool load)
{
    tm_localizer_api = tm_get_api(reg, tm_localizer_api);
    tm_allocator_api = tm_get_api(reg, tm_allocator_api);
    tm_add_or_remove_implementation(reg, load, tm_simulation_entry_i, &simulation_entry_i);
}