Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom ticks from FixedUpdate games #68

Merged
merged 29 commits into from
Oct 2, 2023

Conversation

RJ
Copy link
Contributor

@RJ RJ commented Oct 2, 2023

DRAFT - I got my game mostly behaving well (except predicted spawns) using bevy_replicon with these changes.
I will write some tests if you would like to merge this, or something similar. I'm fairly sure this is a generally useful and necessary feature for games which used fixed timestep.

UserTick support

The ticks replicon uses for tracking are (rightfully) world level ticks from pre/post/update. But fixed timestep games (such as simulations with rollback) need their own tick that increments with no gaps in it, because every tick represents a simulation step.

I made ServerPlugin (and the ReplicationPlugins group) generic over UserTick - a user provided bevy resource which derefs to a u32. This is for the tick from the FixedUpdate step, in games that use FixedUpdate (like for simulations with rollback networking).

// your fixed timestep game tracks simulation ticks with your custom resource:
#[derive(Resource)]
struct SimulationTick{
    tick_number: u32,
    other_stuff: (), // etc
}

// which derefs to a u32
impl std::ops::Deref for SimulationTick {
    type Target = u32;
    fn deref(&self) -> &Self::Target {
        &self.tick_number
    }
}

// Initialize ServerPlugin like this (or via ReplicationPlugins)
let server = ServerPlugin::<SimulationTick>::default();

Now every time replication data is assembled for sending, bevy_replicon will read the
Res<SimulationTick> resource, deref it to a u32, and include it in the replication data.

This value is made available as the user_tick: Option<u32> parameter in the following places:

  • When deserializing component data
  • When removing components from entities
  • When despawning entities

This means a custom deserializer can use the user_tick to apply component updates in the past, to a specific frame.

here's my game client's deserializer for bevy_xpbd's Rotation component, which uses bevy_timewarp to apply the update to a prior frame (thus triggering rollback):

pub fn deserialize_rotation_timewarp(
    entity: &mut EntityMut,
    _entity_map: &mut NetworkEntityMap,
    mut cursor: &mut Cursor<Bytes>,
    user_tick: Option<u32>,
) -> Result<(), bincode::Error> {
    let sin: f32 = bincode::deserialize_from(&mut cursor)?;
    let cos: f32 = bincode::deserialize_from(&mut cursor)?;
    let comp = Rotation::from_sin_cos(sin, cos);
    // ServerSnapshot is a component added by bevy_timewarp
    if let Some(mut ss) = entity.get_mut::<ServerSnapshot<Rotation>>() {
        // insert this component into the correct frame
        // which will trigger a rollback and resimulate to update the present value.
        ss.insert(user_tick.unwrap(), comp);
    } else {
       // if this is the first time an entity got a Rotation component, it won't have the timewarp
       // ServerSnapshot<Rotation> wrapper yet. Inserting Rotation will trigger that:
        entity.insert(comp);
    }
    Ok(())
}

These changes unfortunately mean initialization of ServerPlugin and ReplicationPlugins is uglier, because you have to specify UserTick, even if you don't need it. I provided a NoUserTick as a dummy value when you don't need the feature. Most of the diff is updating existing tests and docs to change how plugins are initialized 😬 Perhaps there's a better way to do this?

The UserTick is an Option<u32> which i'm encoding into the replication buffer after the existing current_tick. This will cost 1 bit when not in use. Seemed a worthwhile tradeoff vs. extra code complexity, but open to suggestions!

Configurable despawning function

I added a configurable despawn function to ReplicationRules, in case you want clients to do something different than just despawn_recursive when the server despawns something. The default behaviour is unchanged here.

In the case of rollback, I need to be able to insert a Despawn component instead of immediately despawning, so rollback systems can do stuff.

Configurable remove component function

In addition to replicate_with there is now a replicate_and_remove_with which allows providing a custom function to remove components from entities. The default behaviour is unchanged here.

@Shatur
Copy link
Contributor

Shatur commented Oct 2, 2023

Some time ago I used Bevy's tick, but since I wanted to open door for rollback implementation for other people, I switched to NetworkTick that I map to Bevy's tick to compare changes.
Right now NetworkTick incremented every time we send data. But this doesn't work for you, because you want to increment this tick as part of FixedUpdate, right?
But maybe can we increment NetworkTick inside FixedUpdate in replicon system (just put your system before or after mine) and run send system only if current network tick changes? So keep passing the tick to functions, but use NetworkTick instead of generic tick.

