Blog
Menu
Blog
lines

Developers

Intro

Aether Engine is a new way to think about performing massive simulations. Instead of having to worry about the fine details of communications between processes and machines in a cloud, HadeanOS takes care of the process communication, spawning, etc. and Aether Engine maps processes to 3-dimensional space.

In this tutorial, we will show Aether Engine in more detail, walking through a short demo.

Firstly, if you haven’t already it may be a good idea to watch the SDK walkthrough video to give yourself some context.

Great. In the video, you can see there is an OpenGL client connected to the simulation, with our Aether Engine plugin to receive data from and interact with the simulation, however we won’t worry about that aspect in this tutorial.

Let’s talk about the simulation! In the window, you can see many agents bouncing about in a confined space. The cool thing about this is how little code has to be written, compared to other systems where communication is handled by the user. In Aether Engine the user can choose sane default communications and extend or replace with their own if/when necessary.

We’ll walk through it in more depth in a moment, but all you have to worry about as the developer is what the balls do when they reach the edge of the current space. That’s it. Everything more complex (handover, provisioning, spawning etc.) is handled by calling into the Aether Engine library.

Aether Engine maps space to processes, one to one or zero (so a process may exist for any point in the space). It has a hierarchical spatial encoding, which takes points in space from N dimensions and maps them to a 1D bit vector. The specific spatial encoding we use for this is an N dimensional morton coding. The advantages of this are laid out in a blog post here..

But from the developer perspective, they don’t need to worry about this. They are provided with a simple, volume space that they execute the simulation inside. If the simulation is agent based we provide functions for automatic handover etc., which make complex behaviours effortless to implement. For other kinds of simulation, e.g. fluid based, we provide all the functions for mapping any point to a single process, and any volume to a set of processes which can be communicated with easily, using HadeanOS.

Okay, let’s take a look at the code in manager.cc.

The Manager

Here, we have a main function which is the entry point of the Manager process, which becomes the managing process for Aether Engine. As you can see, the developer writes the main tick loop, controlling the calls and timing (so they could also interact with some external systems here, e.g. game marketplace). The simulation tick is invoked by octree.tick().

// manager.cc

int main() {
	using aether::timer;

	hadean_init();
	
	constexpr uint64_t WORKERS_TO_PRESPAWN = 2;
	constexpr uint64_t TICKS_TO_SIMULATE = 10;
	constexpr auto TICK_LENGTH = to_chrono_nanoseconds(1.0/30.0);  // 30 Hz
	
	octree_params_type params;
	params.initialise = &initialise;
	params.initialise_cell = &initialise_cell;
	params.cell_tick = &cell_tick;
	aether::octree octree(WORKERS_TO_PRESPAWN, params);

	for (uint64_t tick = 0; tick < TICKS_TO_SIMULATE; tick++) {
		const auto start_of_next_tick = timer::add(timer::get(), TICK_LENGTH);
  
		octree.tick();
		
		aether::timer::sleep_until(start_of_next_tick);
	}
}

Workers

simulate.cc contains the meat of the simulation - the behaviour. This is callback based, with the three main functions being:

  • initialise_cell - run on each cell when it gets created (can be used to, for example, initialize ECS for each cell).
  • initialise - used to initialise the simulation world, e.g. creating the world volume or the initial agents, run only on first worker (and after it’s cell is initialised).
  • cell_tick - it’s the Worker tick that should handle the simulation logic and is called over and over again while the simulation is running..

Initialise the world

// simulate.cc

void initialise(user_cell_state* state, tree_cell cell) {
	build_balls(*state, cell);
}

In our tutorial demo we call build_balls(), that initialises our world with colourful flying balls. Let’s see what it looks like:

// simulate.cc

static void build_balls(user_cell_state& state, const tree_cell& cell) {
	auto update = state.create_update_set();

	for (uint64_t i = 0; i < NUM_BALLS; ++i) {
		auto entity = update.new_entity_local();

		auto ball_component = entity.create_component();
		ball_component->unique_id = generate_unique_id();
		ball_component->colour = random_colour();
		
		auto physics_component = entity.create_component();
		physics_component->position = random_position();
		physics_component->velocity = random_velocity();
		physics_component->radius = 0.5;
	}
}

First we create an Entity Component System (ECS) Update Set (ECS transaction), and add new entities to it.

Also it’s worth noting that Aether Engine already takes care of calling srandom, so you don’t need to call it.

Cell initialisation

// simulate.cc

void initialise_cell(user_cell_state* state, tree_cell cell) {
	state->add_system();
	state->add_system();
	state->user_data = nullptr;
}

Here we initialise our local ECS with hand-rolled systems for physics (in a larger application this might use a physics library, such as PhysX or Havok).

// simulate.cc

void cell_tick(const aether_cell_state& aether_state, user_cell_state& state, float delta_time) {
	state.tick(aether_state, delta_time);
}

And the last bit - Worker tick - which here is very small; it just invokes the ECS tick.

Init

struct physics_init_system {
	typedef std::tuple accessed_components;
	using ecs_type = aether::constrained_ecs<user_cell_state, accessed_components="">;

	void operator()(const aether_cell_state& aether_state, ecs_type& state, float delta_time) {
		for (auto entity: state.local_entities()) {
			auto entity_physics = entity.get();
			entity_physics->acceleration = ZERO_VECTOR;
		}
	}
};

We use physics_init_system to initialise the variables which will be used in other systems.

This demo doesn’t need to separate this, but in more complex scenarios you could have some other systems modifying the acceleration before physics_update_system.

Update

struct physics_update_system {
	typedef std::tuple<c_physics, c_constraint=""> accessed_components;
	using ecs_type = aether::constrained_ecs<user_cell_state, accessed_components="">;

	void operator()(const aether_cell_state& aether_state, ecs_type& state, float delta_time) {
		for(auto p : state.local_entities<c_constraint, c_physics="">()) {
			auto entity_physics = p.get();

			entity_physics->velocity += entity_physics->acceleration * delta_time;
			entity_physics->position += entity_physics->velocity * delta_time;
		}
	}
};

In physics_update_system we handle velocity and position changes of our local agents.

Outro

We’ve built up quite a complex agent based simulation in very few lines of code. The complexity is dependant on the simulation, not on the state passing and management between processes. This means that you could create entities that are subject to a series of relatively simple, interleaving systems that together produce complex emergent behaviour, without worrying how those entities will communicate with each other or move around your simulation space.