#bevy_replicon
1 messages Β· Page 8 of 1
Could you elaborate?
Under comparsion I meant using %, not just compare u16 to u32 of course :D
But if last re-constructed tick was received longer then u16 / 2 ago, it will break.
You can network the u16 version, but then turn it into a u32 by just checking what the latest of any received type is
Could even include events (if those even get a tick)
That way it would be essentially impossible to get an 8 minute gap
Like if you send 0 packets for 8 minutes your connection won't even be alive anymore
I track last received init tick and I need to compare it with events.
I.e. it's two different ticks, one is event's tick, another is last received init tick.
I think I get what you meant, but I can't track separately for each message. I need to compare last received init tick with event's tick.
You don't track it separately for each message, you track 1 tick for any message received. If you receive an update that's 2 ticks newer, it goes up by 2
That value is used to convert all the u16 values to a full tick so you don't need to worry about wrapping anymore
No, better with events rework, will postpone for the next release
Waiting for your feedback on ticks and I will draft a new release
Events rework? π€
Ah, you mean the single system thing?
Yeah, otherwise this tick that I compare to other ticks will be present in user-defined systems, not convenient. I better rework events first and then introduce the optimization.
What happens if I spawn an entity on the server, get its ID, and immediately send a packet out?
Does the client receive an entity ID it doesn't know about? what does it do with it?
Mapping happens on client.
You can't receive an invalid ID
Isn't there a possibility to receive a message with a client ID before it has been sync'd to the client?
On server? No, connection handing happens before sending.
So no race conditions here?
spawn an entity, immediately send a packet with the entity id
For some reason, I dont ever get that packet on the client
wait I may be dumb
Yes, it should work...
Could you enable trace logging level for bevy_replicon?
Ah, you are not on the latest master...
We have a better logging on master
Could you try the latest master and enable logging?
Are u listen to the packet with custom system? Bevy replicon has a system that read all the packet in PreUpdate schedule. It may read your packey already
No, my mistake was not wrapping it in ToClients<>
However, for some reason I thin Im getting the wrong mapped entity ID back
oh, there is a special add_mapped_client_event
I wonder if that could be automatic...
@dire aurora honestly, what if we did just use FixedUpdate as the basis for rolling back/resimulating
systems in FixedUpdate should already expect to be ran more than once a frame
and will generally be worried about simulation
the rollback already needs to be outside of it in RunFixedMainLoop I believe
I thought about it a lot, but couldn't find any solution :(
Even if I detect the trait impl via reflection, user can still forgot to impl MapEntities
the only thing I can think of more is maybe Time?
nvm, that should still stay the consistent tick rate...
The issue I'd think of would be systems that only need to run in FixedUpdate ... Generating and storing inputs only goes in FixedUpdate, while loading inputs from history only goes before the simulation (tho that could just be its own schedule)
You could add run conditions, but that seems a bit sketchy, ideally systems wouldn't be able to observe the difference between real simulations and resimulations π€
you'd want to generate inputs outside of FixedUpdate I feel like
and then finalize them at the start of a FixedUpdate tick
or well a game tick
Yea this is how it works now, but once we get input timestamps we would generate them in FixedUpdate
true
One potential solution would be to say that needs to go in FixedPreUpdate/FixedPostUpdate, and we only resimulat FixedUpdate π€
Would still need an extra schedule for resimulation stuff but that could be provided by the rollback crate
well, I'm thinking all of FixedMain should be re-ran, since Pre/Post are still useful for gameplay
First/Last... less so, but consistency
@spring raptor RepliconTick not being constructable is a little bit annoying for rollback
That's already fixed in a PR ... Also you can ::default() then .increment_by() as a hack π
lol
@dire aurora are you thinking the entire simulation schedule is just in FixedUpdate then?
That's essentially how my simulation works, tho ofc I just run the simulation schedule in FixedUpdate π
Honestly, I think I see inputs as the same as other components to rollback
the only difference being they are "client authoritative" components
Or just switch to this branch since you already on master: https://github.com/projectharmonia/bevy_replicon/pull/254
The thing is that inputs are generated on real frames and "rolled back" on resimulated frames ... They're also subtly different from the rest in that you normally load data from tick-1 for the state, but for inputs you just load them for tick
ah true
I've also realized you kind of need 2 separate snapshots or at least a marker
1 for confirmed components from the server, 1 for predicted
ideally you didn't get the server ticks out of order, but could cause issues if you receive a more recent message and then an older message after that and overwrite the recent message
Yes, rollback is an absolute nightmare ... I have all of this stuff working, but I still need to do correct removal and despawning (which I'm hesitant to put more effort into considering how the Disabled marker PR went)
#1211045598854127636 message
This is with full support for out-of-order and missing messages, you can see just how out-of-order things are about halway trough the video when enemies show up (they aren't predicted yet)
Oh in case you're wondering how it can still roll back accurately that way: If a tick is not confirmed (nor a newer one without changed data) you load the predicted value, then before every resimulated frame after the first you check if there is authoritative (so from the server) data for that tick for that component, and load that
ya, that's how I was thinking about it
I think I'll just wait for you to publish your rollback stuff and work on some character controller stuff for now
maybe bug people about the DefaultQueryFilters stuff
the more I think about networking the more I feel like it'd be fairly valuable to just have all networked components in the same place in memory
I'm also kind of feeling like reference counted entities might be so much better for despawning things
but maybe thats too extreme of a solution
This would definitely be a fairly neat solution, the other one would be to slap a component on an entity that tracks when it was disabled, then despawn it when that tick is out of history. But that would increase the chance of small issues like entities staying around despite having been repredicted out of existance π€
Not using the type in rollback yet, but the changes look fine and my app works on this branch. I did ofc have to change a few things and finally remove that RepliconTick on the client side (because calling it ServerTick makes it a lot more obvious just how wrong that is)
Great! Marking as ready for review then.
Would you like me to wait a bit with the release until you finish with the rollback?
I guess with could patch things later...
Hmmmm ... Did we add a "this was the oldest and newest tick received this frame" feature yet? π€
I think truly "finished" rollback would take until at least bevy 0.15 π
No, but maybe better to calculate it on the plugin side?
Hmmm, I guess with how simple Confirmed is now it would be fairly doable ... Guess I'll try that for now and see if it causes any unexpectedly long mispredictions later π€
But maybe it will be slow for you to iterate over all entities? π€
Shouldn't be a large issue, atm it's mostly that I'm not sure what needs to trigger a rollback
It's obvious that changes to authoritative data should trigger a rollback, but I'm not sure about events or updates to non-rollback entities π€
Got it, then yes, better to experiment with Confrirmed for now.
But I'm open to provide the necessary API if implementing it outside of replicon is slow or impossible.
I would rename ServerInitTick into ReceivedTicks (much better name) and add minimum and maximum ticks.
Anyway I think we're almost ready for a release, I'm pretty much at the point where I'm more blocked by bevy changes than bevy_replicon changes π
Okay, I will draft a new release and if something will be needed - we add it later.
I usually do releases often. This one is exception because I wanted to flesh out the API for rollback + groups.
Frequent releases is good, that way a lot of the changes can be done outside of bevy releases ... It's really annoying when a new bevy release shows up and you have to fix every bevy change + 3 months of changes to every third-party crate you use π
Ah, that's so annoying, I totally get you!
Especially with how Bevy developing velocity accelerated. I suggested a few times to provide a more frequent releases, maybe 2 months, but didn't find any support :(
People usually make smaller games with Bevy, they don't get it.
Especially last 4-5 releases.
tbh idk if more releases would help here
I think there is more just internal pr merging issues and backlogs on that
I think the main issue is that releases get arbitrarily big because they get delayed last moment and people start cramming in all features they can π
Last bevy release would've been tiny, it only had primitives as a real big feature, except somehow while writing the blog people merged 5 large features
My problem is that I have to deal with all the breakage at once. It's quite overwhelming π
Especially when you discover a few bugs like in the previous release.
I hope that the mentioned new approach with release candidate will help.
I've hit at least one 0.x.0 bug the past 3 releases π
@dire aurora one more thing!
Not sure about ComponentWorldMapper and ComponentMapper... Maybe left their implementation to users?
They aren't used inside replicon and users may try to use them on already mapped components.
The use case is very niche and users can implement them just in a few lines of code.
I just trying to keep things minimal, but provide the necessary API to implement anything needed on top.
The one potential risk there is that people might forget to add Replicated on spawned entities, tho I'm not sure what the actual impact of that is π€
Also I think the only tests on the server mapping functions are from those types right?
Entities like this won't be replicated.
But if such entity present in a mapping, it will be spawned.
But ComponentMapper and ComponentWorldMapper are not related. They needed to map a component after replication.
We test many mapping cases in integration tests, these types aren't related to them.
In my case the components I map contain the Entity of a skill, and those are replicated ... It would be weird to map server entities to client entities that the client never gets any data for, tho I guess that's also possible π€
That's the default behavior. You can implement your own mapping behavior when replication happens and even after it.
To calrify, I suggesting to remove the following: https://github.com/projectharmonia/bevy_replicon/pull/256
Users can implement them.
Yea that's the one I was talking about
I mainly suggested them because replicon had them before the Context types. Not sure if many people used them tho π€
Yeah, I just doubt that they were used outside of deserialization functions before...
Yea and ideally we'd have a better solution for the usecases where it would be used today ... Tho I feel like that's gonna need observers and possibly relations π€
Agree, it would be nice to have a better solution for it.
Also marking as ready for review then.
Maybe it worth to adding your other plugins to the readme?
This way when I draft a new release, links will be visible on crates.io even if you release them later.
Links wouldn't help much on this release since it's gonna be 0.14 at earliest when they might work for others (assuming DefaultQueryFilters gets merged, and I finish up some of the other crates anytime soon)
Okay!
I think the earliest we could get this in a nicer state would require observers (hopefully in 0.14) and we add some feature to replicon to force certain entities to be grouped, and that could then later be replaced with a way to mark certain relations to require grouping them ... Tho the latter doesn't have the issue where you could encounter the entities twice, since you could just ignore those archetypes π€
Right, grouping also needed... Yes, relations would be so convenient!
Yea grouping is kind of the key feature for situations like this. If you can get invalid state by having two entities be at different timestamps the only solution is to either group them, or send all changes in the world as a single packet ... And the latter is not ideal when your world needs more packets than just the grouped entities alone do
One last question. Ho you have a Mastodon/Pleroma account?
I usually duplicate release announcements there and I think that it would be nice to mention you as well.
I don't ... Discord is the closest I get to social media π
I get you π
But you will need some social media later to promote your game.
Yea once I get to the point where I can market my game I'll definitely need to work on some social media presence π
I'll need some actual graphics first tho, because those capsules get old fast
Hopefully once I get to a point where I can market the game and do playtests I'll at least have all the networking stuff in a state where no one ever needs to complain about it. Not having significant networking issues is one of the top goals of my game, since networking problems are annoying for users, introduce limitations to what the game's logic can and can't do, and restricts people to playing on regional servers, making the game look dead outside of "rush hours"
I would recommend to create early. You can attract some initial attention from developers even on prototyping stage.
For example, on Mastodon there is Bevy official account that will boost your posts.
@dire aurora could you update bevy_bundlication to the release version? Because right now it points to a path π
https://github.com/NiseVoid/bevy_bundlication/blob/806b93fd8ddf77bad55841f90b1161a644a7f920/Cargo.toml#L44
I think these should work π€
Is there a path/reccommended way to replicate the dont_replicate_with functionality?
You can use replicate_group instead. It's basically a reverse rule.
I planning to add support for Without for a group definition in the future.
Ok. Can you clarify for me how children work, if I have a entity that's being replicated and it has children that don't have a Replicated component but have registered components will that child entity be replicated?
Because if not then I probably didnt need that feature in the first place
No, only entities with Replicated component will be replicated.
Also hierarchy is not replicated by default, unless you use SyncParent component.
Also hierarchy is not replicated by default, unless you use SyncParent component.
It replicates the children-parent relation.
So if SyncParent is present but Replicated isnt on a child then it still wont replicate it?
Correct. SyncParent is just a regular replicated component.
Perfect thanks for helping me out!
Hi @spring raptor I'm working on https://github.com/projectharmonia/bevy_replicon/issues/245 and making some progress now. However Bevy lacking API like https://discordapp.com/channels/691052431525675048/742569353878437978/1239902003862179931 in order to complete the API. I'm probably not understand the internal of bevy and unsafe Rust enough to send a PR. Can you help with this?
Here is what I have now, pretty messy for now
struct ClientEventFns {
event_component_id: ComponentId,
channel_component_id: ComponentId,
from_client_component_id: ComponentId,
send: fn(&mut RepliconClient, Ptr, Ptr),
resend_locally: fn(Ptr, Ptr),
}
impl ClientEventFns {
fn new<T: Event + Serialize + Debug>(
event_component_id: ComponentId,
channel_component_id: ComponentId,
from_client_component_id: ComponentId,
) -> Self {
Self {
event_component_id,
channel_component_id,
from_client_component_id,
send: send::<T>,
resend_locally: resend_locally::<T>,
}
}
}
fn send<T: Event + Serialize + Debug>(client: &mut RepliconClient, events: Ptr, channel_id: Ptr) {
unsafe {
let events = events.deref::<Events<T>>();
let channel_id = channel_id.deref::<ClientEventChannel<T>>();
events.get_reader().read(events).for_each(|event| {
let message = DefaultOptions::new()
.serialize(&event)
.expect("mapped client event should be serializable");
info!("Sending event: {}", std::any::type_name::<T>());
client.send(*channel_id, message);
});
};
}
fn resend_locally<T: Event + Serialize + Debug>(events: Ptr, from_client: Ptr) {
unsafe {
let mut events = events.deref::<Events<T>>();
for event in events.get_reader().read(&events) {
info!("Resending event: {}", std::any::type_name::<T>());
}
}
}
#[derive(Resource, Default)]
struct ClientEventRegistry {
events: Vec<ClientEventFns>,
}
impl ClientEventRegistry {
fn register_event<T: Event + Serialize + Debug>(
&mut self,
event_component_id: ComponentId,
channel_component_id: ComponentId,
from_client_component_id: ComponentId,
) {
self.events.push(ClientEventFns::new::<T>(
event_component_id,
channel_component_id,
from_client_component_id,
));
}
}
fn send_system(world: &mut World) {
world.resource_scope(|world, registry: Mut<ClientEventRegistry>| {
world.resource_scope(|world, mut client: Mut<RepliconClient>| {
for event in ®istry.events {
let Some(untyped_event) = world.get_resource_by_id(event.event_component_id) else {
continue;
};
let Some(channel) = world.get_resource_by_id(event.channel_component_id) else {
continue;
};
(event.send)(&mut client, untyped_event, channel);
}
})
})
}
fn resend_locally_system(world: &mut World) {
world.resource_scope(|world, registry: Mut<ClientEventRegistry>| {
for event in ®istry.events {
let Some(event_id) = world.get_resource_by_id(event.event_component_id) else {
continue;
};
let Some(from_client_id) = world.get_resource_by_id(event.from_client_component_id)
else {
continue;
};
(event.resend_locally)(event_id, from_client_id)
}
})
}
pub struct NetWorkEventPlugin;
impl Plugin for NetWorkEventPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ClientEventRegistry>().add_systems(
PostUpdate,
(
send_system.run_if(client_connected),
resend_locally_system
.run_if(has_authority)
.chain()
.in_set(ClientSet::Send),
),
);
}
}
Something here seems wrong considering the .chain() is only on a single system
What are these channel and from client component ids?
fixed version
pub struct NetWorkEventPlugin;
impl Plugin for NetWorkEventPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ClientEventRegistry>().add_systems(
PostUpdate,
(
send_system.run_if(client_connected),
resend_locally_system.run_if(has_authority),
)
.chain()
.in_set(ClientSet::Send),
);
}
}
they are ComponentId of Events<T> and ClientEventChannel<T>>
I can push to Github if you want to play with it
Hi, sure!
About resource_scope, do you need it for ClientEventChannel<T>?
Because this part is also need to be refactored in this PR.
Yea ClientEventChannel<T> could just be removed since the registry would be the only place that needs it π€
It will be easier for me to understand, yes. Open a draft PR, I can guide / help you.
If that channel no longer exists I think there should be no double borrow problems in either of the functions anymore π€
And instead of passing Ptrs into a function, I would just extract it from the world... The API will be safer.
I.e. the signature will be like this:
send: fn(&mut World, &mut RepliconClient),
resend_locally: fn(&mut World),
To clarify, instead of storing resource ID:
channel_component_id: ComponentId,
store the channel ID directly.
I open a PR here https://github.com/projectharmonia/bevy_replicon/pull/259
yeah, but we still need mutable access to the Events<FromClient<T>
Just do world.resource_scope inside send function.
If you need a reference to anything else in the world
So I suggesting to just pass World to functions.
the send function is working now, since it dont need mutable access, but the resend_locally need mutable access to Events<FromClient<T>>
That's fine, you can do resource_scope as many times as you need. Just nest them.
We will polish the API later when we will see full picture.
ok I will try that tomorrow, have to sleep now.
Good night!
Feel free to ask any questions
If you get stuck, I will help you finish the PR.
If my app pauses for a while (bevy does this when the app is on another desktop for some reason) it crashes after it resumes because the Confirmed::contains_any ranges get too big π€
bevy_replicon-0.25.0/src/client/confirmed.rs:72:21:
attempt to shift left with overflow
Not a major bug since networked apps ideally don't pause in the background, but we probably don't want it to crash either
Will take a look, thanks!
I will draft a patch release after merge
Is replicon still intended to be used for games where a client is also a server?
I'm updating a project from 0.9 -> 0.25 and before it treated my server like a client and worked fine but now its not working and the readme/getting started isnt super clear
Yes, it's called listen server.
That's a huge leap π
But listen server functionality is probably the same.
Yeah replicon updates fast lol. Is has_authority still intended to be used for a listen server?
has_authority isnt running for me anymore but as far as I can see my setup matches the tictactoe plugin
hmm, maybe it is sometimes. I'll keep debugging on my side
Yep.
Just a guess, but some updates before we used resource_exists<RenetClient/Server>, maybe this causes the problem? You still can check if RenetClient/Server exists, but it's better to use built-in conditions that relies on replicon-specific client.
Also replicon resources always exist, unlike resources from renet.
The ago > u64::BITS feels a bit weird to me since it was >= on the other check, and I feel like the new testcases would miss that one ... Maybe we can make it a bit more robust by passing trough all values from say 70 ago to a few ticks after last_tick π€
hi @spring raptor I finish the implementation for client now. Things work really well. However we lose ability to use custom send and receive system. My plan is add an enum to ClientEventFns
enum SystemType {
BuiltIn
Custom(Box<impl IntoSystem>)
}
when the SystemType is custom we run the system using one shot system API. Is that fine for you?
If you want to use a oneshot system you'd probably want to accept a SystemId instead of a boxed system π€
yep that fine as well
I think that custom systems can be replaced with the introduced custom functions. I.e. just let user override send and receive functions.
That would leak the send function signature. I'm fine with that, just don't want to make a breaking change.
and seem more performance
I think it's fine to introduce a breaking change here...
We don't need custom systems to handle events, it will just make the API more complex.
I will help you to update the docs
The idea is to make API similar to components, but without markers / commands complexity.
Just a simple way to register event and optinally pass functions
So let go with that. I will update PR soon
One more thing. In the PR I mentioned that we probably want to have 3 channels for each event type. I think I rushed a bit with this suggestion.
Having a separate channel for each event is better for bandwith. When messaging backend sends a message, it includes the channel ID into it. And if we use only 3 channels for each event type, we will also need to include event ID.
So let's keep separate channel for each event. That's basically what your PR is doing right now.
yep, this PR only merge system into 1, the channel still the same as before.
I think the PR is ready now. For the server event, I will send a seperate PR
Until renet comes along and is like "Whats this messages on separate channels? Sure let me make that one packet each
"
I think its better do in a single because having different API for server and client events would be strange...
Ah, renet doesn't group messages across channels?..
Seemed that way when I had logging enabled in my transport at least ... Not sure if that's intended however
That's unfortunate. But let's consider channels as a separate PR to keep this one simple.
Fixed and made tests more robust.
@glacial ridge left a review with more concrete suggestions about the API.
Published a new release.
I have update the PR with the implementation for server event and address your feedbacks. I also modify the example to send an empty server message. I can revert if you think that is unnecessary
Thanks, answered! Feel free to ask more questions if something is unclear. It's not an easy PR for a new contributor :)
About the example - not sure. The event in that specific example is not very useful... Maybe later I will make a chat example to showcase messages in both directions, but I need bevy_ui to have input fields π
Make a chat where you can only write binary messages using two buttons 
So the add_server_event_with signature will be something like this right?. The send_fn and receive_fn only use to serialize and deserialize right?
fn add_server_event_with<T: Event>(
&mut self,
channel: impl Into<RepliconChannel>,
send_fn: fn(T, EventContext) -> Bytes,
receive_fn: fn(Bytes, EventContext) -> T,
) -> &mut Self;
The example I added just an easyway to test the feature lol, have no idea to make it more like real world use case
Yes, but maybe T should be passed by reference and channel probably should be calculated inside.
Sure. But we also have integration tests for all event types, maybe will be easier for you to run them insead.
I managed to get a working prototype for client event, only serialize for now. That is a only way I can erase the types. When I run the example, I dont believe it will work on first try but some how it work without any crash lol
My laptop always hang and I have to restart when I try to run test. Maybe the integration test run too many bevy app or something
please take a look at my latest commit and see if the solution is fine for you. Also suggest me if you have anything better. I'm new to type erased Rust.
We have a lot of examples in rust doc and cargo tries to run them in parallel. If you have a lot of cores or small amount of RAM, it may hang your machine, yes π
Try limiting the number of threads --test-threads=n.
running rustdoc tests consumes a lot of ram and if you run it in parallel, it can easily eat all RAM
yeah that might be the problem, it also often hang when build bevy from scratch and I open too many other App. I think I need to download more RAM π
Looked into it!
No, that's not what I meant. I highly suggest to take a look at rule_fns module.
The idea is this:
- Pass a nice
SerializeFn<T>as usual, but transmute it into a type-erased pointerunsafe fn()and store it like this inNetworkEventFns. - When you call your send function, transmute
unsafe fn()back toSerializeFn<T>(the send function knows whichT).
That's it.
I would probably use SerializeEventFn<T> name to distinguish it from components. And since we have two functions (serialize and deserialize), I would create an abstraction, similar to RuleFns. You can copy-pase the code, just change signatures and methods.
Feel free to ask follow-up questions if anything is uhnclear.
So the NetworkEventFns should be something like
struct NetworkEventFns {
channel_id: u8,
send: unsafe fn(),
resend_locally: unsafe fn(),
receive: unsafe fn(),
reset: unsafe fn(),
serialize_fn: unsafe fn(),
}
also the public API is correct right?
This stuff
fn add_client_event_with<T: Event + Serialize>(
&mut self,
channel: impl Into<RepliconChannel>,
serialize_fn: SerializeFn<T>,
receive_fn: ReceiveFn,
) -> &mut Self {}
No, no, you don't need to erase types for all functions.
Only for serialize_fn. It's because we need to erase T from the signature.
Yes, but when you provide deserialize, we will pass it instead of ReceiveFn. And channel not needed, can be detected internally.
@spring raptor Do you want to serialize_fn return Option<Bytes> instead? Same for the dereceive_fn to return Option<T>. Because they can be failed to ser/der and we just ignore if they return None.
Replace Option with bincode::Result.
Just take a look at already existing SerializeFn inside the repo
We trying to make a simillar API :)
I update the PR, please check the latest commit if I understand correctly your idea.
Yep! This is correct.
A few suggestions. Instead of using resource_scope inside receive/send, use it inside their systems and just pass resources to receive/send via Ptr (you can get them by ComponentId). Also is resource_scope for AppTypeRegistry is needed, can we just use resource::<AppTypeRegistry>?
I.e. you won't need to pass World to receive and send.
which resource are you referring to? The resource_scope for AppTypeRegistry is unneed, I just removed it.
Right now you pass world and for each event type obtain RepliconClient and AppTypeRegistry. I suggest to obtain them inside your systems and just pass to send/receive
RepliconClient.
It seem I still have to pass world down to resolve other stuff
I need to reolsve Events<T>
For the server event, the serialize and deserilize seem need to take &mut Bytes, is it ok to make the api for the server different from the client? I can not make it the same interface.
That's why I saying to retrieve resource by ComponentId and pass Ptr into send where you downcast it to Events<T>.
Yeah, it's fine. But why they are different?
but then I have to pass bunch of Ptr to resolve all needed deps, I think just pass world down for more flexible, that is what I tried to to in the first place
Why bunch of Ptrs? You only need a single Ptr for Events<T>.
What kind of flexibility are we talking about? send is non-overridable by user.
the server event has some special ser/der to optimize permance that it take &mut Cursor in stead.
How to get Ptr for Events<T> when I dont have T in the system? Store the ComponentId in the event?
Ah, right, it's because we deserialize tick first. In theory we could just unify them by using cursor in both places...
When you register event, you know the T and can call world.component_id::<Events<T>>().
It will return ComponentId which you can store in the event. Then in systems you just get the component by this ID.
It's a bit hard PR for a new contributor. If you want, I can finish the work for you.
yeah please, feel free to push in
Okay :) I will be able to start working on it in a few hours.
I think that just make the event more bloat and might affect the performace. Although we still need to move non T resource to the system and pass them down.
On the contrary, it will be faster.
Right now each time you convert T into ComponentId and then do a lookup. What I suggesting is to do it once on registration in App, cache it, and then use it in systems each time.
Unless you spam a ton of events the difference would be hard to even measure tho π€
The main overhead with events tends to be the number of systems and early on in the lifecycle of the app maybe the buffers getting expanded a few times
Yeah, it's a tiny optimization. But since it doesn't affect user API, I think it worth it.
Hi! I am writing a client-server game and when I switch the server visibility_policy from All to Blacklist, only some of the entities are replicated (even though I haven't changed any visibility yet) and sometimes this expect fails: https://github.com/projectharmonia/bevy_replicon/blob/v0.25.1/src/server.rs#L385
I'll try to debug what is happening but would appreciate any pointers
Server-authoritative networking crate for the Bevy game engine. - projectharmonia/bevy_replicon
Sounds like a bug... Could you provide a minimal example to reproduce? I will debug it.
I'll try. Thank you
https://github.com/Sorseg/replicon_crash
This crashes for me. It is the moving box example separated into two crates (not sure if this matters), but adding the blacklist visibility with some other params cause server to crash
added readme
Thanks a lot, catched and fixed: https://github.com/projectharmonia/bevy_replicon/pull/261
Will wait for a review from @echo lion, merge it and draft a new patch release.
@fluid moth released a new version with the fix.
Many thanks! π¦
Busy with a different thing, but I remember about the PR and will start working on it soon.
My current plan is to draft a small release with the events rework before Bevy 0.14. I want the next release with the upgrade to 0.14 to be without any breaking changes or features.
Hello guys, I've started a project with bevy-renet, however I found out that It's more about message delivery, and doesn't natively integrate with ECS very well, so there's a need to make a lot of boilerplate code (entity mapping, message collection for further event distribution between systems etc.).
My question: Does replicon aim for better ecs integration? Is it easier to use with bevy?
Yes and that depends on your game, if you want to synchronize some entities from the server to the client it's a lot easier
Yep, it's an abstraction for high-level networking features like replication and messages as events. And you can use it on top of renet.
Thank you for the replies
@glacial ridge done: https://github.com/projectharmonia/bevy_replicon/pull/262
I decided to create a separate PR. Feedback is welcome.
I leave some comments. Thank you for finishing it.
i lost momentum with my bevy stuff, inc. timewarp lib for rollback. trying to get back into it and update to latest replicon api changes. noticed some chat about rollback further up - are you writing something for rollbacks that plays nicely with replicon, @dire aurora π ?
Yea, I started writing a no compromise rollback implementation that actually plays nicely with all the extra issues created by client-server and using authoritative replication rather than purely deterministic replication
how far have you got?
(also did you look at how lightyear does it? i just started looking at that too)
A large portion of it works now, but it still requires a bevy fork for disabling entities
for despawning during rollback?
To handle the spawn/despawn edges yea
All the other approaches I looked at would break in my game because I have fun things like status effects being entities that aren't directly replicated
Hadling jitter was the biggest pain, but it works now, here's a comparison between predicted entities (the spheres) and unpredicted entities (the enemies) #1211045598854127636 message
cool, looking good. lmk if you want me to try it out β curious about your approach. i found testing rollback stuff to be rather awkward
Yea rollback is a pain to debug. I just ended up writing unit tests for every edge case I could think of
for me it's mostly tests which are a result of observing weird behaviour in my game, eventually leading to isolating wtf happened in a test
Yea that's essentially what I had too "There's something weird going on but I don't understand what", and then I wrote tests for everything related to the histories ... Still need to write tests for a few of the other things tho, but at least those are simple to debug, like it rolling back to the correct frame and running the simulation the correct number of times
Feel free to ask questions about the update, I will try to help.
The API for rollback changed quite a lot in the latest release, but it's much more flexible now.
thanks π yeah, i was previously deserializing by writing to my history component. api for those callbacks all changed, but i noticed a similar thing in the docs so will try and fully absorb the docs before i pester you
were the api changes for rollback/custom serializers etc in pursuit of a specific goal or to support some new use-case? or just general tidying?
Previously you just override deserialization that also function as a writing and that's where you wrote into your history component.
But it's quite limiting. You may want to predict one entity and interpolate another. Yes, you could have a check like if entity.contains<History>() {}, but it will be quite slow.
Also it's impossible to have a custom serialization and prediction/interpolation, user need to manually merge hist deserializaiton logic with a crate-provided rollback logic.
Now we have a different API. First, ser/de and writing are separated. You can override ser/de and have your custom writing for prediciton completely independent. When you register a replicated component, you assign ser/de.
And to overwrite writing (insert data into your history component, for example), you can create a marker, assign a writing function to it and select which components it will affect. We cheaply check entity markers and call appropriate writing functions for each components. You can also overwrite the default writing function that called when no markers are registered.
So the idea is similar to what it was before, just a bit more flexible.
Huge credit to @dire aurora for all of this :)
thanks for the explanation, sounds very sensible
Can I get pointed to how/where replicon decides which replicated entities to send to a client? Does it use change detection or something?
I'm running into a really weird issue where a entity replicates normally 90% of the time but when a certain field changes it doesnt
Ok I think somehow remove_component of an unaffiliated component is breaking replication for the frame that it occurs on. I don't know enough about Replicons internals to experiment with it to be completely sure but... If I have commands.entity(entity).remove_component::<RandomComponent> call in my system then it won't send the other components on the entity but if I remove that call then it works
Are you using replicon in your project?
yeah
Could be a bug around archetype changes... Can you make a minimum reproducible? Or link to a commit with the issue?
Yeah I'll work on a minumum reproduction because its still being kindy iffy around actually working rn. It wasn't ever working before and now its sometimes working after trying to remove insert/removals for the entity thats having issues
Great thanks :) this crate is really turning into a team effort haha. So many edge cases and considerations...
If it's removals it's probably other things like inserts too ... Could try making a system that removes a component if it exists, and inserts it if it doesn't, and then see if it ever replicates correctly anymore π€
I have tests for a change with despawn and insertion, but not with removal π
And looks I can even reproduce it... Let me try to debug
Did you reproduce it? I tried to make a super minimal repro and it seemed to be working ok but I didn't really get to try it lol
Yes, looks like it fails to replicate when there is a removal and a change at the same time. Looking into it closely right now...
Mmmm, that would make sense with why it was weird and also why after I changed it it was intermittently working. Maybe also try multiple removals? Glad its for once not me doing something wrong lol
Debugging, I will ping you when I figure it out :)
This just goes to show how hard it is to test replicon, which is fairly well scoped, with a small number of people ... I hit a bunch of replicon features but missed visibility and this π
I figured it out. We collect removals first (because its serialized first) and I bump last entity tick if we find a removal. And after it we collect changes and the code thinks that all updates are already received because the entity tick is higher.
The easiest way to solve it would be to move collecting changes first, but it will break the case where we have a removal and insertion at the same tick π
I am reviewing #262 now.
Going to sleep right now, will take a look into it tomorrow :)
Sorry for stupid question, but why RepiconTick exists? Why don't we just use u32?
For one it needs to newtype the u32 anyways to store as a resource
but internally it is a wrapping u32
Makes sense
It's not a resource anymore, we now have ServerTick that wraps RepliconTick.
But RepliconTick is needed because it have a special wrapping comparsion.
@distant shore fixed: https://github.com/projectharmonia/bevy_replicon/pull/264
I will draft a new patch release after the merge
Awesome! I'll test the patch in an hour or so and let you know if it works for me!
Looks good! My stuff is working fine now as far as I can tell
Great!
Does replicon checks if component was changed before sending it? Or does it send all component data every tick?
I looked into source code, and it does check for changes (ComponentTicks struct contains "changed" field, which according to docs is updated on mutable dereference) but as my rust skills are still not very good, i'd like to be sure.
Does replicon checks if component was changed before sending it?
Yep
Thanks, very cool to hear!
@willow osprey what is your plan about timewarp? Are you planning to abandon it and use lightyear for your game?
just investigating the competition at the moment π i decided i need to make a stripped down client/server demo to test rollback in a more useful way, so i want to write a small sandbox to play around. seeing how things are done elsewhere before i dive back in
Ah, got it!
Wanted to ask to know if I should keep your crate in the readme.
@spring raptor PRs approved
Thank you very much!
I think we should rename Confirmed to ConfirmHistory or LastConfirmed. Working on updating bevy_replicon_repair and got a little confused.
I like ConfirmHistory, sounds like a better name.
Could you open a PR with the rename? I think we can make a deprecated alias like I did for Replication.
is there a way to use replicon with wasm now?
I think the main issue used to be renet, in theory you can swap that out now (or maybe renet finally has a WebTransport option or something)
Tha name definitely makes sense, it's functionally almost identical to my history types too: It has a history (mask) with some values, and a last tick ... The only reason I didn't suggest it myself is because my code was already covered in the word History π
Speaking of Confirmed, did I mention we should make a getter for mask? π€
Somewhere in my code I transmute it to grab the mask
Yep, renet doesn't support it. We also don't depend on it anymore, and it's very easy to integrate a new backend. Right now we have only these two: https://github.com/projectharmonia/bevy_replicon?tab=readme-ov-file#messaging-backends
I think it should be possible to integrate https://github.com/MOZGIII/xwt, maybe talk to @drowsy lance.
No, I didn't know that you need it π
I can export it, sure. I thought that you were interested only in contains_any.
That was the case until I added the logic to check for newly confirmed ticks
The goal is to provide a single integration as an example and to make the library immediately usable. And let users or library authors provide theirs. Having a lot of integrations inside replicon would be hard to maintain.
thanks, hadn't seen xwt yet π
@dire aurora added: https://github.com/projectharmonia/bevy_replicon/pull/266
renet2 has a web transport socket. I still need to make a bevy_replicon_renet2 crate though.
@willow osprey I added bevy_replicon_renet2 to renet2: https://github.com/UkoeHB/renet2/tree/main
nice π also sensible to be able to disable built in encryption as soon as browsers get involved, since they are already encrypting.
Maybe add it to replicon's readme for discoverability? Maybe even with the mention of its advantages.
@dire aurora Have you thought about publishing your rollback crate and adding it to the readme as well? π€
You can just make it depend on the latest Bevy release in Cargo.toml and just mention in the readme that [patch.crates-io] section with your fork is required.
It would be at least something π
bevy_replicon_snap doesn't have prediction and bevy_timewarp doesn't have a direct integration with replicon.
I'd need to do something about the weird Tick situation first, and I'd probably want DefaultQueryFilters to at least be merged, so people don't start writing code for something that might never get merged π€
In fact if it gets merged I need to first refactor some things so I actually use it instead of the older Disabled PR π
About Tick you mean the mask getter? I planning to draft a new release today or tomorrow, just want to squeeze a few more refactor PRs into it π
Regarding DefaultQueryFilters, I think this is an implementation detail. I mean even if Bevy decide to deny DefaultQueryFilters, I pretty sure that they decide on some other way to achieve it and in this case and you will just need to refactor the code (again, like from Disabled to DefaultQueryFilters). But it won't affect users.
No I mean the Tick vs RepliconTick thing, I still haven't fixed that yet π
It would still be use to use RepliconTick, since it's on the client, but the plugin could probably get a generic arg for Resource + Into<RepliconTick> ... That way Tick can live in the user's code until we have a official bevy solution for this
Also Disabled vs DefaultQueryFilters vs whatever potential other solution would affect users, since they aren't allowed to despawn entities. If you despawn entities rolling back will always produce some weird incorrect state, especially with things like status effect entities or short lived things like projectiles
Not sure if I get you... You mean if Bevy decide on other solution, users won't be able to despawn entities?
If bevy decides on another solution, the way in which users avoid despawning entities changes, especially if the goal isn't just to leave those entities around forever but still despawn them when they fall out of history (the rollback crate would obviously need to provide some sort of API for this kind of thing)
Ah, I get it now!
Well, I would still publish. This could bring more attention to the PR. Other existing solutions have the same flaw anyway.
But I'm not pushing, just wanted to suggest.
Hey guys :),
Currently trying to improve the client-side prediction API in bevy_replicon_snap and I could use some input.
To handle prediction two systems per Event <-> Component pair are needed:
- A server-only system that just runs some logic:
Aand directly applies any mutation to the relevant component - A client-side system that runs/replays the same logic
Amultiple times from the latest state snapshot it received from the server
I am currently solving this using generic systems and the Predict<E,T> trait to define the common logic:
/// This trait defines how an event will mutate a given component
/// and is required for prediction.
pub trait Predict<E: Event, T>
where
Self: Component + Interpolate,
{
fn apply_event(&mut self, event: &E, delta_time: f32, context: &T);
}
pub fn server_update_system<
E: Event,
T: Component,
C: Component + Interpolate + Predict<E, T>,
>(...
pub fn predicted_update_system<
E: Event,
T: Component,
C: Component + Interpolate + Predict<E, T>,
>(...
which can be registered by a library user with this app extension:
app.predict_event_for_component::<MoveDirection, MovementSystemContext, PlayerPosition>()
However this feels kind of clunky since you need to have a system that pre-collects all needed data for the calculation into a context component,
it would be much nicer to just be able to pass in a system into the extension function instead.
Do you have any Idea how I could implement this or any other Ideas on how to improve the API?
For details the current main is up to date with this: https://github.com/Bendzae/bevy_replicon_snap
Why an event is needed?
To be able to replay changes in the prediction system I need to know the history of inputs at least I don't know a way to decouple it from the Inputs/Events
Looking into the code deeply, am I understand correctly that you register an event from client that you apply directly on server and on client you write it directly into the component too + store the value into a buffer?
Yes, on the server I just receive the event from the client and immedeatly apply it, on the client the Event is saved in a event history and then all past event since the last received snapshot from the server are applied.
But I dont understand a few things:
- Looks like you use
Updateand predict components separately, but why? You probably want to run all systems inFixedUpdateand replay each buffer at the same time... - Looks like on client you apply event directly, but I don't see when you re-apply all events logic after receiving a value from server...
@granite hill ^
it would be much nicer to just be able to pass in a system into the extension function instead.
You mean that you want to run a system instead of writing function? You can trigger a one-shot system using Bevy'sCommands.
- Not a 100% sure I understand what you mean by that, I don't see a way to update different component types on the same system with my current (generics based) implementation
- The reapplication happens in this function, the
corrected_componentis the latest update from the server, and starting with that the newer inputs are replayed in the loop after that:
// Client prediction implementation
pub fn predicted_update_system<
E: Event + Clone,
T: Component,
C: Component + Interpolate + Predict<E, T> + Clone,
>(
mut q_predicted_players: Query<
(&mut C, &SnapshotBuffer<C>, &Confirmed, &T),
(With<Predicted>, Without<Interpolated>),
>,
mut local_events: EventReader<E>,
mut event_history: ResMut<PredictedEventHistory<E>>,
time: Res<Time>,
) {
// Apply all pending inputs to latest snapshot
for (mut component, snapshot_buffer, confirmed, context) in q_predicted_players.iter_mut() {
// Append the latest input event
for event in local_events.read() {
event_history.insert(
event.clone(),
confirmed.last_tick().get(),
time.delta_seconds(),
);
}
let mut corrected_component = snapshot_buffer.latest_snapshot();
for event_snapshot in event_history.predict(snapshot_buffer.latest_snapshot_tick()) {
corrected_component.apply_event(
&event_snapshot.value,
event_snapshot.delta_time,
context,
);
}
*component = corrected_component;
}
}
I will give that a try, I forgot those exist π
- Not in a single system. What I would expect is that you register your systems in some special schedule and re-run all systems for each input client have since the received snapshot. This will probably require user to register its systems in this schedule as well. You maybe want to take a look at
bevy_timewarpfor the implementation. - Got it! Found a marker for writing into the buffer.
Okay that makes sense I will look into how timewarp does it! Thanks for the pointers and feedback ππ
How do I initialize ParentSync component when the field is private?
Isn't there a plugin for that?
Just insert it with ParentSync::default(), it will just work.
Probably the description is a bit confusing, I should remove some implementation details from it...
ParentSync is a built-in primitive to sync the hierarchy.
Maybe it worth to move it into a separate plugin, it's built-in for historical reasons, that's what I use in my game :)
I mean there's already a ParentSyncPlugin ... I don't load it in my app at least π€
Yep, it can be disabled completely.
We could maybe go a step further and make some feature flags to disable things people don't use ... I'll probably make a PR at some point to make some more things optional, mostly so I don't accidentally mix up client/server things between my apps ... Ofc the default would still want most of these enabled
ParentSync, the scene stuff and having both client/server at once seem like pretty common usecases, it's just that they could be optional
Makes sense to me.
I meant not a Bevy plugin, but a separate crate here. But what you suggesting is better. Scenes and hierarchy sync are quite common and they are small modules.
And to clarify, both needed, the plugin and the component. The parent-children relation will be replicated only for entities with ParentSync.
@glacial ridge opened a PR that adds a usage example: https://github.com/projectharmonia/bevy_replicon/pull/274
thanks, that's much more clear.
It doesn't really need to be behind a feature flag though, does it?
My initial thought was that since we usually only provide core functionality, things like these usually are separate plugins.
But these are simple one-file modules that are quite useful, so moving them into a separate crate doesn't make much sense, only make the maintenance harder for me.
Moving them under a feature enabled by default is an option, but yes, they don't pull other dependencies and it's just two small files (scene.rs and parent_sync.rs), it won't save compile time a lot...
Since it's small it's not a big issue now, but it could be useful if not having the feature makes it not be in the replicon plugins thing ... Just like how bevy does it π€
The scene stuff would be a bigger issue since it does enable a bevy feature iirc
Ah, right
Why is FromClient in client::events when it's the server that needs it? π€
Because it's a part of a client event. It used only inside ClientEventPlugin.
The ClientEventPlugin vs ServerEventPlugin distinction also feels weird. This wouldn't play nicely with adding features that will actually avoid adding pointless client/server system to the opposite app π€
Things seem to work fine after updating (well after I fixed the renames and got the plugins right after 5 wrong tries), except I now get flooded by these warnings:
WARN bevy_replicon::server::events::event_data: discarded 1 client events due to a disconnect
``` I might be able to fix some of them but should these really be sent for broadcasts? π€
Agree. Plugins are separated by "systems for a client event' and "systems for a server event". It just how it was before the refactor, I didn't change it.
But they probably need to be separated by "what runs on client" and "what runs on server", like we do for replication, just move some shared event resourced initialization into core.
This is strange, I wouldn't expect any change in behavior. How do you trigger it?
I started the server, connected, then closed the client, which disconnects it after a while, then prints these messages until the server closes because there are no players
Could you check it was like this before?
I didn't get these messages in the previous release
Found the root of the issue. Caused by a copy-paste, working on a fix.
@dire aurora could you try https://github.com/projectharmonia/bevy_replicon/pull/275 ?
Looks like that fixes the warnings
Thanks, I checked it myself, of course, but wanted to be sure.
I will draft a patch release after the merge.
But this one will go into the next release :(
Just keep both plugins enabled on client and server for now.
Opened an issue about it: https://github.com/projectharmonia/bevy_replicon/issues/276
Maybe I can make it non-breaking change if I consider the current behavior as a bug? π€
No, it requires moving some public items.
Will do in the next release. I think drafting releases too often will disrupt plugin authors. So maybe in the next week or so.
Maybe next release will be all about adding features, the feature release that lets you make replicon have less features by disabling them π
Published a small patch release with doc updates and the server event reset fix.
is the component change dection of replicated component base on client or server? For example, when the client connected to the server that already has some entities and the client will receive relication, will this query Query<(), Added<Component>> run on client match those entites?
No, server don't know about any entities on client and will consider all entities as new.
Are you working on reconnects?
No, just enter some issue with synchronizing, turn out to be system order. I think I fixed, hopefully lol.
All my crates are updated to bevy_replicon v0.26 now.
am i right in thinking that rooms are implemented now but not covered in the docs?
i got engulfed by work stuff for the last couple of months but i noticed that the rooms PR/issue got closed
There are no rooms, but visibility has been implemented
Correct, we implemented barebone visibility toggle per-entity because it's faster and other approaches, like rooms, can be implemented on top.
For example, check https://github.com/UkoeHB/bevy_replicon_attributes
is it possible to change ClientId after connect?
I'm trying to add mock client, I also want to act as the mock as well.
No, after connection ClientId can't be changed. You can only disconnect and connect again with a different ID.
Are you planning to publish renet2 on crates.io?
I think your fork deserves more attention, especially because of the webtransport support.
Canβt publish the webtransport stuff since it depends on an unpublished branch of h3.
Ah, I see :(
The author of nevy recently pushed changes to https://github.com/DrewRidley/nevy, you mentioned that it may help to get rid of the heavy deps.
It's too tightly coupled with Bevy right now, but could potentially be adjusted to work for renet2.
i have a bunch of components like this to describe units:
#[derive(Component, Debug)]
pub struct Health {
pub current_health: f32,
pub max_health: f32,
}
how can i make the bundle that represents a player's stats replicated? i would rather not call app.replicate::<Health>() etc for every component
currently spawning like this on the server but seeing nothing on the client using bevy-inspector-egui:
pub fn spawn_test_unit_system(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
TestUnitBundle {
health: Health {
current_health: 100.0,
max_health: 100.0,
},
combat_stats: CombatStats {
base_attack_damage: 10.0,
current_attack_damage: 10.0,
base_attack_speed: 1.0,
attack_speed: 1.0,
attack_range: 1.0,
},
movement_stats: MovementStats {
base_speed: 1.0,
current_speed: 1.0,
},
},
PbrBundle {
mesh: meshes.add(Capsule3d::new(1.0, 4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_xyz(0.0, 0.0, 0.0),
..Default::default()
},
Replicated,
));
}
There are a few options:
- You can manually register components of your bundle like this
app.replicate_group::<(A, B, C)>. - You can manually implement
GroupReplicationtrait for your bundle and useapp.replicate_group::<MyBundle>(). - Use https://github.com/NiseVoid/bevy_bundlication, it basically automates option 2.
So you probably want option 3.
If you have any issue with replication, enable tracing.
You can do it like this RUST_LOG=bevy_replicon=tracing <your cargo command>. And post the log here for server and client.
got it, thank you
might be nice to have this as a native feature though
perfect, thanks again
Our current approach is to provide a non-opinionated core crate and let users create their extension crates.
It's possible that we merge some extensions in the future, but for now it's much easier to maintain crates this way - multiple developers are involved.
me and drew just did a lot of work on it, can't guarantee we wont make more changes to the api but you should be able to play around with it
Does replicon have a Resource containing the ClientId? I find I have to constantly go into the client transport and ClientId::new(transport.client_id().get()) and wonder if there's a better way.
The use case would be e.g. to add a leafwing_input_manager InputMap to an entity whenever a Player(ClientId) component is added where its ClientId == Res<ClientId>
Not sure, but I always just make my own resource for this.
Okay, I ended up doing this as well. Naming was annoying though, I didn't want to have another ClientId besides the one from bevy_replicon and the separate ClientId from the actual transport, so now it's a Res<ClientIdResource> π
Depending on the transport replicon uses it stores it here: https://docs.rs/bevy_replicon/latest/bevy_replicon/client/replicon_client/enum.RepliconClientStatus.html#variant.Connected
There is a getter inside RepliconClient: https://docs.rs/bevy_replicon/latest/bevy_replicon/client/replicon_client/struct.RepliconClient.html#method.id
Stores information about a client independent from the messaging backend.
Wow that's wonderful thank you both! π
Cool thanks, will check it out
@spring raptor can you do v0.26.2 for the recent small merges?
Sure! Will do in an hour
Done!
Nice thanks :)
This change in 0.14 from world field to getters hits hard, we have 400+ usages of it π
Because sometimes I need world() and sometimes world_mut().
Luckily I use Neovim, it's quite convenient for such tasks π
You can use my branch, I do a lot find and replace though lol
I used it as a reference, but decided to do the replace from scratch since a lot of code changed since then and I wanted to completely replace all occurences, even docs.
@echo lion thanks for the review and sorry for a few last minute changes π
Thinking about releasing an RC version that supports 0.14.0-rc.1. I can't release bevy_replicon_renet, but other messaging won't be blocked by replicon.
Drafted a release
@echo lion you are still blocked by h3?
I remember that you considered integrating nevy.
nevy still needs some more work to support custom connection validation.
Ah, found the issue. Looking forward to it :)
assuming no pre-existing code to work around, what would you guys argue is the best way to handle lobby code (tracking players joining/leaving, syncing the player list with colours, readiness to start the game etc)
Resources are the most practical for accessing from other systems, however then you have to deal with replication and message passing yourself
One invisible entity per player with components for that kind of info also sense, especially regarding replication but then detecting players joining/leaving becomes a hassel (if you have to use entity change detection)
i'm curious to hear how you guys implemented it
I don't have lobby in my game, but I think would use entities for it.
Even with resource replication, you would send the whole resource each time which is a bit wasteful.
Yeah, thanks!
I have configured a max tick rate of 60, and even without any replicated entities and with a single client connected, the client receives about 75kB/s, and it seems to climb over time. After 2 minutes its at 95kB/s.
I use bevy_replicon_renet::renet::RenetClient::bytes_received_per_sec to measure.
Any ideas on what could be wrong?
I think if you enable trace logging you can see what is being sent/received
I think when I checked last time replicon sent out more packets than necessary, and in addition renet didn't make any attempt at grouping them, which wouldn't help with bandwidth either π€
Try running with RUST_LOG=bevy_replicon=trace <cargo command>.
Maybe you have a component that constantly changes.
It's because we use separate channel for each event. We probably could group events with the same send settings into a single channel.
Or maybe it worth to implement it on backend side...
Doesn't seem like it, or at least this is the only log I get repeated on the server
2024-06-08T16:31:37.055536Z TRACE bevy_replicon::server: incremented ResMut(ServerTick(RepliconTick(762)))
2024-06-08T16:31:37.055633Z TRACE bevy_replicon::server::replication_messages: no init data to send for ClientId(8450268747470345538)
2024-06-08T16:31:37.055689Z TRACE bevy_replicon::server::replication_messages: no updates to send for ClientId(8450268747470345538)
Removing any replicated components (as in not registering them for replication) and bundles (from bundlicon) has no effect either
What do you get on the client side?
Other than a log confirming connection, nothing
So looks like replicon doesn't send anything.
You can also take a look at ClientStats, but you will need to add ClientDiagnosticsPlugin for it.
I suspect that renet sends keepalive messages or something like this. Let's try it again with RUST_LOG=renet=trace π
Was double checking with client diagnostics, and yup, nothing. Renet is sending tons of packets through, avg of 3002/s going of off log timestamps only
Strange, take a look at renet logs, maybe you will spot something.
Because looks like we don't do anything at replicon side with it.
You can even try renet alone, without replicon
There are a bunch of examples in the renet repo
Logs are sadly only "received/sent" packet. Thanks for the help through, now I know its renet.
Please, double check renet without replicon. Just in case.
Maybe it's a problem with bevy_replicon_renet (the integration). It's pretty straightforward, but who knows.
π will do
the demo_bevy example in renet works as expected (https://github.com/lucaspoffo/renet/tree/master/demo_bevy), 54kB/s~ received but that's with sending player transform every frame. Drops to 5kB/s if I comment out broadcasting the transform.
Renet has an issue with that: https://github.com/lucaspoffo/renet/pull/156
Why the demo works as expected? π€
Maybe each channel is sending an ack and he has a lot of channels?
@long folio how many replicon-registered events do you have?
I have not registered any events, commenting out all app.replicate::<T>() has no effect either on received bytes.
Let me try to add more logging π
What version are you using? 0.26.x?
@long folio could you try with this branch?
https://github.com/projectharmonia/bevy_replicon/pull/284
You need to specify it as patch in your Cargo.toml
TRACE bevy_replicon::server::replicon_server: received 0 messages over channel 0
INFO bevy_replicon::client::replicon_client: sending 0 bytes over channel 0
Only getting these, both only ever on channel 0.
Client never sent anything.
Server received 1 message 150 times over 1.5~ seconds. (and received 0, 8000~ times over the same 1.5 seconds)
Here are the logs, sorry for delay (completely forgot how to redirect output on windows π )
Ah, sorry, I rushed a little and forgot to save on client, pushed one more commit, could you run cargo update -p bevy_replicon?
This is suspicious:
sending 0 bytes over channel 0
Why do we send 0 bytes?
Looks like we found a bug.
Let me double check the logic
Wrong branch name? No new commit on send-receive-logging, ill use send-logging
Found it! It's a bug π I forgot to check if there are any acks after recent major refactoring.
Yeah, ignore this one, I will update send-receive-logging in a moment.
@long folio Done!
Please, try the latest commit in send-receive-logging.
In the example in the docs, why is receive_events the one with the run condition? Seems like if you're sending events from server to client, you should only send the message if has_authority
Seems like the client won't receive events
You are right, It's a mistake. Let me push a fix...
Ok, just making sure I'm understanding. Thanks!
I thought we already fixed that one, someone was confused about that exact same example before iirc π€
Seems to work π No data being sent when there are 0 replicated entities
25-30kB/s with a single entity and no registered components for replication through, but maybe that's unavoidable overhead?
Thanks for helping to investigate!
Now this one looks like an overhead from renet itself.
We always create two channels for replication.
This still sounds like a lot more than is needed
@long folio could you send me full logs? Just in case.
poffo said it should only be around 2kbps when idle: https://github.com/lucaspoffo/renet/pull/156
Unless you are running at 600hz lol.
Ofc, this time sat at about 55kB/s, one replicated entity and no components registered for replication
This is strange.
Now we definitely not sending anything... I attached logs directly to send/receive methods.
I should mention, the numbers are from RenetClient and not ClientStats. Through I do not use renet directly to send/receive anything
Having 1000 replicated entities has basically no change on the amount of data sent, if that helps
Strange. And no issues with demo_bevy despite it also uses 2 channels?
From renet
I can send you my project, or hop into a vc, if you want and have any debugging ideas
I will try to reproduce on my own, will write you back if I will have any troubles with it.
In the meanwhile, marking the PR as ready for review.
There was a bug on our side. @dire aurora maybe renet groups messages between channels, the issue you saw could be caused by this bug π’
I don't remember, maybe I forgot to fix. I checked the commit history and "blame", no changes to this example...
Anyway, opened a PR: https://github.com/projectharmonia/bevy_replicon/pull/285
@vivid quartz ^
We have no actual control over what renet does and doesn't group tho, if it's small it would group them. But I've seen renet send 4 tiny packets for 2 events + replication π€
π
Ah, got it. Then it's unrelated.
@echo lion What do you think about keeping rc in a separate branch? I think it may provide a better experience since users may try to download the repository and they won't be able to run examples due to renet.
Fine with me
Swapped branches. Now master tracks the latest release.
I measured the simple_box example from the replicon repo and I having ~420 bytes per second on idle. It's because renet always sends an ack packet and it's 7 bytes. 7 bytes * 60 times per second = 420 bytes per second.
Maybe you counted bytes_per_second as kb/s? Because 55kB/s = 440kb/s, which is suspiciously close to the number I'm getting.
You can try applying this patch on the latest master to see the mentioned results.
Drafted a new patch release with this fix.
I guess renet only measures the non-transport bytes? Every packet is gonna have 24-32 bytes overhead from encryption probably as well as however many is needed for UDP π€
Tho realistically it doesn't really matter if a packet is 5 bytes or 200 bytes, tiny packets are awful for troughput
On latest master, with the applied patch, a single client, and no movement I measure 998-1007 bytes/s up and down.
Measuring in my own project, using the master branch of bevy_replicon I measure 55000-80000 bytes/s (using the same function as the patch). This is without registering any components for replication, and with a single replicated client. Changing the max tick rate through TickPolicy on the server has no effect either.
I'll try to narrow it down later today when I'm home.
Correct! I just measured the data produced by RenetClient, but that's what Volc measures.
I think you see different results on the example due to time variation.
But in your project results are strange...
Here is how I debugged it. Clone renet repo and add a local folder as a patch.
Then inspect these parts:
- https://github.com/lucaspoffo/renet/blob/b22876cb018523c8cb6a02614277f5dc9b5f1580/renet/src/remote_connection.rs#L503-L515 - here we read bytes from channel.
packetsvariable after this block should have zero len when there are no changes. - https://github.com/lucaspoffo/renet/blob/b22876cb018523c8cb6a02614277f5dc9b5f1580/renet/src/remote_connection.rs#L517-L524 - here renet adds his ack.
packetsvariable should have len equal to 1. - https://github.com/lucaspoffo/renet/blob/b22876cb018523c8cb6a02614277f5dc9b5f1580/renet/src/remote_connection.rs#L611 - here is where the number of bytes are calculated.
bytes_sentshould be equal to 7.
Seems like renet is sending a single packet each frame in PostUpdate. Starts with 6 bytes and grows to 9 over a few seconds. Given not much is happening on the server right now, it runs at 7000-9000 fps. Which at 9 bytes amounts to 63000-81000 bytes per sec, around what I am seeing. Also makes sense why it was fluctuating quite a bit, due to other stuff happening on my pc.
Now I guess the question is why renet is sending these packets.
Yeah this is what I thought. Early you claimed to be running at 60hz.
6 -> 9 is probably a varint growing
I linked this PR a few times, did you check it out?
What I meant is I set TickPolicy to 60Hz, my bad for not clarifying through.
You can put a timer on the RenetReceive/RenetSend system sets, or use the schedule runner to set an app tick rate. In the long run renet needs a better ack sending policy but it isnβt likely to arrive soon unless someone makes a good PR (to renet or renet2).
I havenβt studied that part of the design yet, so not sure what solution is needed exactly.
- An app tick rate is probably better then renet timers because otherwise you will introduce latency if renet and replicon get out of sync.
Is there a Wireshark protocol parser for replicon + renet (maybe + netcode) by any chance? Anything that lets me read into "Data (1078 bytes)" π
You can also try bevy_quinnet as alternative messaging backend.
Do you want to read what's inside each message?
The format is very simple. There are two kind of messages.
First kind contains reliable data (like removals, insertions, etc.), we call it "init message". Second kind contains component updates, we call it "update message".
Init message header is the current replicon tick. Update message header consists of two ticks and u16 index.
Each message body consists of arrays. First comes the array len and then its data.
1 array contains entity mappings. Each element consists of 2 entities
2 array contains despawns. Each element consist of 1 entity.
3 array contains removals. Each element consist of an entity and an inner array with size and component IDs as its element.
4 array contains changes. Like 3, but inner array element consist of ID and component data.
Sizes serialized as u16. Entities serialized using this optimized function: https://github.com/projectharmonia/bevy_replicon/blob/434837b34c6616bc0edee44e99c9bd224bd3e32a/src/server/replication_messages.rs?plain=1#L680
But I think that there may be additional bytes from the used messaging backend.
Server-authoritative networking crate for the Bevy game engine. - projectharmonia/bevy_replicon
Thanks! I'll try to write a parser for Wireshark based on this & see how well that works out π
Simpler question -- in which use cases should I use NetcodeServerTransport::disconnect_all vs RenetServer::disconnect_all ?
It seems both disconnect the client, but the client sees different DisconnectReason (Transport vs DisconnectedByServer). I'm not sure what the semantics of these cases are though
I'd use RenetServer for disconnecting from user code. The transport is lower-level.
Looks like the visibility API only has entity granularity. Is there a way to conditionally replicate components to clients?
Not yet, but there has been discussion about the idea of creating VisibilityLayers for components
I'm not sure if that would be blocked on anything, it might be possible for someone to implement it. The basic idea is that besides just storing the visibility per entity per client you can store the visibility layers (a bitmask just like in most physics engines), and when registering rules you also register which layers it is on ... There are some nuances that might be hard to get right tho, like you might want replication group A when the visibility doesn't match and replication group B when it does match. Those issues could be solved with existing things like priority, but it might be difficult when processing the archetype cache into some per-layer format
Hmmmmmmmmmm ok so picture a visibility system like FTL has, where visibility on hostile ships depends on the upgrade level of your sensors subsystem. Maybe the way to do it would be to spawn an entity with progressively more intel about each ship, but then only make the relevant ones visible to all clients? Is there an easier way to do what I'm thinking?
To clarify, I would spawn an entity with basic information (damage level), then an intermediate with more info (crew locations), then a maximum with even more (weapon charge levels), all per ship.
Then I would make each visible to a client based on that client's sensor level, so if a client's sensors are level 2, we make the basic and intermediate visible to that client
In a world with only entity visibility you'd have to make them separate entities yes
Or you could just do what I do: Ignore the problem until visibility layers are added π€
With visibility layers you would simply look at the sensor level of the client when deciding the visibility layers of entities
Next question: I'm getting NetcodeServerTransport does not impl Resource. bevy_replicon_renet pulls in bevy_renet, which pulls in renet with bevy feature enabled. Doesn't that mean pulling in replicon renet should be enough to get the impl Resource?
Oh, do I need the renet_transport feature?
Nope, that's on by default...
Nope, it should just work
I just checked in my game just in case - it works.
Hmm weird, I'll come back here once I figure out what's broken
Oh, pebcak
the trait Resource is not implemented for Result<NetcodeServerTransport, ...> π©π©π©
It's things like this that make me want to be a goose farmer instead of a software developer
It's fine, that's the only way to learn
One more question... I can't get the examples to connect over local network, I keep getting "Can't assign requested address". Is this something obvious I'm doing wrong?
Feel free to ask as many questions as needed.
So examples are untouched, you just passing IP over CLI?
Try running first on the same machine, but use your public IP instead of the default 127.0.0.1
when you say public IP do you mean internet-visible? I've been doing my local 192.168.x.x ip
Which is what I'd assume would work without hole punching/nat traversal
No, no, I meant local network ip. So you tried with 192.168.x.x on the same machine and it works?
Yup, no crash
Could you try to run pure renet examples?
Will do
And just to make sure this isn't what's wrong, if I don't pass anything for the port on the host or client, it should use the same port for both. That's what I want, right? server_addr on the client uses port 5000 by default, but the udp socket it opens supplies 0, meaning OS assigns an out port?
I guess that's probably all right or else it wouldn't work locally π€·
Yes, it should work.
Oh, I've no idea if this is your case, but I've seen my client crashing on the same error connecting to a server on a different machine, same LAN -- I fixed it with
let socket = UdpSocket::bind(Ipv4Addr::UNSPECIFIED, 0))?;
(Ipv4Addr::UNSPECIFIED instead of server_addr). I still don't know why it worked for me
Are you opening the socket on the server side with UNSPECIFIED?
On the server side:
let public_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
let socket = UdpSocket::bind(public_addr)?;
And that earlier code was my client side (which was crashing with "can't assign requested address")
This example code does look kind of sus, the client is binding to the same IP as the server
Which is definitely not correct, unless both are running on the same system
The only way I could get connecting to work in a stable way over the internet is with this cursed function:
pub fn try_connect(target_addr: &str) -> Option<UdpSocket> {
for bind_addr in ["::0:0", "0.0.0.0:0"] {
let socket = UdpSocket::bind(bind_addr).unwrap();
for addr in target_addr.to_socket_addrs().unwrap() {
info!("Trying to connect to {:?} from {:?}", addr, socket.local_addr());
if socket.connect(addr).is_ok() {
return Some(socket);
}
}
}
None
}
The addresses are about the same as using UNSPECIFIED for both IPv4 and IPv6 I guess? π€
Unspecified is 0.0.0.0 iiuc
Both of those are what rust to be considers unspecified
assert_eq!(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)).is_unspecified(), true);
assert_eq!(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)).is_unspecified(), true);
I switched the client to bind to 0.0.0.0:0 and... that seems to have done something. At least, it's connecting properly. What I'm more worried about is that that's like an unsafe thing to do or something
With UDP anyone can kind of send you packets anyway, tho normally you'd use .connect(server_addr) so you only ever receive packets from the server's address
With UdpSocket::bind(0.0.0.0:0) will I get every udp packet coming to the machine?
That's certainly what that seems to be saying
Ah, it's my fault, sorry
I copied the renet example which runs only locally.
Let me put a fix.
@vivid quartz There are 2 places that needs to be fixed. I will open a PR in a moment.
In this regard UDP functions similarly to if both sides have a TCP server running, except there are no TCP connections, just an address to receive packets on
Then there's a lot of magic rules in firewalls that allow you to send packets back and forth despite at least one of the sides probably being behind NAT and not having that port forwarded
So when I bind a udp socket to a specific address... does that mean I'm listening for traffic from that address? Or traffic addressed to that address?
Those rules basically work on the concept that if you send a packet over UDP, you can receive a reply to that same address for a limited time
bind is like starting the TCP server, you listen on that address for packets
This stuff is a bit more familiar to me, that's why we need e.g. matchbox
Why do you need to listen on an address? Can't I just listen to traffic coming to me?
@vivid quartz try this: https://github.com/projectharmonia/bevy_replicon/pull/290
The traffic coming to you needs a destination, and the destination is that address
Rust also allows you to call connect which makes it so that you can only receive packets from the address you connected to
In theory it would be possible to check the network interfaces, see which one allows you to connect to the server, then listen only on that one. But you'd need to use extra crates for it so that's probably not worth it π€
Examples work over lan for me ππππππ
Ohhhhhhhhhhh so the destination would be the public IP of, say, my router? Not my local machine's local IP?
Or
Wait
Oh, this makes sense!
So you'd never listen on your public IP? For external traffic, you'd just bind to 0.0.0.0, and for local traffic, you could bind to your local ip?
Well if it's a server that's not behind a router 0.0.0.0 would bind to it's public IP too, but in that case that's probably what you want, otherwise people can't connect
Ahhhhhhhhhhhh ok
Thank you all for the help! Very much appreciated. Network is still an area of exploration for me so I'm glad to have help figuring things out
You are welcome!
under what circumstances should we be using renet::ClientID as opposed to just handling the raw u64
it seems like a largely redundant distinction
Probably just for type safety I'd think
fair enough
so for our purposes we just use ClientId?
i ask because ClientId doesn't seem to implement Reflect so you can't put it in a replicated component
bevy_replicon has its own ClientId that's a bit easier to use.
is there any feasible way to send a client event from the same system where the client's connection is established?
trying to avoid a separate scheduled system to send my player hello event (username etc) so i don't have what amounts to a one-shot event with a run condition evaluating to false every update
Do you mean the system where you read the ClientConnected event? You can add an event writer ToClient here, sure.
i'm talking about the system where the client is initialised i.e. same system where RenetClient::new() is called
Client events sent while not connected are dropped, you need to wait until connected.
are you 'connected' as soon as you finish inserting the client and transport resources or does the connection span multiple game ticks?
i.e. if the last thing i do in the system where the renetclient, transport etc are initialised is push a client event to the eventwriter (assuming that the client connection was initialised using direct world access)
or do i have no choice but to cache the event in a component on a temporary entity and have another system with a run condition process it later
Hmm I checked again and looks like messages can be submitted to renet clients while connecting, but replicon clients require you to be fully connected.
There is a handshake, so you are not connected immediately.
ah my bad, i thought NetcodeClientTransport::new blocked until after the handshake
Why does it need to be on an entity? Store that information in a resource and wait until the client is connected. The run condition is quite cheap.
it didn't need to be on an entity, it was the additional system that i was trying to avoid
If you use RenetClient::send manually then you can avoid it. Idk if it's worth the effort though.
how would i receive it on the server then?
You could use RepliconServer::receive for your custom channel.
is it possible for the replicon handshake to fail but renet still fires ServerEvent::ClientConnected?
cause i just noticed i already have a system for dealing with the renet connected event
oh, so i could just receive it on the server side like i would a normal replicon event?
No, you'd need to pull the bytes from RepliconServer::receive and deserialize them manually.
No, if handshake fails then the server won't emit anything.
hmm, if i used a custom channel and sent it just before i add the renet client as a resource, would the event be guarunteed to have arrived at the server by the time ServerEvent::ClientConnected fires?
It will arrive after the ClientConnected event.
aight
out of curiosity, is there a particular reason that replicon drops events sent before connection but renet doesn't?
cause i can't think of a situation where you'd be sending events preemptively but would want them to be dropped
RepliconClient persists between reconnects, whereas RenetClient is discarded, so replicon is more careful about synchronizing with connection events.
ahhhh
makes sense
hmm, did you guys intend to remove std::fmt::Display from bevy_replicon::core::ClientId?
just updated from 0.12 to 0.13 and that was one of the breaking changes apparently
hmm, last time i read the docs this wasn't here, does that mean that in bevy 0.13 i can't have the Replicon client and server loaded on the same bevy app?
I'm not sure how this is supposed to work with LAN hosting, where one instance typically acts as both client and server
in 0.12.X i had that working fine, did something change that will irreparably break it now or was I just always violating best practices?
Ah, I remember that we decided to clean events on reconnect in order to mimic renet: https://github.com/projectharmonia/bevy_replicon/pull/163#issuecomment-1885148968
We should change it then. I think that the use case brought by @candid eagle is valid.
You need to use Debug
It was always there, I just turned it into a more noticeable warning.
where one instance typically acts as both client and server
This can be done, the warning itself points you to the headings to read.
This way it will be even faster.
yep, so am i right in thinking that to do client-server i just load the server but with the full complement of bevy plugins instead of just the minimal ones?
Yes, and just run your regular client systems. Just make sure you use the correct condition on them. It should just work :)
cool
currently trying to figure out why my clients arent connecting anymore
the renet client connected event isn't firing on the server either π€
Logs could help. Try running RUST_LOG=bevy_replicon=trace cargo run ...
yeah, interestingly i get nothing when i set bevy_replicon to trace
Even on server?
pretty sure this is the cause but if i remove it i get no logs cause at some point last year i disabled the Log Plugin
i tried setting replicon to trace there too but still nothing from replicon
@candid eagle you mentioned that you migrating from an old version... Do you have bevy_replicon_renet as a dep?
yeah i realised when i had a look over the examples in the replicon repo that you'd moved stuff to bevy_replicon_renet so i added that and changed the client and server connection code to match in case that was the problem but it doesn't seem to be
Do you have bevy_replicon_renet enabled as well?
where do i need to enable it?
ohhh
yeah i missed that in the changes to the example code
yep that fixed it
thanks @spring raptor!
i take it the replication loop mentioned in the article looks like a LOT of entities getting added with the ConfirmHistory component attached?
i accidentally ran my old client-server code :P
Is there a simple way to replicate resources to clients?
There are at least two problems. 1. Unreliable messages are guaranteed to fail. 2. Reliable messages will back up in the renet client and cause transport errors every tick.
Ah, I see...
Not right now. It's easy to implement, I will get to it eventually.
For now just spawn an entity and assign the needed data as components to it.
Or use events
Most of the time you don't want to replicate resources. For example, if a resource is an Vec - you will send the whole Vec each time.
I mean writing an event that's sent when a resource is changed is trivially simple
But yea replicating resources would only make sense if we first had delta compression probably
What I'm mostly wanting it for is states but events work for now
Isn't the difference here that with renet you might be doing handshake stuff? IMO replicon shouldn't be sending/receiving messages until the layer below it considers the connection ready π€
IMO replicon shouldn't be sending/receiving messages until the layer below it considers the connection ready
Yes, that's how it works now and we will keep it this way
Hmm is there a way for a client to check its id?
Or do I have to send it a message myself?
If the transport sets it, it can be found on RepliconClient (using .id()) once the client is in the Connected state
What should I call when I want to stop a server (before returning to main menu), given that I might want to start another server again, or become a client, in the future? For now I'm getting ResMut<RenetServer> and calling .disconnect_all(). I think I also need to remove the RenetServer and its transport's resources? Any other steps I missed?
Calling disconnect and getting rid of the RenetClient/Server and transport seems to have been the right solution for me
Tho there are some issues where you need at least 1 frame before being connected again, otherwise replicon won't pick that disconnect up correctly and might leave some state around
Trying to set up bevy_replicon with bevy_replicon_renet in a server-client architecture (no single player here).
According to the displaying the connection states, my connection looks ok but events seems not to be sent.
I strictly followed the code from here: https://docs.rs/bevy_replicon/latest/bevy_replicon/#network-events
Both server and client events looks muted.
Try enabling logs via RUST_LOG=bevy_replicon=trace cargo run ...
You should see a connection
Well, indeed it helps, the client looks to receive the events
Or does it? I thought that all of these message was the server events, but it's written "received 0 message"
wait a minute 
Ok, I receive the server event, it works better when you don't forget to register the server system -_-
Verifying the other side...
Well no, the client to server events doesn't seem to be ok:
// Client side
app.add_plugins((RepliconPlugins, RepliconRenetClientPlugin));
app.add_client_event::<DummyClientEvent>(ChannelKind::Ordered);
app.add_systems(Update, send_events);
fn send_events(mut dummy_events: EventWriter<DummyClientEvent>) {
dummy_events.send_default();
}
// Server side
app.add_plugins((RepliconPlugins, RepliconRenetServerPlugin));
app.add_systems(Update, (receive_events, handle_events_system).chain());
fn receive_events(mut dummy_events: EventReader<FromClient<DummyClientEvent>>) {
for FromClient { client_id, event } in dummy_events.read() {
info!("received event {event:?} from {client_id:?}");
}
}
fn handle_events_system(mut server_events: EventReader<ServerEvent>) {
for event in server_events.read() {
match event {
ServerEvent::ClientConnected { client_id } => {
println!("Client {client_id} connected");
}
ServerEvent::ClientDisconnected { client_id, reason } => {
println!("Client {client_id} disconnected: {reason}");
}
}
}
}
All I get is the client connection etablishment
The trace from the client side show me that it sent the events correctly
2024-06-13T22:21:53.133721Z TRACE bevy_replicon::client::events::event_data: sending event `mmopixel::events::DummyClientEvent`
2024-06-13T22:21:53.133745Z TRACE bevy_replicon::client::replicon_client: sending 0 bytes over channel 2
2024-06-13T22:21:53.135991Z TRACE bevy_replicon::client::replicon_client: received 0 message(s) from channel 0
2024-06-13T22:21:53.136049Z TRACE bevy_replicon::client::replicon_client: received 0 message(s) from channel 2
2024-06-13T22:21:53.136641Z TRACE bevy_replicon::client::events::event_data: sending event `mmopixel::events::DummyClientEvent`
You forgot to register the event on the server
From the quick start guide: ... The event must be registered on both the client and the server in the same order.
Hmm, I would love that it was that, but it's just my copy/paste mess
Ah, okay :)
Ah, so it doesn't work for you for some reason?
Yeah, really not sure why.
Should the server trace log works as it does on the client side?
Sure
I have a really clear trace log on client but can't make it work with the same command
Enable it on server and show me the output
This part looks expected. You send 0-sized structs, so you have 0 bytes.
In fact it's already enabled
Nothing is logged, maybe all of that is due to some deeper bevy config? Like I only run the MinimalPlugin on the server side
Ok, logging work with the DefaultPlugins (nice to know that it doesn't work with the MinimalPlugins)
And the log are fine, the server receives the events...
2024-06-13T22:33:41.186740Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
2024-06-13T22:33:41.186750Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
2024-06-13T22:33:41.186758Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
2024-06-13T22:33:41.186765Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
2024-06-13T22:33:41.186772Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
2024-06-13T22:33:41.186779Z TRACE bevy_replicon::client::events::event_data: applying event `mmopixel::events::DummyClientEvent` from `ClientId(1718318018071)`
You need to add LogPlugin for logging on server.
Then everything should work :)
Yeah, everything works with the DefaultPlugins
With MinimalPlugins I mean
Hmm, let me verify something
I mean that you can have MinimalPlugins and just add LogPlugin from Bevy directly.
Logging on server is quite useful.
Ok, so the LogPlugin is also responsible for the prinln! macro, I didn't know
info! works without LogPlugin, but not println!
But anyway, MinimalPlugins + LogPlugin is what I need, thanks!
Hope I'll do something awesome with all that stuff, thanks for the work on the crate π
I would expect it to be vice versa, but glad it works
Did you manage to find the problem?
With the MinmalPlugins + LogPlugin, I don't have any problem anymore. And I need to fix some of my words: println! works without LogPlugin but info! doesn't
What caused me trouble is that in the crate documentation, info! is used. Not sure when but I modified this by a println for the connection/disconnection message but not for the receive_events message. So I received the "connection etablished" message but not the events message which mislead me into "damn I can't make it works"
Maybe changing the documentation to use println would avoid some future timeloss with this missing LogPlugin. I'd be happy to do a PR to modify the documentation to thank you ;p
Ah, I see!
Hm... Not sure, I personally prefer to use logging. But we could add a small paragraph about headless server.
This is for sure a wise decision.
Here's an attempt at "generic just works" resource replication. It seems to work with my limited testing, feel free to give feedback/point out possible issues!
Looks like that would definitely "just work", not the most efficient approach like shatur already mentioned, but there's lot of room for hacky solutions early in the development of anything networked
Until your game gets to the internet bandwidth usually doesn't even matter
Yeah, I anticipate blowing this away at some point in favor of a first party solution
But glad it doesn't look obviously wrong at least π
About the "right" way to stop a server in a reusable way -- it seems if I RenetServer.disconnect_all() and then remove_resource<RenetServer> & remove_resource<NetcodeServerTransport> in the same system, the disconnect packets never get sent and the client-side doesn't disconnect (until it times out waiting for a server message). So that doesn't seem ideal...
You'd need to call disconnect_all before PostUpdate, then remove the server/transport the after that or the next frame I think π€
Digging through the source it seems disconnect_all() doesn't send the actual socket right away, it waits for a NetcodeServerPlugin system that runs in PreUpdate if(resources exist(RenetServer, NetcodeServerTransport), to find all the disconnected clients to send the disconnect packets
Hmmm I see, so it sounds like I need to keep track of this in a temporary "Stopping server" state/resource/global
It would be nice if there were some magical RenetServer::stop(&mut self) that would handle sending the disconnects + removing itself and its transport afterwards, all in the right order, and I'd just have a system .run_if(server_just_stopped) to handle the result πΆ
Updated the crate to the latest Bevy rc. It's just a version bump in Cargo.toml
If you've got an app with the Server plugins loaded acting as a lan host, can you reuse your systems with client event writers on that server and have them be recieved properly or does it not work like that?
Yes, it's basically the idea of how listen server mode works.
Sweet
This is me right now
I've been trying to get my replicon server running in an Azure container for like 3 days
You're making my struggle to get my game playing nicely over the internet when IPv6 and DNS resolution are involved sound easy
Like at least I only had to upload a binary to my VPS and run it π
It's looking like this is probably the way to go. I got it running in a container instance but I... wildly overestimated the performance I'd get. I had a multiplayer game running with 3000ms ping πππ
3s ping? That's insane π
I have about 14-16ms ping to my game server. Would've been lower if my VPS was in the netherlands instead of germany ... but hetzner has been a much better option than ones that offer locations in the netherlands
Mind sharing what you're using for vps?
https://www.hetzner.com/cloud/ The cheapest AMD version ... With a whole 2 cores and 2GB RAM ... Actually runs my game surprisingly well ... And it's not even using both of those cores π
Ooooh they charge hourly? So you can shut down your server when you're not using it and it won't charge you?
Also looks like the ones with 4gb are cheaper?
Neat, they even let you save money by running ipv6
Never tried it tbh, mine is on 24/7 since it runs my git and some sites too
How does that save money?
It just means that instead of the core being dedicated to you alone, there's multiple servers running on the cpu, and they'll swap in and out. Similar to just having multiple processes running, except they're all completely sandboxed from each other
Right, I don't mean shut down the machine, just the service. I'm just looking for something I can pay pennies for access to occasionally
Ah, I think you should look into something like serverless then
That's usually paid per time unit of computation you use (and/or the amount of data you transfer)
Something like container instances? I tried something like that and it didn't give me near the performance I needed
Also I don't mind having a vps running full time in the future, I just like that I can just shut the thing down when I'm not going to be working on the networking for a month or two
The most basic VPS is only like $5/Β£5β¬5 tbf, it's not too much
Yep I spooled one up already, it's working great π
Idk ask their sales people
Oh, I thought was a statement you invented eather than repeated π
It's simple, IPv4 addresses are hard to get, so if you don't need one you can pay less
Or rather by default you pay more to have one π
No, their pricing just says βpay 60c less for ipv6 onlyβ.
@dire aurora reorganized events as we discussed: https://github.com/projectharmonia/bevy_replicon/pull/292
I usually don't make functional changes when Bevy updates, but it's just a few imports change for end users, so it should be fine...
@dire aurora splitted by client and server as suggested:
https://github.com/projectharmonia/bevy_replicon/pull/298
Other features were also separated this way in the previosly merged PRs π
What if we start to depend on renet directly?
bevy_renet is just 2 simple files, we can just copy the needed logic.
This will allow us to release quicker. And we can have proper rc releases.
I talking about bevy_replicon_renet of course.
That might work yea ... You'd need to use renet without the bevy feature and put it in your own resource tho π€
Ah, there is renet feature...
I forgot about it
Won't be nice then
It can be hidden from users by creating structs with the same name and providing Deref, but it's hacky and could confuse people
I think it's fine. Be sure to include this run condition like I have in bevy_renet2: https://github.com/UkoeHB/renet2/blob/77cd4f0cff5bb2f173ea4345433d96c3aef2f207/bevy_renet2/src/lib.rs#L109
It's used in the client plugin: https://github.com/UkoeHB/renet2/blob/77cd4f0cff5bb2f173ea4345433d96c3aef2f207/bevy_renet2/src/transport.rs#L69
I mean fwiw it's called bevy_replicon_renet and not bevy_replicon_bevy_renet 
Alternatively you could fork renet, migrate it yourself and call it renet3 π
Will experiment with this then, I will ping you when the PR will be ready.
@echo lion @dire aurora opened: https://github.com/projectharmonia/bevy_replicon/pull/299
I think it's nice overall. But I not sure about using structs with the same name. After update users will confusing errors like RenetServer doesn't implement Resource. And they will have to change the import struct.
renet is not actively maintained and bevy_renet is often behind a Bevy version.
But since bevy_renet is very small (2 files), I decided to bundle it and depend on renet directly. The only downside ...
What do you think?
Maybe just use different names?
Like ServerEvent -> RenetEvent.
But not sure what to do with RenetServer/RenetClient.
But this could cause even more confusion. I probably more inclined to use the same names and export them to prelude...
@spring raptor if you do a cargo patch will it prevent publishing to crates?
Never tried it. But I doubt that it will work
Also forgot to mention, but I will apply the client condition in a separate PR, wanted to isolate it.
Sure
Guess not https://stackoverflow.com/a/69235309
Honestly I'm not sure what to do. If only there was no orphan rule..
Yeah, probably a wrapper is our best bet.
So you don't think it's a good solution ?
It's just a little janky. As long as someone doesn't include the Renet{Client,Server}Plugins in their app the compiler/crashing should help them migrate.
I think just do a wrapper around the client/server with Deref/DerefMut and that will be good enough.
Ah yeah the ServerEvent can be re-exported as RenetServerEvent.
The plugins will probably just work, I kept paths the same. But users will need to change paths for stuff like RenetServer. That's why I thinking about creating prelude (we don't have a prelude in renet integration) and moving wrappers into it.
The potential problem is users will have bugs from referencing the bevy_renet RenetServer/RenetClient when it should be bevy_replicon_renets. If they don't have the renet plugins then their code will crash due to missing resources.
It even won't compile, unless they enable renet feature directly.
I mean bevy feature on directly included renet crate
Hey Shatur, quick question, is there any way for a server to send a client event and have it added to its own client event queues?
I'm handling player input with a client system that gets input from leafwing and sends it with an event to the server for processing, but i want to have headed server (lan host) instances which can run this system as well. But when the event gets sent from the server it doesn't make it to the listeners. Do i have to get rid of the FromClient<> on my listener systemparameters?
Hi! By headed server (lan host) do you mean listen server? Where server is also a client.
Then all events should be available on listen server just like on a client. So if you send event ToClients<T> from server, it's not only avaiable as T on clients, but on the listen server as well. Similar for client events: you just send T on listen server and it appears locally as FromClient with ID == SERVER_ID.
Hmm, is setup any different for listen servers compared to normal servers (other than loading the client related game systems as well)
No difference :)
You slap client logic to your regular server and it just becomes listen server.
After experimenting with direct renet wrapping I summarized my thoughts on the problem in https://github.com/projectharmonia/bevy_replicon/issues/300
I think that 3(.1) is the best one, but I would appreciate any opinions on this topic.
hmm, is client_just_connected() ever true on a server?
No, no, use has_authority.
Maybe worth considering renaming to is_listen_server
Gotcha, tysm Shatur!
Now will be located in a separate repo.
Nothing else will change. This will allow us to draft new releases without waiting for renet compatibility with Bevy or disabling CI for this crate.
Drafted a release for the new RC.
How is replicon doing for syncing thousands of entities? I am thinking about whether I need determinism for my coop TD or can just brute force it :X
On my machine, replicating 1k entities with a 60-character string component takes only ~100ns (sending a bit faster and receiving a bit slower).
But it's a benchmark without renet layer. So testing in a real world scenario is needed to say for sure.
@plush sparrow oh, sorry, I meant to write ΞΌs, not ns. I typed from my phone.
Is something like bevy_replicon::server::VisibilityPolicy only available for whole entitites? I want to use it to only update entities that are in render distance. But I also want to only update an entities components for some clients. Imagine characters like in Rimworld with a whole bunch of stats and traits. In my game a player "owns" multiple characters like that and I want only that client to be able to see those stats, among other things. Is that something that bevy_replicon already supports or do I have to build something myself? Great crate btw, for my other needs it just works
Glad you like it!
Right now visibility is per-entity, yes. But we do plan to add per-component visibility with API similar similar to how collision layers work in physics engines.
As a temporary workaround you can split your character data across several entities. For example, you can store store the mentioned stats in a separate entity and reference the main entity from it.
Yeah I was thinking about that workaround. But I don't quite like it. For now I'll just not show the info client side.
When do you think that component based visibility API will be around? Anytime soon or should I expect some months?
Yeah, the workaround is not very nice :(
I wouldn't expect it very soon, but the feature is on my TODO. Alternatively, I can help you contribute this feature if you want. I will point you to places that you need to adjust.
Sure, I could give it a try
Great!
I started writing a quick code walkthrough, but realized that we probably need to talk about the API first π
The API was originally suggested by @dire aurora. The idea is to have component access levels via bitmasks like in physics engine. User define their meaning. Some examples:
- "Owner", "Party member", "Allied", "Neutral", "Hostile"
- "Very close", "Close", "Far away", "Very far away", "Extremely far away"
- "Friend", "Guild Member", "Alliance member", "Stranger"
To achieve this, we assign a mask to each component. Like "send this component only to owner and party members".
Now we need to assign this layers clients. I think that we need to turn hashsets for blacklists/whitelists into hashmaps that point to masks. So users enable/disable visibility for an entity and optionally set a mask for it.
Does it make sense?
Should this API be per component or per component group (i.e. per replication rule)?
Yea, I think I understand where you are going. Using masks sounds like a good idea
Haven't written Trait Extensions yet, but I'll figure it out
Great! I will continue writing a quick overview. But before you start working on it, let's wait for confirmation from @echo lion and @dire aurora about the idea :)
Sure. I was just reading the bevy_replication code a bit. Should I be on the 0.14.0-rc branch?
Yes, let's use rc branch that tracks latest Bevy rc, it's more up to date.
0.14 looks like it's just around the corner π
We have two kind of messages for replication:
- Init message. This message is for reliable channel and contains all insertions, removals, despawns, etc.
- Update message. This message is for unreliable channel and contains all component updates.
This separation is needed for bandwidth optimization. For component updates client care only about the last value, so if it fails to arrive, we always send the last value.
Both messages serialized sequentially and manually (also for optimization).
It's just a quick summary to give you the context, not related to the feature directly.
Here is the system that sends the data (note that I use rc branch): https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server.rs#L217
The system collects data into messages and sends them. Let's omit removals for now and focus on component changes only first.
It's called here: https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server.rs#L243
We iterate over all archetypes, then over entities and then over components. If you're not familiar, an archetype in Bevy stores all the entities for each set of components. Example: an archetype with all entities that have (A, B, C).
Here we cache visibility for the entity: https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server.rs#L340
And we use it later to detect if we need to send the entity or not.
To make iteration faster, we cache all archetypes. We do it here: https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server/replicated_archetypes.rs#L38
We iterate over all archetypes and determine which archetypes are replicated. So we iterate over these archetypes in the system I mentioned above.
You don't have to understand all the details, I just giving you the context :)
Here is what needs to be changed.
- Add the ability to register a mask per component or per component group. Depends on what we decide on the API.
- Add the ability to set mask for an entity inside
ClientVisibility. - When we cache archetypes, also store masks from 1 inside
ReplicatedComponent(here: https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server/replicated_archetypes.rs#L84). - When we cache visibility (https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server.rs#L340), we should also cache the mask (since we switched from hashset to hashmap)
- Here we should additionally check the mask: https://github.com/projectharmonia/bevy_replicon/blob/aefac55d099ddf26807a81e02ccf295e4f11f319/src/server.rs#L376
That's it :)
Feel free to ask as many questions as needed.
Yeah I have a question for 2.: That mask just for the component visibility or you want me to also implement masks for entity visibility?
I thinking about having a mask for the whole client. For example, you set something like 0b011 for ClientVisibility and all components that have these bits set will be considered visibile. Does it make sense?
Yeah okay. So the current VisibilityPolicy with Blacklist & Whitelist stays the same? Btw maybe they should be renamed to DenyList & AllowList or something, not wanting to get too political tho
Yeah, I would consider per-entity visibility as unrelated thing, but something that takes precedence. So if an entity is visible, then we consider its visibility components.
Fine with changing names, but this should be in a separate PR.
Wait, no, this won't be flexible. Per-client mask and global per-component mask is too limiting...
You thinking about having a mask for each client and component, so each client has a map of masks for each component or something? Maybe it's flexible enough when you can update the masks
Sorry for confusion, just trying to figure out the best API. I edited the message here: #1090432346907492443 message
Let me edit how implement it...
@low minnow edited the API description here (again): #1090432346907492443 message
And the proposed implementation here: #1090432346907492443 message
As you can see, there is one open question about the granularity. But I think that the idea in general is clear.
Could you open an issue with the text I wrote and wait for the response from @echo lion and @dire aurora. I would like to hear their opinions.
I just currently at work right now, so I can't open an issue myself π
Will do
@low minnow thanks!
For removals we will need additional logic, but we will back to it later... Maybe it worth removing "needs to be changed" at all, I probably started thinking about it too early π€
In the meanwhile I continue thinking about it. How to track changes in masks? Maybe also store the previous mask internally. So instead of the proposed hashmap with entity -> mask, we probably want something like entity -> (mask, mask).
Hmm I will think about it and leave a reply on the issue.
Do we need to keep RC versions in the changelog and Bevy compatibility table? π€
Probably don't need them in the table once the full version is out.
Sorry for a potentially very dumb question, but assuming the ClientId of a unsecured renet connection is the same, does the replication happen automatically upon re-connection? Or it has to be some kind of re-initialzation function?
- [x] make sure the server actually spawns the
WantReplicatedcomponent received from ClientEvent - [x] make sure it is not a mapping issue
- [x] make sure it is visible
Re-replication will happen, but you need to manually clear your previous scene.
There is also https://github.com/UkoeHB/bevy_replicon_repair if you want to keep original entities.
Appreciate it! Then I must be doing something else wrong. At the moment, I have a flashcard app that replicates the cards at the server, but upon relaunch, it fails to replicate the flashcards in the World at server.
Thanks for the speedy reply!
You can try debugging it by running with RUST_LOG=bevy_replicon=debug cargo run ...
There is also trace with more diagnostics, but you probably want debug.
In case someone also runs into a similarly shaped problem, in my case, I forgot to include the Replicate component with the bundle of thing I wanted replicated. It is silly, but my mental model was that app.replicate<Flashcard> was sufficient and it will ear mark all flashcards for duplication.
But the doc is clear on that
The component will be replicated if its entity contains the Replicated marker component```
@dire aurora @echo lion quick remainder about this issue: https://github.com/projectharmonia/bevy_replicon/issues/304
Could you write your opinions about it? I just want to make sure that I understand @dire aurora suggestion correctly and @echo lion also agree with this.
The functional description looks correct, idk about the implementation details tho. Using masks to blacklist seems a bit odd, and it doesn't really mention a technical solution beyond how it's stored π€
I planning to write the implementation details later once we agree on the API.
I mentioned masks and lists to clarify that each mask will be assigned per entity and per client. Am I understand your suggestion right?
Regardless blacklist, you mean that it would be more consistent if we ignore components that are set in a mask? For example, if we use blacklists and for entity X we have mask that means "Guild Member", we need to hide all components that can see a "Guild member"?
You set a mask per entity per client yes. I think I would just avoid the whole blacklist thing, it makes sense to say "this client is a guild member of this entity", but it's really weird to say "this client is everything except a guild member"
I also feel like generally the way things should work is: Allow/deny is set as a "default policy", then for each entity you can specify allow/deny/inherit for entity visibility. Meanwhile component visibility would be entirely separate from that besides being stored in the same per-client per-entity list
I like this suggestion. And in case of allow variant we could have store a mask. But will be a little weird to express "inherit"...
How useful is to have denylist in general? Maybe we could have only allowlist? Or will it be too limited?
For me it's hard to predict the best API because I don't need visibility control for my game π
Added my comment. Not sure about implementation details, but should be possible to not pay for per-component visibility when you don't want it (i.e. you just want per-entity).
I think per-component visibility is not needed for high-volume replicated entities, so it should be ok to be a little heavy-handed in the implementation.
@spring raptor #engine-dev message might be the bug you encountered in the example.
Ah, yes!
I had to report it :(
I'm not sure about this... Left my thoughts on it.
I think generally per-component visibility is not what we want. It wouldn't be fun to maintain the systems for this, and you lose the ability to do things like "Send this format if A, or this other format if B"
Could you elaborate?
The part of it being hard to maintain is mostly because you write your visibility logic based on groupings that would've matched layers, not components. You'd need to mark every component as visible or not visible from some centralized place, which is really annoying dependency wise. The alternative would be to split it up and do it seprately everywhere, or make yet another registration system to handle it for you.
As for the other part, groups and priority allow you to change how things are replicated, and having visibility based on groups would mean you can change the format for some clients but not others, your allies for example might get a few more details on your skill's stats than your enemies
Ah, when you write "per-component visibility", you mean visibility system based on ComponentId? I agree with you.
But I used this term to refer to any system that tweaks visibility on components, including groups. I more inclined to group-based visibility as well. I wrote my thoughts about it here: https://github.com/projectharmonia/bevy_replicon/issues/304#issuecomment-2212605711
So what's the best solution we have no to the bevy_renet not being migrated yet problem? π€
We moved bevy_replicon_renet into a separate repository to draft new releases for bevy_replicon independently.
Try using bevy_renet2. I remember that you have your custom transport, but maybe you can use just the regular netcode untill bevy_renet updates?
You can also try bevy_quinnet.
The author didn't separate features yet (https://github.com/Henauxg/bevy_quinnet/issues/25).
But it shouldn't be a problem, just keep both features enabled, like it was in 0.13 when we didn't have feature split.
I can't use the regular netcode, but I'd imagine bevy_renet2 doesn't break the ability to add custom transports π€
Yes, I only suggested the netcode becuase you may need to port your transport.
But maybe the porting will be trivial.
@low minnow I have outlined a complete description and implementation details for component visibility in this message: https://github.com/projectharmonia/bevy_replicon/issues/304#issuecomment-2217522473
I think it's pretty complicated for a beginner, so I think it would be best for me to implement it :D I'm quite busy right now, but I will put it on my TODO list.
If anyone want to try to implement it - I can't stop you and will definitely help or answer any questions about the implementation.
Seems porting was mostly doable, except that renet2 refers to a lot of crates by the bevy_* name which means I needed to add a whole bunch of them to my patch.crates-io (becuase I need to pull in my dfq-0.14 branch)
Gotta love how confusing the errors from bevy get when it secretly uses two different crates ... Guess it's a good thing we didn't do that using renet directly hack π
Oh yeah, I understand your pain: https://github.com/projectharmonia/project_harmonia/blob/75d65af0fd45a10c3fe188ea75e169d67e1888c4/Cargo.toml#L50
@echo lion highly suggest to avoid depending on bevy_* crates. It makes it very inconvenient to patch :(
And it's something that many people do.
Usually the only ones I'd expect to patch are bevy_app, bevy_ecs, bevy_reflect, and bevy_math, since those are usable standalone
Hmm what's the problem exactly? I haven't had to patch anything so not sure what the issue is.
bevy_replicon_renet2 has a dep on bevy with default-features = false, and bevy_renet2 has deps on app/ecs/time/window subcrates.
If you use bevy you just have to patch the bevy crate, but if crates use a subcrate directly you also have to patch them
Overlap a few crates that use different subcrates and you end up with a list that contains every bevy crate ... And of course the errors are usually pretty abstract
IIRC it came from this PR https://github.com/lucaspoffo/renet/pull/104
I kind of prefer having minimal deps especially for headless servers
Ah yes, that matches, the ones I had to add were bevy_app and bevy_time
I wonder what deps you actually save by not using bevy directly π€
I guess you can drop a few things bevy doesn't make optional because they're super common or not all that heavy ... log and hierarchy for example ... I mean I don't actually use hierarchy in my server so I guess that one is sort of reasonable actually
Looks like I can remove that bevy_window dependency. *Done
The problem with bevy_renet2 which uses app/ecs/time subcrates.
If there is any bug in Bevy and I need to apply a patch, I have to patch these subcrates :(
Just take a look at #1090432346907492443 message π
(caused by other crates doing it in some of my pre 0.13.1 commits)
I wonder if there's any plans on the cargo side to fix cases like this ... Maybe namespaces could be relevant somehow π€
Isn't this mainly a copy-paste job for the patch lines? And a little headache identifying compilation errors.
It's definitely not ideal but I think the reduced build footprint is higher priority unfortunately.
It's sometimes hard to identify what crates do you need to patch. If a bug was in crate bevy_x, you need patch bevy and all other crates that in dependencies of bevy_x
You save a single bevy package with re-exports, I doubt that it improves the compilation speed by a lot... But it's up to you, of course.
@spring raptor would you consider having this system run in a lifecycle schedule (pre/update/post) with .run_if(resource_added::<RepliconClient>) rather than on Startup? https://github.com/projectharmonia/bevy_replicon/blob/master/src/client.rs#L54 it would be nice for my application to insert ClientPlugin and decide for itself when to enable has_authority for example by removing/reinserting RepliconClient alongside e.g. RenetClient. (if there is a workaround you had in mind let me know!)
You don't need to remove or insert RepliconClient, it should always be present, otherwise it can break things.
It's inserted automatically by ClientPlugin
Is there a replicon crate out there for async request/response?
Or should I make it tonight?
Not that I'm aware of. We usually use events for it.
But yes, you can create yours :)
You mean async as in "spans multiple frames"?
If you want it to be async as in "no frame-based delays" the transports are really going to fight you π€
Async as in the language feature
Yeah I'd just build it around events for sure. The thing is I run into scenarios where I send an event and then want a response, so my thought was just to add a thin wrapper where you could register a request/response, which would register two channels and assign a monotonically increasing counter so you could tell which response corresponds to which request. Then you slap an async API on top and you can just request(MyRequest).await. Tbh feels a bit roundabout to build something like that when replicon itself is built on top of something with arguably better support for that but π€·
Well there's a lot of stuff you can use async for. You can make it run a system across multiple frames, or make it do something faster than having to wait for a frame
Fair point π yes I want it to span multiple frames
Try it out and see :)
I'm actually really liking how the API is turning out:
fn open_pod_bay_doors(mut requests: RequestHandler<OpenPodBayDoors>) {
requests.handle_requests(|_request| Err(MutinyError));
}
Not sure if I understand what is happening
Probably custom system param wrapping eventreader and a resource that tracks requests.
Just an event writer and a reader
in RepliconChannel there is a resend_time. by default it's a Duration::ZERO I believe. what is the best way to figure out what to set this value to?
I'm thinking that the resend time should probably vary with the connection RTT rather than being a fixed value
RFC 9002 describes an algorithm for finding this PTO value actually: https://www.rfc-editor.org/rfc/rfc9002.html#name-computing-pto
This document describes loss detection and congestion control mechanisms for
QUIC.
6.2.1. Computing PTO
I would keep it default
Probably, it's just a hint for the transfer. Transfer can ignore it.
makes sense
is there any benefit to having different channels have different resend times?
You can use bigger resend time for non-sensetive data.
In renet you can set it like this, so I just allow to configure it.
Yes, originally replicon was coupled with renet.
@dire aurora 2 weeks ago I mentioned the filter PR in #ecs-dev to get attention, and your last comment was even upvoted by Alice, but I'm afraid it will be stuck for a while.
Have you considered releasing a version with entity disabling under a feature? Users who need it, can activate it and patch Bevy.
I think the community will benefit from your work. No other crate with rollback handles entity disabling and I think it's a situation where "the perfect is the enemy of the good" π
If your crate gains some popularity, I think it may draw additional attention to the problem.
Hmmm, that might be possible, if I feature flag disabling it would at least be buildable and possible to make a crate out of it π€
Currently I think it's probably stuck on fixing that unsoundness with sparse filters
Which is luckily no longer a thing specific to the PR but existing undefined behavior that can be triggered trough safe APIs
It's pretty hard to do rollback correctly without entity disabling or some ugly workaround tho. That would require essentially never spawning new entities in the simulation
Yeah. We even have the same bug in dynamic queries: https://github.com/bevyengine/bevy/issues/14348
If they find a solution, it potentially can be applied to your PR as well.
If this is solved correctly I should only need to rebase π€
What if you do a regular despawn and spawn during rollback and just rely on hooks?
Never mind, systems still can be triggered
Yeah, it's hard.
You also can't really do any despawns in general, since all the references to entities break
Yep
And I think that's what other crates do right now.
So I would just mention this in the readme to make people aware about the Bevy limitation.
And other similar crates as well*
I'd imagine the only crate that might handle spawning/despawning correctly would be ggrs π€
It's so easy to fall into the trap of just not handling it because some games don't use it yet, and then once you need it it's not supported so you just work around it
Exactly, most people even don't know about this issue.
How they avoid it?
You delay despawns until they fall out of history, and accept the weird side effects of spawning
And avoid creating X as Entities patterns, since those will introduce more spawns/despawns
Ah, interesting
I also need to make a better API for passing in the ticks, both the input queue stuff and rollback still rely on the tick for bundlication iirc, but with the current API it should probably just be a resource passed to the plugin when adding it
I haven't touched those two crates since I started on this crate tho https://github.com/NiseVoid/bevy_march
Writing a raymarcher has been a lot more work than I anticipated π
Sounds good. Let me know if you need anything from my side, but I think all suggestions related to ticks were addressed.
Yeah, I can imagine π
@echo lion could you quickly review https://github.com/projectharmonia/bevy_replicon_renet/pull/4 ?
You will need to apply the same changes to bevy_replicon_renet2.
Just published a new version of bevy_replicon_renet that supports the latest versions of replicon and renet.
Released a new version of snap, now that bevy_replicon_renet is up to date π
https://github.com/Bendzae/bevy_replicon_snap
Great!
I would suggest to add features client and server as I did in the latest bevy_replicon. It's useful for games with dedicated servers.
And highly suggest to avoid adding bevy_replicon_renet dependency, your logic shouldn't depend on the used backend.
Shouldn't interpolation only be used in a client in the first place? π€
Tho in that case it would probably still need to disable default features
if I want to have different connection states or phases in replicon, what's the solution to that? for example, an initial setup phase where a client sends its username and config to the server, then the connection transitions to playing phase, and actual component replication etc. is performed
Ah, right, only client feature then.
When the feature is disabled, replicate_interpolated behaves like replicate. Just to avoid calling different functions on server and client.
Are you developing a messaging backend or a game?
a game
You can control it with entity visibility.
I assume that what you planning to replicate is based on what the client can see.
I mean, when the connection starts, nothing gets replicated. Like at that stage it's basically just RPC calls to set up the connection. Then eventually it transitions to an actual playing phase, and that's when replication starts
is that possible?
Ah, I used to have that in bundlication but I'm pretty sure replicon doesn't have that. There's no "start replication now" signal
But you can work around it with visibility ofc
that feels really hacky to me
I might have to write this at the transport layer itself
but then I would have to renegotiate lanes aaaughhh
Why do you need to hold the replication?