Skip to content

Commit

Permalink
Merge pull request #19 from cBournhonesque/cb/fix-relations
Browse files Browse the repository at this point in the history
Improve entity mapping, interpolation, prediction
  • Loading branch information
cBournhonesque authored Dec 21, 2023
2 parents c9908c3 + 291a920 commit 06bd9bf
Show file tree
Hide file tree
Showing 48 changed files with 2,267 additions and 416 deletions.
60 changes: 53 additions & 7 deletions NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,57 @@
- the cube goes all the way to the left and exits the screen. There is continuous rollback fails
- on interest management, we still have this problem where interpolation is stuck at the beginning and doesn't move. Probably
because start tick or end tick are not updated correctly in some edge cases.



- TODO:
- implement Message for common bevy transforms
- maybe have a ClientReplicate component to transfer all the replication data that is useful to clients? (group, prediciton, interpolation, etc.)



- add PredictionGroup and InterpolationGroup.
- on top of ReplicationGroup?
- or do we just re-use the replication group id (that usually will have a remote entity id) and use it to see the prediction/interpolation group?
- then we add the prediction group id on the Confirmed or Predicted components?
- Then we don't really need the Confirmed/Predicted components anymore, we could just have resources on the Prediction or Interpolation plugin
- The resource needs:
- confirmed<->predicted mapping
- for a given prediction-group, the dependency graph of the entities (using confirmed entities?)
- The prediction systems will:
- iterate through the dependency graph of the prediction group
- for each entity, fetch the confirmed/predicted entity
- do entity mapping if needed
- users can add their own entities in the prediction group (even if thre )



- DEBUGGING REPLICATION BOX:
- the sync from confirmed to predict might not only be for replicated components, but also components that were
spawned on confirmed world directly.
- which means it's not to apply topological sort during replication; we need to apply it on prediction level directly
- create a 'PredictionGroup': all predicted entities must be in the same group, and we apply topological sort on that group
- we actually could have different prediction groups, for entities that we know are not interacting at all!
- each group has a dependency graph as well
- maybe maintain a topological sort for each predicted replication group?
- what about adding new entities to the prediction group? because that's the main problem, otherwise if all the entities
are known at the beginning we are good!
- maybe don't need toplogical sort but can just use the vec from the replication to have the order
- but then how do we iterate through the entities in that order?
- the components during prediction sync need to be mapped!
- do we need to introduce the concept of a PredictionGroup, which is a super-set of a replicationGroup? (because some of the entities
might not come from replication?)
- how to get smooth interpolation when there can be diagonal movements?
- allow custom interpolation, so that we can make sure that interpolation respects corners as well. The interpolation follows the path
- WEIRD: when we just do normal interpolation for player heads, and just use 'interp=start' for tails, it actually looks really smooth!
- TODO: tried to make my custom interpolation logic working, but there still seems to be edge cases that are not handled well.
- there's weird panics now, and sometimes the interpolated entity doesn't move at all
- INTERP TIME is sometimes too late; i.e. we receive updates that are way after interp time.
- SYNC:
- seems to not work well for at the beginning..
- PREDICTION; rollback is weird and fucked
- looks like sending pings more frequently fixed the issue?, to investigate..
- is it that we don't receive the inputs on time at the client?
- imagine we lost some packets and server didn't receive the inputs... then when server receives a later packet it should receive the past 15 inputs.
server should then use this to rollback?
- server should ask client to speed up (via a message), when we have lost inputs (to increase the buffer size)
- it should re-use the previous input as a best guess estimate
- it looks like our input buffer on server is too big; it contains like 15 ticks worth of inputs, even though the client messages should arrive right before.
is it just because of the margin we took?
- applied a best guess estimate for lost inputs that uses the last input sent as fallback, works well!

- FINAL CHOICE:
- send all actions per group on an reliable unordered channel
Expand Down Expand Up @@ -362,6 +404,10 @@ ROUGH EDGES:

TODO:

- Inputs:
- instead of sending the last 15 inputs, send all inputs until the last acked input message (with a max)
- also remember to keep around inputs that we might need for prediction!

- Serialization:
- have a NetworkMessage macro that all network messages must derive (Input, Message, Component)
- DONE: all network messages derive Message
Expand Down
84 changes: 84 additions & 0 deletions book/src/concepts/advanced_replication/interpolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Interpolation

## Introduction
Interpolation means that we will store replicated entities in a buffer, and then interpolate between the last two states to get a smoother movement.

See this excellent explanation from Valve: [link](https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking)
or this one from Gabriel Gambetta: [link](https://www.gabrielgambetta.com/entity-interpolation.html)


## Implementation

In lightyear, interpolation can be automatically managed for you.

Every replicated entity can specify to which clients it should be interpolated to:
```rust,noplayground
Replicate {
interpolation_target: NetworkTarget::AllExcept(vec![id]),
..default()
},
```

This means that all clients except for the one with id `id` will interpolate this entity.
In practice, it means that they will store in a buffer the history for all components that are enabled for Interpolation.


## Component Sync Mode

Not all components in the protocol are necessarily interpolated.
Each component can implement a `ComponentSyncMode` that defines how it gets handled for the `Predicted` and `Interpolated` entities.

Only components that have `ComponentSyncMode::Full` will be interpolated.


## Interpolation function

By default, the implementation function for a given component will be linear interpolation.
It is also possibly to override this behaviour by implementing a custom interpolation function.

Here is an example:

```rust,noplayground
#[derive(Component, Message, Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Component1(pub f32);
#[derive(Component, Message, Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Component2(pub f32);
#[component_protocol(protocol = "MyProtocol")]
pub enum MyComponentProtocol {
#[sync(full)]
Component1(Component1),
#[sync(full, lerp = "MyCustomInterpFn")]
Component2(Component2),
}
// custom interpolation logic
pub struct MyCustomInterpFn;
impl<C> InterpFn<C> for MyCustomInterpFn {
fn lerp(start: C, _other: C, _t: f32) -> C {
start
}
}
```

You will have to add the attribute `lerp = "TYPE_NAME"` to the component.
The `TYPE_NAME` must be a type that implements the `InterpFn` trait.
```rust,noplayground
pub trait InterpFn<C> {
fn lerp(start: C, other: C, t: f32) -> C;
}
```


## Complex interpolation

In some cases, the interpolation logic can be more complex than a simple linear interpolation.
For example, we might want to have different interpolation functions for different entities, even if they have the same component type.
Or we might want to do interpolation based on multiple comments (applying some cubic spline interpolation that relies not only on the position,
but also on the velocity and acceleration).

In those cases, you can disable the default per-component interpolation logic and provide your own custom logic.
```rust,noplayground```



4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ publish = false
name = "interest_management"
path = "interest_management/main.rs"

[[example]]
name = "replication_groups"
path = "replication_groups/main.rs"

[[example]]
name = "simple_box"
path = "simple_box/main.rs"
Expand Down
16 changes: 12 additions & 4 deletions examples/interest_management/protocol.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use bevy::prelude::{default, Bundle, Color, Component, Deref, DerefMut, Entity, Vec2};
use bevy::utils::EntityHashSet;
use derive_more::{Add, Mul};
use lightyear::prelude::*;
use lightyear::shared::replication::components::ReplicationMode;
use serde::{Deserialize, Serialize};
use tracing::info;

// Player
#[derive(Bundle)]
Expand Down Expand Up @@ -56,9 +58,15 @@ pub struct Circle;
#[message(custom_map)]
pub struct PlayerParent(Entity);

impl MapEntities for PlayerParent {
fn map_entities(&mut self, entity_map: &EntityMap) {
self.0.map_entities(entity_map);
impl<'a> MapEntities<'a> for PlayerParent {
fn map_entities(&mut self, entity_mapper: Box<dyn EntityMapper + 'a>) {
info!("mapping parent entity {:?}", self.0);
self.0.map_entities(entity_mapper);
info!("After mapping: {:?}", self.0);
}

fn entities(&self) -> EntityHashSet<Entity> {
EntityHashSet::from_iter(vec![self.0])
}
}

Expand All @@ -84,7 +92,7 @@ pub struct Channel1;
#[derive(Message, Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Message1(pub usize);

#[message_protocol(protocol = "MyProtocol", derive(Debug))]
#[message_protocol(protocol = "MyProtocol")]
pub enum Messages {
Message1(Message1),
}
Expand Down
24 changes: 24 additions & 0 deletions examples/replication_groups/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Replication groups

This is an example that shows how to make Lightyear replicate multiple entities in a single message,
to make sure that they are always in a consistent state (i.e. that entities in a group are all replicated on the same tick).

Without a replication group, it is possible that one entity is replicated with the server's tick 10, and another entity
is replicated with the server's tick 11. This is not a problem if the entities are independent, but if they depend on each other (for example
for client prediction) it could cause issues.

This is especially useful if you have an entity that depends on another entity (e.g. a player and its weapon),
the weapon might have a component `Parent(owner: Entity)` which references the player entity.
In which case we **need** the player entity to be spawned before the weapon entity, otherwise `Parent` component
will reference an entity that does not exist.


## Running the example

To start the server, run `cargo run --example replication_groups server`

Then you can launch multiple clients with the commands:

- `cargo run --example replication_groups client -c 1`

- `cargo run --example replication_groups client -c 2 --client-port 2000`
Loading

0 comments on commit 06bd9bf

Please sign in to comment.