1
\$\begingroup\$

I have created a client-server system which handles multiplayer for a game I've been making with Unity. Problem is, it feels very clunky and complex and I want to know how the design could be simplified and streamlined?

The broader context for this is a RTS game whose multiplayer behaves more like a FPS, with the sever generating output from client input, instead of more traditionally sending just inputs (simulation is not deterministic).

... I realise I may have left out classes or functions which are necessary to analysing the problem. I will likely need to edit this question.


/* NornNetworkBuffer synchronises inputs for human and computer players.
 * 50 Frames per FixedUpdate second.
 * To modify FixedUpdate: Edit/Project Settings/Time.
 * 
 * CLIENT PLAYER INTERACTION!
 * Human input is registered in PlayerHuman/GeneralInputEvents.
 * Input passed to NornNetworkClient to be sent to server.
 * NETWORK TRANSPORT...
 * Input arrives in NornNetworkServer/Update.
 * Server logs inputs in NornNetworkBuffer.
 * NornNetworkBuffer processes inputs into outputs at correct time index and then sends to clients.
 * Clients log outputs in NornNetworkBuffer.
 * ReadBuffer function actions outputs at correct time index.
 * CLIENT PLAYER GETS RESULT!
 */

Player class responds to human input... creates NInput.

protected void CommandThing(Vector3 origin, Vector3 direction, bool clear)
{
     /* Blah, blah, blah...*/
     NInputMove move = new NInputMove(id, timeIndex, clear, hitTileVector.x, hitTileVector.y, hitTileVector.z);
     Game.HandleInput(move, false);
}

NInput passed to the Client class via the Game class.

Game class.

public static void HandleInput(NInput input, bool actionNow)
{
    if (isServer)
    {
        byte[] inputBuffer = networkManager.GetServer().WriteNInput(input);
        networkManager.GetServer().HandleInput(inputBuffer, 999);
    }
    else
    {
        /* actionNow used when player needs immediate feedback.
         * For example, when selecting units.
         * This requires server does not send output to the source player.
         */
        if (actionNow)
        {
            NornNetworkBuffer.AddInput(input);
            networkManager.GetClient().AddPendingInput(input);
        }
        else
        {
            networkManager.GetClient().AddPendingInput(input);
        }
    }
}

Client class.

void Update()
{
    if (Game.active)
    {
        SendPendingInputs();
    }
}

private void SendPendingInputs()
{
    if (pendingInputs.Count > 0)
    {
        byte[] buffer = WriteNInput(pendingInputs);
        Game.networkManager.SendBuffer(buffer, NornConnectionChannel.InputsFromClientToServer);
        pendingInputs.Clear();
    }
}

NornNetworkRole is the parent of NornClient and NornServer, and receives and sends data across the network.

public void HandleInput(byte[] buffer, int sourceConnectionId)
{
    List<NInput> inputs = ReadNInputs(buffer);
    NInput input = inputs[0];

    if (Game.isServer)
    {
        if (sourceConnectionId != 999)
        {
            // Correct time index when input is received by server.
            input.index = NornNetworkBuffer.GetNextBestIndex();
            buffer = WriteNInput(input);
        }

        // Action inputs locally.
        NornNetworkBuffer.AddInput(input);
    }
    else
    {
        NornLobbyPlayer server = Game.networkManager.GetClient().lp;
        NetworkTransport.Send(server.nHostConn.hostID, server.nHostConn.connectionID, (int)NornConnectionChannel.InputsFromClientToServer, buffer, buffer.Length, out error);
    }
}

Meanwhile in the Buffer class...

public class NornNetworkBuffer : MonoBehaviour
{
    public static NornNetworkBuffer singleton;
    private static int timeIndex = 0;
    public static int timeIndexLimit = 0;
    private int tick = 0;
    System.Diagnostics.Stopwatch lagsw = new System.Diagnostics.Stopwatch();
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

    // Server uses only bufferInputs and requires one input list per player.
    private static List<List<NInput>> bufferInputs = null;
    // Client uses only bufferOutputs and requires only one output list.
    private static List<NOutputBundle> bufferOutputs = null;

