Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visibility layers for Minecraft Entities #362

Closed
wants to merge 18 commits into from

Conversation

Bafbi
Copy link
Contributor

@Bafbi Bafbi commented Jun 11, 2023

Ref

#342

Description

I use the word 'mob' to reference minecraft entities and client for the player.

Implementation of a layer system for mob, the client can have a ClientLayerMask component which determine for what layer he can see mobs. The mobs them can have a Layer component which determine on what layer they are.

Implementation

  • Client see only update for mask layer
  • Spawn or Sync mob status when arriving on a new layer
  • Despawn mob when quitting a layer
  • Don't spawn in not in visible layer at connection
  • Having the possibility to have infinite numbers of layers

Potential Upgrade

  • Not have one buffer per layer
  • Extend layers to particle and other
  • Make the layer component be able to have the ref of an client to not use a layer per client when we just need to sned to one player

Playground

code
use std::f64::consts::TAU;

use glam::{DQuat, EulerRot};
use valence::client::despawn_disconnected_clients;
use valence::client::layer::ClientLayerSet;
use valence::client::message::ChatMessageEvent;
use valence::entity::Layer;
use valence::nbt::compound;
use valence::network::ConnectionMode;
use valence::prelude::*;

#[allow(unused_imports)]
use crate::extras::*;

type SpherePartBundle = valence::entity::cow::CowEntityBundle;

const SPHERE_CENTER: DVec3 = DVec3::new(8.0, SPAWN_POS.y as f64 + 2.0, 8.0);
const SPHERE_AMOUNT: usize = 200;
const SPHERE_MIN_RADIUS: f64 = 6.0;
const SPHERE_MAX_RADIUS: f64 = 12.0;
const SPHERE_FREQ: f64 = 0.5;

const SPAWN_POS: BlockPos = BlockPos::new(8, 100, 8);

/// Marker component for entities that are part of the sphere.
#[derive(Component)]
struct SpherePart;

pub fn build_app(app: &mut App) {
    app.insert_resource(NetworkSettings {
        connection_mode: ConnectionMode::Offline,
        ..Default::default()
    })
    .add_plugins(DefaultPlugins)
    .add_startup_system(setup)
    .add_system(init_clients)
    .add_system(update_sphere)
    .add_system(set_layer)
    .add_system(despawn_disconnected_clients)
    .add_system(toggle_gamemode_on_sneak.in_schedule(EventLoopSchedule));
}

fn setup(
    mut commands: Commands,
    server: Res<Server>,
    biomes: Res<BiomeRegistry>,
    dimensions: Res<DimensionTypeRegistry>,
) {
    let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);

    for z in -5..5 {
        for x in -5..5 {
            instance.insert_chunk([x, z], Chunk::default());
        }
    }

    for z in 2..14 {
        for x in 2..14 {
            instance.set_block([x, SPAWN_POS.y, z], BlockState::GLASS);
        }
    }

    instance.set_block(
        [SPAWN_POS.x, SPAWN_POS.y + 1, SPAWN_POS.z],
        Block::with_nbt(
            BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_1),
            compound! {
                "Text1" => "Set your layers".into_text(),
                "Text2" => "in chat, send => 0 1 2".into_text(),
                "Text3" => "set your layers to 0, 1, 2".color(Color::GREEN),
            },
        ),
    );
    instance.set_block(
        [SPAWN_POS.x + 1, SPAWN_POS.y + 1, SPAWN_POS.z],
        Block::with_nbt(
            BlockState::OAK_SIGN.set(PropName::Rotation, PropValue::_4),
            compound! {
                "Text1" => "Type in chat:".color(Color::RED),
            },
        ),
    );

    let instance_id = commands.spawn(instance).id();

    let mut i = 0;
    commands.spawn_batch([0; SPHERE_AMOUNT].map(|_| {
        i += 1;
        (
            SpherePartBundle {
                location: Location(instance_id),
                ..Default::default()
            },
            SpherePart,
            Layer(i % (SPHERE_AMOUNT / 50) as u8),
        )
    }));
}

fn init_clients(
    mut commands: Commands,
    mut clients: Query<(Entity, &mut Location, &mut Position, &mut GameMode), Added<Client>>,
    instances: Query<Entity, With<Instance>>,
) {
    for (entity, mut loc, mut pos, mut gamemode) in clients.iter_mut() {
        loc.0 = instances.single();
        pos.set(SPHERE_CENTER);
        *gamemode = GameMode::Creative;
        commands.entity(entity).insert(ClientLayerSet::new(vec![0]));
    }
}

