Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multiplayer

Multiplayer netcode for video games can be really hard to write. SGE has a system to make this task easier.

First, create a state struct to hold the data you want associated with each player. This struct must implement Clone, and be annotated with #[persistent(diff)]. The diff is to allow the multiplayer system to tell what part of the struct changed between updates, so it can efficiently only send what’s necessary.

#![allow(unused)]
fn main() {
#[derive(Clone)]
#[persistent(diff)]
struct State {
    position: Vec2,
}
}

Then create a MultiplayerState<T> object with the default instance of the struct, a username, and room name. Your player will be connected to all other players in the same room, so make sure that it is, at least, unique to the video game you are developing by adding some random characters at the top for all rooms part of your game.

#![allow(unused)]
fn main() {
let mut state = MultiplayerState::new(
    State {
        position: Vec2::ZERO,
    },
    "Lily".to_string(),
    "YOUR_GAME_NAME_1897".to_string(),
);
}

From this point, you can call state.update(), at a rate of your choosing. I would recommend not sending it too often as this is bad for performance and will use more bandwidth, I would recommend 10 or 20 times a second.

// 10 times a second
const UPDATE_RATE: f32 = 0.1;

fn main() -> anyhow::Result<()> {
    // ...
    
    loop {
        // ...
        
        if once_per_n_seconds(UPDATE_RATE) {
            state.update()?;
        }
    }
}

You can access your state (i.e. the state of the player) with state.your_state(), state.your_state_mut(), state.your_username(), and state.your_user_data(). You can get the states of other users with state.other_users(), state.get_user(), and state.get_user_mut(). Note that any changes you make to other users states will not be reflected for other people, and will be updated by new data from that user changing their own state.

In your cleanup (end of main function), it is best to add this code:

#![allow(unused)]
fn main() {
state.disconnect();
// so it has time to send the disconnect message before the process is killed
std::thread::sleep(Duration::from_millis(50));
}

This will tell all other clients in the room that you have disconnected, and will remove that user from their list of users, so they won’t still show up as a frozen player in game.

Interpolation

If you create something like this:

struct State {
    position: Vec2,
}

const UPDATE_RATE: f32 = 0.1;

#[main("Multiplayer")]
fn main() {
    let mut state = MultiplayerState::new(
        State {
            position: Vec2::ZERO,
        },
        "Lily".to_string(),
        "YOUR_GAME_NAME_1897".to_string(),
    );
    
    loop {
        // add controls for user to move around, and update the state periodically
        
        for (_, user) in state.other_users().iter() {
            draw_circle_world(user.position, 50.0, Color::RED_500);
        }
        
        // ...
    }
}

…you will notice that people jump around on screen in large intervals, because their positions are being updated at a rate less than the frame rate. To fix this, without reducing performance, we can use interpolation. You can implement interpolation manually using the history of previous state values stored in UserData, or use the builtin automatic interpolation. To use automatic interpolation, you need to annotate the state struct with #[persistent(diff, lerp)] instead.

#![allow(unused)]
fn main() {
#[derive(Clone)]
#[persistent(diff, lerp)]
struct State {
    position: Vec2,
}
}

This will implement PartialLerp for that type, which interpolates only the fields that can be interpolated (f32, f64, Vec2, Vec3, Vec4, Color). With this, on types that implement PartialLerp, you can use something like this instead, for smooth interpolation without suttering even at low update rates. You can implement PartialLerp manually if you need more control.

#![allow(unused)]
fn main() {
const UPDATE_RATE: f32 = 0.1;
const INTERPOLATION_DELAY: f32 = UPDATE_RATE * 2.0; // can add more for more reliability; 2*update-rate is standard

// ...

// render target time is the time in the past we should pretend it is,
// and interpolate based on updates we have gotten from the future (aka present)
let render_target_time = time() - INTERPOLATION_DELAY;
for (_, user) in state.other_users().iter() {
    if let Some(interpolated_state) = user.current_lerped(render_target_time) {
        draw_circle_world(user.position, 50.0, Color::RED_500);
    }
}
}

Notifications

If you want to communicate directly with other clients, for example when adding a chat system to your game, you can use notifications. Send a notification with state.send_notification(data), and receive with state.drain_notifications(). The data sent in a notification is a Vec<u8>, allowing you to send any data you want in any form. You could do this by reserving the first number in the series as a type specifier, and interpreting the rest of the sequence based on the parsed type. Remember that you can convert any type annotated with #[persistent] to and from bytes with .to_bytes() and ::from_bytes(), and convert strings to and from bytes with string.as_bytes() and String::from_utf8().

#![allow(unused)]
fn main() {
let mut state = MultiplayerState::new(...);

let mut messages = vec![];

for notification in state.drain_notifications() {
    let Some(user) = other_state.get_user(notification.user_id) else {
        break;
    };
    let username = &user.username;
    let text = String::from_utf8(notification.data).unwrap();

    messages.push(text);
}

let message = "hello".to_string();
state.send_notification(message.as_bytes().to_vec());
}

Backends

The multiplayer system is generic over a backend. The default backend, IttyBackend, uses itty-sockets to transmit data, but any struct that implements MultiplayerBackend can be used, for example you could write one that sends data over the LAN instead. The MultiplayerBackend interface is quite websocket/stream centered, but could easily be made to work with a database instead; anything that will broadcast received messages to all connected users and supports separate rooms will work.


See: multiplayer module

See: /examples/multiplayer.rs