Serialization
When the serde-serialize feature of bevy_rapierPhysicsPipeline
and the CollisionPipeline which don't hold any useful state:
// Serialize the whole physics world.
let serialized = bincode::serde::encode_to_vec(&world, bincode::config::standard()).unwrap();
// Deserialize it.
let (mut deserialized, _): (PhysicsWorld, usize) =
bincode::serde::decode_from_slice(&serialized, bincode::config::standard()).unwrap();
// The simulation can continue using the deserialized world.
deserialized.step();
The complete state of the simulation is serialized by serializing the components of its
physics context: RapierContextSimulation, RapierContextColliders,
RapierRigidBodySet, and RapierContextJoints all implement the Serialize and Deserialize traits of serde. The
RapierContext structure given by the ReadRapierContext system parameter implements Serialize as well, so the
four of them can be serialized at once. The RapierConfiguration component of the context implements these traits
too, so it can be saved with them, as in the following example. Restoring a snapshot is done by inserting the
deserialized components on the entity of the physics context, replacing the previous ones:
/// The components holding the complete state of the simulation of a physics context, and its
/// configuration.
type ContextComponents = (
RapierContextSimulation,
RapierContextColliders,
RapierRigidBodySet,
RapierContextJoints,
RapierConfiguration,
);
/// Serializes the default physics context when S is pressed.
fn save_snapshot(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
context: Query<
(
&RapierContextSimulation,
&RapierContextColliders,
&RapierRigidBodySet,
&RapierContextJoints,
&RapierConfiguration,
),
With<DefaultRapierContext>,
>,
) {
if keys.just_pressed(KeyCode::KeyS) {
let components = context.single().unwrap();
let serialized =
bincode::serde::encode_to_vec(components, bincode::config::standard()).unwrap();
commands.insert_resource(Snapshot(serialized));
}
}
/// Restores the snapshot when R is pressed.
fn restore_snapshot(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
snapshot: Option<Res<Snapshot>>,
context: Query<Entity, With<DefaultRapierContext>>,
) {
if let (true, Some(snapshot)) = (keys.just_pressed(KeyCode::KeyR), snapshot) {
// The maps from entities to handles are rebuilt automatically by the deserialization.
let (components, _): (ContextComponents, usize) =
bincode::serde::decode_from_slice(&snapshot.0, bincode::config::standard()).unwrap();
// Replace the components of the physics context by the deserialized ones.
commands.entity(context.single().unwrap()).insert(components);
}
}
Some parts of the physics context are skipped by the serialization: the PhysicsPipeline and the CollisionPipeline
(which don't hold any useful state), the event handler installed with RapierContextSimulation::set_event_handler,
the events not sent yet, and the step statistics. The maps from entities to handles aren't serialized either: they
are rebuilt automatically when the components are deserialized, from the user-data of the Rapier objects (which
contain the bits of their entity). If you restore the sets of a physics context in another way, call
RapierContextColliders::rebuild_entity_maps, RapierRigidBodySet::rebuild_entity_maps, and
RapierContextJoints::rebuild_entity_maps to rebuild them.
The Rapier objects refer to their entities by their identifier. Therefore a deserialized physics context is only
meaningful in a world where the same entities exist with the same identifiers (e.g. when restoring a snapshot of the
same application). Note as well that the components of your entities (RigidBody, Collider, Transform, etc.),
and the TimestepMode resource, are not part of the serialized components: they must be saved separately (the
Collider component and the TimestepMode resource implement the traits of serde as well, whereas most of the other
components can be serialized through the reflection of Bevy).
If the enhanced-determinism feature of Rapier is enabled, and if your platform fulfills the required determinism
requirements, then you have the guarantee that running the exact same simulation on two different machine
will result in the exact same byte vectors if the physics state is serialized on both machines after the same
number of timesteps.
It is possible to take a snapshot of the whole physics world with world.takeSnapshot. This results in a byte array
of type Uint8Array that may be saved on the disk, sent through the network, etc. The snapshot can then be restored
with let world = World.restoreSnapshot(snapshot);.
The complete state of the simulation can be serialized by taking a snapshot of the
physics world with r3SerializeWorld. This results in an R3Bytes
object owned by the application, which bytes are borrowed with r3Bytes_Data (e.g. to save them on the disk, or to
send them through the network), and which must be freed with r3FreeBytes. The snapshot can then be restored with
r3DeserializeWorld, which creates a new world (freed with r3FreeWorld like any other world) in the exact same
state as the serialized one:
- Example 2D
- Example 3D
/* Serialize the whole physics world. */
R2Bytes *snapshot = r2SerializeWorld(world);
/* The serialized bytes, borrowed from the snapshot, e.g., to be written to a file. */
R2ByteView bytes = r2Bytes_Data(snapshot);
printf("The snapshot takes %zu bytes.\n", bytes.count);
/* Deserialize it: this creates a new world, independent from the original one. */
R2World *deserialized = r2DeserializeWorld(bytes.data, bytes.count);
r2FreeBytes(snapshot);
/* The simulation can continue using the deserialized world. */
r2Step(deserialized, NULL, NULL);
/* Serialize the whole physics world. */
R3Bytes *snapshot = r3SerializeWorld(world);
/* The serialized bytes, borrowed from the snapshot, e.g., to be written to a file. */
R3ByteView bytes = r3Bytes_Data(snapshot);
printf("The snapshot takes %zu bytes.\n", bytes.count);
/* Deserialize it: this creates a new world, independent from the original one. */
R3World *deserialized = r3DeserializeWorld(bytes.data, bytes.count);
r3FreeBytes(snapshot);
/* The simulation can continue using the deserialized world. */
r3Step(deserialized, NULL, NULL);
The snapshot contains everything the world is made of: the rigid-bodies, colliders, joints, and soft-bodies
(including their user data), the gravity, the integration parameters, the island manager, the broad-phase, and the
narrow-phase (with its contacts). However, the thread pool configured by r3SetNumThreads and the profiling counters
are not part of the snapshot, nor is anything owned by your application (the event collectors, the physics hooks, the
controllers, etc.): they must be configured again for the deserialized world. Keep in mind that the handles of the
serialized world keep referring to the serialized world: the handles of the deserialized world must be retrieved from
it (each object keeps the index and the generation it had in the serialized world):
- Example 2D
- Example 3D
/* The handles of the original world don't refer to the deserialized world: get new ones. */
size_t num_bodies = r2RigidBodyHandles(deserialized, NULL, 0);
R2RigidBodyHandle *bodies = malloc(num_bodies * sizeof(R2RigidBodyHandle));
num_bodies = r2RigidBodyHandles(deserialized, bodies, num_bodies);
for (size_t i = 0; i < num_bodies; i++) {
printf("Restored rigid-body {%u, %u} at altitude %f\n", bodies[i].index, bodies[i].generation,
(double)r2RigidBody_Translation(bodies[i]).y);
}
free(bodies);
/* The handles of the original world don't refer to the deserialized world: get new ones. */
size_t num_bodies = r3RigidBodyHandles(deserialized, NULL, 0);
R3RigidBodyHandle *bodies = malloc(num_bodies * sizeof(R3RigidBodyHandle));
num_bodies = r3RigidBodyHandles(deserialized, bodies, num_bodies);
for (size_t i = 0; i < num_bodies; i++) {
printf("Restored rigid-body {%u, %u} at altitude %f\n", bodies[i].index, bodies[i].generation,
(double)r3RigidBody_Translation(bodies[i]).y);
}
free(bodies);
A snapshot is not a stable file format: it can only be deserialized by the exact same build of the Rapier library
(same version, dimension, precision, and features) as the one that serialized it, and r3DeserializeWorld rejects the
snapshots it recognizes as incompatible with the R3_INVALID_ARGUMENT error. Only deserialize snapshots coming from a
trusted source.
If the library is built with the enhanced-determinism feature, and if your platform fulfills the required
determinism requirements, then you have the guarantee that running the exact same simulation on
two different machines will result in the exact same snapshot bytes if the world is serialized on both machines after
the same number of timesteps.
The complete state of the simulation can be serialized by taking a snapshot of the
physics world with PhysicsWorld.snapshot. This results in a bytes
object that may be saved on the disk, sent through the network, etc. The snapshot can then be restored with the
PhysicsWorld.restore static method, which creates a new world in the exact same state as the serialized one:
# Serialize the whole physics world.
serialized = world.snapshot()
# Deserialize it.
deserialized = rp.PhysicsWorld.restore(serialized)
# The simulation can continue using the deserialized world.
deserialized.step()
The world also supports the pickle module of Python, which relies on these snapshots. Therefore, a world can be saved as part of any picklable structure of your application:
# The world can be pickled like any other Python object, e.g., to save it on the disk.
data = pickle.dumps(world)
unpickled = pickle.loads(data)
The snapshot contains everything the world is made of: the rigid-bodies, colliders, joints, and soft-bodies
(including their user data), the gravity, the integration parameters, the island manager, the CCD solver, the
broad-phase, and the narrow-phase (with its contacts). However, the thread pool configured by
PhysicsWorld.set_num_threads, the PhysicsWorld.event_error_policy option, the performance counters, and the objects of your application given to the world (the event handler and the physics hooks) are not part of the
snapshot: they must be configured again for the deserialized world. Each object of the deserialized world keeps the
index and the generation it had in the serialized world, so the handles of the serialized world can be used with the
deserialized world as well:
# The handles of the serialized world refer to the same objects in the deserialized world.
restored_ball = deserialized.rigid_bodies[ball_handle]
print("Ball altitude in the deserialized world:", restored_ball.translation.y)
# The event handler and the physics hooks aren't part of the snapshot: they must be given again.
deserialized.event_handler = rp.ChannelEventCollector()
For debugging purposes, PhysicsWorld.snapshot_json gives a human-readable snapshot as a JSON string instead, which
is restored with PhysicsWorld.restore_json. It is much larger and slower than the binary snapshot:
# A human-readable (but much larger and slower) JSON snapshot, e.g., for debugging.
json_snapshot = world.snapshot_json()
from_json = rp.PhysicsWorld.restore_json(json_snapshot)
Most of the other objects of the bindings can be pickled on their own too, e.g., the rigid-bodies, colliders, and joints (as well as their builders), the shapes, the handles, the sets, the integration parameters, or the mass properties. On the other hand, the pipelines and the event collectors can't be pickled:
# Most objects can be pickled on their own too, e.g., the rigid-body and collider builders.
ball_builder = rp.RigidBody.dynamic(translation=(0.0, 10.0, 0.0))
restored_builder = pickle.loads(pickle.dumps(ball_builder))
new_ball = world.add_body(restored_builder, colliders=[rp.Collider.ball(0.5)])
A snapshot is not a stable file format: it should only be restored by the same version of the bindings as the one
that serialized it. The snapshots recognized as incompatible (or corrupted) are rejected with a SerializationError.
Like any pickled data, only restore snapshots coming from a trusted source.
If the bindings are built with the determinism feature, and if your platform fulfills the required
determinism requirements, then you have the guarantee that running the exact same simulation on
two different machines will result in the exact same snapshot bytes if the world is serialized on both machines after
the same number of timesteps.