Physics Tutorial

Overview

This tutorial will introduce the Physics APIs available in the Aether Engine SDK, along with the provided Physics demos.


Prerequisites

You should have:

  • The SDK installed and available on your local machine

  • Familiarity with the concepts behind the Aether Engine, including the ECS and how space is divided.

  • Familiarity with physics concepts as they apply to game engines


Physics entry point

The Aether Engine physics API should feel familiar to developers who have previously used the PhysX API, and indeed the physics features of the engine can be found in the aether::physx namespace. We can create a physics system to integrate with the Aether Engine ECS:

using c_physx = aether::physx::physx_c;
struct c_trivial {
struct colour agent_colour = {1.0f, 0.0f, 0.0f};
uint64_t id;
float size;
};
AETHER_SERDE_DERIVE_TRIVIAL(c_trivial);
using component_types = std::tuple<c_physx, c_trivial>;
using octree_traits = aether::octree_traits<morton_code<3, 21>>;
using user_cell_state = aether::ecs<octree_traits, component_types>;
struct physx_update_system {
using accessed_components = std::tuple<c_physx, c_trivial>;
using ecs_type =
aether::constrained_ecs<user_cell_state, accessed_components>;
void operator()(
const aether_cell_state<octree_traits> &aether_state,
ecs_type &store, float delta_time
) {
aether::physx::physx_state *physx_state = store.user_data.get();
// Advance the simulation.
// This will be called once per tick, per cell
physx_state->scene->simulate(delta_time);
physx_state->scene->fetchResults(true);
}
};

Here c_physx is the component for providing physx integration, provided by Aether. c_trivial is a simple struct of our design to allow us to render the physics controlled agents. Now given we have an physics system, we can initialise it for each cell in initialise_cell:

void user_cell_state_impl::initialise_cell(const aether_state_type &aether_state) {
store.user_data = std::make_unique<aether::physx::physx_state>(aether::physx::physx_state::basic_scene(new event_callback(store)));
store.add_system<physx_update_system>();
aether::physx::physx_state *physx_state = store.user_data.get();
// Lets add a physics aware plane
physx::PxMaterial* material =
physx_state->physics->createMaterial(0.0f,0.0f,1.0f);
auto plane = physx::PxCreatePlane(
*physx_state->physics, physx::PxPlane(0,0,1,150), *material
);
material->release();
physx_state->scene->addActor(*plane);
}

For more information about using event_callback to receive events from PhysX, see Physics.

Notice how here we are creating a plane in initialise_cell - this will cause the plane object to exist once for each cell in the simulation, and will not be shared across cells. This is fine for static meshes (e.g. ground floors) that are expected to remain in a consistent state, but will not work for any entities that we expect to move around and could become inconsistent between workers. This includes the bouncing boxes in our demo, let’s look at how we can resolve that and have physics that works across multiple cells. Naturally we will want to avoid initialise_cell, and instead focus on initialise_world.

void user_cell_state_impl::initialise_world(const aether_state_type &aether_state) {
aether::physx::physx_state *physx_state = store.user_data.get();
// Create a material and physics shape
physx::PxReal extent(1.0f);
physx::PxMaterial* material =
physx_state->physics->createMaterial(0.0f, 0.0f, 1.0f);
physx::PxShape* actor_shape =
physx_state->physics->createShape(
physx::PxBoxGeometry(extent, extent, extent),
*material
);
// Position our actor
physx::PxTransform actor_position =
physx::PxTransform(physx::PxVec3(0.0f, 0.0f, 0.0f));
// Create a physics enabled actor
physx::PxRigidDynamic* actor =
physx_state->physics->createRigidDynamic(actor_position);
actor->attachShape(*actor_shape);
physx::PxRigidBodyExt::updateMassAndInertia(*actor, 10.0f);
actor->setLinearVelocity(physx::PxVec3(0.0f, 1.0f, 0.0f));
// Add our new agent to the ECS
auto update_set = state.create_update_set();
auto agent = update_set.new_entity_local();
auto physx_component = agent.create_component<c_physx>();
physx_component->add_actor(*actor);
physx_component->add_to_simulation();
// Add a helpful component for rendering
auto trivial = agent.create_component<c_trivial>();
trivial->id = 0;
trivial->size = 1.0f;
trivial->agent_colour.r = 1.0f;
// Cleanup
material->release();
actor_shape->release();
}

Currently it is not possible to share materials between actors - it is important to create a unique material for each actor in the simulation.

Of course, we need to have some way of advancing the simulation on each tick. However as we’ve integrated our physics system with the ECS, this is very straightforward:

void user_cell_state_impl::cell_tick(const aether_state_type &aether_state, float delta_time) {
store.tick(aether_state, delta_time);
}

The Physics Demo

The physics demo uses exactly the above setup to simulate 500 cubes bouncing whilst constrained inside a cubic bounding box. The physx_update_systemhas been described above and needs no further work. In our initialise_world call we can initialise 500 boxes rather than the single box previously. The setup for a single actor is identical to what we have previously, just wrapped in a for loop. Finally, rather than creating a single plane in initialise_cell we create the six bounding planes of the bounding box.


Serialisation

Now that we have our boxes being properly simulated across multiple machines, we need to serialise them to be rendered by our clients. We handle this in cell_state_serialize, by simplying copying the data from the respective components.

void user_cell_state_impl::serialize_to_client(const aether_state_type &aether_state, client_writer_type &writer) {
protocol::base::client_message header;
const auto cell = aether_state.get_cell();
auto marshaller = marshalling_factory().create_marshaller();
marshaller.reserve(store.num_agents_local());
header.cell = cell;
header.cell.dimension = 3;
header.cell.pid = static_cast<uint64_t>(hadean::pid::get());
header.stats.num_agents = store.num_agents_local();
header.stats.num_agents_ghost = store.num_agents_ghost();
header.cell_dying = aether_state.is_cell_dying();
marshaller.add_worker_data(aether_state.get_worker().as_u64(), header);
for (auto agent: store.local_entities<c_physx, c_trivial>()) {
auto physx = agent.get<c_physx>();
auto trivial = agent.get<c_trivial>();
// Create a point to send
protocol::base::net_point_3d point;
// Extract the position and orientation
PxTransform t = physx->actor->getGlobalPose();
// Send the position
vec3f position = vec3f_new(t.p.x, t.p.y, t.p.z);
point.net_encoded_position = position;
// Send the orientation
protocol::base::net_quat q;
q.x = t.q.x;
q.y = t.q.y;
q.z = t.q.z;
q.w = t.q.w;
point.net_encoded_orientation = q;
// Send our trivial rendering information
point.net_encoded_color = net_encode_color(trivial->agent_colour);
point.id = trivial->id;
point.size = trivial->size;
marshaller.add_entity(point);
}
const auto data = marshaller.encode();
writer.push_bytes(&data[0], data.size());
writer.send();
}