Skip to content

Commit

Permalink
improve visual interp (#825)
Browse files Browse the repository at this point in the history
  • Loading branch information
cBournhonesque authored Jan 17, 2025
1 parent b357491 commit 6574b19
Show file tree
Hide file tree
Showing 15 changed files with 118 additions and 96 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
.DS_Store
/examples/target/
/examples/Cargo.lock
.aider*
10 changes: 8 additions & 2 deletions examples/avian_3d_character/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use leafwing_input_manager::prelude::*;
use lightyear_examples_common::shared::FIXED_TIMESTEP_HZ;
use serde::{Deserialize, Serialize};

use crate::shared::color_from_id;
use lightyear::client::components::{ComponentSyncMode, LerpFn};
use lightyear::client::interpolation::LinearInterpolator;
use lightyear::prelude::client::{self, LeafwingInputConfig};
use lightyear::prelude::server::{Replicate, SyncTarget};
use lightyear::prelude::*;
use lightyear::utils::avian3d::{position, rotation};
use lightyear::utils::bevy::TransformLinearInterpolation;
use tracing_subscriber::util::SubscriberInitExt;

use crate::shared::color_from_id;

// For prediction, we want everything entity that is predicted to be part of
// the same replication group This will make sure that they will be replicated
// in the same message and that all the entities in the group will always be
Expand Down Expand Up @@ -105,5 +105,11 @@ impl Plugin for ProtocolPlugin {
.add_prediction(ComponentSyncMode::Full)
.add_interpolation_fn(rotation::lerp)
.add_correction_fn(rotation::lerp);

// do not replicate Transform but make sure to register an interpolation function
// for it so that we can do visual interpolation
// (another option would be to replicate transform and not use Position/Rotation at all)
app.add_interpolation::<Transform>(ComponentSyncMode::None);
app.add_interpolation_fn::<Transform>(TransformLinearInterpolation::lerp);
}
}
47 changes: 16 additions & 31 deletions examples/avian_3d_character/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ impl Plugin for ExampleRendererPlugin {
),
);

// Set up visual interp plugins for Position and Rotation. This doesn't
// do anything until you add VisualInterpolationStatus components to
// entities.
app.add_plugins(VisualInterpolationPlugin::<Position>::default());
app.add_plugins(VisualInterpolationPlugin::<Rotation>::default());
// Set up visual interp plugins for Transform. Transform is updated in FixedUpdate
// by the physics plugin so we make sure that in PostUpdate we interpolate it
app.add_plugins(VisualInterpolationPlugin::<Transform>::default());

// Observers that add VisualInterpolationStatus components to entities
// which receive a Position or Rotation component.
app.add_observer(add_visual_interpolation_components::<Position>);
app.add_observer(add_visual_interpolation_components::<Rotation>);
// which receive a Position and are predicted
app.add_observer(add_visual_interpolation_components);
}
}

Expand All @@ -56,42 +53,30 @@ fn init(mut commands: Commands) {
));
}

/// Add the VisualInterpolateStatus component to non-floor entities with
/// component `T`. Floors don't need to be visually interpolated because we
/// Add the VisualInterpolateStatus::<Transform> component to non-floor entities with
/// component `Position`. Floors don't need to be visually interpolated because we
/// don't expect them to move.
///
/// We query Without<Confirmed> instead of With<Predicted> so that the server's
/// gui will also get some visual interpolation. But we're usually just
/// concerned that the client's Predicted entities get the interpolation
/// treatment.
///
/// Make sure that avian's SyncPlugin is run in PostUpdate in order to
/// incorporate the changes in pos/rot due to visual interpolation. Entities
/// rendered based on transforms will then have transforms based on the visual
/// interpolation.
fn add_visual_interpolation_components<T: Component>(
trigger: Trigger<OnAdd, T>,
// TODO: how to guarantee that Predicted has been added before the component gets added?
query: Query<Entity, (With<T>, With<Predicted>, Without<FloorMarker>)>,
fn add_visual_interpolation_components(
// We use Position because it's added by avian later, and when it's added
// we know that Predicted is already present on the entity
trigger: Trigger<OnAdd, Position>,
query: Query<Entity, (With<Predicted>, Without<FloorMarker>)>,
mut commands: Commands,
) {
if !query.contains(trigger.entity()) {
return;
}
debug!(
"Adding visual interp component for {:?} to {:?}",
std::any::type_name::<T>(),
trigger.entity()
);
commands
.entity(trigger.entity())
.insert(VisualInterpolateStatus::<T> {
// We must trigger change detection so that the SyncPlugin will
// detect and sync changes from Position/Rotation to Transform.
//
// Without syncing interpolated pos/rot to transform, things like
// sprites, meshes, and text which render based on the *Transform*
// component (not avian's Position) will be stuttery.
.insert(VisualInterpolateStatus::<Transform> {
// We must trigger change detection on visual interpolation
// to make sure that child entities (sprites, meshes, text)
// are also interpolated
trigger_change_detection: true,
..default()
});
Expand Down
28 changes: 14 additions & 14 deletions examples/avian_3d_character/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,20 @@ fn init(mut commands: Commands) {
group: REPLICATION_GROUP,
..default()
};
commands.spawn((
Name::new("Block"),
BlockPhysicsBundle::default(),
BlockMarker,
Position::new(Vec3::new(1.0, 1.0, 0.0)),
block_replicate_component.clone(),
));
commands.spawn((
Name::new("Block"),
BlockPhysicsBundle::default(),
BlockMarker,
Position::new(Vec3::new(-1.0, 1.0, 0.0)),
block_replicate_component.clone(),
));
// commands.spawn((
// Name::new("Block"),
// BlockPhysicsBundle::default(),
// BlockMarker,
// Position::new(Vec3::new(1.0, 1.0, 0.0)),
// block_replicate_component.clone(),
// ));
// commands.spawn((
// Name::new("Block"),
// BlockPhysicsBundle::default(),
// BlockMarker,
// Position::new(Vec3::new(-1.0, 1.0, 0.0)),
// block_replicate_component.clone(),
// ));
}

pub(crate) fn replicate_inputs(
Expand Down
15 changes: 6 additions & 9 deletions examples/avian_3d_character/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ impl Plugin for SharedPlugin {

// Position and Rotation are the primary source of truth so no need to
// sync changes from Transform to Position.
// (we are not applying manual updates to Transform)
app.insert_resource(avian3d::sync::SyncConfig {
transform_to_position: false,
position_to_transform: true,
Expand All @@ -111,18 +112,14 @@ impl Plugin for SharedPlugin {
after_physics_log.after(PhysicsSet::StepSimulation),
);

// We change SyncPlugin to PostUpdate, because we want the visually
// interpreted values synced to transform every time, not just when
// Fixed schedule runs.
app.add_plugins(
PhysicsPlugins::default()
.build()
.disable::<SyncPlugin>()
.disable::<PhysicsInterpolationPlugin>(), // disable Sleeping plugin as it can mess up physics rollbacks
// TODO: disabling sleeping plugin causes the player to fall through the floor
// .disable::<SleepingPlugin>(),
)
.add_plugins(SyncPlugin::new(PostUpdate));
.disable::<PhysicsInterpolationPlugin>(),
// disable Sleeping plugin as it can mess up physics rollbacks
// TODO: disabling sleeping plugin causes the player to fall through the floor
// .disable::<SleepingPlugin>(),
);
}
}

Expand Down
2 changes: 0 additions & 2 deletions examples/avian_physics/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ fn add_ball_physics(
Entity,
(
With<BallMarker>,
// insert the physics components on the ball that is displayed on screen
// (either interpolated or predicted)
Or<(Added<Interpolated>, Added<Predicted>)>,
),
>,
Expand Down
4 changes: 2 additions & 2 deletions examples/avian_physics/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
use serde::{Deserialize, Serialize};

use crate::shared::color_from_id;
use lightyear::client::components::{ComponentSyncMode, LerpFn};
use lightyear::client::interpolation::LinearInterpolator;
use lightyear::prelude::client;
use lightyear::prelude::server::{Replicate, SyncTarget};
use lightyear::prelude::*;
use lightyear::utils::avian2d::*;

use crate::shared::color_from_id;
use lightyear::utils::bevy::TransformLinearInterpolation;

pub const BALL_SIZE: f32 = 15.0;
pub const PLAYER_SIZE: f32 = 40.0;
Expand Down
28 changes: 27 additions & 1 deletion examples/avian_physics/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use bevy::color::palettes::css;
use bevy::prelude::*;
use bevy::render::RenderPlugin;
use lightyear::client::components::Confirmed;
use lightyear::prelude::client::{InterpolationSet, PredictionSet};
use lightyear::client::interpolation::VisualInterpolateStatus;
use lightyear::client::prediction::Predicted;
use lightyear::prelude::client::{InterpolationSet, PredictionSet, VisualInterpolationPlugin};

#[derive(Clone)]
pub struct ExampleRendererPlugin {
Expand All @@ -23,6 +25,14 @@ impl Plugin for ExampleRendererPlugin {
.after(InterpolationSet::Interpolate)
.after(PredictionSet::VisualCorrection),
);

// add visual interpolation for Position and Rotation
// (normally we would interpolate on Transform but here this is fine
// since rendering is done via Gizmos that only depend on Position/Rotation)
app.add_plugins(VisualInterpolationPlugin::<Position>::default());
app.add_plugins(VisualInterpolationPlugin::<Rotation>::default());
app.add_observer(add_visual_interpolation_components);

if self.show_confirmed {
app.add_systems(
PostUpdate,
Expand All @@ -34,6 +44,22 @@ impl Plugin for ExampleRendererPlugin {
}
}

fn add_visual_interpolation_components(
// We use Position because it's added by avian later, and when it's added
// we know that Predicted is already present on the entity
trigger: Trigger<OnAdd, Position>,
query: Query<Entity, With<Predicted>>,
mut commands: Commands,
) {
if !query.contains(trigger.entity()) {
return;
}
commands.entity(trigger.entity()).insert((
VisualInterpolateStatus::<Position>::default(),
VisualInterpolateStatus::<Rotation>::default(),
));
}

fn init(mut commands: Commands) {
commands.spawn(Camera2d);
}
Expand Down
1 change: 0 additions & 1 deletion examples/avian_physics/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ impl Plugin for SharedPlugin {
.build()
.disable::<ColliderHierarchyPlugin>(),
)
.insert_resource(Time::<Fixed>::from_hz(FIXED_TIMESTEP_HZ))
.insert_resource(Gravity(Vec2::ZERO));

// add a log at the start of the physics schedule
Expand Down
10 changes: 8 additions & 2 deletions examples/spaceships/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use leafwing_input_manager::prelude::*;
use lightyear_examples_common::shared::FIXED_TIMESTEP_HZ;
use serde::{Deserialize, Serialize};

use crate::shared::color_from_id;
use lightyear::client::components::{ComponentSyncMode, LerpFn};
use lightyear::client::interpolation::LinearInterpolator;
use lightyear::prelude::client::{self, LeafwingInputConfig};
use lightyear::prelude::server::{Replicate, SyncTarget};
use lightyear::prelude::*;
use lightyear::utils::avian2d::*;
use lightyear::utils::bevy::TransformLinearInterpolation;
use tracing_subscriber::util::SubscriberInitExt;

use crate::shared::color_from_id;

pub const BULLET_SIZE: f32 = 1.5;
pub const SHIP_WIDTH: f32 = 19.0;
pub const SHIP_LENGTH: f32 = 32.0;
Expand Down Expand Up @@ -286,5 +286,11 @@ impl Plugin for ProtocolPlugin {
.add_prediction(ComponentSyncMode::Full)
.add_interpolation_fn(rotation::lerp)
.add_correction_fn(rotation::lerp);

// do not replicate Transform but make sure to register an interpolation function
// for it so that we can do visual interpolation
// (another option would be to replicate transform and not use Position/Rotation at all)
app.add_interpolation::<Transform>(ComponentSyncMode::None);
app.add_interpolation_fn::<Transform>(TransformLinearInterpolation::lerp);
}
}
37 changes: 15 additions & 22 deletions examples/spaceships/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,12 @@ impl Plugin for SpaceshipsRendererPlugin {

app.add_plugins(EntityLabelPlugin);

// set up visual interp plugins for Position and Rotation.
// this doesn't do anything until you add VisualInterpolationStatus components to entities.
app.add_plugins(VisualInterpolationPlugin::<Position>::default());
app.add_plugins(VisualInterpolationPlugin::<Rotation>::default());
// set up visual interp plugins for Transform
app.add_plugins(VisualInterpolationPlugin::<Transform>::default());

// observers that add VisualInterpolationStatus components to entities which receive
// a Position or Rotation component.
app.add_observer(add_visual_interpolation_components::<Position>);
app.add_observer(add_visual_interpolation_components::<Rotation>);
// a Position
app.add_observer(add_visual_interpolation_components);
}
}

Expand All @@ -78,20 +75,13 @@ impl Plugin for SpaceshipsRendererPlugin {
// We query filter With<Predicted> so that the correct client entities get visual-interpolation.
// We don't want to visually interpolate the client's Confirmed entities, since they are not rendered.
//
// If your game uses Interpolated entities as well as Predicted, change the filter to:
//
// Or<(With<Predicted>, With<Interpolated>)>
//
// We must trigger change detection so that the SyncPlugin will detect and sync changes
// from Position/Rotation to Transform.
//
// Without syncing interpolated pos/rot to transform, things like sprites, meshes, and text which
// render based on the *Transform* component (not avian's Position) will be stuttery.
//
// (Note also that we've configured avian's SyncPlugin to run in PostUpdate)
fn add_visual_interpolation_components<T: Component>(
trigger: Trigger<OnAdd, T>,
q: Query<Entity, (With<T>, Without<Wall>, With<Predicted>)>,
// We must trigger change detection so that the Transform updates from interpolation
// will be propagated to children (sprites, meshes, text, etc.)
fn add_visual_interpolation_components(
// We use Position because it's added by avian later, and when it's added
// we know that Predicted is already present on the entity
trigger: Trigger<OnAdd, Position>,
q: Query<Entity, (Without<Wall>, With<Predicted>)>,
mut commands: Commands,
) {
if !q.contains(trigger.entity()) {
Expand All @@ -100,7 +90,10 @@ fn add_visual_interpolation_components<T: Component>(
debug!("Adding visual interp component to {:?}", trigger.entity());
commands
.entity(trigger.entity())
.insert(VisualInterpolateStatus::<T> {
.insert(VisualInterpolateStatus::<Transform> {
// We must trigger change detection on visual interpolation
// to make sure that child entities (sprites, meshes, text)
// are also interpolated
trigger_change_detection: true,
..default()
});
Expand Down
4 changes: 1 addition & 3 deletions examples/spaceships/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ impl Plugin for SharedPlugin {
});
// We change SyncPlugin to PostUpdate, because we want the visually interpreted values
// synced to transform every time, not just when Fixed schedule runs.
app.add_plugins(PhysicsPlugins::default().build().disable::<SyncPlugin>())
.add_plugins(SyncPlugin::new(PostUpdate));
app.add_plugins(PhysicsPlugins::default().build());

app.insert_resource(Time::<Fixed>::from_hz(FIXED_TIMESTEP_HZ));
app.insert_resource(Gravity(Vec2::ZERO));
// our systems run in FixedUpdate, avian's systems run in FixedPostUpdate.
app.add_systems(
Expand Down
1 change: 1 addition & 0 deletions lightyear/src/protocol/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ mod interpolation {
)
});
}

pub(crate) fn interpolation_mode<C: Component>(&self) -> ComponentSyncMode {
let kind = ComponentKind::of::<C>();
self.interpolation_map
Expand Down
Loading

0 comments on commit 6574b19

Please sign in to comment.