dimforge / bevy_rapier

Official Rapier plugin for the Bevy game engine.
https://rapier.rs
Apache License 2.0
1.26k stars 259 forks source link

Rapier and rollback state #344

Closed kshaa closed 1 year ago

kshaa commented 1 year ago

I'd like to use bevy_rapier in a networked game which uses state rollback. I was planning to use bevy_ggrs for that, but in order to rollback the whole physics state i.e. RapierContext bevy_ggrs requires the context to have Reflect:

    pub fn register_rollback_resource<Type>(self) -> Self
    where
        Type: GetTypeRegistration + Reflect + Default + Resource,

Is there any reason why RapierContext resource doesn't support Reflect?

kshaa commented 1 year ago

Finally got time to check this out again. I just configured bevy_ggrs to rollback all of my entities, including their physics components and it seems to have done the job just fine. Also had to setup enhanced determinism and fixed timestep.

The RapierContext isn't being rolled back, but it doesn't seem to result in problems. Maybe this isn't perfect, but works for me, so will close this ticket.

For context I'll add some code if anyone else tries this. It's a bit all over the place, but I hope the gist of it makes sense.

Step 0. Configure enhanced timestep in cargo features Step 1. Define timestep as fixed in rapier config

    pub fn rapier_config(&self) -> RapierConfiguration {
        RapierConfiguration {
            timestep_mode: TimestepMode::Fixed {
                dt: 1.0 / f32::from(self.fps),
                substeps: 1
            },
            ..RapierConfiguration::default()
        }
    }

Step 2. Insert Rapier config & plugin into app

    game.insert_resource(config.rapier_config())
        .add_plugin(
            RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(config.pixels_per_meter)
                .with_default_system_setup(false));

Step 3. Define your synchronized game logic for GGRS to run for every tick

    let mut synchronized_stage = SystemStage::single_threaded();
    synchronized_stage
        .add_system(my_custom_system_which_adds_forces_based_on_ggrs_inputs)
        .add_system_set(
            RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsStages::SyncBackend))
        .add_system_set(
            RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsStages::StepSimulation))
        .add_system_set(
            RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsStages::Writeback))
        .add_system_set(
            RapierPhysicsPlugin::<NoUserData>::get_systems(PhysicsStages::DetectDespawn));

Step 4. Setup GGRS

fn build_network(game: &mut App, config: &GameConfig, synchronized_stage: SystemStage, session: P2PSession<GGRSConfig>) {
    // Setup GGRS
    GGRSPlugin::<GGRSConfig>::new()
        // define frequency of rollback game logic update
        .with_update_frequency(usize::from(config.fps))
        // define system that returns inputs given a player handle, so GGRS can send the inputs around
        .with_input_system(synchronized_input)
        // register types of components AND resources you want to be rolled back
        .register_rollback_component::<Transform>()
        .register_rollback_component::<Velocity>()
        .register_rollback_component::<ExternalForce>()
        .register_rollback_component::<ExternalImpulse>()
        // .register_rollback_resource::<RapierContext>() // TODO maybe :shrugs:
        // these systems will be executed as part of the advance frame update
        .with_rollback_schedule(
            Schedule::default().with_stage(
                "rollback_default",
                synchronized_stage
            ),
        )
        // make it happen in the bevy app
        .build(game);

    // Add session to game
    game.insert_resource(Session::P2PSession(session));
kshaa commented 1 year ago

Turns out I also had to do some copying magic w/ the rapier context:

        unserialized.islands = bincode::deserialize(&serialized.as_ref().islands).unwrap();
        unserialized.broad_phase = bincode::deserialize(&serialized.as_ref().broad_phase).unwrap();
        unserialized.narrow_phase = bincode::deserialize(&serialized.as_ref().narrow_phase).unwrap();
        unserialized.bodies = bincode::deserialize(&serialized.as_ref().bodies).unwrap();
        unserialized.colliders = bincode::deserialize(&serialized.as_ref().colliders).unwrap();
        unserialized.impulse_joints = bincode::deserialize(&serialized.as_ref().impulse_joints).unwrap();
        unserialized.multibody_joints = bincode::deserialize(&serialized.as_ref().multibody_joints).unwrap();
        unserialized.ccd_solver = bincode::deserialize(&serialized.as_ref().ccd_solver).unwrap();
        unserialized.query_pipeline = bincode::deserialize(&serialized.as_ref().query_pipeline).unwrap();
        unserialized.integration_parameters = bincode::deserialize(&serialized.as_ref().integration_parameters).unwrap();
        unserialized.pipeline = PhysicsPipeline::new();

Essentially had to store (deserialize) and then load (serialize) each serializable engine context piece on each frame. Looks fairly deterministic. Not sure if it is. But perhaps it's helpful to someone else too. Sorry if this is spam.

kshaa commented 1 year ago

If anyone wants the full code from my attempt to integrate Bevy, GGRS and Rapier, here it is - https://github.com/kshaa/zoop.

Notes: