Setting up the client and server
Client
A client is simply a bevy plugin: ClientPlugin
You create it by providing a ClientConfig
struct.
You can see how it is defined in the example here.
Shared Config
Some parts of the configuration must be shared between the server and the client to work correctly, so we define them in a separate function that can be re-used for both:
#![allow(unused)] fn main() { pub fn shared_config(mode: Mode) -> SharedConfig { SharedConfig { /// How often the client will send packets to the server (by default it is every frame). /// Currently, the client only works if it sends packets every frame, for proper input handling. client_send_interval: Duration::default(), /// How often the server will send packets to clients? You can reduce this to save bandwidth. server_send_interval: Duration::from_millis(40), /// The tick rate that will be used for the FixedUpdate schedule tick: TickConfig { tick_duration: Duration::from_secs_f64(1.0 / 64.0), }, /// Here we make the `Mode` an argument so that we can run `lightyear` either in `Separate` mode (distinct client and server apps) /// or in `HostServer` mode (the server also acts as a client). mode, } } }
ClientConfig
The ClientConfig struct lets us configure the client. There are a lot of parameters that can be configured, but for this demo we will mostly use the defaults.
let client_config = client::ClientConfig {
shared: shared_config(Mode::Separate),
net: net_config,
..default()
};
let client_plugin = client::ClientPlugin::new(client_config);
The NetConfig doesn't have any Default value and needs to be provided; it defines how (i.e. what transport layer) the client will connect to the server.
There are multiple options available, but for this demo we will use the Netcode
option.
netcode is a standard to establish a connection between two hosts, and we can use any io layer (UDP, WebSocket, WebTransport, etc.) to send the actual bytes.
You will need to provide the IoConfig which defines the transport layer (how the raw packets are sent), with the possibility of using a LinkConditionerConfig to simulate network conditions. Here are the different possible transport options: TransportConfig
/// You can add a link conditioner to simulate network conditions
let link_conditioner = LinkConditionerConfig {
incoming_latency: Duration::from_millis(100),
incoming_jitter: Duration::from_millis(0),
incoming_loss: 0.00,
};
/// Here we use the `UdpSocket` transport layer, with the link conditioner
let io_config = IoConfig::from_transport(TransportConfig::UdpSocket(addr))
.with_conditioner(link_conditioner);
With the Netcode
option, we use a ConnectToken to secure the connection.
Normally, a third-party server would generate the ConnectToken
and send it securely to the client.
For this demo, we will use the Manual
option, which lets us manually build a ConnectToken
on the client using a private key shared between the client and the server.
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), server_port);
let auth = Authentication::Manual {
// server's IP address
server_addr,
// ID to uniquely identify the client
client_id: client_id,
// private key shared between the client and server
private_key: KEY,
// PROTOCOL_ID identifies the version of the protocol
protocol_id: PROTOCOL_ID,
};
Now we can build the complete NetConfig
:
let net_config = NetConfig::Netcode {
auth,
io: io_config,
..default()
};
Server
Building the server is very similar to building the client; we need to provide a ServerConfig
struct.
let server_config = server::ServerConfig {
shared: shared_config(Mode::Separate),
net: net_configs,
..default()
};
let server_plugin = server::ServerPlugin::new(server_config);
The server can listen for client connections using multiple transports at the same time! You can do this by providing multiple NetConfig to the server.
The simple_box
example generates the various NetConfig
s by parsing the settings.ron
file, but you can also just
define them manually:
#![allow(unused)] fn main() { let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), self.port); /// You need to provide the private key and protocol id when building the `NetcodeConfig` let netcode_config = NetcodeConfig::default() .with_protocol_id(PROTOCOL_ID) .with_key(KEY); /// You can also add a link conditioner to simulate network conditions for packets received by the server let link_conditioner = LinkConditionerConfig { incoming_latency: Duration::from_millis(100), incoming_jitter: Duration::from_millis(0), incoming_loss: 0.00, }; let net_config = NetConfig::Netcode { config: netcode_config, io: IoConfig::from_transport(TransportConfig::UdpSocket(server_addr)) .with_conditioner(link_conditioner), }; let config = ServerConfig { shared: shared_config().clone(), /// Here we only provide a single net config, but you can provide multiple! net: vec![net_config], ..default() }; /// Finally build the server plugin let server_plugin = server::ServerPlugin::new(server_config); }
Next we will start adding systems to the client and server.