#bevy_replicon
1 messages · Page 13 of 1
When using UDP over localhost, packets don't travel instantaneously.
It will require 2 updates even if you put a 5s sleep after server_app.update(); and the first client_app.update().
It also works with the example backend that uses TCP 🤔
Hmm best guess: the client doesn't detect disconnect until the server app is dropped. It only detects disconnect when the server's udp socket disconnects.
I tried removing renet's server and transport, so I think it should have flushed it 🤔
Have you tried looking at renet logs?
No, I didn’t investigate further - just documented the behavior in tests since it’s not related to Replicon.
You might not even have this issue in renet2.
hey, would be nice to have a better way to skip some field in a Component, for example
#[derive(
Component,
Serialize,
Deserialize
)]
pub struct GoldGeneration(pub u32, #[serde(skip)] pub Timer);
where the timer is ticked on server and client, so not need to replicate it.
having to write a RuleFns and write all 3 methods is quite cumbersome
ill split the u32 and the timer in two different components, to also allows only replicating the Timer once at insertion, which isn't possible with rulefns i think
Aren't #[serde(skip)] skips the ser/de for the field already?
Ah, yes, you will need them to be separate components anyway.
it only solve half the problem, the Timer is not send through the network, but on the client the Timer is overwritten with its default implementation every tick...
I just network tick numbers for this reason
Ah, yes, it would be nice to automate Fns generation 🤔
sorry what do you do with the network tick ?
dunno if its easily doable with Reflect. in any case just splitting into multiples component fix my use case
I use it for timers and the like, I track time based on ticks in the simulation rather than wall time
Even if you use reflection to skip a field, you still need to write custom functions for reflected components 😅 We might want to make them built-in though.
Yes, in your case you need them separate components, but your proposal is good in general.
ohh interesting. for my use case time<fixed> is good enough I think just for a few visuals on the client
Ah, yea if it's visuals then the extra accuracy of a timer also does something
(The max accuracy if my approach is 1 tick, 1/60th of a second in my game for now)
updated aeronet_replicon
Thanks!
Have you tested that the client can receive a message together with a disconnect if there are no packet loss?
Asking because in renet and the example backend I had to adjust the system ordering.
In the renet's draft it's under "Process received data on disconnect" in the description.
It's probably the main reason I made the RC 🙂
I think this is actually io layer dependent in aeronet, maybe I should write some guarantees for this. But since I'm on holiday right now I don't want to touch the code too much
No problem, take your time 🙂
Quinnet author also mentioned that he's out of town.
Just to clarify: it's not a guarantee - we simply give the client a chance to receive the data.
It didn’t work out of the box in our Renet integration - I had to adjust system ordering. Otherwise, all received data was discarded if it arrived along with a disconnect.
It's not super important, but it's pretty useful for debugging.
This doesn't seem right 🤔
2025-06-08T15:38:51.148498Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
2025-06-08T15:38:51.165166Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
2025-06-08T15:38:51.181841Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
2025-06-08T15:38:51.198500Z TRACE bevy_replicon_renet2::server::plugin: forwarding 0 received bytes over channel 6
2025-06-08T15:38:51.198510Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
2025-06-08T15:38:51.215167Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
2025-06-08T15:38:51.231842Z TRACE bevy_replicon::shared::backend::replicon_server: received 0 message(s) totaling 0 bytes from channel 0
So you received a zero-sized message from renet. Looks like it's a channel 6, so it's a zero-sized event?
But from the logs you don't read from this channel later, it's always from the channel 0?
Only ever channel 0 yea. The event is indeed zero size, but replicon doesn't seem to pick it up
Weird, I use mostly zero-sized events in my tests. By any chance you have different protocols?
Have you tried the latest RC? It now disconnects on mismatch.
I ported the renet (not 2) to it, so you could try swapping them.
@echo lion by any chance you could port the renet2 to the RC as well just in case? The migration is most likely identical to the original renet.
Well there is probably some mismatch, the client tries to read about 8 channels, the server only channel 0 and 1
Double check how the server is created, you might not passing the channels to it 🤔
In the latest RC you will be able to RUST_LOG=bevy_replicon::shared::protocol=debug cargo run ... and clearly see what is registered.
Well that's the weird part, renet2 knows about the channels, replicon doesn't
Which is obviously weird because replicon is the one picking the channels 
Almost certainly a version mismatch.
Between yours replicon and renet2's
Everything pulls the exact same version though 
Are you sure? 🤔 So with cargo tree you only have a single replicon version?
They actually communicate correctly too when it's just replication
Yep
Could you send me full log with bevy_replicon=trace for both client and server? Maybe I'll spot something.
I see that renet2 create channels properly.
Replication flows and you send send an event over channel 6 which is net_sync::CurrentTime.
Everything looks correct to me. Are you saying that it's not triggered on client or server?
Also what version are you running?
0.33.0
The client sends a GetTime, which never seems to get emitted on the server, nor does the server logit. The server sends CurrentTimes, but they never seem to get picked up by replicon either 🤔
Do you have ClientEventPlugin and ServerEventPlugin enabled?
Because I don't see any sending event ... in the logs
Correction, I see sending event net_sync::GetTime on the client
But nothing gets send or received on the server
I have the Client one on the client, and the Server one on the server, the old code with both in shared doesn't compile anymore afaict
Yea, the client seems to behave properly afaict, it actually tries to read a bunch of different channels instead of only 0
Yeah, it looks like on the server you don't have channels created
the plugin that reads channel is not created*
@dire aurora ah, by any chance you forgot to call App::finish on the server?
Hmmm, where would I need to call App::finish? 🤔
If you don't use App::run, you need to call App::finish after all plugins to let them properly initialize.
I do use App::run but I have a custom runner which might affect things, I'll try 🤔
OMG that was really it 🙃
Yeah, it's very easy to forget. But it might break plugins badly 😅
Yea, I wonder if this was always how bevy behaved
If so, then the runner API just kinda sucks 🤣
Agree!
I don't even remember when it was introduced.
It worked for you before because we didn't use finish for events 2 Bevy versions ago 🤔
Yea, very possible
Now I'm finally onto my next issue
called `Result::unwrap()` on an `Err` value: DeserializeBadOption
1: bevy_bundlication::deserialize
2: logic::status::NetworkedStatus::read
```... Figuring out whatever the hell `DeserializeBadOption` means 🤣
Okay it's from postcard, but that doesn't look like an error that should be possible
Yeah, not easy to debug... Try to check if what you serialize is deserializable 🤔
I assume you use some custom ser/de
Yea, it's definitely custom, you can tell from the bundlication 😅
Hmmm, commeting that one out the next error is:
bevy_replicon-0.33.0/src/shared/replication/replication_registry.rs:144:32:
replication `FnsId(4292)` should be registered first
stack backtrace:
0: rust_begin_unwind
1: core::panicking::panic_fmt
2: bevy_replicon::shared::replication::replication_registry::ReplicationRegistry::get
3: bevy_replicon::client::apply_replication
``` ... Maybe it isn't as simple as just the ser/de being wrong 🤔
Oh also, current diff:
168 files changed, 3276 insertions(+), 15362 deletions(-)
Might be some registrations mismatch after all...
@dire aurora wait, do you have 4292 replicated components? 🤔
It's probably a ser/de issue 😅
Unlikely 😅
But yea, if it's a ser/de issue this is probably not coming from where the other error started 🙃
Probably just calling the wrong function somehow
It's likely some function don't deserialize a component properly and lefts some bytes which picked up by next function as an ID
Probably, the question is, which function
It's gonna be hard to find it in my diff 😅
Yeah... Try to take a closer look at all custom ser/de functions
Maybe add logging to each
Hmmmm, might be a bundlication bug 🤔
If I disable all groups it stops crashing, if any group is enabled I get crashes
I will see if we can log ser/de process on replicon side.
This would simplify debugging. You could check how much bytes each function takes and compare between client and server.
@dire aurora by any chance you aren't advancing the bytes cursor?
Hmmmmm, possible if there is something weird about the Read impl or how I pass it around 🤔
Double check that Bytes truncated after reading.
If you are streaming, make sure you are using https://docs.rs/bytes/latest/bytes/buf/struct.Reader.html
A Buf adapter which implements io::Read for the inner value.
@dire aurora added more logging, let me know if it helps: https://github.com/projectharmonia/bevy_replicon/pull/504
~~You will need to transform it into a patch like this:
https://patch-diff.githubusercontent.com/raw/projectharmonia/bevy_replicon/pull/504.patch
And then apply it to your local copy of replicon since this branch targets master.~~
@dire aurora never mind, I just retargeted the PR to 0.33.0. You can just patch it with Cargo now.
Should I be getting these for every single entity in the world? 🤔
2025-06-09T10:44:25.484146Z DEBUG bevy_replicon::shared::event::server_event: ignoring broadcast for channel 4 for non-replicated client `0v1`
Seems to not happen consistently somehow, which is even weirder
Looks like this was the issue. I did a very ugly conversion in the macro code to get a type that implements Read in the code, which makes it not advance the cursor
-# The fact that no one noticed proves no one else used bevy_bundlication at least
It's a silly bug I fixed on the latest master:
https://github.com/projectharmonia/bevy_replicon/commit/bd297c2e32745deac8eb784eed997c05cfbaca73
It has no effect on the logic, I just accidentally iterated over all entities in the world during event sending 😅 So feel free to ignore this message.
I noticed that we iterated over all entities in the world.
Yeah, I had situations like this 😅
The Bytes crate is a bit confusing, and it's not helped by the fact that bevy_bundlication relies on Read/Write which are std-only and thus off by default basically everywhere
Though honestly I'm not sure what I was thinking when I wrote this code either 😅
AsRef::<[u8]>::as_ref(cursor)
```"What could possibly go wrong?"
Retargeted to the latest master and tweaked more. Now should be mergable.
I'll draft a new release once messaging backends catch up with RC.
It's just one of the biggest releases and I want to make sure it's easy for backends to handle disconnects.
Is there a way to use the matchbox crate with replicon on top for p2p (one player host others join) or any other way to do nat traversal currently?
Matchbox provides a signaling server as a separate crate.
I haven’t used it myself, so I’m not sure if it’s tied to its I/O. If it is, you’d need to write an integration for it as a messaging backend. That’s not too hard - you can check out the backend module for documentation, look at real-world crates, or use the example TCP backend as a starting point.
Also, just to clarify: you’re describing a client-server setup - and that’s what you want.
P2P refers to a mesh topology.
Matchbox supports both 🙂
Yes a setup that could be used in a jam, I prefer the server <-> client architecture in general, but it’s not really feasible to spin up servers etc. for a jam.
Also for development it would be really convenient if you simply can share a build people can just try.
Having one signaling server is no problem, but having one per instance of a game is problematic.
So I am looking for a good way to get around the NAT problem
I think you still describing client-server topology 🤔 There is a host and clients.
Your host is just also a client. It's usually called listen-server mode. It's not related to the topology.
Can't suggest much about NAT. Maybe ask in #networking?
Another option is to ask matchbox developers to take a look into the ingeration themselves 😅
I remember they were very interesting in having an integration, but were busy with abstracting over p2p to support client-server at the time.
If you decided to use it, of course.
Yes indeed I mean listen server
Will take a look, the simple example uses a simple tcp socket correct?
Are all traffic reliable in replicon?
Will take a look, the simple example uses a simple tcp socket correct?
Yes 🙂
Are all traffic reliable in replicon?
No, replication uses a dual-channel approach (reliable + unreliable). Events also have configurable guarantees.
But if the backend (TCP in this case) doesn't provide an unreliable channel, it's okay to downgrade to reliable - just not the other way around 🙂
It's a simple backend to run examples inside the repo without depending on other crates. And a good example by itself for how to write such backends.
ID of a server replication channel.
@swift hamlet also, if you look at the example backend, look at the latest tagged version:
https://github.com/projectharmonia/bevy_replicon/tree/v0.33.0
Hey, I updated https://github.com/Henauxg/bevy_replicon_quinnet to replicon 0.34.0-rc.1
It worked fine, but I do have some implementation issues with reliably sending pending messages on disconnect, hence I'm not testing it in my tests. I'm just ensuring that the disconnection functions properly.
I know why I have those issues, but I'm not sure yet about when/if I'll update this behavior. I'd need to think about it a bit more.
Oh, I've also found a tiny typo in the tic tac toe example in init_client code doc (https://github.com/projectharmonia/bevy_replicon_renet/blob/bevy-replicon-0.34-dev/examples/tic_tac_toe.rs): "Estabializes "
Just to clarify: you don't have to wait for the client to ack the message. You just need to send it before the disconnect, so the client have a chance to receive it.
Ah, reading the comments, so the problem is to make it work reliably in tests?
The ability to receive such messages is not critical, but it improves the user experience. For example, you can get a nice message on protocol mismatch instead of just disconnect.
Looks like there are no issues from the Replicon's side in the latest RC 🤔
Will draft a new release today.
Is there a simple way inside a system to check if its on the server or the client?
We have built-in conditions if you want to avoid running a system at all.
If you want to check inside a system, you can simply call a condition inside it as a regular function.
Or just get the client resource and check if it's running.
System conditions for RepliconClient and RepliconServer resources.
Did you manage to hook matchbox? 🙂
not yet, gave it a short go, but I didn't have enough time to get it working.
I think it should be possible (even tough it creates a connection between each peer).
Once I have a working prototype with the existing backends I might give it another go.
Is there a way to map entites of a Vec<MyStruct where
struct BigStruct {
stuff: Vec<Vec<MyStruct>>
}
struct MyStruct {
pub entity: Entity,
pub value: f32
}
How could theset entities be properly be mapped?
Ah its a simple as marking the entities fields with #[entities] and then simply implementing map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E).
Didn't expect that these nestings would be so easily possible.
you can derive MapEntities btw not need to manually impl it
That works even with a double nested Vec?
ohh right its merged into main but not in 0.16.1. should be available for 0.16.2 https://github.com/bevyengine/bevy/pull/19071
I think it's available since 0.16.1 🤔
i dont think, it got removed from the milestone and its not in the release diff https://github.com/bevyengine/bevy/compare/v0.16.0...v0.16.1
but maybe i dont understand how the milestone + release diff works
Ah, I didn't know it got removed! I wondering why, I thought it's non-breaking...
yeah dunno :(
Asked: #ecs-dev message
Unfortunately, looks like the change will be available since 0.17.
Hey there, I've just started integrating bevy_replicon (0.34) into one of my projects, I was just wondering - what's the best way to handle client side replicated entity lifetime - specifically when leaving a session? I've been trying to have a required state-scoped state that deswans entities locally after the client has disconnected from a session, but this seems to sometimes cause a panic in bevy_replicon/client.rs (621, in apply_mutations) as it looks like the despawn happens before replication tries to mutate it. I originally didn't bother having the state-scope on the client side, as I figured that anything that was replicated to it would be cleaned up automatically if the session that entity was replicated in despawns/expires, but it doesn't appear to be the case (these replicated entities are still present even after the client entity with Session is gone; after a Disconnected triggers). Perhaps I have an incorrect assumption or setup for how this should work. Any recommendations would be muchly appreciated!
I just found ClientSet::Reset, which seems to describe what's going on here. Makes sense that this isn't automatic now that I look at it, as to handle reconnecting (by the likes of bevy_replicon_repair)! Will read up on the docs there to catch up with the concept
Well, this seems to do the trick, so far without panics, incase anyone else stumbles upon the same issue 🙏
.add_systems(PreUpdate,client_cleanup_replicated.in_set(ClientSet::Reset),);
for entity in replicated.iter() {
commands.entity(entity).despawn();
}
}```
Yes, it's correct 🙂 Another good place to despawn is PostUpdate, this way you won't need precise ordering.
Yes, another use case is to continue playing locally after a disconnect.
We are planning to use Bevy states in the future - this way you could just slap a StateScoped component on and have it clean up automatically. But we need some changes from Bevy first: https://github.com/bevyengine/bevy/issues/15133
oh sad its pretty useful, wouldn't have thought it to cause problem
hey, is there a way to see what is being replicated / updated to the client in realtime ? to investigate what is using the bandwidth. thanks
Set the logging to trace on the server. You should be able to see what kind of data collected.
i can see how many bytes are sent, but not what is being sent. at connecting i get 11000 bytes send to each client, i would like to debug what they are for example what components are being sent
Currently struggling to make the following setup work.
A replicated Component which references another struct which has a reference to to an entity.
#[derive(Component, Debug, Reflect, Serialize, Deserialize, Default, MapEntities)]
#[require(Replicated)]
pub struct GameManager {
#[entities]
pub player_registry: Vec<PlayerRegistryEntry>,
}
#[derive(Debug, Reflect, Serialize, Deserialize, Default, MapEntities)]
pub struct PlayerRegistryEntry {
#[entities]
pub player_entity: Option<Entity>,
}
=> no method named `map_entities` found for struct `std::vec::Vec<PlayerRegistryEntry>` in the current scope
--> src/auction_mechanic/auction.rs:15:10
|
15 | #[derive(Component, Debug, Reflect, Serialize, Deserialize, Default, MapEntities)]
| ^^^^^^^^^ this is an associated function, not a method
even when manually implementing MapEntities it doesn't seem to compile
Ah, it's that one fix that didn't get into 0.16.1 again ...
I think this? #1090432346907492443 message
For now you could make a wrapper type for the collection and implement MapEntities on that, or manually impl the map_entities function in Component
Or I guess patch bevy to include that PR in an 0.16.x release
yes indeed, that seems to be the issue mentioned on these issues.
Thank you!
You should be able to see exactly what you send in the latest release :)
I improved this logging part.
The release also includes replicate_once, which should come in handy for you.
nice thanks :) yes i want to update to 0.34 but waiting on renet2 support, for now its works so its fine
I migrated the original renet and suspect that the migration for renet2 will be basically 1:1. So if you want to speedup the process, you may submit a PR 🙂
See this message if you are interested: #1090432346907492443 message
i tried when you released 0.34 but i wasn't sure of some things. ill try again thanks to your example on renet !
Feel free to ask things if you decide to try again! I'm open to help
@dire aurora lol after time i did my own client predicition / server reconciliation xd
Are you planning to publish it somewhere?
no really, bevy have problems with unreliable Res<Time> so iam not planning to continue it rn, until bevy is more mature
i just did to learn
not hard to be honest once you do one time
but because bevy Res<Time> is unreliable it have reconciliations even with 0 ms
What do you mean by "unrealiable"?
Bevy version: 0.7 and current main. Bevy's Time resource, which is the standard and recommended source of timing information in Bevy, that everything should use for anything timing-related, is ...
but yes do client predicition / server reconciliation is ez as f
Ah, I see.
Making a game is such a long process that people here mostly focus on the gameplay and wait for things like this to be resolved upstream 🙂
ive just made exactly the same changed as you... https://github.com/UkoeHB/renet2/pull/32
hello
2025-06-21T09:34:06.867024Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(0)` for client `226v1`
2025-06-21T09:34:06.867028Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(5)` for client `226v1`
2025-06-21T09:34:06.867032Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(7)` for client `226v1`
2025-06-21T09:34:06.867036Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(9)` for client `226v1`
2025-06-21T09:34:06.867040Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(10)` for client `226v1`
2025-06-21T09:34:06.867043Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(19)` for client `226v1`
2025-06-21T09:34:06.867049Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(20)` for client `226v1`
2025-06-21T09:34:06.867053Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(23)` for client `226v1`
2025-06-21T09:34:06.867057Z TRACE bevy_replicon::server: writing insertion for `215v1` with `FnsId(62)` for client `226v1`
how can i see what the entity is ? i would like to log each entity that is being replicated with the components that are being sent. i can use a local bevy_replicon fork and add some log statements i guess ?
thanks :)
FnsId corresponds to a component. Take a look at the beginning of the log to know which ID represents which component.
We don't print actual name because this information is not preset when we collect changes on the server.
Another option is to take a look at 215v1 in the inspector. Like with the VSCode plugin or via bevy_inspector_egui
hm its going to be a little annoying with +60 fnsid haha. ill save the log and clean it i think to more easily understand the components.
its not possible to also query the Fns registry to get the name for the components when collecting changes on the server ?
oh its the mapped entity on the client ? i thought it was the server entity.
thanks !
Yes, you probably want to use the inspector 🙂
In the log it's a server entity.
my server is headless, i can re add DefaultPlugin and add inspector egui yeah good idea
I think with the mentioned VSCode extension it could work in headless mode 🤔
oh with bevy remote protocol
thought it was bevy inspector egui haha
i dont use vscode but ill try it out thanks
I suggested both 🙂
Same! I'm a former Neovim user, who recently migrated to Zed.
But might be easier to fire VSCode with this plugin then turning server into non-headless 🤔
zed is pretty cool, personally i miss too many plugins from the nvim ecosystem...
yeah BRP is pretty cool ill do that
sadly the vscode inspector dont support a search bar for the entity haha
Same! But they continue to extend the functionality from plugins: https://zed.dev/docs/vim#supported-plugins
Another option is to log from commands. I.e. commands.entity(entity).log_components(). But it will require iterating over all replicated entities.
wow thanks for the link they have improved the vim mode by a lot from last time i check. its awesome ! the only thing clearly missing for me is still a flash.nvim/leap implementation.
also, do you use your mouse often with zed or are you able to have everything with keymaps like vim ? my setup is working well with LazyVim and it would feel like a downgrade having to use the mouse again
I miss Leap too! They implemented a Vim-sneak style motion, which is inferior in my opinion.
For some reason, they don’t mention it in the plugins list, but you can search for that keyword on the same page. Also, there are no labels when you jump - but they plan to improve it 🙂
I use the mouse sometimes, but it’s not like in VSCode or other editors because the Vim support is built-in.
zed has multiples small things better than nvim, ill have to try it out lol. but right now my nvim config is working well, LazyVim is really cool too. ill switch one day when zed has matured more, with flash/hop which are the best implementation imo.
Zeta looks cool, but i would like to run it locally to have unlimited generations.
looking at their top issues, they have multiples things missing that outweigh the little benefits for now. ill subscribe to their release note im hyped haha
Makes sense!
What I really like in Zed compared to Neovim is how multiple cursors work 🙂 And diffs and search as a single mutable buffer. It's similar to mutable quickfix, but better.
yeah it looks nice
btw im having a problem with replicon 0.34 that wasn't happening before (commands batching i guess ?). i have multiples Tiles entity, on client connection i have a trigger for OnAdd ConnectedClient. i add a new building entity for the player with ChildOf(tile_entity) but on the client i get a warning that the ChildOf tile_entity doesn't exist. i guess its because the tiles entities are inserted after the building entity on the client.
also it seems that server trigger made in the ConnectedClient trigger are not sent to the client that just connected
Finally have physics and movement working ... Besides some issues at the start, and needing to do some ugly workarounds for the lack of support for networked assets, I've had very little network issues with my migration so far ... The current diff is scary though:
167 files changed, 1553 insertions(+), 13991 deletions(-)
On clients all components now just inserted together and no commands are flushed. So you saying ChildOf component doesn't exist at the moment of insertion? Do you replicate the hierarchy? If yes, I would expect them to exist.
You mean trigger inside a <OnAdd, ConnectedClient> observer doesn't work? 🤔
Awesome! So the migration is done now?
on the client, the entity pointed by the ChildOf doesn't exist. the entity is mapped I think
sorry if I wasn't clear
it's basically the parent and the child are both inserted on the client in the same tick, and it seems the child is inserted before the parent ?
Even if child processed first, the parent entity should have been created during entity mapping 🤔
I'll take a look
Sadly not, still need to fix shaders, test combat, add some support for missing SDFs to my trees, and make it so that my inputs reset between ticks correctly (pressing space once now means bunnyhopping forever)
Sounds like a lot of work 😅
It sure is 😅
I also got one rollback-related crash, if that keeps happening I might have to investigate whats going wrong too
But it's to do with status effects so I don't really want to touch it, that code needs to be rewriten using 🌈 and first-party entity disabling after all
Yeah, I feel like rewriting things into new Bevy concepts is the hardest part of each update.
It's harder to first migrate the half-broken old patterns though
Which is what I did 😅
So many assumptions in my code broke due to required components existing 🤣
Things like one frame delay?
No, I had code that was like "if X doesn't exist, add it with this value"
And because other crates added their component later in the frame, it was always filled with that value, unless spawned otherwise
Ah, got it 🙂
Bevy evolved a lot, it's so much more convenient now. I hate migration, but I love the new features.
@thorn forum could you try this branch?
https://github.com/projectharmonia/bevy_replicon/pull/508
yep fixed it !! thanks a lot :)
Awesome, will merge and draft a patch
but i still have this warning also "Entity 227v1 with the GlobalTransform component has a parent without GlobalTransform." where the entity is the building and the parent entity is the tile. both are spawned with a Transform on the server (i directly .replicate Transform to test, normally i use another struct for bandwidth saving).
i think its the same “architectural” problem as the previous warning, the child commands ? (here the required component) are run before the parent
also this 🤷
reproduced the event problem on the simple box example
I think this warning is a false-positive since the parent just replicated after it. Might worth opening an issue on the Bevy side 🤔
Looking into it
Ah, I think it's expected. It's because connected != authorized. Just replace ConnectedClient with AuthorizedClient.
hm how can i create a simple reproduction for the issue ?
oh right
thanks for the release btw
I don't think we need a reproduction in this case 🤔
Bevy checks for the existence of the parent global transform in a hook. Since we sometimes process a child first, the hook runs, sees that the entity exists, and emits a warning. This entity is empty because the replication for it hasn't been processed yet.
We just need to request a setting resource that disables this warning and we temporarily turn it off during the receive.
I mean we on the replicon side should toggle it.
on the bevy side, its not possible to run the check parent has component only after all the commands are exhausted instead of in the component hook ?
On our side we don't do any commands, we just directly insert into the world for performance.
And hooks run immediately
ohh ok
so the issue would be more like a way to disable the warnings ? per entity and or/per components ?
maybe the warning about the ChildOf(entity) being invalid can also be disabled, since its the same timing problem ?
i dont know how disabling the warning would work but it could be dangerous if the warning is suppressed while it was actually legitimate, it happened to me multiples times that these warning were true and i forgot to insert some things on the client side
I think it should be a global toggle 🤔
Since we want to temporarily disable B0004 while applying replication in Replicon, you should still be able to get these warnings in your own code.
Not sure if it should be configurable per-warning or just for this specific check.
maybe the warning about the ChildOf(entity) being invalid can also be disabled, since its the same timing problem ?
What do you mean?
my previous problem for which you added a world.flush(), its the same problem no ? the ChildOf(entity) the entity was gonna be inserted, just not yet and the ChildOf(entity) was inserted. so the warning can be supressed
my safety concerns are, this disable the warning entirely, even if it was legitimate. for example if i replicate a child with a Transform, and the parent doesn't have one. the child is gonna have a GlobalTransform, while its parent not. it should still print the warning no ?
No, it's a bit different 🙂 Your problem is that the entity inside ChildOf is not yet valid at all. It's because we allocate entity during mappings using Entity::reserver which requires flushing later to make them valid. Without it you can't even insert new component on such entities since they are not yet valid.
I fixed it by adding flushing.
It's a valid concern. But we can't distinguish between an actually missing GlobalTransform and one that just hasn't been processed yet 😢
Another option is to somehow fix it on our side, but I'm not sure how... Maybe we could flush at the end of the entire replication process. I'll think about it more. I agree that adding a way to temporarily suppress the warnings isn't a good idea.
Just ignore these warnings for now 🙂
It's okay, there is nothing wrong in asking questions!
so can't the validate_parent_has_component system run only after everything has been inserted ?
when using world, does observers also run directly like hook or On* triggers are run when flushing ? if not, validate_parent_has_component being a OnInsert trigger instead would allow it to run only when flushing after everything has been inserted ?
ive stopped opening the issue then as the requested solution is suboptimal
and bevy_replicon using commands instead of directly inserting components would be huge performance loss ?
Both hooks and observers run immediately.
Yes and it wouldn't solve the problem.
Because hooks and observers don't wait for the flushing
i mean using commands + a bevy side change where validate_parent_has_component only runs after all commands are applied
It's done using hooks for performance reasons
I'm not sure if it's even possible to run something after commands 🤔
sad
replicon side, maybe sort the order of the insertions, where the parents would be put first ?
Thinking about it more. No, same issue, we can't guarantee the order of applying changes 🤔
Not an easy thing to do... Relations are generics.
i dont like having a warning, so ill delay the spawn of the child one frame so parent has time to settle on the client haha
You can create a special component that on insertion adds ChildOf
im clearly not qualified enough to give ideas lmao
oh yeah ill do that its better
It's okay, I don't expect people to have a deep knowledge of how Replicon works 😅
@thorn forum thinking about it more.
This problem should be even present in scenes when children defined before parents.
A possible solution would be to check for GlobalTransform in a system with Added 🤔
i thought of that 🤓🧠 when talking about running validate_parent_has_component after all commands but thought there was a better way internally than a system, but yeah actually system is just fine and that's what bevy use anyway for it's things
(message failed to send yesterday)
hi, still having problem with ChildOf 🤔
#[derive(Component)]
pub struct Test(#[entities] pub ChildOf);
error[E0599]: no method named `map_entities` found for struct `bevy::prelude::ChildOf` in the current scope
--> game_shared/src/game/replicon/mod.rs:161:10
|
161 | #[derive(Component)]
| ^^^^^^^^^ this is an associated function, not a method
|
= note: found the following associated functions; to be used as methods, functions must have a `self` parameter
note: the candidate is defined in the trait `bevy::prelude::Component`
--> /home/mirsella/.local/share/cargo/git/checkouts/bevy-659f18987ae20c74/f27ff60/crates/bevy_ecs/src/component.rs:563:5
|
563 | fn map_entities<E: EntityMapper>(_this: &mut Self, _mapper: &mut E) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the derive macro `Component` (in Nightly builds, run with -Z macro-backtrace for more info)
i have the bevy prelude imported
oh its because ChildOf impl Component but not MapEntities 🤡
how can i work around that for the issue we talked about ?
manually implemented Component
impl Component for DelayedChildOf {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = component::Immutable;
fn map_entities<E: EntityMapper>(this: &mut Self, mapper: &mut E) {
Component::map_entities::<E>(&mut this.0, mapper);
}
}
Yes, it's the only way 😢
Also I think in your impl you can just call this.0.map_entities(mapper); 🤔
can I open a PR to also implement MapEntities on ChildOf ? or it has not been done for a reason ?
there a issue about Component using the MapEntities impl I think 🤔
Maybe just create DelayedChildOf(Entity)? You don't actually need to store ChildOf inside.
oh of course !
I talked about doing a PR just to improve for other user on the long run etc, im fine with the manual impl
I think there are no cases to store ChildOf 🤔
yeah you're right, apart from using a type to be more clear on the usage
I have an event called
trigger: Trigger<FromClient<ClientSendsTrackRequestToServer>>,
it implements MapEntities (manually), and all the entities inside the struct are properly mapped.
but the trigger.client_entity always returns PLACEHOLDER,
What could I be missing?
(this only happen if the listen server sends the request, the server which is also hosting a client, if a normal client sends the request it works as expected)
client_entity is a client identifier, it's not an entity of the trigger
You probably looking for trigger.target()
I expected that the client_entity would be the entity which holds the client connection (to send data to etc.)
(I also added some player data to that entity when the client connects)
I know, I wasn’t using the trigger_target variant for this.
That's correct.
By any chance you sending it from server?
It’s a listen server so technically yes
Then yeah, it will be a placeholder. It's equal to SERVER constant which is a placeholder
When we get resources as entities, it'll be the entity with RepliconServer.
hey ! does replicon use bevy DetectChanges to send a mutation or not ? so if i use bypass_change_detection replicon will not send it to clients ?
Correct!
So if you mutate something often, I'd recommend checking if the value is actually changed
There is also set_if_neq
Types that implement reliable change detection.
yep im using set_if_neq :)
it was for a component Target which hold a entity + its translation, for easy acess to the translation even if the entity is dead. i had this component replicated a LOT
i parsed the data from the replicon log btw:
❯ open mutations.json
╭───┬─────────────────────────────────────────────────────────┬────────╮
│ # │ name │ count │
├───┼─────────────────────────────────────────────────────────┼────────┤
│ 0 │ game_shared::card::Target │ 197666 │
│ 1 │ game_shared::card::AttackRelation │ 7053 │
│ 2 │ game_shared::card::components::Health │ 3976 │
│ 3 │ game_shared::card::TroopState │ 3352 │
│ 4 │ game_shared::game::grid::pathing::Pathing │ 2883 │
│ 5 │ game_shared::card::cards::castle::Gold │ 1786 │
│ 6 │ game_shared::card::components::LastHitBy │ 803 │
│ 7 │ game_shared::card::troops::necromancer::NecromancerMark │ 88 │
│ 8 │ game_shared::game::replicon::Owner │ 38 │
╰───┴─────────────────────────────────────────────────────────┴────────╯
❯ open insertions.json | first 10
╭───┬──────────────────────────────────────────────────┬───────╮
│ # │ name │ count │
├───┼──────────────────────────────────────────────────┼───────┤
│ 0 │ bevy_transform::components::transform::Transform │ 1816 │
│ 1 │ game_shared::game::replicon::Owner │ 1586 │
│ 2 │ game_shared::card::CardRef │ 1418 │
│ 3 │ game_shared::card::components::Damage │ 1378 │
│ 4 │ game_shared::game::grid::Tile │ 1114 │
│ 5 │ game_shared::card::components::Projectile │ 1015 │
│ 6 │ game_shared::card::Target │ 832 │
│ 7 │ game_shared::card::Level │ 764 │
│ 8 │ game_shared::card::components::Hitbox │ 764 │
│ 9 │ game_shared::card::components::Health │ 754 │
╰───┴──────────────────────────────────────────────────┴───────╯
Nice!
We need something like this built-in...
yeah would be cool :) maybe a feature flag that record the count of each event for each component, and then log them regularly for example
I were thinking about an inspectable resource 🤔
Users could manually print info from it or use editor/inspectors to look inside.
We currently have https://docs.rs/bevy_replicon/latest/bevy_replicon/client/struct.ClientReplicationStats.html, but I think it would be much more useful to store it like you do.
oh yeah a resource is much better
and that would be per client so nicer yeah
but if you want to do the total of all the clients it might not be straightfoward
or also having a ServerReplicationStats
Yeah, I think it worth adding a server statistic later too 🤔
Hmmmm, I feel like I've seen this one before 🤔
bevy_replicon-0.34.0/src/client/server_mutate_ticks.rs:161:9:
assertion failed: self.received <= self.messages_count
When I move from my town instance to a dungeon instance it just panics with this message after a while
This is super weird. It means server sent X mutate messages split in parts, but you received at least X + 1 parts 🤔
Could be a bug, will investigate.
@dire aurora could you apply this patch, enable tracing and send us the logs?
https://github.com/projectharmonia/bevy_replicon/pull/511
@dire aurora sorry, but could you send it again with the latest changes in the branch?
No fix yet, just making logging and panicking informative.
bevy_replicon-39b0dac7548b572d/6219ba3/src/client/server_mutate_ticks.rs:170:9:
expected at most 2 messages, but confirmed 1
``` 
Oops, I need to swap the numbers 😅
So for some reason we expect only a single message, but receive 2 for this tick. Could you enable logging just like before?
And what server sends, also 2 messages? 🤔
If there are no "splitting ...", then it's a single message
2025-06-29T14:21:34.515074Z TRACE bevy_replicon::client::server_mutate_ticks: confirming `RepliconTick(450)` which is 63 ticks ago
This sounds very much like a resetting issue, the moment we hit 63 ticks ago it crashes 🤔
Would explain why the longer I stay in the town, the longer it takes for the dungeon to crash
So it crashes when it confirms a tick 63 ticks ago twice? 🤔
I trying to reproduce it locally, but can't
The following test works for me:
let mut ticks = ServerMutateTicks::default();
ticks.confirm(RepliconTick::new(1), 1);
ticks.confirm(RepliconTick::new(u64::BITS + 1), 1);
ticks.confirm(RepliconTick::new(2), 1);
ticks.confirm(RepliconTick::new(u64::BITS * 2 + 1), 1);
ticks.confirm(RepliconTick::new(u64::BITS + 2), 1);
Output:
[TRACE bevy_replicon::client::server_mutate_ticks] confirming `RepliconTick(1)` which is 1 ticks since last
[TRACE bevy_replicon::client::server_mutate_ticks] confirming `RepliconTick(65)` which is 64 ticks since last
[TRACE bevy_replicon::client::server_mutate_ticks] confirming `RepliconTick(2)` which is 63 ticks ago
[TRACE bevy_replicon::client::server_mutate_ticks] confirming `RepliconTick(129)` which is 64 ticks since last
[TRACE bevy_replicon::client::server_mutate_ticks] confirming `RepliconTick(66)` which is 63 ticks ago
Could you provide the rest of the log, so I could try to reproduce it closely?
It's not just 63 ticks ago, the moment I connect it starts with confirming ticks that were over 200 ago 🤔
confirming `RepliconTick(8)` which is 547 ticks ago
```Yea, it's pretty clear this hasn't been reset after disconnecting and reconnecting 😅
Then the moment it comes to a tick that is inside history it'll add a confirm beyond what it should receive and it panics
2025-06-29T14:46:49.469505Z DEBUG bevy_replicon::shared::backend::replicon_client: changing status to `Disconnected`
2025-06-29T14:46:49.485761Z DEBUG bevy_replicon::shared::backend::replicon_client: changing status to `Connecting`
2025-06-29T14:46:49.501657Z DEBUG bevy_replicon::shared::backend::replicon_client: changing status to `Connected`
2025-06-29T14:46:49.501699Z DEBUG bevy_replicon::client: sending `ProtocolHash(17816712812252468640)` to the server
```Looks like it is going trough the disconnect, connecting, connect cycle properly, so the reset should happen, right? 🤔
Ahhh, right, I forgot to clear it on disconnect! Let me put a fix...
@dire aurora could you try again the same branch?
Wait, no 🙂
Should be good now!
hey ! it seems ProtocolHasher output is different on wasm web than on native 😃
exact same version, i greped all the "adding" log in a file, and the only diff between web and native is the hash. all the adding statement are the same, in the same order
edit: reproduction https://github.com/mirsella/bevy_replicon/tree/wasm_protocol_hasher
clone and run wasm-pack test --node while the test pass on native
edit2: its becuase of foldhash, it produce different hash on different platform.
their readme:
You expect foldhash to have a consistent output across versions or platforms, such as for persistent file formats or communication protocols.
foldhash::quality doesn't work either. no_std so no std hasher, we might need to pull in another hashing crate... ?
@spring raptor bit of an in-depth question when you have a few minutes...
I watched your Bevy Meetup #9 talk - really enjoyed it, thanks for putting it together.
I've also read the getting started guide and think I understand how to implement a listen server (basically start a network server and run both the server logic and client logic).
I have a question about implementing a mechanism in my game that achieves the same as Minecraft's "Open to LAN" feature.
I think I need to:
- Start a single player configuration game (no client or server connection, but both client and server game logic running).
- When 'opening to LAN', create a network server and add as a bevy resource, like this...
E.g. let's say I'm using renet:
let server_channels_config = channels.server_configs();
let client_channels_config = channels.client_configs();
let server = RenetServer::new(ConnectionConfig {
server_channels_config,
client_channels_config,
..default()
});
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?;
let server_config = ServerConfig {
current_time,
max_clients: MAX_CLIENTS,
protocol_id: PROTOCOL_ID,
authentication: ServerAuthentication::Unsecure,
public_addresses: Default::default(),
};
let transport = NetcodeServerTransport::new(server_config, socket)?;
commands.insert_resource(server);
commands.insert_resource(transport);
This essentially transitions my 'singleplayer' setup into a 'listen server' setup.
And as long as I've setup my replication component annotations, and configured the run conditions correctly in my bevy systems, then things should just work.
Am I correct in my understanding? Is there anything I'm missing?
Seems fixed now, I go to about 500 ticks in my first instance, then switch to another one and get up to a couple thousand no crashes ...
2025-06-29T18:17:10.885056Z TRACE bevy_replicon::client::server_mutate_ticks: confirming `RepliconTick(3411)` which is 1 ticks since last
2025-06-29T18:17:10.885075Z TRACE bevy_replicon::shared::backend::replicon_client: received 1 message(s) totaling 23 bytes from channel 5
2025-06-29T18:17:10.885085Z DEBUG bevy_replicon::shared::event::server_event: receiving event `bevy_rewind_input::HistoryFor<movement::motion::MoveInput>` with `RepliconTick(524)`
```Bit confused about why this event from thousands of ticks ago keeps being received though 🤔
Nice catch! Looks like we need a different hasher, yes.
Could you suggest any other hasher? 🤔
As a temporary workaround you can disable authorization.
Yes, that's correct! 🙂
Great!
As for the weird first tick - the initial tick is zero. This is why it's a bit weird in the logs. But it works correctly.
Let me think how to adjust it 🤔
Ah, wait, the event is received 🤔
maybe https://github.com/cbreeden/fxhash ? its small and fast
How about https://github.com/rust-lang/rustc-hash ?
It's also fxhash, but better maintained and already in the dependency tree
So it's constantly received?
Yea, and I do feel like that event should be getting received but the tick staying the same seems odd
Do you also get applying event...?
oh perfect then lets use that :)
side question, why is bevy_render/core_pipeline/sprite etc loaded ? (in the dep tree)
Because of the dev-dependencies
Could you double check if it works? 🙂
there is not build script, so the dev deps are only loaded with cargo tests / example ? so when using bevy_replicon as a library in a project, the dev deps are not pulled ? so its the same for library usage ?
but anyway i agree rustc-hash seems really good
ill test !
Didn't see one, but now I rerun it and it works fine 🤔
Correct. cargo tree includes dev-dependencies.
Once merged, I'll draft a patch for this and ticks reset fixes
If the tick doesn't change, it could mean that the backend for some reason considers the same message as received 🤔
Let me know if it happens again.
different hash :(
ill test fnv hash maybe
Good idea!
also different
neither fxhash or wyhash have the same hash on wasm and native
but all of them have the same difference, the wasm hash is a lot smaller than the native hash, maybe on wasm rand or something use u32 instead of u64 values ?
I don't think they use rand at all 🤔
oh right since its a fixed seed
How about rapidhash?
This should be cross-platform
@thorn forum could you compare strings we hash?
Becase we use type_name which might or might not be stable.
ill check
how do you deal with no println! or dbg! because the whole crate is no_std 😭
also wasm-pack test dont show the stdout of the tests so ill find another way of getting the type_name
i tried just panic with the type name given to the hash method and its the same thing, so its seems its not coming from this
tried with only the type_name and it works. its coming from the protocolpart enum
I use log in combination with use test_log::test;
Another option is to just write extern crate std; anywhere and then std::println
Interesting. Maybe its size is platform-dependent? Could you directly hash it on wasm and native to compare?
If yes, does #[repr(u8)] solves the issue?
the hash are different for the same ProtocolPart yes. repr(u8) change the hash of the enum, but sadly native and wasm still compute different hash.
as a last resort we can impl Hash ourself i think ?
I wonder if we need to use repr(C) or is it just hashers aren't cross-platform?
I'd prefer to find a cross-platform one...
It's okay to pull a dependency. We'll move it under a feature in the next major release
repr(C) doesn't work
Tiny Rust library to create deterministic hashes regardless of architecture. This library is no-std compatible and uses no allocations or dependencies.
Looks good. Does it work? 😅
no 😭
wtf manual impl of Hash also doesn't work 🤔
starting to think its the compiler 😂
With both deterministic hash and repr?
no i removed the repr as i thought it wouldn't be used with the manual Hash impl
It depends on the impl
impl Hash for ProtocolPart {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
ProtocolPart::Replicate { priority } => {
0u8.hash(state); // Use a fixed discriminant for this variant
priority.hash(state);
}
ProtocolPart::ReplicateBundle => {
1u8.hash(state); // Fixed discriminant
}
ProtocolPart::ClientEvent => {
2u8.hash(state);
}
ProtocolPart::ClientTrigger => {
3u8.hash(state);
}
ProtocolPart::ServerEvent => {
4u8.hash(state);
}
ProtocolPart::ServerTrigger => {
5u8.hash(state);
}
ProtocolPart::IndependentEvent => {
6u8.hash(state);
}
ProtocolPart::IndependentTrigger => {
7u8.hash(state);
}
}
}
}
Ah, it's the priority which is usize
commented:
// hasher.replicate::<StructA>(1);
// hasher.add_custom(0);
so there shouldn't be any other variable possible and i still get a different hash !!
wtf
No, no, you need to replace usize inside the enum.
even if its not used ?
ok it worked, it was becuase of rustc-hash
tried with rapidhash and got the same hash
And without the usize?
All required? 😅
yes haha
removed my custom Hash impl
maybe another hash algo works tho
but not rustc-hash
fnv works
I think it doesn't use the Hash trait? 🤔
it does i think, we call value.hash(&mut hasher)
value.hash is the Hash impl on the values
Ah, let's use it then 🙂
re tried deterministic_hash+foldhash, foldhash, rustc-hash, they dont work.
we have
rapidhash 40kib
fnv 10kib
so lets go for fnv i guess ?
Yes, + fnv is already in the dependency tree
Ah, is it const-fnv1a-hash?
no just fnv ?
Ah, const-fnv1a-hash is the one that doesn't use Hash and in the dependency tree.
Let's use fnv, sure!
oh ok !
its only in the dev dependencies tree, any user pulling bevy_replicon as a library will not have it anyway i think.
No, no, the hashing of the protocol done inside the library, it will be pulled.
Thanks for testing!
I'll setup CI and apply the fix
heyo, has anyone successfully used bevy_replicon with bevy_rapier2d? i'm getting segmentation faults inside bevy_rapier2d when I try
specifically, i get a segmentation fault on the client only, in the drop implementation of Collider, after it's called in a pile of unsafe functions by bevy_replicon, which makes me think the issue is in replicon somewhere
You can network Collider? That sounds a bit sketchy considering it's an Arc for a trait 🤔
yeah it occurred to me much later that networking it doesn’t make any sense :p
it implements Serialize ¯_(ツ)_/¯
perfect thanks ! the CI test is cool its better than what i did with wasm-pack test.
just found out i can do PR review on the repo haha
This is weird...
Yes, looks like it's an Arc, but it somehow implements the ser/de traits. The only thing I can think of that might cause this is buffered component insertions. But I tested it with a droppable type 🤔
I'll double check this evening.
Yeah, I'd suggest to use the required components or the blueprint pattern from the quickstart guide to automatically insert it. You will save a lot of traffic this way.
Thanks! It's based on the code and investigations you shared.
I forgot to add you as a commit co-author - done now.
I looked into it more- what I suspect is happening based on coredumps is somehow the Arc is getting directly copied without the data it points to being copied. Then, something else is causing a panic, and the Drop implementation tries to drop the data that the Arc would normally contain which is somehow not there and that triggers the segfault. I’m really not sure, it’s super strange. Anyways, thanks for the help!
Will investigate
Assigning with *ptr.deref_mut() = component is incorrect: it drops the uninitialized memory at ptr, causing UB. This can be reproduced with Box, Arc, etc.
Fixed it by switching to ptr::write.
@dire aurora since I borrowed your approach, you might also need a similar fix 🙂
Here you cast uninitialized memory into &mut C, but writing into it like this *ptr = component will trigger drop for the old uninitialized ptr value which is UB.
Awesome! Grazie
Oh, I guess you wrote a sane write function
@dire aurora @thorn forum the patch-release is up, please run cargo update -p bevy_replicon 🙂
@dire aurora I added your crate to the ecosystem crates list. I remember you wanted to do this. Let me know if the description works for you. Feel free to suggest changes directly in the browser.
https://github.com/projectharmonia/bevy_replicon/pull/516
LGTM
How is the best way to sync the server tick with the client?
I saw that entities have their last update tick from the server, but I could not find a way to access a client in a system.
Here is how you can get information about ticks: https://docs.rs/bevy_replicon/latest/bevy_replicon/#ticks-information
What are you trying to do?
A server-authoritative replication crate for Bevy.
I try to sync timed events ideally in a deterministic way.
So there is a countdown time on the server and I would like to align that countdown that when the client has the countdown run out it’s as close as possible to the server countdown time.
On the server you have RepliconTick and on the client you can look at ServerUpdateTick and ServerMutateTicks (use the greater from the two).
Should I tick the client by myself to keep track of the current tick? Or is there already a client tick which increments every replicon update?
The mentioned ticks will be automatically incremented, you can use them.
Ah even if there is no change to the entity (no update from the server?)
I think what I don’t fully grasp is what “tick” revers to. For example here:
ServerUpdateTick is greater than the tick
Which tick would I compare it to on the client?
Ah, for this you need to call track_mutate_messages() on the app
API documentation for the Rust TrackAppExt trait in crate bevy_replicon.
The RepliconTick resource on the server.
And on the client what coudl I compare it to?
To ServerUpdateTick and ServerMutateTicks. You need to take a bigger value from these two, that's the tick the client received.
I might be completely interpreting this wrong but I have the following scenario:
server starts countdown at server tick 10. It sets a parameter on the countdown timer
pub struct CountDown{
started_at: Tick(10),
expires_at: Tick(30),
}
Now a client joins the game, how can the client display how long the countdown still lasts (in ticks or time), what I don't really understand how the client can know at what tick it currently should be.
What is Tick?
Yeah, not happy about the PLACEHOLDER either. Maybe wrap the entity into enum? 🤔
It would be the server RepliconTick a u32
I'd expect something like this:
pub struct CountDown{
started_at: RepliconTick(10),
expires_at: RepliconTick(30),
}
Then on the client you get ServerUpdateTick and compare it with ServerMutateTicks::last_tick. That's your current client tick.
And don't forget to enable track_mutate_messages to receive messages even if there are no mutations.
API documentation for the Rust TrackAppExt trait in crate bevy_replicon.
It's two resources instead of one because we need granular control for prediction crates.
And as a side note, very pleasant api surface, and straight forward to implement networking with replicon, especially if the game aligns well with the out of the box features.
Thank you!
Glad you enjoy it 🙂
I have been thinking about an enum like this:
enum Sender {
Server,
Client(Entity),
}
This way you can store this enum instead of the entity. When resources become entities, we will be able to write it like this:
enum Sender {
Server(Entity),
Client(Entity),
}
Ok getting closer, I could run the examples with matchbox. Still far from a publishable crate, but at it seems to be feasable:
Awesome!
One thing which is confusing to me is why the RepliconChannels have client and server parts.
int the tcp example we simply add the channel_id to the package (which I did up to now and that works), but ideally I would like to directly setup the different channels with the guarantees, but matchbox expects only one socket.
What is the difference between the server and client channels?
Ok I think I figured it out 🤔
Client and server can have different sending and receiving channels. This is why we have this separation. For example, server sends replication using 2 channels, client receives them, but sends back acks using only a single channel.
Matchbox also supports channels:
https://docs.rs/matchbox_socket/latest/matchbox_socket/struct.WebRtcSocketBuilder.html#method.add_channel
Builder for WebRtcSockets.
So you can have a single socket with multiple channels.
Yeah that I have setup now, what i'm not sfully sure is how I should set this up.
Is the server and client set the same but may have additional channels?
can I assume if the replicon_channels.server[0] == replicion_channels.client[0]? (meaning they represent the same channels)
All backends usually differentiate between send and receive channels. Let’s imagine we create a server event — it results in a single server channel. RepliconChannels::client won’t contain it.
And when you create a server, you use server channels for sending and client channels for receiving. Here is an example from Aeronet. Looks like Matchbox doesn’t differentiate between sending and receiving, and a single channel can be used for both?
If so, just consider server and client channels as a single array.
I think I have everything working now. Need to do more testing...
Great!
To test it better you can port tests from the example backend. Just copy-paste and only adjust the setup.
That's what I did for bevy_replicon_renet: https://github.com/projectharmonia/bevy_replicon_renet/blob/master/tests/netcode.rs
I manage to get most of the tests to pass, but events aren't beeing sent.
Is there anything particular that needs to be implemented for events to work?
Triggers/Observers work as espected, but somehow events are never sent over the network (the client never receives the event)
Interesting because remote triggers are remote events under the hood 😅
Its very strange I copy pasted the example from the docs for the events, but none of them are received...
So I suspect that the problem might be in the ported tests for events
But also in the simple box example if I added the events from the documentations, they never reach the client 🤔
A server-authoritative replication crate for Bevy.
Let's debug. Add test-log as a dev-dependency to your code. Then add use test_log::test; to your integration test imports.
And then run the test like this RUST_LOG=bevy_replicon=trace cargo run --test <integration test name>. And send the logs here.
@swift hamlet ^
RUST_LOG is the environment variable. If you use Windows, take a look how to properly set it for a single command run, the example above is for Linux.
Will be back in the office shortly and will test!
[TRACE bevy_replicon::server::server_world] marking `ArchetypeId(4)` as replicated
[DEBUG bevy_replicon::shared::event::server_event] sending event `netcode::TestEvent` with `Broadcast`
[TRACE bevy_replicon::shared::backend::replicon_server] sending 0 bytes over channel 5
[DEBUG bevy_replicon::shared::event::server_event] sending event `bevy_replicon::shared::event::server_trigger::ServerTriggerEvent<bevy_replicon_matchbox_backend::server::OnHostDefinitionTrigger>` with `Direct(7v1#4294967303)`
[TRACE bevy_replicon::shared::backend::replicon_server] sending 18 bytes over channel 3
[DEBUG bevy_replicon::shared::event::server_event] resending event `netcode::TestEvent` locally
so it seems the event is sent, but
let events = client_app.world().resource::<Events<TestEvent>>();
is always 0
and there is never a "receive" from the client.
here is the complete log, I log the number of events after multiple updates on the client side, but they stay at 0.
Can you change your event to make it non-zero sized?
Just add an integer or something like that.
If it works, then something is wrong with how you handle zero-sized messages.
Because triggers aren't zero-sized, they include the entity.
Result is the same
I see that the event is sent, but never received. You should see logs about receiving a message.
Technically all is sent the same way, the strangest part is that triggers work
I'd suggest to now enable logs for you backend and see what messages you received.
And sent
will double check the channels, maybe something is off with how I handle channels
From the logs I don't see any "received"
It looks like you don't receive any messages at all
found the coulprit... added something to test the disconnect... and forgot to remove it 🤦♂️
It's okay, we all humans 😅
So it works now? Or did it fixed receiving for everything, except events?
Still validating, but I have at least 1 event received on the client side, now need to validate the correct order of disconnect elements (like sening all events before actually disconnecting etc.)
Ah, yeah, that usually requires careful system ordering.
@swift hamlet here is how I fixed it for the example backend: https://github.com/projectharmonia/bevy_replicon/pull/494
So the idea is to enter the disconnected state in PostUpdate, allowing the client to process the received messages.
Ok its was a combination of both, it seems I don't send events that are 0 sized
That's what I suspected 😅 Is this a matchbox issue or you just need to properly handle zero-sized messages?
appears to be a matchbox issue, 0 sized messages seem to simply be dropped.
I remember renet had a similar issue: https://github.com/lucaspoffo/renet/issues/60
I'd suggest to open an issue on the matchbox repo and advice to add integers to events 😅
Or manually add a special byte at the beginning of the each message.
will do, initially I constructed my messages all on simply one channel, so adding the channel number prevented any 0 size messages to ever be sent.
I was thinking to simply add 1 byte to each message, which I drop at the other side... but not ideal
Yes, it's definitely a bug
I'd open an issue and add a byte as a workaround with a comment in the code.
Ok added and strip a 1 byte marker, and now events work properly.
Now the replication on the other hand seems to not work.
(well it work in my test project, but it fails in the test)
assert_eq!(
replicated.iter(client_app.world()).len(),
1,
"last replication should be received"
);
this test fails 🤔
So the assertion last event should be received above passes, but not replication?
indeed, the event is there, also if I send multiple they all show up, but the replication is at 0
Try to enable logging and compare both tests
I.e. with the example backend test and yours
Ah its the AuthorizedClient component which was missing. Up to now, I allowed the application to handle insertion of AuthorizedClient on the server.
Is it expected that the backend implementation automatically adds this component on successfull connection?
No, it's added by the replicon itself. Or by user if manual authorization is enabled in settings
That seems to not be triggered for me. If I add it manually on the ConnectedClient entity the protocol works.
Even if I set:
.set(RepliconSharedPlugin {
auth_method: AuthMethod::ProtocolCheck
}),
It seems to not be added
(I'm using replicon 0.34.x in case this has changed.)
The AuthorizedClient inserted on ProtocolHash event sent by the client.
Enable logging, run any test and send the logs here
It's purpose to verify protocol compatibility. This helps to avoid silly errors like having events registered differently on client and server.
I don't receive anything from matchbox directly this time.
this is the test I currently run to isolate the issue
Sorry, I meant logs with trace level
Yes, let's see the logs from it
I'm not seeing any received message on the client or the server.
I see client sending the message. But the server never receives it.
Not in this test
no in this test I specifically only do replication
Yes, but I don't even see the received replication.
This is the log if I manually insert the AuthorizedClient
could it be a timing issue?
without calling drain_sent() the messages stay queued up?
Looking into the original test. Looks like the received logging message is not printed for some reason. Will investigate.
So you likely receive messages, just not the authorization.
Could be, yes. The authorization happens immediately after connect. By any chance you mark the client as connected too early?
That could be, so as matchbox doesn't have a concept of "host/server" and client, I have to inform a newly connected client which of the peers is the host.
So until the client receives that event, it doesn't call drain_sent, I expected that the messages would be queing up until I call drain_sent but if that is not the case I likely skip that early message (as its not clear which peer the host is)
They are queued up until you drain them.
Do you connect everyone to everyone or can you connected peers only to the server?
Everyone is connected to everyone, but I send only the messages between the peer identified as host and the clients.
Probably a requirement from matchbox or you just want this behavior?
Its how it is implemented by default if you want to use their matchbox server etc.
(which I assume most want to do)
Got it!
Double check the timings. Maybe you discard this message on the server because it was sent too early?
checking currently, the client does send it properly it seems, so now validating the server
ok no it is the client
now when I queue up the message if it is sent too early, it seems to work.
Which is strange as nothing else drains the client events 🤔
Events are drained when you change the status.
But the event is sent only when you set the status to connected
Double checked, the original example logs sending/receiving properly.
Yes I set connected too early, will try to set it only once the host has been identified
Here is how it should look like:
[DEBUG bevy_replicon::shared::event::client_event] sending event `bevy_replicon::shared::event::client_trigger::ClientTriggerEvent<bevy_replicon::shared::protocol::ProtocolHash>`
[TRACE bevy_replicon::shared::backend::replicon_client] sending 10 bytes over channel 1
[TRACE bevy_replicon::shared::backend::replicon_server] received 1 message(s) totaling 10 bytes from channel 1
[DEBUG bevy_replicon::shared::event::client_event] applying event `bevy_replicon::shared::event::client_trigger::ClientTriggerEvent<bevy_replicon::shared::protocol::ProtocolHash>` from client `5v1`
[DEBUG bevy_replicon::shared::event::client_trigger] triggering `bevy_replicon::shared::event::client_event::FromClient<bevy_replicon::shared::protocol::ProtocolHash>` from `5v1`
[DEBUG bevy_replicon::server] marking client `5v1` as authorized
[TRACE bevy_replicon::server] incremented ResMut(ServerTick(RepliconTick(2)))
[TRACE bevy_replicon::shared::backend::replicon_client] trying to receive from 0
[TRACE bevy_replicon::shared::backend::replicon_client] trying to receive from 2
[TRACE bevy_replicon::server] incremented ResMut(ServerTick(RepliconTick(3)))
[TRACE bevy_replicon::server::server_world] marking `ArchetypeId(5)` as replicated
[TRACE bevy_replicon::server] writing empty `6v1` for client `5v1`
[TRACE bevy_replicon::shared::backend::replicon_server] sending 4 bytes over channel 0
[TRACE bevy_replicon::shared::backend::replicon_client] trying to receive from 0
[TRACE bevy_replicon::shared::backend::replicon_client] received 1 message(s) totaling 4 bytes from channel 0
[TRACE bevy_replicon::client] applying update message with `UpdateMessageFlags(CHANGES)` for RepliconTick(3)
[TRACE bevy_replicon::shared::backend::replicon_client] trying to receive from 2
sending ... and then received ...
The problem is that I use a simple replicon event for setting the host, but that only is sent when I set the connection as connected.
Will have to find a better flow and probably send the "which peer is the host" event not through replicon, but rather direclty through matchbox
Maybe you can avoid connecting clients together? If you connect only to a single client which is the host, you will know which client is the host 🤔
You can send independent events even when clients aren't authorized.
but they are only received when the client sets the connected to true on the RepliconClient correct?
Yes!
So I'd expect you to set the connected first on actual connect and then identify the host.
So I have two choices, either I set the connected to true early, queue up the protocol hash message, and send it when I received which one is the host.
Or I add an additional channel which handles the host communication through matchbox, and then only set the connected to true once the host has been identified.
Correct!
I’d probably handle the communication through Matchbox, since it sounds like something that’s part of the connection.
But are you sure it's not possible to use matchbox as a true client-server? I remember seeing a client-server example in the repo 🤔
from what I could find there are 3 samples in the bevy matchbox repo:
- client only (requires separate signaling server)
- host (client + signaling)
- Signaling in bevy
So although from the samples, you could identify who the host is by running directly the signaling server inside the bevy application, I argue that would defeat the main benefit of matchbox, as the signaling server needs to be accessible from anywhere.
I managed now to have all tests pass, some I modified a slight bit, (primarily the server stop.
I have one strange issue, that I need to run the updates more than once after the event is send in the world, or a replicated component is spawned.
Bu otherwise it seems the tests all pass.
https://crates.io/crates/bevy_replicon_matchbox
published it to crates. I would appreciate anyone else testing and report any issues if they arise.
Awesome! Maybe it's because of the initialization? I.e. you need to receive the information from matchbox who is the host.
That could be, all tests pass and every example project I tried worked. So I thought it’s probably good enough for other to experiment with.
Especially for the next jam this would allow for some simple multiplayer games
Or it could be caused by system ordering. Not a big deal, but I would investigate 🤔
Congrats with the release! Could you add it to the bevy_replicon README? And maybe to the matchbox repo README too?
As far as I could find out the messages are received on matchbox layer, but have not yet passed to replicon until another update cycle.
So a simple pull request with an updated readme pointing to the crate? Or what did you have in mind?
Then it's likely a system ordering...
So a simple pull request with an updated readme pointing to the crate?
Yep 🙂
I thought matchbox was p2p, how does it work with a more client-server crate like replicon
Yeah, It's a bit confusing, while they mention P2P in their readme, the signaling server actually supports both topologies. At least from a quick look at the repo, I never used matchbox myself 🙂
Here is the example: https://github.com/johanhelsing/matchbox/blob/main/matchbox_signaling/examples/client_server.rs
And the code: https://github.com/johanhelsing/matchbox/blob/main/matchbox_signaling/src/topologies/mod.rs
Oh cool
I don't really get how pure p2p topologies are even supposed to work. Does one client still act as the lead?
I think it depends on the replication approach:
- With deterministic replication all clients just exchange the inputs.
- With state replication, you need a consensus model.
I think with mesh topology there are no lead, otherwise it would be client server with extra connections 🤔
But it's all quite niche. For games, client-server is usually the better option: lower bandwidth usage, no NAT/firewall issues, and no complex consensus models.
I remember in the original Bevy Replication RFC, Joy even suggested supporting only a client-server topology.
There are scenarios where a pure p2p could be interesting, but in most cases a client server architecture is often easier to reason about and often better for performance.
Even factorio moved from p2p to a server/client one and had a lot improved performance and less bugs
@swift hamlet looking into the code, you probably want to update the comments 🙂
https://github.com/occuros/bevy_replicon_matchbox/blob/6f9c4da9bb9beaf526b38652bbca5daea96ceef8/src/lib.rs#L1
https://github.com/occuros/bevy_replicon_matchbox/blob/6f9c4da9bb9beaf526b38652bbca5daea96ceef8/src/server.rs#L13
Also looks like you forgot to use msg from the loop here and instead using the hardcoded value:
https://github.com/occuros/bevy_replicon_matchbox/blob/6f9c4da9bb9beaf526b38652bbca5daea96ceef8/src/lib.rs#L163
I'd suggest to setup CI. You can copy my workflows from the renet repo:
https://github.com/projectharmonia/bevy_replicon_renet/tree/master/.github
But if you copy dependabot.yaml you will need either setup labels with the same name or remove labels from the file.
Ah yes indeed that’s from a leftover of the tcp sample.
Will take a look at setting up some CI.
How was your experience with porting? Which parts was unclear? I guess channels? 😅
Yeah the channels confused me a lot at the start, Especially server/client etc. and how to map them to the socket channels.
Now what took the longest to get right is the timing of when what should happen (for the tests).
Overall it was both harder and easier then I expected 😅 .
I think the reason why I currently need an additional update for the tests is the timing of when the sockets sends the messages.
I currently don't see an easy way to work around that, as they are not on a bevy schedule (as far as I can tell).
Got it, thanks! Will try to clarify the channels in the docs 🙂
In renet its updated in systems, so I just ordered them:
https://github.com/projectharmonia/bevy_replicon_renet/blob/1eea6d88196fdb1dd4a0e40cdc604ca893cbb11e/src/client.rs#L23
its not the part when I push the messages to the socket, its when the socket sends/receives the packages (I think at least at the moment)
I might simply not understand enough about how sockets work, but it seems when send is used it is put on a queue which is executed on a later time point. And I don't know how I would make sure that my systems run after these socket channels received the data.
I saw a blog post from Glenn fiddler where he mentions that he would never recommend true P2P. He would always add a relay server in between
Looks like they use Bevy's Task 🤔 So yeah, there are no ordering guarantee, so don't worry.
thank you for confirming, and the CI suggestion, it found quiet a few things 😅
You mean signaling server like with Matchbox? 🤔
Looks like your formatting fails. It's because of taplo that formats TOML files.
And you might want to remove the codecov job. Unless you want to register on their website, start to track coverage and add a nice badge 🙂
Yep I saw, still need to cleanup the .toml for strings, and for some reason one test fails in ci but never on my machine
ah its the disconnection of the client, as there is no guarantee when (in terms of update sicles) the server detects that the peer has disconnected...
Just add a bunch of updates 🙂
Improved the channels docs as promised:
https://github.com/projectharmonia/bevy_replicon/pull/519
All tests pass now, added a more defined disconnect behaviour for clients (which will inform the server directly of the disconnect instead of waiting for the matchbox update).
Awesome!
One nitpick: you might want to rename netcode.rs into backend.rs 🙂
I used netcode because renet has multiple inner backends.
@swift hamlet one last tip, in replicon I also have a job to test compilation on wasi, might be useful for your crate too:
https://github.com/projectharmonia/bevy_replicon/blob/bb24013bb13f12598e506f8e68ee1011fe0e11ee/.github/workflows/main.yml#L123
Actually, I have another one 😅
Looks like you don't collect the coverage. So you don't need to run tarpaulin, so you can replace all these steps with a single cargo test.
Might have been slightly tired when updating the readme 😅
It's okay 🙂
And feel free to open a PR to add your crate to the list of messaging backends. I could do it myself, but I prefer authors to write description for their crates. Matchbox repo also have a showcase section, I'd recommend adding it there too.
Just to let people know about your crate. Announcement in #crates also helps.
@spring raptor happen to have any idea as to changes since 0.14 that could cause both my visibility to sometimes break and spawns of entities to occasionally get duplicated? 🤔
Considering it only seems to affect enemy entities, it's probably something silly in my code, but I have no clue what would've changed to make such issues possible 🤣
Hm... Maybe some component is not mapped properly?
Hmmmm, might just be despawning behavior being broken there. If I leave a room, then exit it again, enemies sometimes duplicate ... And I guess the initial visibility thing might be visibility not always getting set correctly the first time around depending on system ordering 🤔
Are you using the latest patch release and not master? The master version has some breaking changes.
Latest patch ... But I think this might actually be caused by bevy_rewind_entity_management ... Since that's where I modify despawn behavior, and it might not be compatible with hierarchies somehow 🤔
Let me know if you want me to add logging to the visibility system to help you debug the issue.
I noticed that we don't log visibility changes
The visibility changes are easy enough to log in my own code luckily
Okay turns out that if you actually want entities to disappear ... You should actually despawn them on the client 
My code just disabled them the first time around, then during rollback re-enable them, and then never re-disable or despawn them
Ah, makes sense 🙂
Does replicon ever reuse previously spawned client entities when visibility is involved?
It just despawns an entity if it's hidden and spawns again when it becomes visibile.
So I don't have to worry about the case where I disable an entity that becomes invisible, mark it to be despawned, and it then gets reused?
Yes, we just trigger the defined despawn function, which you can overwrite and do whatever you want. Once the entity becomes visible, it will be spawned again with a different Id on the client.
#[derive(serde::Deserialize, Event, serde::Serialize, Clone)]
pub struct TransformFromClient { pub entity: Entity, pub transf: Transform, pub time: SystemTime }
#[derive(serde::Deserialize, Event, serde::Serialize, Clone)]
pub struct TransformFromServer { pub entity: Entity, pub trans: Transform, pub time: SystemTime }
#[derive(Component, Clone, Copy, Deref)]
pub struct MpAuthority(pub Entity);
pub fn handle_movement(
mut commands: Commands,
time: Res<Time>,
mut query: Query<(Entity, &InputMoveDirection, &mut Transform), With<Being>>,//ignore this query not being correct (it is not suitable yet for multiplayer I have to fix it)
) {
for (ent, input_move_direction, mut transform) in query.iter_mut() {
let speed = 1000.0;
let delta = time.delta_secs();
let movement = input_move_direction.0 * speed * delta;
transform.translation += movement;
commands.client_trigger(
TransformFromClient {
entity: ent,
transf: transform.clone(),
time: std::time::SystemTime::now(),
}
);
}
}
pub fn receive_transf_from_client(
trigger: Trigger<FromClient<TransformFromClient>>,
mut commands: Commands,
mut query: Query<(&MpAuthority, &mut Transform,)>,
) {
let (mp_auth, mut transf) = query.get_mut(trigger.entity).unwrap();
if mp_auth.0 == trigger.client_entity {
if transf.translation != trigger.transf.translation || transf.rotation != trigger.transf.rotation || transf.scale != trigger.transf.scale{
commands.entity(trigger.entity).insert(trigger.transf.clone());
commands.server_trigger(
ToClients { mode: SendMode::BroadcastExcept(trigger.client_entity), event: TransformFromServer::from(trigger.event().event.clone()) },
);
}
}
}
uh is this an acceptable way to sync characters' transforms for a realtime top-down 2D multiplayer game? (without complicating things too much for now and reducing latency).
Sorry if this has been asked before but I can't ctrl-F within this specific thread.
To summarize: Having the client locally compute his new transform, then sending it to the server through a Trigger, and then the server broadcasts it to the rest of the clients via another Trigger
(all this unreliably and unordered)
Seems I can't get the wasi tests to run properly. For the tests I need to run a signaling server, and that is not wasm compatible.
Ah, makes sense. Not applicable to your crate then 🙂
Depends on the game 🙂 But sending positions directly usually not a good idea.
Could you describe the gameplay a bit?
I am making a Rimworld-like co-op game but with the capability of taking over and manually controlling pawns with WASD keys + mouse (to aim ranged weapons or spells for example). Players aren't strongly binded to a specific pawn/character and are able to take control of another if no other player is controlling it. But, the combat would be similar to Necesse (fast-paced, realtime, being able to accurately dodge projectiles is necessary) so low latency is ideal.
Ah, sorry, I should have read this earlier.
No, for a fast-paced game you need proper rollback/prediction.
Here are the very basics:
- Clients broadcast inputs to the server (not transform!).
- Server simulates the logic and sends the state back.
- But clients don't wait for the response and continue to simulate with the local input (predict the result, basically). So the server is behind in time.
- Clients receive the state and roll back their state in time to the server timestamp.
- Then they snap to the received state from the server and re-play all the inputs up until the current time.
So basically, the client predicts the result, the server corrects it, and the client re-applies inputs since the last correction.
But there's a whole lot more to it.
For example, you don't want to predict everything. Imagine you predict death, and on misprediction, enemies wake from the dead 😅 So some things, like damage or deaths, usually wait for confirmation from the server and are just hidden behind visual effects.
You also may want to interpolate between the corrected state and yours for things like transforms to avoid jittery movement.
There are also 2 rollback strategies: rollback the entire world or specific entities individually. Depends on whether your game is physics-based or not.
There's also deterministic replication - it's where you only exchange inputs. But for your game (and most games), you want authoritative replication. That's where you replicate the state from the server, like I described above.
Can't describe everything in a discord message, but I can give some learning materials if you want.
So, for Replicon you need a solution on top of it.
I expose the necessary API for it.
It's not built-in because, as I mentioned, there are several strategies, and for my game I don't even need prediction/rollback. So I decided to make the crate modular to avoid maintaining things I don't need. Plus, this way more people can get involved.
It worked really well for messaging backends, but for prediction/rollback - not so much 😅 Probably because it's much harder to implement than messaging.
Currently, the only crate that utilizes the mentioned API is bevy_rewind. The developer is very knowledgeable, but the crate is not finished yet because she is busy with life stuff.
It's possible to try to integrate other crates. For example, there is Naia, which is engine-agnostic. And Lightyear recently refactored the prediction and rollback logic into separate crates, so it should be possible to integrate with those.
And implementing it manually is also an option. But as I mentioned, it's not an easy task 🙂
A server-authoritative replication crate for Bevy.
Thank you very much for the very thorough answer 😀 , the learning materials would be useful if you have them. Doesn't seem easy to implement, as you said, but I will later see what solution I end up using. I will probably try the bevy_rewind crate first
Yeah, definitely try it out! Pretty sure @dire aurora will appreciate your feedback. It was a part of her game that was extracted into crates. Right now, the current focus is on porting the game between Bevy versions and to these crates, which takes time 🙂
As for learning materials, here are my favorites that are must to read/watch:
- https://www.youtube.com/watch?v=W3aieHjyNvw — best GDC talk about networking. Surprised that it's unlisted. Luckily, it's so popular that we still have a direct link working.
- https://www.gabrielgambetta.com/client-server-game-architecture.html — good article that explains prediction/interpolation in detail, even including lag compensation.
And for more materials, just see this repo.
yeah it helped me a lot to make my roolback stuff
Realized we don't include descriptions for messaging backends - only optional notes if the author wants. So I went ahead and added bevy_replicon_matchbox to the README myself:
https://github.com/projectharmonia/bevy_replicon/pull/521
@swift hamlet fine with you?
Great thank you!
@spring raptor trying to port some code away from bundles, and it very much feels like I just wanna define rules as follows:
- Send X and Y if we have both (we already have this)
- Send X in a special way, and also Y (we also have this
- Send X in this way if the entity has Y
- Don't send X if the entity has Y
- Send X and Y if it has both and Z
If we had filters, and ways to exclude components from replication under certain conditions, I think bevy_bundlication and its deref macro would become unnecessary
Makes sense. We have this issue that should address this.
Would be cool to just allow Bevy's With and Without.
I will prioritize this feature. Just need to make some final touches for BEI and I'll start working on it.
Yea, the only thing that might not work with just that is excluding, but that might be somewhat doable if we already have filters working (+ I might be able to work around it, even if it's a bit ugly)
Good point about exclusion. I will think about the API more when I get to it. Hopefully soon 🙂
Trying to fix some tests in bevy_rewind and I can't seem to figure out how to construct a DeferredEntity, should I take a different type on my functions so tests can use EntityMut still or am I missing something? 🤔
trait MutableEntity {
fn get_mut<C: Component<Mutability = Mutable>>(&mut self) -> Option<Mut<'_, C>>;
fn contains<C: Component>(&self) -> bool;
fn id(&self) -> Entity;
}
```Fixed it with this really dumb trait for now
I think it's a nice solution.
Alternatively, I can release a patch with DefferedEntity::new and DeferredChanges (reusable buffer that holds data, needed for construction) exposed. No sure if it will be more convenient then EntityMut though.
I use it like this btw, just repeated blocks of this (sometimes 2 writes)
let mut entity_mut = EntityMut::from(world.entity_mut(e1));
write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(0), A(1), frames);
```Maybe we could have an API that just gives you a DeferredEntity within a scope from a `World` method 🤔
With the scoped version you would discard the buffer every time. I know it's for tests, but still...
Maybe just make things public? Opened: https://github.com/projectharmonia/bevy_replicon/pull/522
This will remove the trait requirement for you. But you will have to construct the buffer once somewhere. It's cleared on apply automatically.
Is entity.flush() whats used to apply the deferrd changes?
Ah, yes, I need to export it too 🙂
Done. Drafting a new release?
Continuing from #networking message: @vocal violet If you starting a new project, all others backends are also nice.
I probably should update the README to put the regular renet down on the list 😢
Ah, yeah, then renet2 would be the best choice for you.
When people see "Maintained by the authors of this crate.", they might assume it's the best option, but it wasn't updated for 7 month 😢
The renet itself.
@dire aurora I need your confirmation on this. Did you find it useful, or did you decide to stick with EntityMut and the generic?
You won't have an entity for a server. We need resources as entities.
You can, but not for the server.
You can create your own server entity if you want.
Yeah, server can be a client too.
But it doesn't have its own entity, like clients do. Clients represented by entities with ConnectedClient component. But server is a resource for now.
Why not? Not sure If I get the question
If you want to use entities with ConnectedClient component to store your own data and need to support "server as a client", you can create your own server entity and check if it's a placeholder. Not Ideal, I know. I planning to make it strongly-typed in the future.
We just need resources as entities.
Another option is to use your own entities to represent players. You probably did it before since we didn't always have clients as entities.
Do you need to access client data on every message? 🤔
Are you sure we are talking about the same thing? It's not entities for which you trigger events. It's actual clients stored on the server in form of entities.
Ah, got it. Then a resource with client data would also work.
We also have NetworkId
A unique and persistent client ID provided by a messaging backend.
Ah, I'll see if I can test this tonight or tomorrow
For server you don't have an ID because it doesn't have a connection, you could just store None or some special hardcoded value.
And we don't have a server entity because the server is not an entity. You can create an entity for it yourself, however. There is just nothing I could do, we need resources as entities. The work is started on it, but we won't get it in 0.17, sadly.
-let mut entity_mut = EntityMut::from(world.entity_mut(e1));
-write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(0), A(1), frames);
+world.entity_scope(e1, |e| {
+ write_history_internal::<A>(comp_a, e, r_tick(0), A(1), frames);
+});
```Seems to work, just had to change a lot of lines like so
Yes, I really made a wrapper for that API to keep my tests clean
- // Write A(2) to e1 for tick 1
- let mut entity_mut = EntityMut::from(world.entity_mut(e1));
- write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(1), A(2), frames);
+ world.entity_scope(e1, |e| {
+ // Write A(2) for tick 1
+ write_history_internal::<A>(comp_a, e, r_tick(1), A(2), frames);
- // Write A(4) and A(1) to e1 for tick 3 and 0 respectively
- write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(3), A(4), frames);
- write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(0), A(1), frames);
+ // Write A(4) and A(1) for tick 3 and 0 respectively
+ write_history_internal::<A>(comp_a, e, r_tick(3), A(4), frames);
+ write_history_internal::<A>(comp_a, e, r_tick(0), A(1), frames);
- // Write A(3) to e1 for tick 2
- write_history_internal::<A>(comp_a, &mut entity_mut, r_tick(2), A(3), frames);
+ // Write A(3) for tick 2
+ write_history_internal::<A>(comp_a, e, r_tick(2), A(3), frames);
+ });
```Actually might prefer how it looks for multiple writes in a single test
Yeah, the DeferredEntity is a bit low-level 🤔
Do you want me to provide such API or do you think it makes sense to keep it as a helper for your crate?
If yes, you can just share the snippet.
Works fine as a helper in my crate, this helper isn't super efficient after all, so others might prefer a more efficient impl that isn't as clean
Okay! Drafting a patch release with more pubs for DeferredEntity?
Patch release works, keeping my [patch.crates-io] for a while longer also works since it's only bevy_rewind's tests after all
The patch is up 🙂
It’s just a single console command and a few lines in the changelog.
Hi there i am using a mapped server trigger:
//declaration
#[derive(Event, Deserialize, Serialize, MapEntities)]
pub struct NegateDamageTrigger {
pub attack_id: u64,
pub owner: Entity,
pub victim: Entity,
pub island: u64,
pub offset: IVec3,
pub damage: u64,
}
//in the plugin
.add_mapped_server_trigger::<NegateDamageTrigger>(Channel::Unordered)
//later in the code
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
event: NegateDamageTrigger {
attack_id: negate_instance.0,
owner: damage_trigger.owner, //this is an entity, should be mapped
victim: victim, //this is also an entity, should be mapped
island: damage_trigger.island,
offset: damage_trigger.offset,
damage: damage_trigger.damage,
}},
);```
But on both the server and the client i get the same entity outputs:
```server: Mapped entities, victim: 44v2#8589934636, owner 3371v1#4294970667```
```client: Mapped entities, victim: 44v2#8589934636, owner 3371v1#4294970667```
I am most likely missing something, but I can't figure out what
You need to annotate your entities inside the event with #[entities].
See https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#method.map_entities
A data type that can be used to store data for an entity.
Thanks! Will try it tomorrow
Opened a quick PR to improve the docs for it:
https://github.com/projectharmonia/bevy_replicon/pull/524
yea I think some of it comes from me not having experience with some bevy concepts, that replicon is built on, the new comments you added seem perfect!
Made a small post about my project, which mentions this crate too 🙂
#1264625779296174110 message
Starting working on replication filters now.
Made our custom entity packing compatible with serde attributes, so users can now do the following:
#[derive(Serialize, Deserialize)]
struct Enemy {
#[serde(with = "bevy_replicon::compact_entity")]
entity: Entity
}
I'm unsure about the expected behavior of exclusion.
Consider these two rules:
- Replicate both X and Y.
- Exclude X if the entity has Z.
If an entity has components X, Y, and Z, should we replicate only Y or skip the entity entirely?
With only inclusion filters, the behavior is more explicit:
- To skip this rule, add
Without<Z>to rule 1. - To send only Y, create a higher-priority rule with
With<Z>.
Do you think this would require too much typing? How did you handle this with bundlication?
With bundlication I register things like position and linear velocity absolutely everywhere in random bundles to avoid replicating it where unnecessary
I would expect this to replicate only Y if an entity has X, Y, Z. Excluding would be a bit like setting a high priority rule to just never send that thing
(Perhaps we could implement it literally like that too, so it's not a seperate concept and follows all the same rules)
You mean emulate it with inclusion filters?
Yes, and a send policy of never
I like this idea!
Will think about the API now. Sadly, Rust doesn't provide default parameters for functions.
So writing replicate<A, ()> would be ugly. And adding _filtered variant to 5 replication functions is not very nice too...
I considered a wrapper like Filtered, but it's even more ugly 😅
I'll probably go with _filtered.
Will look weird, since rules have associated functions for components.
Probably should be a separate feature then.
But can't you emulate it with filters?
Like if you want to always exclude B if A is present, register B and Without<A> filter. If you need multiple rules for B you will need to write Without<A> for all of them, of course, but do you have that many places?
Yea we can emulate it with filters, it's just a lot messier
Registering Without<A> wouldn't work, but we could make a DontReplicateB marker that is added as a require somewhere by A
If we can't easily set it up I guess we'll have to life with the workaround 🤔
In a way it's a bit like the problem entity disabling solves, but on a smaller scale
Got it! Not necessarily hard, just a separate concept.
Something that could be added after filters.
@dire aurora filters are ready:
https://github.com/simgine/bevy_replicon/pull/535
I can port renet2 for you if you want to try this in action.
The breakages in the latest master are minimal.
Gonna need to wrap up my migration first ... Game is mostly functional again, just need to get my game off of all the local change sets now 
Okay, feel free to let me know!
While breaking changes on the latest master are minimal, you will need to patch both the replicon and its renet2 integration.
I planning to draft a new release before the first Bevy RC though. So patching might not be needed by that time.
How do i get the client id from
Trigger<OnAdd, ConnectedClient>
I can't seem to figure this out ;-/ or is there another way to retrieve it.
The entity is the ID.
Query<&NetworkId>.get(trigger.target())
Or this if you want the backend-provided identifier which may be persistent across reconnects ^
Working on prioritization system, similar to Iris from Unreal Engine.
@waxen barn Are you still interested in it?
I remember you were interested in creating an example once the API was out. I'm hoping to implement it for the next release.
It's fine if you've moved on - I can take inspiration from your crowdsim code myself (if you're fine with it) 🙂
Honestly the crowdsim stuff is way too simple compared to Iris but ya im still around and interested!
Getting back into Bevy stuff again so this is good timing
The implementation itself will be similar to Iris, but I want to have a runnable example in the replicon repo, for which I plan to take inspiration from your crowdsim.
If you’re interested in trying the API early by porting your crowdsim into an example, I can ping you once I've implemented the feature. The built-in API for prioritization should allow you to crank up the simulation numbers and simplify the code 🙂
hi, no way with postcard to use #[serde(skip_serializing_if = "Option::is_none")]
on a field in struct ?
getting
bevy_replicon::client: unable to apply update message: unable to apply changes: Found an Option discriminant that wasn't 0 or 1
postcard is not a self describing format something like that i think ?
Not exactly...
You can't to serialize nothing. It's because everything messages are streams of bytes. So after your component may come another struct and components aren't size-prefixed. So for deserializer there is no way to know if the struct is ended and it tries to pull next byte, which comes from other part of the message.
i wanted to skip the None field because i also serialize the same struct into my db (its a Card identifier, each user has certains cards unlocked, with a level etc, and optionally custom stats). but in json the field is included even if its None, wanted to skip unused data in the db
It's already optimized, it's basically serialized as a single 0 🙂
yeah replicon/postcard side, but not db side where there a json column. in conclusion i dont think what i wanted to do is possible, ill clone my struct and specialize it for the db. thanks !
Removing replicate_periodic, it will be replaced with the new system inspired by Iris that handles updates starving:
https://github.com/simgine/bevy_replicon/pull/541
How do you know how Iris does things? The source is public or do they have documentation?
UE source code is available on their GitHub page:
https://github.com/EpicGames/UnrealEngine
But you need to request the access
The documentation also helps.
For me its about its high level ideas too from the perspective of the user. So, like they encourage you to use a push model instead where you explicitly say precisely when a specific thing replicates.
And then the whole spatial stuff is cool. Nice and simple to setup.
The point of the push model is how they managed to squeeze our more performance doing it that way. Don't know how relevant it is under such a different stack
But Iris is basically made for MMOs
Ive been in their code before its quite interesting but very abstract I find
I currently borrowing only their prioritization logic 🙂 It's simple and effective.
Oh yeah. Enhanced Input plugin was somewhat managable, but this Iris thing is something 😅
@thorn forum bevy_replicon_repair is archived now, sorry about that. Reconnects should do a full clean up and connection cycle instead of trying to maintain state.
Agree.
But we need to be move it to the "unmaintained" section 🤔
@spring raptor shatur my boy, since you are good with network stuff can i dm you for a question?
Not that good, but sure 🙂
@waxen barn the PR is up:
https://github.com/simgine/bevy_replicon/pull/553
I ended up implementing it differently from Iris, but it fits Bevy better this way.
Opinions are welcome 🙂
Oh right, AuthMethod::Custom is the new version of not starting replication immediately right?
Correct!
If I set it to Custom, does the ProtocolHash check still happen?
No, it disables it too. But we have an example that shows how to do this check:
https://docs.rs/bevy_replicon/latest/bevy_replicon/shared/struct.RepliconSharedPlugin.html#structfield.auth_method
It's just a few lines of code.
Not entirely sure about the current design, but can't think of anything better 🤔
Initializes types, resources and events needed for both client and server.
Having fun working on examples for Replicon.
Implemented Boids as an example to showcase deterministic replication.
https://github.com/simgine/bevy_replicon/pull/554
That's a very nice example of combining state synchronization and detereminism 🙏
@thorn forum You're making a TD game, right? Is your enemies’ movement server-authoritative?
hey ! team vs team clash royal like game 2d. i replicate_once the transform, to save bandwidth. the systems to move projectiles, troops etc run both client and server
So you rely on determinism for moving things like troops and projectiles?
while im here i was wondering if its possible to manually trigger a resync of a Component ? for example on command to update the transform to a certain client (to resync in case of desync if the client lagged out). currently i just have a struct holdings timers and transform that i manually get and set
yes the moving logic is deterministic (there multiples factor, like buff, debuff, targetting changing the target)
but i fail to understand what does it have to do with replication, please enlighten me
No. But I think you can use replicate_periodic 🤔 In the current master I replaced it with prioritization which works a bit better.
I asking because I thinking about writing an example TD game to showcase authoritative replication 🙂
@stone horizon Answering #networking message
You don’t need to create your own backend if you only want to manually manage some channels.
Replicon reserves certain channels from the backend, and this information is stored in the RepliconChannels resource.
However, you can still create your own channels on top of it - just after the channels reserved by Replicon.
Or do you simply want to have your own I/O?
A resource with all channels used by Replicon.
currently using one tokio task per client(connection) and one per bevy_ecs world with channels in between them. the client connection task initially does session (~a loaded bevy world) lookup / startup so needs to run before anything on bevy side. i think i need to keep a separation between the game instance (game messages) because there are certain things i cant/dont want to handle in the bevy app. (e.g. monitoring of the server across game instances)
ideally i would just forward a filtered stream of websocket messages to something in the bevy app and 🪄
Ah, got it. You probably want a backend then.
That's basically what backends do 🙂
We have a guide how to write a backend:
https://docs.rs/bevy_replicon/latest/bevy_replicon/shared/backend/index.html
You just feed received messages to Replicon resources and read which messages to send. Plus you need to manage a few other things.
API for messaging backends.
i had assumed so, so didnt specifically ask about it but happy to have it confirmed. also: orphan rule 😭
Replicon doesn't require implementing any traits for a backend, so no orphan rule problems 🙂
yeah my point was that having traits serve as implementation guides would be a good thing, but that's a rust problem ofc
Ah, true.
But I actually like the current approach 🤔 Things like entity management can't be nicely expressed via traits anyway.
Just realized that due to a silly mistake my boids were half-blind 😂
https://github.com/simgine/bevy_replicon/pull/554/commits/8a2cbb17f0275ca8147e951e57aa07efc676bbae
Ended up working on an authoritative RTS example.
Anyone knows a good article about RTS unit movement? Looks like there are a lot of information scattered around the internet.
Yeah, looks like using a modified version of boids + formations is a way to go...
Since it's an example, I won't need pathfinding 🙂
Making progress. Now let me try to network it...
If I send a DisconnectRequest the client entity should despawn eventually right? 🤔
Yes, the backend should despawn it.
From the example backend:
https://github.com/simgine/bevy_replicon/blob/72fb4a194dcbe49739e491c12f66f39174ca7022/bevy_replicon_example_backend/src/server.rs#L143
Ah, looks like my problem was elsewhere, my logic to despawn the entity a client owns tried to despawn the client again instead of the entity they own 😅
Replaced a simple_box example with a new example called simple_button:
https://github.com/simgine/bevy_replicon/pull/560
I think it's more useful for beginners.
The RTS example is still in the works.
Polished the example. Server executes all the logic and clients just render it. Now the only thing left is to add interpolation.
@dire aurora I'm curious, do you interpolate positions in your game?
I don't 
So you just run fixed update at 60hz? 😅
Exactly!
I should really fix that, but I haven't gotten around to doing that yet
It's not super important for the current state of my game
I'll take a look into this.
Maybe it's worth providing a built-in functionality for it in Replicon.
There is also https://github.com/Jondolf/bevy_transform_interpolation, maybe I could create an integration crate for it. But this would work only for transforms 🤔
Transforms would probably be the biggest thing to interpolate, and a lot of other things that need it (like animations) shouldn't live inside the simulation but rather use the tick + offset directly
Makes sense.
It's the same in Unreal: there's no option to enable interpolation for replicated data. Only movement is automatically interpolated.
Anything that doesn't get synced outside of your control would need manual systems to get visualized anyway, which means you could just account for it there
Makes sense.
I'll explore options, maybe it worth to provide a built-in integration (and gate it via feature) or create a separate crate for integration with Jondolf's work.
I mean, it's quite an important feature, and my game will eventually need it too. Life simulators are similar to RTS games: you right-click to move to a location.
I'll look into this. But it really is usually just a position 🤔
Yeah, I mean it's usually only needed for position.
So I thinking wheter it makes sense to make it more specialized or not.
@thorn forum I see you opened a few issues on https://github.com/Jondolf/bevy_transform_interpolation. Did you try using it?
Hm... Looking into the code and looks like the interpolation happens only on the client 🤔
@viscid jacinth is that right? You only interpolate client entities?
yeah ive been using for a few months, but i was thinking of forking it or making my own custom for my game.
with the Transform being DerefMut every frame, my main problem with it especially is that i can't update the Transform in Update, only in FixedUpdate. it should work i think, its implemented in this plugin, but it doesn't, on my project at least.
so on the client i have some systems that adds visuals/sprites and change the transform to add some visuals offset, and they need to all be in FixedUpdate instead of Update, right after received the entity.
You probably also need to interpolate on server based on FixedUpdate ticks and on the client based on network ticks? 🤔
i only use this plugin for smoother visuals on the client, between game tick which are in FixedUpdate
for example i replicate_once my Translation(Vec2) component, and i have a system move_troop which runs on both server and client in FixedUpdate. i use the plugin only on the client to smooth the movements
Ah, your game doesn't need a listen server, so it's fine not to interpolate on the server.
Yes I only interpolate on the client to interpolate between 2 network updates!
I think it's worth interpolating for the listen server as well, otherwise it won't be smooth if the framerate is higher than the update rate.
Yes that's a good point! For listen server you should interpolate between each FixedUpdate, I do this in lightyear_frame_interpolation, which is similar to what avian_transform_interpolation does
Ah, so you have both, they just configured separately, neat!
Hmmm, do we have any patterns for matching predicted entities with server entities outside of prespawning and sending the ID?
Specifically for status effects I'd like to avoid prespawning (as there is no way to reliably network that, since it's a side effect of the simulation, not a player controlled thing), but if the server sends say Poison, AppliedBy(12v1), but I already predicted that, then they should become the same entity 🤔
Could you use a hook to merge them?
Ah, yea that might work in this case. Wouldn't work as a good general solution since the server might reference it elsewhere, but why would anything other than the relationship target reference a status effect
Might be worth considering a more general entity matching system, the prespawning thing we have now is already somewhat clunky to being able to just recognize two entities are the same like I did in bevy_bundlication (though arguably a more ECS-friendly system would be better, like specifying components that can uniquely describe an entity)
Didn't lightyear do something with hashing a registered set of components? 🤔
Agree, it would be nice to improve. If you open an issue with some design pointers, I'll take a look into it.
You mean similar to ProtocolHash in Replicon or related to matching 2 entities?
I think it was for matching 2 entities, though I don't remember the specifics
I do something similar for rollback, I just keep a map from some "spawn reason" to the entity I spawned, then reuse it if it was there
Opened: https://github.com/simgine/bevy_replicon/issues/563
Will take a look later, right now my priorities are:
- 2nd attempt to migrate to Bevy states. I think it shapes nicely, I almost done.
- Interpolation.
- Per-component visibility.
Did someone say per-component visibility? 👀
Yes, I'll try my best to get it ready before 0.17 🙂
That would be great, though I'm still behind on catching up to other features
As you can probably guess from my previous question, I'm only now finally starting to try the related entities stuff to replace my awful ActiveStatuses map I used to replicate 🤣
Removing all the bookkeeping on that one map, as well as the weird rollback logic necessary for it, already removed over 500 lines of code 🫠
I can imagine. It's been two Bevy versions 😅
And the next one is around the corner.
Entities that were synchronized to the client get Replicated on them right? 🤔
Yes
The states migration is up:
https://github.com/simgine/bevy_replicon/pull/565
Opinions are welcome 🙂
I like our "when ServerTick changes, run replication", but our tick configuration in the plugin isn't nice.
It's time to polish it a little:
https://github.com/simgine/bevy_replicon/pull/567
Is there a reason we don't just pass an interned schedule label? 🤔
This was my original thought, but add_systems accept impl ScheduleLabel, so no dynamic typing 😢
On second thought, I can make the plugin generic 🤔
And just default to FixedPostUpdate
pub store_schedule: Interned<dyn ScheduleLabel>,
``````rust
.add_systems(
self.store_schedule,
set_store_tick::<Tick>.before(RollbackStoreSet),
)
```This JustWorksTM though 🤔
-# Example from bevy_rewind
I missed Interned, this is awesome, pushed the changes.
Looking into visual interpolation between fixed updates for the listen server.
bevy_transform_interpolation is quite nice. It even includes Hermite! I think it's better to use it instead of rolling our own.
It also doesn't require any special integration with Replicon: you just slap the interpolation component into listen server or singleplayer modes and it will work.
However, it needs a small adjustment: avoid triggering extra change detection. I will try asking about this in #1124043933886976171.
Tick-based interpolation for clients is a bit trickier. It requires having a buffer and interpolates between network ticks.
But the rest is similar to the interpolation between FixedMain runs.
Unfortunately, bevy_transform_interpolation is not flexible enough for it.
I'll try discussing it with the author.
Asked: #1124043933886976171 message
If you have other opinion - let me know 🙂
We'll be able to use bevy_transform_interpolation. It already works as-is for a listen server or deterministic simulation on the client.
For interpolating the received transform from the server, I'll write a custom backend for it. This will require me to PR a small feature to customize t. Jondolf agreed to this change.
But it looks like I need tick synchronization and component history to implement it 🤔
@dire aurora what do you think about moving some of your code to Replicon?
Tick syncrhonisation doesn't currently live in bevy_rewind, but I can try to grab all that code from my codebase for you
As for the component history stuff, feel free to copy the code if you need it
It does still have some issues though. But those are mainly in the deduplication when saving and loading, idk how relevant those would be
Yep, I remember you wanted to turn it into a separate crate in the rewind repo.
Plus it might be worth seeing if there is any value in pulling some of the blob storage types in bevy out into a crate and adding a deque version, that's easily the majority of the scary rewind code
Glad you're on board, I'll start with the buffer!
Question, from what I can see replicon does not support fps, style of replication. If this were to be implemented (interpolation between client in general) would it be in a new crate or replicon itself?
I quickly filtered trough some of my connection logic, and this is all the stuff that is in some way related to connecting. About half of it is purely clock sync, the other half is possibly relevant for some context, but would probably look different in the context of replicon or a standalone crate (be it for interpolation, rollback, or both)
Interpolation will be part of the crate - that's my current priority.
Originally, I hadn't planned for it and only exposed the necessary API, but then I realized I need it for my game.
Since it requires clock sync and component history in the crate as well, I asked @dire aurora about moving some parts of bevy_rewind and the clock sync from her game into Replicon.
I think this is a good change, as it could be reused for both FPS- and Rocket League–style rollbacks.
FPS-style rollback still needs to be implemented. I'm not currently planning to include it - I'd prefer someone to write a crate on top of mine. But I'm open to discussion.
Great, thanks! I'll look into it soon and start asking questions.
Well defini what sort of replciation there is is something that a library called replicon should do, not to sound rood but thatg is just my take
What you're mentioning is not replication - it's a rollback strategy.
The main reason it's not included is that my game doesn't need it 🙂
I also think it's fine to keep them separate, since there are several approaches to it.
I know it's not working in Replicon's favor, as it means users still don't have an option for client-side prediction. But I hope someday bevy_rewind becomes a solid option - the main dev is just quite busy with her own game.
If you think about prediction is just client replication + speculation
- rollback strategy i guess
Well, a bit more than mere speculation. The best prediction comes from guessing inputs and running the entire simulation for whatever is being predicted. This then requires rollback to resimulate
You could also extrapolate things, but that has very bad results
Yeah, replication is included. What I trying to say is all these things implemented on top.
So it is like replicon implements client_replication
Other crates implement the speculative side/ rollback?
Yep, technically you don't need a crate for prediction, since your simulation is probably the prediction
yes true
Though there are some enhancements you can do if you send inputs that arrived early at the server rather than just repeating the previous inputs
What do you mean under "client replication"?
If sending authoritative state from client to server - no. And most networking crates usually don't allow to replicate client state to server by design.
If sending authoritative state from server to client - yes.
From client you can also send events, so clients can replicate their state to server by sending inputs. You can also make it optimized, which is what bevy_rewind_input does.
So replicating client's state to a server in automatic way? No and it's usually a bad idea, unless you have a very niche use case, like VR.
Clients can only send events (messages), which you can verify on the server and then apply the state.
Just to clarify: the client doesn't need to be the source of truth for prediction/rollback.
It's actually the opposite. It's the reason why rollback happens 🙂
Maybe we'll get the client authority someday, but I need to come up with a design that prevents users from abusing it 😅
It's a very niche use case, most of the time users need the server authority.
Yea that was the issue I had with bevy_bundlication in the past too
fn serialize_as_past_tick_u16<T: Component + std::ops::Deref<Target = Tick>>(
ctx: &SerializeCtx,
value: &T,
message: &mut Vec<u8>,
) -> Result<()> {
let tick = *value.deref();
let ago = Tick::from(ctx.server_tick) - tick;
message.extend(ago.to_be_bytes());
Ok(())
}
fn deserialize_from_past_tick_u16<T: Component + From<Tick>>(
ctx: &mut DeserializeCtx,
message: &mut Bytes,
) -> Result<T> {
let mut b = message.iter();
let v = [*b.next().unwrap(), *b.next().unwrap()];
let ago = u16::from_be_bytes(v);
Ok(T::from(Tick::from(ctx.message_tick) - ago as u32))
}
/// A replication rule to (de)serialize a value as a u16 offset into the past
pub struct AsPastTickU16<T: Component + std::ops::Deref<Target = Tick> + From<Tick>>(
PhantomData<T>,
);
impl<T: Component + std::ops::Deref<Target = Tick> + From<Tick>> Default for AsPastTickU16<T> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<T: Component<Mutability: MutWrite<T>> + std::ops::Deref<Target = Tick> + From<Tick>>
IntoComponentRule for AsPastTickU16<T>
{
fn register_component(
self,
world: &mut World,
registry: &mut ReplicationRegistry,
) -> ComponentRule {
let (id, fns_id) = registry.register_rule_fns::<T>(
world,
RuleFns::new(serialize_as_past_tick_u16, deserialize_from_past_tick_u16),
);
ComponentRule::new(id, fns_id)
}
}
```Also I have been slowly replacing bevy_bundlication with hacks like this 😅
Which then registers like so:
.replicate_with((AsPastTickU16::<AppliedAt>::default(),))
Is this like timers for your game? 🤔
So you convert u32 ticks into u16 to save traffic?
Ah, you are sending the diff, smart 🙂
The original component in question btw:
#[derive(Component, Deref, DerefMut, Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct AppliedAt(pub Tick);
I could probably make something a bit cleaner for this usecase, or maybe we should add it to replicon directly
Similar stuff with bevy_bundlication looked like this:
pub struct Vec3Data;
impl<T: Component + std::ops::Deref<Target = Vec3> + From<Vec3>> NetworkedWrapper<T> for Vec3Data {
...
}
```Though it might be possible to change up the traits so it's not this weird Deref + From thing but just a simple `From`/`Into` you impl for your own component
Both are still uglier than the serde as/with thing it's trying to replace though 🤔
So efficiently send types with a tick?
I'm on board. Yeah, From would be better. But I also wonder if we can do is similarly to entity mapping.
Well we could have first party stuff for tick too, but I mean more generally make replicate_with nicer to use first party
I.e. provide a special trait that maps the tick inside into a more efficient representation.
Could you elaborate?
Well the way I used this on bevy_bundlication is like so:
#[bundlication(as = Vec3Data)]
pub position: Position,
#[bundlication(as = QuatXYData)]
pub rotation: Rotation,
#[bundlication(as = Vec3Data)]
pub velocity: LinearVelocity,
I basically just say "This is my type, please network it as X instead"
And I internally just register a function that converts say Position to Vec3Data, then networks it
ah
a good way of converting those pesky wrapper component
Ah, agree, it would be awesome!
Also makes cases like say the Transform but without scale thing easy. You just impl a From<Transform> for TansfomWithoutScale. a Fom<TansfomWithoutScale> for Transform, slap Serialize, Deserialize on TransformWithoutScale and call replicate_as::<Transfom, TransformWithoutScale>
This is so much better 🤔
Ofc replicate_with can still be useful, but only when you want something fancy like compression, a different serialization format, etc
But it would work only for single types, right? 🤔
wdym single types?
I mean single-component rules. Or do you think we can come up with something fancy for groups? 🤔
Hmmm, I wonder if we could do something similar to that SendRate trick
Might not work if we just say (T, From<T>+Serialize+Deserialize) though, I'd imagine that conflicts with the SendRate one because you could add those traits there
Maybe some wrapper struct to hold those two args could make that work
Though we have the same issue with replicate_with already, no?
My new status effect replication doesn't use the bundle thing either atm 😅
app
.replicate::<Affects>()
.replicate_with((AsPastTickU16::<AppliedAt>::default(),))
.replicate_with((AsFutureTickU16::<ExpiresAt>::default(),))
.replicate::<AppliedBy>();
(ExpiresAt is also optional though, so it could never all be 1 bundle)
-# Which is funny because I once removed the optional property from bevy_bundlication, convinced a bundle would never get an optional field
With filters that would be easy to handle at least
Maybe we could have a special constructor for RuleFns for it? + replicate_as for a single component rules.
But even this will require adding replicate_as, replicate_once_as, replicate_filtered_as, replicate_once_filtered_as 😅
I wish we could have default types on functions. The lack of it is the reason we need _filtered.
Yea, I wonder if we could go for a different pattern there 😅
I explored a few options, couldn't find anything better 😢
If we make an impl for the trait replicate_with expects we could use ReplicateAs::<Transform, TransformWithoutScale> everywhere at least, though that's not the most discoverable thing
Yeah, special constructor for RuleFns would be exactly this.
The mentioned _as helpers would just help the discoverability.
They usually passed through each other, so not a lot of code duplication.
Opened: https://github.com/simgine/bevy_replicon/issues/572
My backlog is really growing, but I might take a crack at it tomorrow since it should be easy to add. It will be a nice addition to filters in this release.
I still haven't looked into buffers and tick sync yet, as I was quite busy with other stuff today.
If 0.17 drops earlier, I will publish what I have and switch to working on RC, which will require quite a few adaptations due to the events rework in Bevy.
But even if I don't implement per-component visibility and interpolation before 0.17, they will remain my priority, so I'll draft another release right after they are ready. I wonder if I can even make these features part of a patch release, since they shouldn't be breaking 🤔
It's not that niche. It's relatively common in a game like valheim or minecraft that the player simulates their player locally for game feel reasons. It's okay when the game is co-op/session based like that and the risk of cheating/hacking is low or doesn't matter.
I've seen in some networking designs the server still spawns the gameobject/entity with authority first, then it can transfer authority to a client. So a client can never just "do whatever", it is a controlled process the server initiates.
aside but I'm watching this atm 🙂 cool interview with Photon employee https://www.youtube.com/watch?v=zDM9HnuXYX0
This is a Game Dev Podcast with my guest Erick Passos from Photon Engine (Exit Games Inc.), a company that provides networking solutions and server architecture for online multiplayer games. I try to understand more about how how online multiplayer works in general and in particular how to use Photon's Quantum and Fusion to develop online games ...
I'm writing my own visibility layer over replicon atm, was just wondering if there was anything like that in the pipeline atm? So I can put a VisibileToEveryone component on an entity and it automatically sets client visibilities. Or visibility based on proximity to location etc.
I think you're confusing client-side prediction with client authority over the state.
Fast-paced games usually predict certain things by simulating on the client and sending the input to the server. The server simulates too and sends the state back. So client is ahead of the server. Then the client either compares the simulation in the past and rolls back if necessary (or does it unconditionally, depending on the strategy), and then replays the input from that point.
The same thing applies to spawning. The client can predict the spawn, but if the server denies it for whatever reason, the client has to despawn (or hide the misprediction somehow).
So the server has authority over the state; the client can't dictate what’s happening.
All of this is possible with Replicon, exept we don't provide a high-level client-side prediction, only expose the necessary API to implement it on top.
When I say "client authority," I mean the client’s ability to dictate the state, like "I have this position."
In Replicon, you can simulate it with events, but there is no automatic replication from the client to the server.
Will watch, thanks!
You mean providing something like this out of the box? Not planned at the moment. But I think it's fine to have it as a third-party crate since there are many possible strategies to it.
For example, there is https://github.com/UkoeHB/bevy_replicon_attributes. If you publish a crate - let me know, I'll be happy to add it to the list of crates.
When I say "client authority," I mean the client’s ability to dictate the state, like "I have this position."
That's exactly what I mean, though it's not full authority. It's limited authority to certain objects, usually the player character
Fully shared willy nilly distributed authority is a whole other beast, I think only Unity has it with their "distributed authority" topology
Yeah, I meant authority on certain entities - i.e., replicating their state from the client to the server.
Unity doesn't provide client-to-server replication. You can only use RPCs and write into certain variables if you have authority, similar to events in Replicon.
In Unreal, position replication from the client isn't part of the general replication API - it's special to movement. Which isn't a bad idea, since I can't think of many use cases for client replication besides movement. That kind of API could also be implemented on top using Replicon events.
Godot is the only one that does this, but I think that's because they support P2P 🤔
I'm not against adding this feature, but I want to prevent users from abusing it. That's why I think it shouldn't just be just replication from client to server.
I like Unreal's approach, but I'm open to exploring other ways that better fit ECS.
I thinking about additionally adding a 3rd trait, so when the deserialization in place happens, you can avoid overwriting things like scale.
But always writing it might be too much, so I thinking about making it optional and require writing replicate_with(RuleFns::new_as<C>().patch_inplace()). But not entirely satisfied with the ergonomics of this solution...
Hmmm, I'm not sure if the in place would even work well with the semantics of as 🤔
It should work, just requires overriding the way the result is written.
Hah, it can't get much more cursed than this, can it? 🤣
#[require(IsValid(true), Attached)]
#[cfg_attr(feature = "server", require(Replicated))]
#[cfg_attr(feature = "client", require(Predicted))]
#[relationship(relationship_target = AffectedBy)]
Hmmm, interesting ... Well if it could work with would be worth having as an optimization when dealing with alloc types I guess. Worst case people can always go for replicate_with directly when necessary ofc
@dire aurora replicate_as is up: https://github.com/simgine/bevy_replicon/pull/573
Most of the code are just examples, it's a very simple addition 🙂
Thank you for the suggestion.
Looks like RC dropped. I'll release the current changes for 0.16 and start working on RC.
A bit too early, I'll wait for the migration guide 😅
That replace in hooks hack sure creates annoying edge cases 😅
Currently dealing with:
- Stasus gets spawned
- Status gets replaced
- Status gets rolled back to befote it was supposed to exist
- Status gets spawned again, it should reuse the server one, but it got despawned somewhere
Thus a new one is spawned and replicon spams errors
Oh ... Does bevy_replicon remove RepliconClient while applying changes?
Which one?
Yep. Do you need access to messages or to the state?
Yea, it's for state, but for now I just check if the client is connected once so I don't suddenly fail to find the resource
The one koe suggested to work around our limited prespawning support
Will be solved by https://github.com/simgine/bevy_replicon/pull/565
Koe promised to review it all soon and I plan to draft a release for 0.16 right after that
Got it. I agree it's annoying.
How important is this for you?
Today I looked into bevy_rewind closely. I think we may need to move AuthoritativeHistory into Replicon. It's a bit fancier then needed for interpolation, but we need to make the API compatible for both rollback and interpolation.
The better pre-spawning*
Ah, that would be great. Being able to check if we have authority is very useful for some of my helper functions dealing with entity reuse
I think I have all the edgecases fixed now, so it's not a major issue for now, but it just shows that we're gonna need a better pre-spawning solution eventually 😅
The hacks I have now are still a lot smaller than what I had before
Would also be useful if I can then use a matching design for reuse of spawns, my current API is not the most intuitive
The current API just relies on creating a SpawnReason which is some arbitrary struct with fields that should uniquely explain a spawn:
fn reuse_spawn<'a, Reason: SpawnReason>(
&'a mut self,
reason: Reason,
bundle: impl Bundle,
) -> EntityWorldMut<'a> {
Not great to have to introduce something like that 😅
Could you elaborate on this a bit more?
I think it's worth looking into pre-spawning now...
Interpolation and per-component visibility are bigger features.
But pre-spawning is something we might be able to solve before 0.17 🤔
Well, if we decide on say the ability to match on a unique combination of registered components, then I could use a similar approach for my bevy_rewind_entity_management crate, rather than having a completely different API like I do now (because replicon has no features to match entities atm)
What approach are you using now?
^
Ah, got it now!
An example of this in action for spawning a projectile:
let _entity = commands.reuse_spawn(
&spawns,
SkillSpawn {
source: *cast.skill,
tick: *tick,
trigger: SpawnTrigger::StatusTick,
number: *frame / spread,
},
(bundle)
);
In fact, having those APIs line up is probably very important. Because ultimately I probably need to network skill effects like this to clients too, especially clients that weren't around when a skill actived
(Currently if you don't observe an entity casting a skill, you won't see the effects from it either, and every side effect besides status effects will not be networked either, even though they might affect you)
@dire aurora okay, so right now we have the following API:
The client spawns an entity and sends a user-defined event. On this event, the server also spawns an entity and associates a mapping from the entity in the event.
It's unergonomic.
In Lightyear, it looks like they attach a special component that hashes all the components on the entity and stores a tick. (link)
Then the server matches it. Do you think something automatic like this would be better than yours?
Yea, I think something automatic would work better, however I think it might make more sense to work with only components registered to be used as part of the has, and send it from the server to the client (that way you can never miss any matches for any reason)
Yeah, it definitely should be only replicated components.
But we don't advance a tick on the client 🤔 We just store the last received tick from server.
If you're replicating a player for example you might replicate PlayerId, which the client could then insert correctly on its own entity.
If we're replicating a status effect we can replicate the effect, source, and time it started.
If we're replicating a projectile we could use some components that relate it back to a certain origin and time of creation, without creating the need to have the exact position be perfectly deterministic
Ah, I get the idea. So we need to hash things depending on what we are spawning.
I think it might make sense for the replicated components and hashed components to be two seperate sets. They might overlap a good share of the time, but there are also cases where both sides can know information to match things that isn't worth networking (like how or why a projectile was created)
Yes, I totally agree. It's a really good idea.
Yep, but we can just hash all the hashable components ... Or maybe they have filters too, but either way if it matches the hashing rules we include those components when hashing
Oh, and we probably need some component informing replicon it should be hashed
Yeah. I think we can provide a component with a generic constructor which accepts a bundle and stores its TypeId.
Then we create a hash of components inside a hook and send it to the server. If an entity with the same component is spawned on the server, we send a mapping to the client during replication.
So the API for creating such component would look like this:
EntityMatcher::new::<(A, B, C)>()
Maybe not a Bundle, but our custom trait that implemented for tuples of components.
Then we create a hash of components inside a hook and send it to the server.
Better to send it the other way around I think, the client can just keep a list of all spawned entities it hashed, and the server just sends the hash once when it replicates them
Ah, right, makes total sense.
Sounds simple to implement. Will start working on it soon.
If you have ideas or maybe naming suggestions - please, let me know 🙂
Yea, you need extra bounds too after all, like Hash, and probably the ability to obtain hash function pointers for a Ptr
@dire aurora there is a small disadvantage with this approach: hashes will be sent to all clients 😢
Actually, I think we can just add an optional ClientId to this component.
Shouldn't it be client entity or NetworkId?
ClientId is an enum with the entity ID or ClientId::Server in the latest master. We use it instead of the plain Entity for events now.
But I think storing a plain client entity makes more sense, this doesn't make sense for the server 🤔
Heya!
Question about deterministic_boids example.
When boids are replicated from server to client, we don't read any tick information.
Wouldn't that mean due to latency client's local simulation will be couple ticks "in the past" from the server?
Also - FixedUpdate from bevy probably doesn't guarantee it being deterministically fixed?
(I do understand that this is just an example. Just trying to wrap my head around crate to see whether I want to throw away Unity).
- Correct!
- I think it will remain deterministic 🤔 I tried running the simulation for quite some time, it looked the same.
@spring raptor I must say, documentation is very good.
At first I was - "well, how the hell I should do anything".
Then I actually started reading through getting started, and one-be-one it answers all my questions.
Very impressed! Like it alot.
Thank you! 🙂
I'm glad you liked it.
The latest master contains many improvements since the last release, and some recently added examples, such as boids or RTS, require them.
I'm planning to draft a new release within a few days.
Is there somewhere an example how to implement snapshot interpolation for transform? (I assume it should based on markers + history)
This is something I plan to provide out of the box. The RTS example needs it badly 😅
Unfortunately, I won't be able to make it in time for the next release. I'm currently working on a more ergonomic entity matching and planning to draft a new release right after I done for 0.16, and then focus on porting to 0.17 to provide an RC.
But this feature is on top of my priority list, so expect it to be present in the next Replicon release.
I assume it should be based on markers + history
Correct, that's how it will be implemented! There are actually 2 kinds of interpolations:
- Interpolation between fixed updates for the listen server. No buffering needed. You can already just use https://github.com/Jondolf/bevy_transform_interpolation for it.
- Interpolation between snapshots. This requires history. I plan to re-use https://github.com/Jondolf/bevy_transform_interpolation for it, just write a custom backend. This also requires adding PRing a customizable overstep fraction. I asked Jondolf about it and he agreed.
So I will re-export bevy_transform_interpolation from Replicon with special helpers for snapshot interpolation.
Sounds amazing!
I will then ignore snapshot interpolation in my prototype for now ✅
Yeah, just set a high tick rate for now 🙂
I went with Signature name. This hash-based API is so convenient.
Already refactored our Tic-tac-toe example with it. Just need finish the docs.
Good to hear. Sounds promising!
I started working on the docs, but realized that I'm not quite satisfied with the API for global vs. client-specific signatures.
Right now, if the optional client entity on the signature is set, it will be replicated only to that specific client. But it's a bit awkward, because on the client the SignatureMap resource (which maps hashes to entities) stores all hashes, both temporary and global, while on the server it stores only global ones.
I opened a draft: https://github.com/simgine/bevy_replicon/pull/574
I will think about it for one more day.
Made a few adjustments and finished the docs. Should be good now. Marked as ready for review.
@dire aurora you might be interested ^
Let me know if you have any suggestions 🙂
Will draft a new release right after merge.
#[require(Replicated, Signature::of::<ClientPlayer>())]
Arguably too OP 🤣
Nice, all the issues I saw before have been fixed, except that the of vs of_many is kind of ugly
(Was of_single vs of before which was worse)
Do you dislike the naming, or do you think we should have a single method somehow?
I tried to keep only a single method, but couldn't because of the lack of specialization 😢
If the naming, I considered these options:
fromfor a component,from_manyfor many components andfrom_hashfor a user-defined value. But this might be confusing becausefromwont't be from theFromtrait.from_component/from_components. I like the explicitness, but only a single letter difference...
If you have other suggestions - please, let me know. Not really satisfied with of/of_many myself 🙂
Both the same ... It should be possible right, add_systems allows this too after all 🤔
Sorry, I meant orphan rule
And spawn, and insert, etc
Yep, because the trait is defined by Bevy.
In our case, we can't create a function that accepts Component and a tuple of Components :(
Just because upstream can, in theory, impl Component for a tuple of Components. It won't happen, but Rust doesn't know that.
It's kind of both 🤔 With specialization, we would be able to specify a tuple of components that don’t implement Component.
Ah. Is it possibly to make our own trait and blanket impl that?
Or would that still fail on the fsct that it could get impld and thus conflicts?
Well, if we can't I guess we'll have to live with the seperate functions
Maybe shorten of_many to just of_n. And I just realized we might want a dynamic version, like of_ids