VehicleSync - TextGuySemicolon/VPPMultiplayer GitHub Wiki

Overview

The client has full authority over the vehicle they control, and the server is responsible to receive vehicle state from client and send back to all other clients to synchronize vehicle states.

VehicleStatePacket

VehicleStatePacket is the packet that contained everything needed to synchronize the state of a vehicle.

    public struct VehicleStatePacket : INetSerializable
    {
        public byte id;
        public Vector3 position;
        public Vector3 velocity;

        public float timestamp;

        //other data like rotation, wheels etc...
    }

VehicleRunner

VehicleRunner is attached to vehicle to sync the state of the vehicle. VehicleRunner is the base of the networked vehicle, it contains an id that is corresponding to the id of the "NetPeer" that has the authority over the vehicle.

LocalVehicleRunner

"LocalVehicleRunner" is inherited from "VehicleRunner", it is used as the vehicle runner for the local client, it referenced the "VPPController", it is responsible to send the state of the vehicle to the server.

    public void SendUpdate()
    {
        //construct a "VehicleStatePacket" from the attached vehicle, and send to server
    }

RemoteVehicleRunner

"RemoteVehicleRunner" is inherited from "VehicleRunner", it is used as the vehicle runner for the remote client, we don't want to run the physics of the vehicle for any client other than the local client, we spawn remote vehicle to simulate the visual/data of vehicle for other clients.

"RemoteVehicleRunner" will receive "VehicleStatePacket" from the server, updating the vehicle state and do things like interpolation, position prediction etc...

    public VehicleStatePacket lastUpdate;
    public void ReceiveUpdate(VehicleStatePacket _newState)
    {
        lastUpdate = _newState;
    }

Position Prediction

due to the latency, by the time we receive data of the vehicle position, it is already outdated, in order to predict the position, we calculate a "predicted_pos" position that is using the velocity of the vehicle and the time passed.

    private void UpdateState()
    {
        //calculate position
        var current_time = ClientNetworkTime.GetServerTime();
        var timePassed = current_time - lastReceivedState.timestamp; //how long the time passed
        var predicted_pos = lastReceivedState.position + lastReceivedState.velocity * timePassed; //last position + velocity movements
    }

the "UpdateState" function is called in the fixed update, The position of the vehicle will be lerping toward the "predicted_pos " to simulate the movement of the remote vehicles.

    transform.position = Vector3.Lerp(transform.position, predicted_pos, 0.1f);

Additional Vehicle State

there are other vehicle data like rotation, wheels is also synced in the "UpdateState" function.

VehicleManager

"VehicleManager" is a manager script used to manage "VehicleRunner", we keep track of all vehicle runner, and apply vehicle state we received from the server to the vehicle runners.

VehicleSync

"VehicleSync" is split into "ClientVehicleSync" and "ServerVehicleSync" to do network operation for sending/receiving vehicle state, it also handled spawning/despawning of vehicle.

Joining Server

on the client side, we will send a "JoinPacket" to the server on connection to the server.

    private void OnPeerConnected(NetPeer peer)
    {
        JoinPacket _packet = new JoinPacket { username = SRLClientInfo.Username };
        //send _packet to server
    }

on the server side, "OnJoinReceived" is called when we receive a "JoinPacket". In the "OnJoinReceived" callback function, we add the joined peer to our "_playerManager" in order to keep track of all client, every client will be assigned with an "Id".

    //Initialzie player on server
    var player = new ServerPlayer(joinPacket.username, peer);
    _playerManager.AddPlayer((byte)peer.Id, player);

then we will send the data of the joined player to all other players that is already joined before this player.

    //Send new player data to all other players
    var _joinedPacket = new PlayerJoinedPacket
    {
        id = player.id,
        Username = player.Username,
        NewPlayer = true,
    };
    //send the packet to all peers except "peer" that just joined    
    GameServer.netManager.SendToAll(netDataWriter.WritePacket(_joinedPacket, netPacketProcessor), DeliveryMethod.ReliableOrdered, peer);

same for the joined player, we will send all the joined players data to the player that just joined.

    //Send all other player data to new player
    _joinedPacket.NewPlayer = false;
    foreach (ServerPlayer otherPlayer in _playerManager.GetList())
    {
        if (otherPlayer == player)
            continue;
        _joinedPacket.Username = otherPlayer.Username;
        _joinedPacket.id = otherPlayer.id;
        peer.Send(netDataWriter.WritePacket(_joinedPacket, netPacketProcessor), DeliveryMethod.ReliableOrdered);
    }

Leaving Server

on the server side, when a peer is disconnecting, we will send a "PlayerLeavedPacket" to all connected peers containing the "playerId" of the disconnected peer.

    private void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo)
    {
        if (peer.Tag == null) return;

        byte playerId = (byte)peer.Id;
        if (_playerManager.RemovePlayer(playerId) != null)
        {
            var plp = new PlayerLeavedPacket { Id = (byte)peer.Id };
            GameServer.netManager.SendToAll(netDataWriter.WritePacket(plp, netPacketProcessor), DeliveryMethod.ReliableOrdered);
        }
    }

Vehicle Update

this is pretty straigh-forward, every frame the "LocalVehicleRunner" will send the vehicle state to the server. When the server receive a vehicle state, it will send it to all other connected peers, when other peer receive the vehicle state, they will use "VehicleManager" to apply the vehicle state to the corresponding remote vehicle.