Skip to content

Commit

Permalink
Merge pull request #179 from cBournhonesque/cb/0.12.0
Browse files Browse the repository at this point in the history
0.12.0 release
  • Loading branch information
cBournhonesque committed Mar 13, 2024
2 parents 42eb069 + d35eded commit 5077a58
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 68 deletions.
55 changes: 32 additions & 23 deletions book/src/tutorial/basic_systems.md
@@ -1,9 +1,9 @@
# Adding basic functionality


## Initialization

On the client, we can run this system on `Startup`:

```rust,noplayground
pub(crate) fn init(
mut commands: Commands,
Expand All @@ -20,14 +20,13 @@ pub(crate) fn init(
));
client.connect();
}
# fn main() {
app.add_systems(Startup, init);
# }
app.add_systems(Startup, init);
```

This spawns a camera, but also call `client.connect()` which will start the connection process with the server.

On the server we can just spawn a camera:

```rust,noplayground
pub(crate) fn init(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
Expand All @@ -40,25 +39,27 @@ pub(crate) fn init(mut commands: Commands) {
},
));
}
# fn main() {
app.add_systems(Startup, init);
# }
app.add_systems(Startup, init);
```

## Defining replicated entities

We want to have the following flow:

- client connects to server
- server spawns a player entity for the client
- server then keeps replicating the state of that entity to the client
- client can send inputs to the server to control the player entity (any updates will be replicated back to the client)

We will start by defining what a player entity is.
We create a bundle that contains all the components that we want to replicate. These components must be part of the `ComponentProtocol` enum that
We create a bundle that contains all the components that we want to replicate. These components must be part of
the `ComponentProtocol` enum that
we defined earlier in order to be replicated.

(In general it is useful to create separate bundles for components that we want to replicate, and components
that are only used on the client or server. For example, on the client a lot of components that are only used for rendering (particles, etc.)
that are only used on the client or server. For example, on the client a lot of components that are only used for
rendering (particles, etc.)
don't need to be included in the replicated bundle.)

```rust,noplayground
Expand Down Expand Up @@ -87,23 +88,27 @@ impl PlayerBundle {

We added an extra special component called `Replicate`. Only entities that have this component will be replicated.
This component also lets us specify some extra parameters for the replication:
- `actions_channel`: which channel to use to replicate entity actions. I define `EntityActions` as entity events that need
exclusive `World` access (entity spawn/despawn, component insertion/removal). By default, an OrderedReliable channel is used.
- `updates_channel`: which channel to use to replicate entity updates. I define `EntityUpdates` as entity events that don't need

- `actions_channel`: which channel to use to replicate entity actions. I define `EntityActions` as entity events that
need
exclusive `World` access (entity spawn/despawn, component insertion/removal). By default, an OrderedReliable channel
is used.
- `updates_channel`: which channel to use to replicate entity updates. I define `EntityUpdates` as entity events that
don't need
exclusive `World` access (component updates). By default, an SequencedUnreliable channel is used.
- `replication_target`: who will the entity be replicated to? By default, the entity will be replicated to all clients, but you can use this
- `replication_target`: who will the entity be replicated to? By default, the entity will be replicated to all clients,
but you can use this
to have a more fine-grained control
- `prediction_target`/`interpolation_target`: we will mention this later.

If you remove the `Replicate` component from an entity, any updates to that entity won't be replicated anymore. (However the client entity won't get despawned)
If you remove the `Replicate` component from an entity, any updates to that entity won't be replicated anymore. (However
the client entity won't get despawned)

Currently there is no way to specify for a given entity whether some components should be replicated or not.
This might be added in the future.


## Spawning entities on server


Most networking events are available on both the client and server as `bevy` `Events`, and can be read
every frame using the `EventReader` `SystemParam`. This is what we will use to spawn a player on the server
when a new client gets connected.
Expand Down Expand Up @@ -151,13 +156,14 @@ The `context()` method returns the `ClientId` of the client that connected/disco

We also create a map to keep track of which client is associated with which entity.


## Add inputs to client

Then we want to be able to handle inputs from the user.
We add a system that reads keypresses/mouse movements and converts them into `Inputs` that we can give to the `Client`.
Inputs need to be handled with `client.add_input()`, which does some extra bookkeeping to make sure that an input on tick `n`
for the client will be handled on the server on the same tick `n`. Inputs are also stored in a buffer for client-prediction.
Inputs need to be handled with `client.add_input()`, which does some extra bookkeeping to make sure that an input on
tick `n`
for the client will be handled on the server on the same tick `n`. Inputs are also stored in a buffer for
client-prediction.

```rust,noplayground
pub(crate) fn buffer_input(mut client: ResMut<Client<MyProtocol>>, keypress: Res<Input<KeyCode>>) {
Expand Down Expand Up @@ -199,15 +205,16 @@ app.add_systems(
```

`add_input` needs to be called in the `InputSystemSet::BufferInputs` `SystemSet`.
Some systems need to be run in specific `SystemSet`s because of the ordering of some operations is important for the crate
Some systems need to be run in specific `SystemSet`s because of the ordering of some operations is important for the
crate
to work properly. See [SystemSet Ordering](../concepts/system_sets/title.md) for more information.


## Handle inputs on server

Once we correctly `add_inputs` on the client, we can start reading them on the server to control the player entity.

We define a function that specifies how a given input updates a given player entity:

```rust,noplayground
pub(crate) fn shared_movement_behaviour(mut position: Mut<PlayerPosition>, input: &Inputs) {
const MOVE_SPEED: f32 = 10.0;
Expand All @@ -232,7 +239,8 @@ pub(crate) fn shared_movement_behaviour(mut position: Mut<PlayerPosition>, input
```

Then we can create a system that reads the inputs and applies them to the player entity.
Similarly, we have an event `InputEvent<I: UserAction>` that will give us on every tick the input that was sent by the client.
Similarly, we have an event `InputEvent<I: UserAction>` that will give us on every tick the input that was sent by the
client.
We can use the `context()` method to get the `ClientId` of the client that sent the input,
and `input()` to get the actual input.

Expand Down Expand Up @@ -262,6 +270,7 @@ Any fixed-update simulation system (physics, etc.) must run in the `FixedMain` `
## Displaying entities

Finally we can add a system on both client and server to draw a box to show the player entity.

```rust,noplayground
pub(crate) fn draw_boxes(
mut gizmos: Gizmos,
Expand All @@ -279,9 +288,9 @@ pub(crate) fn draw_boxes(
```

Now, running the server and client in parallel should give you:

- server spawns a cube when client connects
- client can send inputs to the server to control the cube
- the movements of the cube in the server world are replicated to the client !


In the next section, we will see a couple more systems.
87 changes: 43 additions & 44 deletions lightyear/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lightyear"
version = "0.11.0"
version = "0.12.0"
authors = ["Charles Bournhonesque <charlesbour@gmail.com>"]
edition = "2021"
rust-version = "1.65"
Expand All @@ -14,30 +14,30 @@ exclude = ["/tests"]

[features]
metrics = [
"dep:metrics",
"metrics-util",
"metrics-tracing-context",
"metrics-exporter-prometheus",
"dep:tokio",
"dep:metrics",
"metrics-util",
"metrics-tracing-context",
"metrics-exporter-prometheus",
"dep:tokio",
]
mock_time = ["dep:mock_instant"]
render = ["bevy/bevy_render"]
webtransport = [
"dep:wtransport",
"dep:xwt-core",
"dep:xwt-web-sys",
"dep:web-sys",
"dep:tokio",
"dep:ring",
"dep:wtransport",
"dep:xwt-core",
"dep:xwt-web-sys",
"dep:web-sys",
"dep:tokio",
"dep:ring",
]
leafwing = ["dep:leafwing-input-manager", "lightyear_macros/leafwing"]
xpbd_2d = ["dep:bevy_xpbd_2d"]
websocket = [
"dep:tokio",
"dep:tokio-tungstenite",
"dep:futures-util",
"dep:web-sys",
"dep:wasm-bindgen",
"dep:tokio",
"dep:tokio-tungstenite",
"dep:futures-util",
"dep:web-sys",
"dep:wasm-bindgen",
]

[dependencies]
Expand Down Expand Up @@ -69,7 +69,7 @@ bevy_xpbd_2d = { version = "0.4", optional = true }

# serialization
bitcode = { version = "0.5.1", package = "bitcode_lightyear_patch", path = "../vendor/bitcode", features = [
"serde",
"serde",
] }
bytes = { version = "1.5", features = ["serde"] }
self_cell = "1.0"
Expand All @@ -80,14 +80,14 @@ chacha20poly1305 = { version = "0.10", features = ["std"] }
byteorder = "1.5.0"

# derive
lightyear_macros = { version = "0.11.0", path = "../macros" }
lightyear_macros = { version = "0.12.0", path = "../macros" }

# tracing
tracing = "0.1.40"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = [
"registry",
"env-filter",
"registry",
"env-filter",
] }

# server
Expand All @@ -98,12 +98,12 @@ metrics = { version = "0.22", optional = true }
metrics-util = { version = "0.15", optional = true }
metrics-tracing-context = { version = "0.15", optional = true }
metrics-exporter-prometheus = { version = "0.13.0", optional = true, default-features = false, features = [
"http-listener",
"http-listener",
] }

# bevy
bevy = { version = "0.13", default-features = false, features = [
"multi-threaded",
"multi-threaded",
] }

# WebSocket
Expand All @@ -112,45 +112,44 @@ futures-util = { version = "0.3.30", optional = true }
# transport
# we don't need any tokio features, we use only use the tokio channels
tokio = { version = "1.36", features = [
"sync",
"sync",
], default-features = false, optional = true }
async-compat = "0.2.3"

[target."cfg(not(target_family = \"wasm\"))".dependencies]
# webtransport
wtransport = { version = "0.1.10", optional = true, features = [
"self-signed",
"dangerous-configuration",
"self-signed",
"dangerous-configuration",
] }
# websocket
tokio-tungstenite = { version = "0.21.0", optional = true, features = [
"connect",
"handshake",
"connect",
"handshake",
] }

[target."cfg(target_family = \"wasm\")".dependencies]
console_error_panic_hook = { version = "0.1.7" }
ring = { version = "0.17.7", optional = true }
web-sys = { version = "0.3", optional = true, features = [
"WebTransport",
"WebTransportHash",
"WebTransportOptions",
"WebTransportBidirectionalStream",
"WebTransportSendStream",
"WebTransportReceiveStream",
"ReadableStreamDefaultReader",
"WritableStreamDefaultWriter",
"WebTransportDatagramDuplexStream",

"WebSocket",
"CloseEvent",
"ErrorEvent",
"MessageEvent",
"BinaryType",
"WebTransport",
"WebTransportHash",
"WebTransportOptions",
"WebTransportBidirectionalStream",
"WebTransportSendStream",
"WebTransportReceiveStream",
"ReadableStreamDefaultReader",
"WritableStreamDefaultWriter",
"WebTransportDatagramDuplexStream",
"WebSocket",
"CloseEvent",
"ErrorEvent",
"MessageEvent",
"BinaryType",
] }
futures-lite = { version = "2.1.0", optional = true }
getrandom = { version = "0.2.11", features = [
"js",
"js",
] } # feature 'js' is required for wasm
xwt-core = { version = "0.2", optional = true }
xwt-web-sys = { version = "0.6", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion macros/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lightyear_macros"
version = "0.11.0"
version = "0.12.0"
authors = ["Charles Bournhonesque <charlesbour@gmail.com>"]
edition = "2021"
rust-version = "1.65"
Expand Down

0 comments on commit 5077a58

Please sign in to comment.