Everything looks good on paper but dedicated benchmarks can show you what to really expect.
Setup
New scene with Camera and directional light.
1 object per player connection.
Object moves and rotates randomly.
Object has a client authoritative NetworkTransform with 100ms send rate.
Object sends two ServerRPCs a second.
Server sends an ObserverRPC for every ServerRPC it receives.
Every client is it's own executable.
Server tick rate is set to 10 (every 100ms).
Gathering Results
Performance results are determined by setting a server at it's highest possible frame rate and comparing frame time as more variables or clients are introduced.
Bandwidth results are discovered by using tools which come with the operating system, and averaging bandwidth per second.
Scripts
These are the scripts created specifically to benchmark and ensure test are performed equally.
MovingAverage.cs
usingFishNet.Documenting;usingSystem;usingUnityEngine;namespaceFishNet.Managing.Timing{ [APIExclude]publicclassMovingAverage {#regionPublic. /// <summary> /// Average from samples favoring the most recent sample. /// </summary>publicfloat Average { get; privateset; }#endregion /// <summary> /// Next index to write a sample to. /// </summary>privateint _writeIndex; /// <summary> /// Collected samples. /// </summary>privatefloat[] _samples; /// <summary> /// Number of samples written. Will be at most samples size. /// </summary>privateint _writtenSamples; /// <summary> /// Samples accumulated over queue. /// </summary>privatefloat _sampleAccumulator;publicMovingAverage(int sampleSize) {if (sampleSize <0) { sampleSize =0; }elseif (sampleSize <2) {if (NetworkManager.StaticCanLog(Logging.LoggingType.Warning)) Debug.LogWarning("Using a sampleSize of less than 2 will always return the most recent value as Average.");
} _samples =newfloat[sampleSize]; } /// <summary> /// Computes a new windowed average each time a new sample arrives /// </summary> /// <paramname="newSample"></param> public voidComputeAverage(float newSample) {if (_samples.Length<=1) { Average = newSample;return; } _sampleAccumulator += newSample;_samples[_writeIndex] = newSample; //Increase writeIndex. _writeIndex++; _writtenSamples =Math.Max(_writtenSamples, _writeIndex);if (_writeIndex >=_samples.Length) _writeIndex =0; Average = _sampleAccumulator / _writtenSamples;/* If samples are full then drop off * the oldest sample. This will always be * the one just after written. The entry isn't * actually removed from the array but will * be overwritten next sample. */if (_writtenSamples >=_samples.Length) _sampleAccumulator -=_samples[_writeIndex]; } }}
DisplayPerformance.cs
usingUnityEngine;usingFishNet;usingFishNet.Managing.Timing;publicclassDisplayPerformance:MonoBehaviour{publicint TargetFrameRate =60;privatefloat _nextDisplayTime =0f;privateMovingAverage _average =newMovingAverage(3);privateuint _frames =0;privatevoidUpdate() {#if UNITY_SERVERApplication.targetFrameRate= TargetFrameRate; _frames++;if (Time.time< _nextDisplayTime)return;_average.ComputeAverage(_frames); _frames =0; //Update display twice a second. _nextDisplayTime =Time.time+1f;double avgFrameRate =_average.Average; //Performance lost.double lost = avgFrameRate / (double)TargetFrameRate; lost = (1d- lost); //Replace this with the equivelent of your networking solution.int clientCount =InstanceFinder.ServerManager.Clients.Count;Debug.Log($"Average {lost.ToString("0.###")} performance lost with {clientCount} clients.");#elif UNITY_EDITOR //Max out editor frames to test client side scalability.Application.targetFrameRate=9999;#else/* Limit client frame rate to 15 * so your computer doesn't catch fire when opening * hundreds of clients. */Application.targetFrameRate=15;#endif }}