Open dw-rs opened 3 months ago
Thanks, edited with my recent thoughts I added in Discord :)
Hmm I think the most flexible API would be:
ClientVisibility::set_visibility(entity, true)
gives full visibility of an entity.ClientVisibility::set_component_visibility(entity, component_id, true)
gives visibility of only that component.For implementation, we need to be careful about whitelist vs blacklist, and what happens if you do set_visibility()
and then set_component_visibility()
(and then set_visibility()
again).
ClientVisibility::set_component_visibility(entity, component_id, true) gives visibility of only that component.
So you suggesting to have per-component toggle instead of per-group? Like no "Owner", "Party member", just set ComponentId
?
I think that people who need per-component visibility could use it often and it will be costly for them. It will require a lookup for each component if component-based visibility is active on an entity.
What do you think about something like this? 2 policies: All
and List
.
Inside List
we will have default_visibility
which is a mask (can be set only on plugin init).
And then user can override visibility like this:
client_visibility.visibility_mut(entity) |= GUILD_MEMBER;
client_visibility.visibility_mut(entity) ^= PARTY_MEMBER;
And by default we define VISIBLE
and HIDDEN
with all 1 and all 0 respectively.
It should be quite cheap to check, instead of HashSet
we will have a HashMap
.
Inside List we will have default_visibility which is a mask (can be set only on plugin init).
Honestly I'm confused how this would work. You have a mask on the entity, a mask on the client, and a mask registered per component..?
No, no, in the snipped above you don't set a mask on a client. Only on component and on entity. Not entirely sold on this idea, just thinking out loud.
No, no, in the snipped above you don't set a mask on a client. Only on component and on entity. Not entirely sold on this idea, just thinking out loud.
Isn't the goal to have different components visible to different clients? So wouldn't clients need some associated info to do that filtering?
You configure masks for entities inside ClientVisibility
which is client-specific.
So you don't set a mask for a client, but set a mask for each entity inside client's ClientVisibility
.
Ok that makes sense, basically setting the client's visibility permissions per-entity. And then a component group-based lookup to get permission requirements when replicating an entity's contents.
Also, I think it's fine to continue replicating empty entities if a client has visibility permissions for an entity that don't intersect with any of the entity's components.
And then a component group-based lookup to get permission requirements
Yes, and the additional lookup should be cheap since it could be index-based.
Also, I think it's fine to continue replicating empty entities if a client has visibility permissions for an entity that don't intersect with any of the entity's components.
You are right! Then we should have component and entity visibility separate. Like you suggested in https://github.com/projectharmonia/bevy_replicon/issues/304#issuecomment-2212052252, but with groups.
Let's summarize.
Component visibility will be separate from entity visibility and implemented in the form of groups. After registering a component, the user can assign a visibility mask to it like this (via an extension trait for App
):
AppVisibilityExt::set_visibility_mask::<C: Component>(mask: u32) { // ... }
Usage example:
const GUILD_MEMBER: u32 = 0b1;
app.set_visibility_mask::<C>(GUILD_MEMBER);
This means that C
will be visible to entities that have GUILD_MEMBER
set.
By default, all components are visible to all replicated entities (i.e., all entities have a default mask of all 1's). However, the user will be able to override it:
ClientVisibility::set_component_visibility(entity: Entity, mask: u32) { } // Set groups for specific entity
ClientVisibility::set_default_component_visibility(mask: u32); // Override the default all 1's.
Usage example:
client_visibility.set_component_visibility(entity, GUILD_MEMBER);
If an entity is considered visible but all its components are hidden, an empty entity will be replicated. If an entity is hidden, it won't be replicated even if all its components are visible. Therefore, entity visibility takes priority.
To store component groups, we need to introduce a separate resource that will be used by the aforementioned AppVisibilityExt
. Let's call it VisibilityGroups
for now. It will contain a ComponentId
-> u32
mask hashmap.
ClientVisibility
API to add component groupsCurrently, visibility lists use a hashset to store entities. To implement this feature, we will need to use a HashMap and store the current mask and the mask from the previous tick (for change detection) as values. The set_component_visibility
method will update the hashmap.
For the blacklist, an additional lookup will be required because it only stores hidden entities, while we need to look at visible entities and their masks. I would consider removing the blacklist and keeping only the whitelist (and renaming it to just "list" maybe). I think it makes things easier to work with for both users and third-party crates (for example, bevy_replicon_attributes
only supports the whitelist).
We iterate over the world for replication and use our cached archetypes. This is where we can cache our component groups as well to avoid a hashmap lookup. When we update archetypes, we will perform a lookup into the VisibilityGroups
resource for each component and store the mask into the newly added visibility_group
field.
The function:
https://github.com/projectharmonia/bevy_replicon/blob/cabebab271d5c99601645a62b45eff0d11427eee/src/server/replicated_archetypes.rs#L38
Here is where the caching is done:
https://github.com/projectharmonia/bevy_replicon/blob/cabebab271d5c99601645a62b45eff0d11427eee/src/server/replicated_archetypes.rs#L83-L87
The struct that will have the additional visibility_group
field:
https://github.com/projectharmonia/bevy_replicon/blob/cabebab271d5c99601645a62b45eff0d11427eee/src/server/replicated_archetypes.rs#L124-L128
After adding the necessary API to ClientVisibility
, we will need to add an additional check to see if a component is visible here, in addition to the entity check:
https://github.com/projectharmonia/bevy_replicon/blob/cabebab271d5c99601645a62b45eff0d11427eee/src/server.rs#L375-L378
Then, in the same function, we need to check if any new component became visible on an entity here: https://github.com/projectharmonia/bevy_replicon/blob/cabebab271d5c99601645a62b45eff0d11427eee/src/server.rs#L415-L419
We cache removals because the information about the removal may not survive until the next network tick, and we need them grouped by entity. It will be similar to component removals: a lookup into ClientVisibility
and store the group in the map:
https://github.com/projectharmonia/bevy_replicon/blob/dd85822fc845e4661c92c3851b31af507ebb1dbc/src/server/removal_buffer.rs#L103-L106
This will require an additional check here to ensure that the entity is visible to a client: https://github.com/projectharmonia/bevy_replicon/blob/dd85822fc845e4661c92c3851b31af507ebb1dbc/src/server.rs#L508-L517
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.
LGTM, will require a lot of careful testing as usual.
I came up with a way to support this in bevy_replicon_attributes
.
Currently each client can be assigned 'attributes' which are just tags. Then entities an be given 'visibility conditions' which evaluate against client attributes to determine visibility.
To support component-level visibility granularity, we can allow users to assign multiple visibility conditions to an entity for different component masks. Then to compute the aggregate mask, evaluate all visibility conditions against the client attributes and XOR the masks assigned to ones that evaluate true.
For example, an entity might have components {A, B} are visible if Team(1) OR IsSpectator
and components {C} are visible if Player(2)
. Where components {A, B}
and components {C}
represent two separate masks.
Sounds great!
I'd like the ability to not only be able to define which entities are visible to specific clients (possible with the bevy_replicon::server::VisibilityPolicy) but also be able to define what components are visible for each client. An example: A client can see the stats of his own troops but not the ones of his enemies.
@Shatur and I had a discussion on bevy discord in the the bevy_replicon ecosystem-crates channel. Here is a little summary from what Shatur wrote: The API was originally suggested by @NiseVoid. The idea is to have component access levels via bitmasks like in physics engine. User define their meaning. Some examples:
To achieve this, we assign a mask to each component. Like "send this component only to owner and party members". And assign masks to client entities.
To achieve this we can turn hashset for whitelist policy into a hashmap and for blacklist policy add additional hashmap. Both hashmaps will map entity to its mask for a client. We also need to store last processed value into order to detect changes. So the map will be entity -> (mask, mask).
Should this API be per component or per component group (i.e. per replication rule)?
@UkoeHB and @NiseVoid what do you think of this proposal?