Synchronization

Pretty much every game has dozens of changing objects, and you need to synchronize their state across all the clients, so their state matches the server state. In the simplest case, all that you need is to collect information about node positions and rotations and send them to the clients:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct NodeState {
    pub node: SceneNodeId,
    pub position: Vector3<f32>,
    pub rotation: UnitQuaternion<f32>,
}

impl Server {
    pub fn sync(&mut self, scene: Handle<Scene>, ctx: &mut PluginContext) {
        let scene = some_or_return!(ctx.scenes.try_get(scene));
        let mut entity_states = Vec::with_capacity(scene.graph.capacity() as usize);
        for (handle, node) in scene.graph.pair_iter() {
            entity_states.push(NodeState {
                node: node.instance_id(),
                position: **node.local_transform().position(),
                rotation: **node.local_transform().rotation(),
            });
        }
        self.send_message_to_clients(ServerMessage::Sync { entity_states });
    }
}
}

If the number of objects is low, it will work fine on most machines and network connections. However, this algorithm is somewhat dumb, because it gathers a ton of useless information. For example, most of the scene objects remain static, and thus their "new" position and rotation can be ignored because it is not new at all—it's the same as in the previous frame.

Delta Compression

Delta compression is used to minimize the amount of data being sent from the server to clients and vice versa. It is a simple, yet very efficient way of reducing the required network bandwidth and performance requirements of your game.

The simplest delta compression could be implemented like this:

#![allow(unused)]
fn main() {
impl Server {
    pub fn sync_with_delta_compression(&mut self, scene: Handle<Scene>, ctx: &mut PluginContext) {
        let scene = some_or_return!(ctx.scenes.try_get(scene));
        let mut entity_states = Vec::with_capacity(scene.graph.capacity() as usize);
        for (handle, node) in scene.graph.pair_iter() {
            let current_state = NodeState {
                node: node.instance_id(),
                position: **node.local_transform().position(),
                rotation: **node.local_transform().rotation(),
            };

            // Simple delta compression.
            let prev_state = self
                .prev_node_states
                .entry(handle)
                .or_insert(current_state.clone());

            if *prev_state != current_state {
                entity_states.push(current_state.clone());
                *prev_state = current_state;
            }
        }

        self.send_message_to_clients(ServerMessage::Sync { entity_states });
    }
}
}

Physics

Client-server model assumes that the server has the priority in calculations and its state has the top priority than clients. This applies to physics simulation as well, no need to run expensive physics simulation if its state is overwritten by the server state anyway. That being said, the client must have physics simulation disabled. This can be done like this:

#![allow(unused)]
fn main() {
    fn on_scene_loaded(
        &mut self,
        path: &Path,
        scene: Handle<Scene>,
        data: &[u8],
        context: &mut PluginContext,
    ) {
        self.scene = scene;

        if self.server.is_none() {
            context.scenes[scene]
                .graph
                .physics
                .enabled
                .set_value_and_mark_modified(false);
        }
    }
}