    public void CreateNewBufferWithPlayers()
    {
        if (Game.isServer)
        {
            bufferInputs = new List<List<NInput>>();

            foreach (Player p in Player.list)
            {
                bufferInputs.Add(new List<NInput>());
            }
        }
        else
        {
            bufferOutputs = new List<NOutputBundle>();
            bufferInputs = new List<List<NInput>>();

            foreach (Player p in Player.list)
            {
                bufferInputs.Add(new List<NInput>());
            }
        }
    }

    void Start()
    {
        singleton = this;
        timeIndex = 0;
    }

    void Update()
    {
        // Client checks timeIndexLimit.
        if (Game.isServer == false && 
            Game.gameOver == false)
        {
            bool awaitingInput = false;

            if (timeIndex >= timeIndexLimit)
            {
                awaitingInput = true;
            }

            if (Game.active)
            {
                // No Continue, no progress.
                if (awaitingInput)
                {
                    if (lagsw.IsRunning == false)
                    {
                        lagsw.Start();
                    }

                    Game.Pause(true);
                }
                else
                {
                    if (lagsw.IsRunning)
                    {
                        lagsw.Stop();
                        Debug.Log(" Time since awaiting input from server: " + lagsw.ElapsedMilliseconds + "ms");
                        lagsw.Reset();
                    }

                    Game.Pause(false);
                }
            }
        }
    }

    void FixedUpdate()
    {
        if (Game.active)
        {
            if (Game.isServer && 
                Game.humanPlayers > 1 &&
                tick >= 5) // Ten times per second ( 5 out of 50 FixedUpdate per second).
            {
                sw.Start();
                Game.networkManager.GetServer().PrepareNOutputs();
                sw.Stop();

                if (sw.ElapsedMilliseconds > 1)
                {
                    Debug.Log(" PrepareNOutputs " + sw.ElapsedMilliseconds + "ms");
                }

                sw.Reset();
                tick = 0;
            }

            ReadBuffer();
            timeIndex++;
            tick++;
            //Debug.Log(" the time is " + timeIndex + " @ " + Time.fixedTime);
        }
    }

    private void ReadBuffer()
    {
        if (Game.isServer)
        {
            ReadBufferInputs();
        }
        else
        {
            ReadBufferInputs();
            ReadBufferOutputs();
        }
    }

    private void ReadBufferInputs()
    {
        for (int i = 0; i < bufferInputs.Count; i++)
        {
            if (bufferInputs[i].Count > 0)
            {
                // Find inputs to be executed right now.
                for (int j = bufferInputs[i].Count; j > 0; j--)
                {
                    NInput pi = bufferInputs[i][j - 1];

                    if (pi.index >= timeIndex)
                    {
                        if (pi.index == timeIndex)
                        {
                            // Action input.
                            Player.list[i].ActionNInput(pi);
                            // Remove input.
                            bufferInputs[i].RemoveAt(j - 1);
                        }
                        else
                        {
                            // Wait.
                        }
                    }
                    else
                    {
                        Debug.Log(" DESYNC WARNING -- INPUT TOO LATE " + pi.ToString() + " @ " + pi.index + " when local time is " + timeIndex);
                        bufferInputs[i].RemoveAt(j - 1);
                    }
                }
            }
        }
    }

    private void ReadBufferOutputs()
    {
        for (int i = 0; i < bufferOutputs.Count; i++)
        {
            // Find inputs to be executed right now.
            NOutputBundle po = bufferOutputs[i];

            if (po.index >= timeIndex)
            {
                if (po.index == timeIndex)
                {
                    // Action outputs.
                    ActionNOutputs(po);
                    // Remove outputs.
                    bufferOutputs.RemoveAt(i);
                }
                else
                {
                    // Wait.
                }
            }
            else
            {
                Debug.Log(" DESYNC WARNING -- OUTPUT TOO LATE " + po.ToString() + " @ " + po.index + " when local time is " + timeIndex);
                bufferOutputs.RemoveAt(i);
            }
        }
    }

    public static void AddOutput(NOutputBundle bundle)
    {
        bufferOutputs.Add(bundle);
    }