fn set_layer(
    mut clients: Query<(&mut ClientLayerSet, &mut Client)>,
    mut chat_message_event: EventReader<ChatMessageEvent>,
) {
    for event in chat_message_event.iter() {
        // Split the message into individual words
        let words: Vec<&str> = event.message.split_whitespace().collect();

        // Parse each word into a layer number
        let layers: Vec<usize> = words.iter().filter_map(|word| word.parse().ok()).collect();

        if let Ok((mut layer_set, mut client)) = clients.get_mut(event.client) {
            layer_set.clear();
            // Toggle true each layer in the set
            for layer in layers {
                layer_set.set(layer as u8, true);
            }

            // Update the client's action bar with the new layer set
            client.set_action_bar(format!("Layers: {:?}", layer_set).into_text());
        }
    }
}

fn update_sphere(
    settings: Res<CoreSettings>,
    server: Res<Server>,
    mut parts: Query<(&mut Position, &mut Look, &mut HeadYaw), With<SpherePart>>,
) {
    let time = server.current_tick() as f64 / settings.tick_rate.get() as f64;

    let rot_angles = DVec3::new(0.2, 0.4, 0.6) * SPHERE_FREQ * time * TAU % TAU;
    let rot = DQuat::from_euler(EulerRot::XYZ, rot_angles.x, rot_angles.y, rot_angles.z);

    let radius = lerp(
        SPHERE_MIN_RADIUS,
        SPHERE_MAX_RADIUS,
        ((time * SPHERE_FREQ * TAU).sin() + 1.0) / 2.0,
    );

    for ((mut pos, mut look, mut head_yaw), p) in
        parts.iter_mut().zip(fibonacci_spiral(SPHERE_AMOUNT))
    {
        debug_assert!(p.is_normalized());

        let dir = rot * p;

        pos.0 = SPHERE_CENTER + dir * radius;
        look.set_vec(dir.as_vec3());
        head_yaw.0 = look.yaw;
    }
}

/// Distributes N points on the surface of a unit sphere.
fn fibonacci_spiral(n: usize) -> impl Iterator<Item = DVec3> {
    let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0;

    (0..n).map(move |i| {
        // Map to unit square
        let x = i as f64 / golden_ratio % 1.0;
        let y = i as f64 / n as f64;

        // Map from unit square to unit sphere.
        let theta = x * TAU;
        let phi = (1.0 - 2.0 * y).acos();
        DVec3::new(theta.cos() * phi.sin(), theta.sin() * phi.sin(), phi.cos())
    })
}

fn lerp(a: f64, b: f64, t: f64) -> f64 {
    a * (1.0 - t) + b * t
}

rj00a pushed a commit that referenced this pull request Jun 15, 2023
## Description

Basic implementation of world border
World border is not enabled by default. It can be enabled by inserting
`WorldBorderBundle` bundle. Currently, this PR only implements world
borders per instance, I'm considering expanding this per client.
However, the same functionality can be achieved by Visibility Layers
#362

<details>
<summary>Playground:</summary>

```rust
fn border_controls(
    mut events: EventReader<ChatMessageEvent>,
    mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With<Instance>>,
    mut event_writer: EventWriter<SetWorldBorderSizeEvent>,
) {
    for x in events.iter() {
        let parts: Vec<&str> = x.message.split(' ').collect();
        match parts[0] {
            "add" => {
                let Ok(value) = parts[1].parse::<f64>() else {
                    return;
                };

                let Ok(speed) = parts[2].parse::<i64>() else {
                    return;
                };

                let Ok((entity, diameter, _)) = instances.get_single_mut() else {
                    return;
                };

                event_writer.send(SetWorldBorderSizeEvent {
                    instance: entity,
                    new_diameter: diameter.diameter() + value,
                    speed,
                })
            }
            "center" => {
                let Ok(x) = parts[1].parse::<f64>() else {
                    return;
                };

                let Ok(z) = parts[2].parse::<f64>() else {
                    return;
                };

                instances.single_mut().2 .0 = DVec2 { x, y: z };
            }
            _ => (),
        }
    }
}
``` 
</details>

example: `cargo run --package valence --example world_border`
tests: `cargo test --package valence --lib -- tests::world_border`


**Related**
part of #210
@Bafbi Bafbi marked this pull request as ready for review June 17, 2023 00:00
@dyc3 dyc3 changed the title Visibility layer Visibility layers for Minecraft Entities Jun 17, 2023
Copy link
Collaborator

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some unit tests for this.

crates/valence_client/src/layer.rs Outdated Show resolved Hide resolved
crates/valence_client/src/layer.rs Outdated Show resolved Hide resolved
crates/valence_client/src/layer.rs Outdated Show resolved Hide resolved
crates/valence_client/src/layer.rs Outdated Show resolved Hide resolved
@dyc3 dyc3 mentioned this pull request Jun 17, 2023
@rj00a
Copy link
Member

rj00a commented Jun 17, 2023

Currently working on a redesign of Instances and Chunks to fix a number of issues. I'd like to finish my redesign before taking a closer look at this, since it may have a large impact on the approach taken here. It should be finished in the next day or two.

@tachibanayui
Copy link
Contributor

