Expand description
A server-authoritative replication crate for Bevy.
§Quick start
The library doesn’t provide any I/O, so you need to add a
messaging backend.
If you want to write an integration yourself, see shared::backend module.
§Prelude
We provide a prelude module, which exports most of the typically used traits and types.
§Plugins
Add RepliconPlugins and plugins for your chosen messaging backend to your app:
use bevy::{prelude::*, state::app::StatesPlugin};
use bevy_replicon::prelude::*;
let mut app = App::new();
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins, MyMessagingPlugins));If you use MinimalPlugins, you need to add StatesPlugin
manually. It’s included by default with DefaultPlugins.
§Server and client creation
This part is specific to your messaging backend. For bevy_replicon_renet,
see this section.
On server connected clients represented as entities with ConnectedClient component.
Their data represented as components, such as ClientStats. Users can also attach their
own metadata to them or even replicate these entities back to clients.
These entities are automatically spawned and despawned by the messaging backend. You can
also despawn them yourself to trigger a disconnect or use the DisconnectRequest message
to disconnect after sending messages.
You can use On<Add, ConnectedClient> to react to new connections,
or use backend-provided events if you need the disconnect reason.
§States
We provide ClientState and ServerState, which are Bevy States.
These are managed by your messaging backend, and you can use them to control when your systems run.
For systems that should run continuously while in a specific state, use IntoScheduleConfigs::run_if
with the in_state run condition:
app.add_systems(
Update,
(
apply_damage.run_if(in_state(ServerState::Running)), // Runs every frame on the server.
display_vfx.run_if(in_state(ClientState::Connected)), // Runs every frame on the client.
),
);To run systems when entering or exiting a state, use the OnEnter or OnExit schedules:
app.add_systems(OnEnter(ClientState::Connecting), display_connection_message) // Runs when the client starts connecting.
.add_systems(OnExit(ClientState::Connected), show_disconnected_message) // Runs when the client disconnects.
.add_systems(OnEnter(ServerState::Running), initialize_match); // Runs when the server starts.You can also use these states with StateScoped, but you’ll need to call
AppExtStates::enable_state_scoped_entities in your app:
app.enable_state_scoped_entities::<ClientState>()
.enable_state_scoped_entities::<ServerState>();Read more about system patterns in the Abstracting over configurations section.
§Replication
It’s a process of exchanging data in order to keep the world in sync. Replicon provides a high-level API to automate server-authoritative replication.
Replication happens only from server to clients. It’s necessary to prevent cheating. If you need to send information from clients to the server, use messages.
For implementation details see ServerChannel.
§Tick rate
By default, updates are not sent every frame in order to save bandwidth. Replication runs
in ServerSystems::Send whenever the ServerTick resource
changes and if the state is ServerState::Running.
By default, the tick is incremented in FixedPostUpdate each time FixedMain
runs. You can configure how often it runs by inserting Time<Fixed>, which is 64 Hz by default.
use bevy::{prelude::*, state::app::StatesPlugin};
use bevy_replicon::prelude::*;
// `Time::from_hz` is implemented only for `Fixed`, so the type is inferred.
app.insert_resource(Time::from_hz(30.0)) // Run 30 times per second.
.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins));You can also configure the schedule via ServerPlugin::tick_schedule.
§Entities
By default no entities are replicated. Add the Replicated marker
component on the server for entities you want to replicate.
On clients Replicated will be automatically inserted to newly-replicated entities.
If you remove the Replicated component from an entity on the server, it will be despawned on all clients.
Entity IDs differ between clients and server. As a result, clients maps server entities to local entities
on receive. These mappings are stored in the ServerEntityMap
resource.
§Components
Components will be replicated only on entities marked for replication. By default no components are replicated, you need to define rules for it.
Use AppRuleExt::replicate to create a replication rule for a single component:
app.replicate::<ExampleComponent>();
#[derive(Component, Deserialize, Serialize)]
struct ExampleComponent;If your component contains an entity, it cannot be deserialized as is
because entities inside components also need to be mapped. Therefore,
to properly replicate such components, mark fields containing entities with
#[entities]. See Component::map_entities for details.
By default all components are serialized with postcard.
In order to serialize Bevy components you need to enable the serialize feature on Bevy.
If your component doesn’t implement serde traits or you want to customize the serialization
(for example, quantize, skip some fields or apply compression), you can use
AppRuleExt::replicate_as or AppRuleExt::replicate_with.
You can also create a rule for multiple components. Use AppRuleExt::replicate_bundle,
or pass a tuple of RuleFns to AppRuleExt::replicate_with. The components will only
be replicated if all of them are present on the entity. This also allows you to specialize
serialization and deserialization based on specific entity components.
All functions also have _filtered variants that additionally accept archetypal filters,
similar to Query. They are separate functions because Rust doesn’t allow default generics
for functions. For more details, see AppRuleExt::replicate_filtered.
§Required components
You don’t want to replicate all components because not all of them are necessary to send over the network. Components that can be calculated on the client can be inserted using Bevy’s required components feature.
// Replicate only transform and player marker.
app.replicate::<Transform>()
.replicate::<Player>()
.add_observer(init_player_mesh);
fn init_player_mesh(
add: On<Add, Mesh2d>,
mut meshes: ResMut<Assets<Mesh>>,
mut players: Query<&mut Mesh2d>,
) {
let mut mesh = players.get_mut(add.entity).unwrap();
**mesh = meshes.add(Capsule2d::default());
}
/// Main player component.
///
/// [`NotReplicatedComponent`] and [`Replicated`] will be implicitly inserted after spawning on
/// both the server and clients.
///
/// [`Replicated`] is always inserted on the client after replication, regardless of whether it is marked
/// as required. However, it may still be useful to mark it as required if you want to avoid
/// inserting it explicitly on the server.
#[derive(Component, Deserialize, Serialize)]
#[require(Replicated, NotReplicatedComponent, Mesh2d)]
struct Player;
#[derive(Default, Component)]
struct NotReplicatedComponent;This pairs nicely with server state serialization and keeps saves clean.
You can use scene::replicate_into to fill DynamicScene with replicated entities and their components.
On deserialization all missing required components will be inserted, and initialization
systems will restore the correct game state.
If a component can’t be used with the required components due to the inability to insert it
without world access, you can create an observer for a replicated marker and insert the actual
component inside it. However, it’s preferred to use required components when possible. For example,
it’s better to require a Handle<T> with a default value that doesn’t point to any asset
and initialize it later in a hook or observer. This way you avoid archetype moves in ECS.
§Mutability
There are two ways to change a component value on an entity: re-inserting it or mutating it.
We use Bevy’s change detection to track and send changes. However, it does not distinguish between modifications
and re-insertions. This is why we simply send the list of changes and decide how to apply them on the client.
By default, this behavior is based on Component::Mutability.
When a component is Mutable, we check whether it already exists on the entity.
If it doesn’t, we insert it. If it does, we mutate it. This means that if you insert a component into an entity
that already has it on the server, the client will treat it as a mutation. As a result, triggers may behave
differently on the client and server. If your game logic relies on this semantic, mark your component as
Immutable. For such components, replication will always be applied via insertion.
This behavior is also configurable via client markers.
§Component relations
Some components depend on each other. For example, ChildOf and Children. You can enable
replication only for ChildOf and Children will be updated automatically on insertion.
This will emit a B0004 warning which can be safely ignored.
See #19776 for more details.
You can also ensure that their mutations arrive in sync by using SyncRelatedAppExt::sync_related_entities.
§Deterministic replication
Up until now, we’ve covered only authoritative replication (AR), where the server is the source of truth and continuously sends changes. However, sometimes you may want to send data only once and simulate independently, relying on determinism. This approach is called deterministic replication (DR).
For example, you might use AR for things like player health, and DR for moving platform positions to reduce network traffic.
Use AppRuleExt::replicate_once to replicate only the initial value of a component.
You can configure this per-component within a replication rule using AppRuleExt::replicate_with.
See also server messages, which are also useful for DR.
§Network messages and events
This replaces RPCs (remote procedure calls) in other engines and, unlike replication, can be sent both from server to clients and from clients to server.
§From client to server
To send a message from client to server, you need to register the message
with ClientMessageAppExt::add_client_message instead of App::add_message.
Messages include Channel to configure delivery guarantees (reliability and
ordering).
These messages will appear on server as FromClient wrapper message that
contains sender ID and the message.
app.add_client_message::<Ping>(Channel::Ordered)
.add_systems(
PreUpdate,
receive
.after(ServerSystems::Receive)
.run_if(in_state(ServerState::Running)),
)
.add_systems(
PostUpdate,
send.before(ClientSystems::Send).run_if(in_state(ClientState::Connected)),
);
fn send(mut ping: MessageWriter<Ping>) {
ping.write(Ping);
}
fn receive(mut pings: MessageReader<FromClient<Ping>>) {
for ping in pings.read() {
info!("received ping from client `{}`", ping.client_id);
}
}
#[derive(Message, Deserialize, Serialize)]
struct Ping;If a message contains an entity, implement
MapEntities for it and use use
ClientMessageAppExt::add_mapped_client_message instead.
There is also ClientMessageAppExt::add_client_message_with to register a message with special serialization and
deserialization functions. This could be used for sending messages that contain Box<dyn PartialReflect>, which
require access to the AppTypeRegistry resource. Don’t forget to validate the contents of every
Box<dyn PartialReflect> from a client, it could be anything!
Alternatively, you can use events with a similar API. First, you need to register the event
using ClientEventAppExt::add_client_event, and then use ClientTriggerExt::client_trigger.
app.add_client_event::<Ping>(Channel::Ordered)
.add_observer(receive)
.add_systems(Update, send.run_if(in_state(ClientState::Connected)));
fn send(mut commands: Commands) {
commands.client_trigger(Ping);
}
fn receive(ping: On<FromClient<Ping>>) {
info!("received ping from client `{}`", ping.client_id);
}For events with entities inside use ClientEventAppExt::add_mapped_client_event.
Similar to messages, serialization can also be customized with ClientEventAppExt::add_client_event_with.
§From server to client
A similar technique is used to send messages from server to clients. To do this,
register a message with ServerMessageAppExt::add_server_message
and send it from server using ToClients. This wrapper contains send parameters
and the message itself.
app.add_server_message::<Pong>(Channel::Ordered)
.add_systems(
PreUpdate,
receive
.after(ClientSystems::Receive)
.run_if(in_state(ClientState::Connected)),
)
.add_systems(
PostUpdate,
send.before(ServerSystems::Send).run_if(in_state(ServerState::Running)),
);
fn send(mut pongs: MessageWriter<ToClients<Pong>>) {
pongs.write(ToClients {
mode: SendMode::Broadcast,
message: Pong,
});
}
fn receive(mut pongs: MessageReader<Pong>) {
for pong in pongs.read() {
info!("received pong from the server");
}
}
#[derive(Message, Deserialize, Serialize)]
struct Pong;Just like for client messages, we provide ServerMessageAppExt::add_mapped_server_message
and ServerMessageAppExt::add_server_message_with.
Event API is available. First, you need to register an event with ServerEventAppExt::add_server_event
and then use ServerTriggerExt::server_trigger:
app.add_server_event::<Pong>(Channel::Ordered)
.add_observer(receive)
.add_systems(Update, send.run_if(in_state(ServerState::Running)));
fn send(mut commands: Commands) {
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: Pong,
});
}
fn receive(_on: On<Pong>) {
info!("received pong from the server");
}And just like for client events, we provide ServerEventAppExt::add_mapped_server_event
and ServerEventAppExt::add_server_event_with.
We guarantee that clients will never receive events or messages that point to an entity or require specific
component to be presentt which client haven’t received yet. For more details see the documentation on
ServerMessageAppExt::make_message_independent.
§Abstracting over configurations
Depending on the game, you may need to support some of these configurations:
- Client
- Dedicated (headless) server
- Listen server (where the server is also a client)
- Singleplayer
There are 2 ways to support multiple configurations at the same time.
§Classic way
Just split client and server logic. Then for listen server and singleplayer run both the server and client, just don’t accept outside connections for singleplayer.
However, running the client and server in a single app is not supported. We rely on change detection to decide on which data to send, and since the world is shared, applying replication will trigger changes. To avoid this, you can use one of the following workarounds:
- Two Bevy apps inside a single process, running in separate threads.
- Two executables. After starting the client app, the server starts in the background.
It’s not easy to set up and requires more resources due to the synchronization between two worlds. This is why, while it’s possible to use Replicon this way, we recommend a different approach.
§The recommended way
Instead of recreating full client-server logic, we provide a way to emulate client and server functionality without actually running them:
- Client configuration runs only the client and its logic.
- Dedicated server configuration runs only the server and its logic (all client logic usually compiled out).
- Listen server configuration runs only the server and both logics.
- Singleplayer configuration doesn’t run the client or server but runs both logics.
To achieve this, use the provided ClientState and ServerState states:
- Use
ClientState::Disconnectedfor systems that require server authority. For example, systems that apply damage or send server events. This basically means “run when not a client”, which applies to both server and singleplayer. - Use
ClientState::Connecting,ClientState::Connected,ServerState::Running, etc. only for miscellaneous things, like display a connection message or a menu to kick connected players (things that actually require server or client running) - For everything else don’t use Replicon’s states.
Everything else is done automatically by the crate. All provided examples use this approach.
Internally we run replication sending system only in ServerState::Running and replication receiving
only in ClientState::Connected. This way for singleplayer replication systems won’t run at all and
for listen server replication will only be sending (server world is already in the correct state).
For messages, it’s a bit trickier. For all client messages, we internally drain messages as E and send
them as FromClient<E> locally with ClientId::Server in ClientState::Disconnected.
For server messages we drain ToClients<E> and, if the ClientId::Server is the recipient of the message,
re-emit it as E locally. The same applies to the event API. This emulates message receiving for both server
and singleplayer without actually transmitting data over the network.
We also provide ClientSystems and ServerSystems to schedule your system at specific time in the frame.
For example, you can run your systems right after receive using ClientSystems::Receive or ServerSystems::Receive.
§Organizing your game code
If you support a dedicated server, it’s recommended to split your game logic into “client”, “server”, and “shared” crates. This way, you can compile out all unnecessary code for the dedicated server configuration. Alternatively, you can use Cargo features and split the logic into modules.
We provide client and server features to disable unneeded functionality for this use case.
You will need to disable similar features on your messaging backend crate too.
Make sure that the component, event and message registration order is the same on the client and server. Simply put all registration code in your “shared” crate.
If you don’t need to support a dedicated server configuration, it’s easier to keep the logic grouped together. Splitting your game into crates is still useful, but it should be done logically rather than on server/client basis.
§Advanced features
§Authorization
Connected clients are considered authorized when they have the AuthorizedClient component. Without this
component, clients can only receive events and messages marked as independent via ServerMessageAppExt::make_message_independent
or ServerEventAppExt::make_event_independent. Additionally, some components on connected clients are only present
after replication starts. See the required components for AuthorizedClient for details.
By default, this component is automatically inserted when the client and server ProtocolHash matches.
This behavior can be customized via RepliconSharedPlugin::auth_method.
§Client visibility
You can control which parts of the world are visible for each client by setting visibility policy
in ServerPlugin to VisibilityPolicy::Whitelist or VisibilityPolicy::Blacklist.
To set which entity is visible, you need to use the ClientVisibility component
on authorized clients.
Check also the corresponding section in our README for more high-level abstractions.
§Prioritization
By default, all unacknowledged mutations are sent every tick. This can be expensive if you
have many entities with mutating components. The PriorityMap component lets you configure
how often mutations are sent for each entity on authorized clients. See its documentation for
more details.
In addition, ClientVisibility can be used to further reduce bandwidth by hiding entities
that are irrelevant to a given client.
§Interpolation and/or client-side prediction
Due to network round-trip time and tick rate, you may notice that the state isn’t updated immediately. This might be fine depending on your type of game. However, sometimes this needs to be visually hidden.
To make value updates look smooth, you can just to interpolate the received state. If the input delay doesn’t matter for your type of game, it can be enough.
But if your game is fast-paced, waiting for server to receive your inputs and replicate the state back might might be unacceptable. The solution is to predict simulation on the client.
However, it introduces another problem - misspredictions. For example, player 1 might predict movement to point X, while player 2 might have stunned it, the player 1 just didn’t receive it yet. To solve this, client must apply the received state from server and replay its inputs.
How much to predict also depends on the game. Common approaches are:
- Predict individual entities. Common for shooters or games where you don’t have many entities. In the case of a misprediction or non-deterministic mismatch, the state will be corrected. Determinism is important for this approach to reduce the number of rollbacks.
- Predict the entire world. Common for physics-based games or games with many entities. With this approach, all predicted entities are rolled back to the oldest received tick. For example, if one entity have confirmed tick 1 and another entity have confirmed tick 2, both entities are rolled back to tick 1. This approach is usually more expensive but produces better results for physics. Additionally, if there are many predicted entities, it might even be faster since there’s no need to check each entity for misprediction. The more entities you predict, the more likely it is that at least one will trigger a world rollback. So with this approach client usually just always rollbacks.
We don’t have these features built-in, but we provide a low-level API to implement these abstractions on top. Check the corresponding section in our README for existing implementations.
§Predicting spawns
Sometimes it’s necessary to spawn an entity on the client before receiving replication from the server. A common example is projectiles. By default, the client will end up with two entities: one locally spawned and one replicated from the server.
We provide the Signature component, which allows replicated entities to be matched with entities
previously spawned on the client.
This is also useful for synchronizing scenes. Both the client and the server can load a level independently and then match level entities to synchronize certain things, such as opened doors.
§Client markers
To apply interpolation or store value history for client-side prediction, you need to override how components are written. However, the server knows nothing about archetypes on the client, and while some entities need to be predicted, others might need to be interpolated.
This is why writing functions are marker-based. First, you register a marker using AppMarkerExt::register_marker<M>.
Then you can override how specific component is written and removed using AppMarkerExt::set_marker_fns<M, C>.
You can control marker priority or enable processing of old values using AppMarkerExt::register_marker_with<M>.
§Ticks information
This requires an understanding of how replication works. See the documentation on
ServerChannel and this section for more details.
To get information about confirmed ticks for individual entities, we provide
ConfirmHistory along with the EntityReplicated
trigger. This component is updated when any replication for its entity is received. However, we don’t update this component if an entity
hasn’t changed for performance reasons.
This means that to check if a tick is confirmed for an entity, you also need to check the received messages for this
tick. The ServerUpdateTick resource stores the last received tick from an update message.
The ServerMutateTicks resource and
MutateTickReceived trigger provide information about received mutate
messages for the past 64 ticks.
A tick for an entity is confirmed if one of the following is true:
ServerUpdateTickis greater than the tick.ConfirmHistoryis greater than the tick.ServerMutateTicksreports that for at least one of the next ticks, all update messages have been received.
§Optimizing entity serialization
Serialization of Entity is optimized for use in scenes, but it’s not very efficient for networking. Because of this, we use our own implementation.
See the compact_entity module if you want to apply this optimization to entities in your own types. You can also use
postcard_utils::entity_to_extend_mut and postcard_utils::entity_from_buf for manual writing.
§Eventual consistency
All events, messages, inserts, removals and despawns will be applied to clients in the same order as on the server.
However, if you insert/mutate a component and immediately remove it, the client will only receive the removal because the component value
won’t exist in the World during the replication process. But removal followed by insertion will work as expected since we buffer removals.
Entity component mutations may be applied to clients in a different order than on the server. For example, if two entities are spawned in tick 1 on the server and their components are mutated in tick 2, then the client is guaranteed to see the spawns at the same tick, but the component mutations may appear later (but not earlier).
If a component is dependent on other data, mutations to the component will only be applied to the client when that data has arrived. So if your component references another entity, mutations to that component will only be applied when the referenced entity has been spawned on the client.
Mutations for despawned entities will be discarded automatically, but events, messages or components may reference despawned entities and should be handled with that in mind.
Clients should never assume their world state is the same as the server’s on any given tick value-wise. World state on the client is only “eventually consistent” with the server’s.
§Troubleshooting
If you face any issue, try to enable logging to see what is going on.
To enable logging, you can temporarily set RUST_LOG environment variable to bevy_replicon=debug
(or bevy_replicon=trace for more noisy output) like this:
RUST_LOG=bevy_replicon=debug cargo runYou can also filter by specific module like this:
RUST_LOG=bevy_replicon::shared::protocol=debug cargo runThe exact method depends on the OS shell.
Alternatively you can configure LogPlugin from Bevy to make it permanent.
For deserialization errors on client we use error level which should be visible by default.
But on server we use debug for it to avoid flooding server logs with errors caused by clients.
Re-exports§
Modules§
- client
client - compact_
entity - Custom functions
serdeto packEntitymore efficiently. - postcard_
utils - Extensions for postcard to make streaming serialization and deserialiation more ergonomic.
- prelude
- scene
scene - server
server - shared
- test_
app serverandclient
Structs§
- Replicon
Plugins - Plugin group for all replicon plugins.