    public static void AddInput(NInput input)
    {
        int playerIndex = input.player - 1;
        bufferInputs[playerIndex].Add(input);
    }

    public static void RaiseTimeIndexLimit(int til)
    {
        timeIndexLimit = til;
    }

    public static int GetNextBestIndex()
    {
        // +1 minimum to ensure inputs are not late.
        // Take ping into account.
        int pingAdjustment = 1;
        int pingAsFixedUpdate = (Game.worstPing > 1) ? Game.worstPing / 40 : 0; // 20 both ways or 40 one way trip.

        if (pingAsFixedUpdate > pingAdjustment)
        {
            pingAdjustment = pingAsFixedUpdate;
        }

        return timeIndex + pingAdjustment;
    }

    public static int GetThisIndex()
    {
        // +1 minimum to ensure inputs are not late.
        return timeIndex + 1;
    }

    public void ActionNOutputs(NOutputBundle bundle)
    {
        foreach (NOutput obj in bundle.outputs)
        {
            NOutputCmd cmd = obj.cmd;

            switch (cmd)
            {
                case NOutputCmd.Victory:
                    ActionNOutputVictory((NOutputVictory)obj);
                    break;

                case NOutputCmd.Select:
                    ActionNOutputSelect((NOutputSelect)obj);
                    break;

                case NOutputCmd.Console:
                    ActionNOutputConsole((NOutputConsole)obj);
                    break;

                case NOutputCmd.Create:
                    ActionNOutputCreate((NOutputCreate)obj);
                    break;

                case NOutputCmd.SpawnMinerals:
                    ActionNOutputSpawnMinerals((NOutputSpawnMinerals)obj);
                    break;

                case NOutputCmd.Button:
                    ActionNOutputButton((NOutputButton)obj);
                    break;

                case NOutputCmd.Tile:
                    ActionNOutputTile((NOutputTile)obj);
                    break;

                case NOutputCmd.PickUp:
                    ActionNOutputPickUp((NOutputPickUp)obj);
                    break;

                case NOutputCmd.Deploy:
                    ActionNOutputDeploy((NOutputDeploy)obj);
                    break;

                case NOutputCmd.Module:
                    ActionNOutputModule((NOutputModule)obj);
                    break;

                case NOutputCmd.RepeatMove:
                case NOutputCmd.ByteXYZD:
                case NOutputCmd.ByteXZD:
                case NOutputCmd.ByteXZ:
                case NOutputCmd.ByteD:
                case NOutputCmd.ByteX:
                case NOutputCmd.ByteZ:
                    ActionNOutputByte(obj, bundle.index);
                    break;

                case NOutputCmd.ByteSpecial:
                    ActionNOutputByteSpecial(obj, bundle.index);
                    break;

                case NOutputCmd.FloatXYZD:
                case NOutputCmd.FloatXZD:
                case NOutputCmd.FloatXZ:
                case NOutputCmd.FloatD:
                case NOutputCmd.FloatX:
                case NOutputCmd.FloatZ:
                    ActionNOutputMoveFloat((NOutputFloat)obj);
                    break;

                case NOutputCmd.Attack:
                    ActionNOutputAttack((NOutputAttack)obj);
                    break;

                case NOutputCmd.Shoot:
                    ActionNOutputShoot((NOutputShoot)obj);
                    break;

                case NOutputCmd.ShootRepeat:
                    ActionNOutputShootRepeat((NOutputShootRepeat)obj);
                    break;

                case NOutputCmd.MindControl:
                    ActionNOutputMindControl((NOutputMindControl)obj);
                    break;

                case NOutputCmd.Cloak:
                    ActionNOutputCloak((NOutputCloak)obj);
                    break;

                case NOutputCmd.DamageByte:
                    ActionNOutputDamageByte((NOutputDamageByte)obj);
                    break;

                case NOutputCmd.DamageInt:
                    ActionNOutputDamageInt((NOutputDamageInt)obj);
                    break;

                default:
                    Debug.Log("Error ActionNOutputs unknown NOutputCmd " + cmd);
                    break;
            }
        }
    }

}

Outputs then received by the client, logged to buffer, played at correct time index... everyone's a winner?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.