Links

Initialising a Simulation

Simulate simulation logic is implemented through a series of lifecycle callbacks that are registered with Simulate on starting the simulation. The callbacks are where the logic for your simulation lives defining what happens on simulation startup and on each incremental tick. Detail of how to implement a class for your simulation logic can be found in the section Simulation Lifecycle
The logic that you implement through the callbacks is passed to the Manager, a process responsible for coordinating the whole simulation, on initialisation along with any configuration parameters such as the tick rate or if the simulation is 2D or 3D.
If you are choosing to make use of Connect to send simulation data out and receive input from connected clients you will also need to register your Connect Nodes.
To start the simulation you will need to initialise the simulations loop. This is done by repeatedly calling the Tick function on the Manager which will cause all simulation processes to complete their individual tick logic, whilst the Manager blocks until all other processes have completed.

Configuring The Simulation

Several option can be passed to the Simulate Manager on initialisation to determine how the situation will run.
Parameter
Description
argument / data type
Initial Worker Count
The number of workers that will be created when first starting the simulation (Integer). A pool of workers will be created ready to take ownership of regions of simulation space. Set this value to a high number if you believe that your simulation will immediately be split into a lot of cells when first created to improve startup times
arguments.workers =(Integer)
Initial Cell Size
Defines the area covered by a cell when it is initially created to cover an area of previously unmanaged virtual space. It is given as a power of the minimum cell size, so the side length of the new cell = (2 ^ n) * unit size
arguments.cell_level = (Integer)
2D vs 3D
The simulation space can be defined as either 2D or 3D. This will define how the region associated with a cell is divided. Simulate provides a definitions for both 2D and 3D representations. See below for more details
passed as an argument when generating situation startup parameters (aether::octree_traits)
Ticks Rate
Given in hertz (ticks per second) to calculate the amount of simulation time elapsed per tick. This will be used to calculate the delta_time value passed to the cell_tick lifecycle function.
arguments.tickrate = (Integer)
Feature Flags
A variety of flags to enable / disable simulation features. See Runtime Configuration for more details. These can be passed as a OR'd value of available flags e.g. DISABLE_SPLIT | FAST_MODE
arguments.flags = (uint64_t)
Once these arguments have been set a static structure should be created to be passed to the Manager on initialisation as shown in the following code extract. The structure created is dependent on the type of simulation desired (2D vs 3D). In order to specify this the convention is to define a struct called octree_traits which contains static information on whether the simulation partitioning is two or three dimensional.
// For a simulation with 2-dimensional partitioning
using octree_traits = aether::octree_traits<aether::vec2i64>;
// For a simulation with 3-dimensional partitioning
using octree_traits = aether::octree_traits<aether::vec3i64>;
Now that you have your arguments struct set up , and have selected which octree_traits type to use you can generate the static arguments and initialise the Manager. Note that you will need to pass in your implementation of the simulation lifecycle logic seen below as user_cell_state_impl. See Simulation Lifecycle for details on how to create this class
// Generate parameter stucture for Manager initialisation
auto static_args = arguments.to_octree_params<octree_traits>();
// Construct the manager
auto manager = aether::build_entity_simulation_manager<user_cell_state_impl>(
arguments.workers,
static_args
);

[Optional] Adding Connect Nodes

If your simulation is making use of Connect to communicate with clients then you will need to register these as part of initialising the Manager. This can done by calling the add_muxer method of the Manager instance you created as part of configuring the simulation.
main.cc
auto manager = aether::build_entity_simulation_manager<user_cell_state_impl>(
arguments.workers,
static_args
);
for (const auto& muxer : arguments.muxers)
{
manager.add_muxer(muxer);
}

Starting the Simulation Loop

After configuring the simulation and creating a manager instance it will now be necessary to start the main simulation loop. An example of how this can be implemented is provided below.
main.cc
for (uint64_t tick = 0; ; tick++)
{
auto loop_time = timer::get();
manager.manager_tick();
AETHER_LOG(INFO)(fmt::format("Tick {} Complete!", tick));
loop_time = timer::add(
loop_time,
static_cast<std::chrono::nanoseconds>(1s) / arguments.tickrate
);
timer::sleep_until(loop_time);
}
return EXIT_SUCCESS;
The call to manager.manager_tick() causes all workers in the simulation to simulate the next time step. The tick function will block until all cells have completed their next tick.
In the example above we start a tick at the same time step interval that the simulation will be using, this would mean that simulation time was progressing approximately aligned to real time. If you wanted to run the simulation faster than real time you could remove the sleep to go as fast as possible, or replace the tickrate argument with a time step of your choosing.

Walkthrough Code Example

The following code shows how you could set up your main function (by convention located in main.cc) including all of the setup steps from this section
main.cc
// Select a 2D Simulation
using octree_traits = aether::octree_traits<aether::vec2i64>;
int main(int argc, char *argv[]){
hadean::init();
aether::log::init("AE_Manager", hadean::pid::get());
aether::log::set_level(aether::log::level::INFO);
// Setup your simuation Configuration
struct arguments arguments;
arguments.workers = 8;
arguments.tickrate = 15;
arguments.cell_level = 6;
arguments.flags = FAST_MODE | PHASE_BARRIERS;
argument_parse(argc, argv, &arguments);
// Generate arguement stucture for Manager initialisation
auto static_args = arguments.to_octree_params<octree_traits>();
// Construct the manager
auto manager = aether::build_entity_simulation_manager<user_cell_state_impl>(arguments.workers, static_args);
// Register the Muxers (Connect Nodes)
for (const auto& muxer : arguments.muxers) {
manager.add_muxer(muxer);
}
for (uint64_t tick = 0; ; tick++) {
auto loop_time = timer::get();
manager.manager_tick();
AETHER_LOG(INFO)(fmt::format("Tick {} Complete!", tick));
loop_time = timer::add(loop_time, static_cast<std::chrono::nanoseconds>(1s) / static_args.ticks_per_second);
timer::sleep_until(loop_time);
}
return EXIT_SUCCESS;
}