I am developing a 3d mobile game with Unity and I've come to a point where I need to write the multiplayer part. I wrote all the matchmaking etc. with node.js/socket.io but I've encountered to a problem on the in-game part. The problem is; Whenever a user moves, he transmits the position of himself to the other users. But altough the server is quite powerful (4 GHz of cpu, 16 GB of ram), users doesn't move like they were on the singleplayer (I mean the AI movement). They seem to slip a couple frames and that makes them move not as smooth as I expected. I have some ideas about what could cause this such as the FPS difference. Since one of the users could emit slower than the others 'cuz of the FPS difference between them. Any ideas how can I get rid of this problem?
It depends a bit on how exactly you transmit the data between the Clients/Server.
(I will assume here you are using only Unity's networking and not a framework)
You never should directly set the new position on the receiver clients as
transform.position = X.Yf;
but rather have some interpolation because of the lag / Time factor you already noticed.
transform.position = Vector3.Lerp(actualPosition, receivedPosition, Time.deltaTime * interpolationRate);
I didn't like Unity's NetworkTransform very much - because I didn't manage to get it to work smoothly. So I wrote my own sripts for syncing (offcourse doing a lot of googling around ;) )
I'll provide an example here for the Position only. You should be able to tweak it than and write the rotation part on your own. It might not be perfect but that's how I use it (quite satisfied so far).
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class CustomNetworkTransform : NetworkBehaviour
{
/* If we are not the localPlayer we will save here
* the last received Position to which we are actually moving.
* (see below in the transmition part) */
private Vector3 _lastReceivedPosition = Vector3.zero;
/*#############################################*/
/* THE RECEIVING PART */
/* The interpolation rate. You have to tweak this maybe
* but 15 seemed a good value so far */
private float _interpolationRate = 15.0f;
private void Update()
{
/* I used hasAuthority here instead of isLocalPlayer
* so it works with a Host-Clients
* setup as well as with a Client-Server-Clients one.
*/
if (hasAuthority) return;
ReceiveData();
}
private void ReceiveData()
{
InterpolatePosition(_lastReceivedPosition, Time.deltaTime * _interpolationRate);
}
private void InterpolatePosition(Vector3 receivedPosition, float factor)
{
transform.position = Vector3.Lerp(transform.position, receivedPosition, factor);
}
/* That's already all for the receiving Part. Maybe that already
* Fits your need. Down here I anyway add the way how I transmit stuff. Maybe it helps you to understand better the way Networking works in Unity.
*/
/*#############################################*/
/* THE TRANSMITION PART */
/* To save a bit of Bandwidth we use this as a treshold
* If you whish you can set this also to 0 */
[Tooltip("The Accuracy of synchronized Positions")]
[SerializeField]
private float _positionAccuracy = 0.05f;
/* Make a forced Transmition every x seconds
* also if nothing was changed */
[SerializeField]
private float _forcedSyncTimeout = 2.0f;
/* Counter for the forced transmition
* I set it to 0 at the beginning to have an instant
* sync when connecting. */
private float _timeout = 0;
/* Wel'll compare the actual position to this one and only
* sync, if the distance is bigger than the _positionAccuracy */
private Vector3 _lastTransmittedPosition = Vector3.zero;
private void FixedUpdate()
{
/* again here I use hasAuthority instead of
* isLocalPlayer so it works in both connection designs */
if (!hasAuthority) return;
TransmitChangedData();
_timeout-= Time.deltaTime;
if (_timeout > 0) return;
TransmitData();
_timeout = _forcedSyncTimeout;
}
/* This is the forced transmition
* which doesn't check for changes */
private void TransmitData()
{
TransmitPositionToServer(transform.position)
}
/* This is the normal transmition which checks
* if changes are big enough before transmitting */
private void TransmitChangedData()
{
if(Vector3.Distance(transform.position, _lastTransmittedPosition) > _positionAccuracy)
{
TransmitPositionToServer(transform.position);
_lastTransmittedPosition = transform.position;
}
}
/* NOTE: for the transmition I don't use SyncVar
* but rather a 3-Step Syncronization:
*
* 1. Client transmits data to Server
* 2. Server transmits data to all Clients
* 3. All other Clients receive and interpolate the position
*
* I felt more comfortable doing this to have
* more freedom and better debug options.
*
* This will also do all the interpolations on the server as well
* so it can be used e.g. to observe. */
//STEP 1.
/*
* Only performed on clients
*/
[Client]
private void TransmitPositionToServer(Vector3 value)
{
CmdPushPositionToServer(value);
}
//STEP 2.
/*
* Invoced from the client but only performed on the server
*/
[Command]
private void CmdPushPositionToServer(Vector3 value)
{
_lastReceivedPosition = value;
RpcProvidePositionToClients(value);
}
//STEP 3.
/*
* Invoced from the server but only performed on all the client
*/
[ClientRpc]
private void RpcProvidePositionToClients(Vector3 value)
{
/* This value is also set on the originally sending client
* but it doesn't matter since we don't move this client
* because he has the local authority (see Update() )*/
_lastReceivedPosition = value;
}
}
Hope it helps.
I once even saw another solution using some kind of piped interpolation storing all received data packages in an array and interpolate through that but it didn't work any better in my opinion.