Currently working on a redesign of Instances and Chunks to fix a number of issues. I'd like to finish my redesign before taking a closer look at this, since it may have a large impact on the approach taken here. It should be finished in the next day or two.

Visibility Layers will impact a lot of features IMO. I think we should take our time and make it right the first time or it gonna be expensive to fix later.

Copy link
Contributor

@tachibanayui tachibanayui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on implementing Visibility Layers. I have a few suggestions and design choices to consider.

crates/valence_client/src/layer.rs Outdated Show resolved Hide resolved
crates/valence_client/src/lib.rs Outdated Show resolved Hide resolved
crates/valence_client/src/lib.rs Outdated Show resolved Hide resolved
crates/valence_client/src/lib.rs Outdated Show resolved Hide resolved
crates/valence_client/src/lib.rs Outdated Show resolved Hide resolved
crates/valence_instance/src/lib.rs Outdated Show resolved Hide resolved
/// Example: you have `Layer(0)` on a cow, it will only be visible to clients
/// with the access to layer 0 in their `ClientLayerSet`.
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
pub struct Layer(pub LayerType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an Entity be on multiple layers? It could be useful for interactions between different entities such as collisions, ai behaviors, hit-scanning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know for this one, cause the use of the layer is primarily for visibility, and having entity on multiple layer pose question like if the entity and client have two layers in common, what append ?
I think that maybe we can rename this one visibility layer and there could be other layer types for collisions and ai.
But even that I don't really see the use case ngl.

@rj00a rj00a mentioned this pull request Jun 28, 2023
56 tasks
@rj00a
Copy link
Member

rj00a commented Jul 1, 2023

Finished the chunk/instance redesign in #402. Let me know if you have any issues.

@rj00a rj00a mentioned this pull request Jul 26, 2023
5 tasks
@rj00a
Copy link
Member

rj00a commented Jul 26, 2023

Here's my take on visibility layers: #424.

rj00a added a commit that referenced this pull request Aug 2, 2023
# Objective

The objective of this PR is to solve the following problems with a
holistic redesign of instances.

Closes #342, #362, but deviates greatly from the solution described in
the issue.

- If an entity is within the view distance of a client, packets for that
entity are sent to the client unconditionally. However, there is often a
need to limit the visibility of entities to certain clients depending on
the situation. Some examples:
- You have a minigame with spectators and players. Spectators can see
both players and spectators, but players can only see players and _not_
spectators.
- An entity or set of entities is used as a makeshift GUI, only visible
to the client using the GUI.
- The server has many separate "arenas" that have identical chunk data,
and you wish to share the chunk data across arenas to save memory.
- You want to make a player truly invisible and potentially have some
other entity take its place.

It is possible to work around the problem by using invisibility effects
or sending packets manually. But these are hacky solutions that are
incomplete, inefficient, and inconvenient.
- Updating clients involves looking up every chunk in the client's view
distance every tick for every client. This wastes CPU time and doesn't
scale well with larger view distances[^1].
- Sometimes we want to broadcast packets to all clients matching some
condition. Conditions could include...
  - If a chunk position is within the client's view.
  - If the client is within a certain radius of some position.
  - If the client is not some specific client (self exclusion).
  
It isn't really possible to add all of these conditions in an efficient
way using the current design.

# Solution

Split the existing `Instance` component into two new components:
`ChunkLayer` and `EntityLayer`. Chunk layers contain all of the chunks
in a world along with some global configuration, like the world's
dimension. Entity layers contain Minecraft entities. A `LayerBundle`
containing both is provided for convenience. Both `ChunkLayer` and
`EntityLayer` implement the common `Layer` trait.

The key idea is this: Clients can only view one chunk layer[^2], but can
view any number of entity layers. These are the `VisibleChunkLayer` and
`VisibleEntityLayers` components respectively. The client will receive
entity packets from only the layers it can see. Clients can add and
remove entity layers at any time to spawn and despawn the entities in
the layer.

Every layer contains a "message buffer" for broadcasting information to
all viewers. Every message contains a "key" and a payload of bytes (its
meaning depends on the message). Clients walk through the list of
messages and use this to update themselves.

There are a few things done to make this faster:
- Message processing is parallelized over all clients.
- The messages are sorted by their "key" and then deduplicated by
merging payloads together. For instance, messages sending packet data on
the condition that a certain chunk position is in view will be merged
together if the chunk position is the same. Now there's only one memcpy
instead of two.
- Messages are split into two categories: "global" and "local". Local
messages are those that have some spatial condition to them. Local
messages are put in a bounding volume hierarchy so that large swaths of
messages do not need to be examined by clients.

[^1]: At least the work is completely parallelized ¯\\\_(ツ)\_/¯.
[^2]: Viewing multiple chunk layers has some technical problems and I
don't see it as a design worth pursuing.

---------

Co-authored-by: Carson McManus <[email protected]>
@rj00a rj00a closed this Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants