valence-rs / valence

A Rust framework for building Minecraft servers.
http://valence.rs/
MIT License
2.81k stars 145 forks source link

Visibility Layers #342

Closed rj00a closed 1 year ago

rj00a commented 1 year ago

Describe the problem related to your feature request.

Sometimes it's necessary to limit what information sets of clients can receive. Here are some example scenarios:

  1. You have a game with players and spectators. Spectators should see other spectators and players, but players should only see other players and not the spectators.
  2. You have two teams: Red and Blue. Everyone on the same team will "glow" and can be seen through walls. But players on the other team will not glow and cannot be seen through walls.

What solution would you like?

Create a new component called LayerMask (name up for debate)

#[derive(Component)]
pub struct LayerMask(pub u64);

LayerMask is simply a bitmask where each bit represents a layer and whether or not it is active. LayerMasks are attached to entities and determine what layers the entity is active on.

Next, we have ClientLayerMask (name up for debate)

#[derive(Component)]
pub struct ClientLayerMask(pub u64);

This is also a bitmask, but it determines what layers a client can see. ClientLayerMasks are attached to clients only.

Entity visibility is not the only thing we want to control with layers. For instance, if an entity is meant to emit a particle effect, the particles should probably follow the same visibility rules as the entity. So a hypothetical API for emitting particles (or sounds, block events, etc.) should look something like this:

pub struct PlayParticleEvent {
    pub instance: Entity,
    pub particle: Particle,
    pub position: DVec3,
    pub layers: LayerMask, // <---
}

Implementation Details

We want to continue to cache packets in per-chunk and per-instance buffers. Therefore, we need one packet buffer per layer wherever we previously had just one.

Vec<u8> // Old
[Vec<u8>; 64] // New

(Instead of a [T; 64], we could use SmallVec<[T; 4]>. The smallvec would grow on demand. But maybe there's an even better way...)

When clients go to read the packet buffers, they use their ClientLayerMask to determine which buffers to read from.

Questions

What alternative(s) have you considered?

You can sometimes send packets manually to achieve the same effect, but that's fraught with inefficiencies and errors.

Additional context

Unity's documentation on layers: https://docs.unity3d.com/Manual/use-layers.html?

dyc3 commented 1 year ago

I think this could technically be achieved already if you use multiple instances, but there would definitely be some double serializations.

Perhaps we could have the visibility layers be per instance. What if we had a builder-like api to be able to specify what layers are allowed to see which packets?

For example, spawning a particle effect (taken from the particle example):

instance.play_particle(particle, true, pos, offset, 0.1, 100); // global to instance
instance.layer(0b1).play_particle(particle, true, pos, offset, 0.1, 100); // local to layer 1, parameter is a bitmask

Alternatively, we could have the parameter not be a bitmask, and limit users to only apply changes to layers one at a time, which would be a little easier to implement and harder for users to mess up.

Also, I think we should limit the scope for now and leave blocks and block entities out, at least for the initial implementation.

jivvy commented 1 year ago

64 layers might be too few for certain applications, for example, if you had a game where you have players in parties of 3-4 and parties have a trail of particles to follow, etc

tachibanayui commented 1 year ago

64 layers might be too few for certain applications, for example, if you had a game where you have players in parties of 3-4 and parties have a trail of particles to follow, etc

Yeah imo 64 might not be enough for uhc, battle royale or open world mmorpg where there could be many parties. Maybe we should use a hashset (hashmap for cache) instead of bitmask?

tachibanayui commented 1 year ago

The solution described doesn't fully account for scenario (2). Because "glowing" is part of the entity's metadata, you'd need to create two identical entities and put each on a separate layer to get the desired behavior. Can we fix that?

Maybe we could use an EntityLayers component:

#[derive(Component)]
pub struct EntityLayers(Vec<Entity>);

When processing an entity with EntityLayers component, it will iterate the Vec<Entity> with LayerMask component (Maybe 0b1 if LayerMask is not attached) and combine components (add/replace) with the base entity on that layer mask. Here's an example using EntityLayer to glow teammate when the game starts.

#[derive(Component)]
pub struct Team {
    layerMask: u64,
    color: i32,
}

fn glow_teammate(
    mut players: Query<(&Team, &mut EntityLayers)>,
    mut event: EventReader<GameStart>,
    mut commands: Commands,
) {
    for _ in event.iter() {
        for (team, mut em) in players.iter_mut() {
            let mask_entity = commands
                .spawn((LayerMask(team.layerMask), GlowColorOverride(team.color)))
                .id();

            em.0.push(mask_entity);
        }
    }
}