@RJ
Copy link
Contributor Author

RJ commented Oct 2, 2023

Hmm, yes...

At the moment my resource containing the fixed timestep tick is accessed in at least 50% of my game systems.
The first system that runs in my fixed timestep increments it.
Rollbacks wind it back to a prior value, and the fast-fwd/resimulate game loop increments it again.

The NetworkTick value is accessible to my server game systems, if they get Res<ServerTicks>.current_tick

If I made the ServerPlugin able to configure where the diffs_sending_system was run, i could run it in my existing OutgoingMessageSending set, near the end of my fixed timestep loop (which wouldn't run during rollback).

If i did that, NetworkTick would be incremented once per non-rollback tick of my fixed timestep, because the server would call diffs_sending_system once per fixed timestep too.

I could then sync my existing tick resource to the value of NetworkTick at the start of fixed timestep on the server, instead of just incrementing it myself.

Rollback should work fine, because that would wind back my tick resource, and simulate forward to the original value without calling into replicon at all.

Meanwhile the client doesn't care about any of this, the tick it sees passed in to the deserialize fns would be the right thing.

I think that could work 👍

This would allow me to get rid of ServerPlugin being abstract over UserTick, which is ugly. If there was an identical setting on both the server and client plugins like send_network_tick, it would only cost bandwidth if the feature was in use. If present, the Option<u32> sent to the deserialize fns would be populated like it is now.

I'll implement this and report back.

@Shatur
Copy link
Contributor

Shatur commented Oct 2, 2023

If I made the ServerPlugin able to configure where the diffs_sending_system was run, i could run it in my existing OutgoingMessageSending set, near the end of my fixed timestep loop (which wouldn't run during rollback).

@RJ If I understand correctly, you don't want to run sending system inside fixed timestep. Ideally you want to run it once inside PostUpdate to send all data from the current frame (including the data that not affected by rollback and changed inside Update).
I would suggest this changes in replicon:

  1. Move current server tick into dedicated resource.
  2. Throw the network tick increment to a separate system that runs in FixedUpdate. Do you need to change the value of the tick manually? If yes, allow to disable the increment system in public API, similar to TickPolicy::Manual and do anything you want with the tick. If you don't need to change the tick yourself, you can configure your systems to run after the tick increment. So depends on how your code work.
  3. Run sending system only if the current tick resource changes.

Will this work for you?

This would allow me to get rid of ServerPlugin being abstract over UserTick

Yep, this is why I suggesting this :) And also because I introduced NetworkTick just to be used in your use case. We just need to tweak it a little bit.

Allows for more flexible fixedtimestep integration, with
manual incrementing of the network tick.
@RJ RJ marked this pull request as ready for review October 2, 2023 19:03
@RJ
Copy link
Contributor Author

RJ commented Oct 2, 2023

Ok, ready for review - all working in my game, and i've added tests for the new features. Thanks 👍

Shatur added 18 commits October 2, 2023 22:46
No longer necessary, looks like a leftover from the previous approach.
- Replace `Option` with default `despawn_recursive` and remove custom
despawn test since now it's not a special case.
- Allow to initialize it inside `ReplicationRules` instead of setting it
as part of plugin (I found it a bit confusing).
Since we have only one such function.
I would prefer `replicate_with` to have all parameters.
I think that this should be a part of third-party crate that implements
it. Using custom functions will just work, I don't see much value in
testing them.
I also removed public access from the tick value. You still can get
its value or create a new one.
Similar to how it's done for Bevy's `Tick`.
@Shatur Shatur requested a review from UkoeHB October 2, 2023 21:14
src/lib.rs Show resolved Hide resolved
src/replicon_core/replication_rules.rs Outdated Show resolved Hide resolved
src/replicon_core/replication_rules.rs Outdated Show resolved Hide resolved
src/server.rs Outdated Show resolved Hide resolved
tests/common/mod.rs Outdated Show resolved Hide resolved
tests/server_event.rs Outdated Show resolved Hide resolved
@Shatur Shatur requested a review from UkoeHB October 2, 2023 22:47
@Shatur Shatur enabled auto-merge (squash) October 2, 2023 22:56
@Shatur Shatur merged commit 73739d4 into projectharmonia:master Oct 2, 2023
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants