4
\$\begingroup\$

I am making a game with C#/Unity, and have devised a Client-Server system to update the positions and directions of all units in play ten times a second. My biggest concern is that the more units running about, the more likely it is that a correction event will come too late.

The simplest way of handling that is pausing the client to apply changes and catch up, but this cannot be used to excuse suboptimal code.

Previously this was happening a lot, and after using Stopwatch I realised the issue was BinaryFormatter being slow (often clocking 7-9ms to Serialize for 80ish units in motion). So I changed BinaryFormatter to BinaryWriter/Reader and things are much smoother now.

However I suspect more issues. Please comment if I need to provide more information or code. Long explanations welcome.

DATA CLASSES

public struct UnitCorrectionFloat
{
    public readonly byte type;
    public readonly ushort id;
    public readonly float x;
    public readonly float y;
    public readonly float z;
    public readonly float direction;

    public UnitCorrectionFloat(byte type, ushort id, float x, float y, float z, float direction)
    {
        this.type = type;
        this.id = id;
        // XYZD are absolute values which overwrite last known value.
        this.x = x;
        this.y = y;
        this.z = z;
        this.direction = direction;
    }
}

public class BufferInputUnitCorrections : BufferInput
{
    public List<object> corrections;

    public BufferInputUnitCorrections(int index, List<object> corrections)
    {
        player = 1;
        this.index = index;
        this.corrections = corrections;
    }
}

[System.Serializable]
public class BufferInput
{
    public byte player;
    public int index;

    public BufferInput()
    {

    }
}

SERVER

DATA GENERATION FUNCTION

private object GenerateUCF(Unit unit)
{
    UnitCorrectionFloat uc = new UnitCorrectionFloat();
    bool sendX = (!unit.transform.position.x.Equals(unit.correctionVector.x)) ? true : false;
    bool sendY = (!unit.transform.position.y.Equals(unit.correctionVector.y)) ? true : false;
    bool sendZ = (!unit.transform.position.z.Equals(unit.correctionVector.z)) ? true : false;
    bool sendD = (!unit.transform.eulerAngles.y.Equals(unit.correctionDirection)) ? true : false;

    // Only send moving data.
    if (sendX && sendY && sendZ && sendD)
    {
        uc = new UnitCorrectionFloat(0, unit.id, unit.transform.position.x, unit.transform.position.y, unit.transform.position.z, unit.transform.eulerAngles.y);
    }
    else
    {
        if (sendX && sendZ && sendD)
        {
            uc = new UnitCorrectionFloat(1, unit.id, unit.transform.position.x, 0, unit.transform.position.z, unit.transform.eulerAngles.y);
        }
        else if (sendX && sendZ)
        {
            uc = new UnitCorrectionFloat(2, unit.id, unit.transform.position.x, 0, unit.transform.position.z, 0);
        }
        else // (sendD)
        {
            uc = new UnitCorrectionFloat(3, unit.id, 0, 0, 0, unit.transform.eulerAngles.y);
        }
    }

    return uc;
}

CORE FUNCTION

public void PrepareUnitCorrections()
{
    List<object> corrections = new List<object>();

    // Correct unit vector and rotation.
    foreach (Unit unit in Unit.list)
    {
        // Only send corrections for units in motion.
        if (!unit.transform.position.Equals(unit.correctionVector))
        {
            corrections.Add(GenerateUCF(unit));
            unit.correctionVector = unit.transform.position;
            unit.correctionDirection = unit.transform.eulerAngles.y;
            unit.correctionIntX = unit.intX;
            unit.correctionIntY = unit.intY;
            unit.correctionIntZ = unit.intZ;
        }
    }

    if (corrections.Count > 0)
    {
        MemoryStream ms = new MemoryStream();
        using (BinaryWriter writer = new BinaryWriter(ms))
        {
            writer.Write(corrections.Count);
            writer.Write(PlayoutDelayBuffer.GetThisIndex());

            foreach (object uc in corrections)
            {
                if (uc is UnitCorrectionFloat)
                {
                    UnitCorrectionFloat ucf = (UnitCorrectionFloat)uc;
                    writer.Write(ucf.type);
                    writer.Write(ucf.id);

                    switch (ucf.type)
                    {
                        case 0: // XYZD
                            writer.Write(ucf.x);
                            writer.Write(ucf.y);
                            writer.Write(ucf.z);
                            writer.Write(ucf.direction);
                            break;
                        case 1: // XZD
                            writer.Write(ucf.x);
                            writer.Write(ucf.z);
                            writer.Write(ucf.direction);
                            break;
                        case 2: // XZ
                            writer.Write(ucf.x);
                            writer.Write(ucf.z);
                            break;
                        case 3: // D
                            writer.Write(ucf.direction);
                            break;
                        default:
                            Debug.Log(" ERROR READING BINARY - UNKNOWN TYPE " + ucf.type);
                            break;
                    }
                    continue;
                }
            }
        }

        byte[] array = ms.ToArray();
        Debug.Log(" package raw bytes " + array.Length);

        // Send corrections to all.
        for (int i = 1; i < Game.MAX_PLAYERS; i++)
        {
            if (lobbyPlayers[i] != null)
            {
                // Send as datagram.
                LobbyPlayer lp = lobbyPlayers[i];
                int correctionsChannelId = 1;
                NetworkTransport.Send(lp.socket, lp.connection, correctionsChannelId, array, array.Length, out error);
            }
        }
    }
}

CLIENT

RECEIVE DATA FUNCTION (from client's socket data function)

private void CorrectionEvent(byte[] buffer)
{
    List<object> corrections = new List<object>();
    MemoryStream ms = new MemoryStream(buffer);
    int timeIndex = 0;

    using (BinaryReader reader = new BinaryReader(ms))
    {
        int count = reader.ReadInt32();
        timeIndex = reader.ReadInt32();

        for (int i = 0; i < count; i++)
        {
            byte type = reader.ReadByte();
            ushort id = reader.ReadUInt16();
            float xf = 0, yf = 0, zf = 0, directionf = 0;

            switch (type)
            {
                case 0: // XYZD
                    xf = reader.ReadSingle();
                    yf = reader.ReadSingle();
                    zf = reader.ReadSingle();
                    directionf = reader.ReadSingle();
                    break;
                case 1: // XZD
                    xf = reader.ReadSingle();
                    zf = reader.ReadSingle();
                    directionf = reader.ReadSingle();
                    break;
                case 2: // XZ
                    xf = reader.ReadSingle();
                    zf = reader.ReadSingle();
                    break;
                case 3: // D
                    directionf = reader.ReadSingle();
                    break;
            }

            corrections.Add(new UnitCorrectionFloat(type, id, xf, yf, zf, directionf));
        }
    }

    BufferInputUnitCorrections iuc = new BufferInputUnitCorrections(timeIndex, corrections);
    PlayoutDelayBuffer.AddInput(iuc);
}

CLIENT PROCESSING FUNCTION (called by PlayoutBuffer at correct time index)

private void UnitCorrections(BufferInputUnitCorrections piuc)
{

    foreach (object uc in piuc.corrections)
    {
        // UnitCorrectionFloat teleports unit to new position.
        if (uc is UnitCorrectionFloat)
        {
            UnitCorrectionFloat ucf = (UnitCorrectionFloat)uc;
            Unit unit = Unit.list.FirstOrDefault(u => u.id == ucf.id);

            if (unit != null)
            {
                float x = (ucf.x.Equals(0)) ? unit.transform.position.x : ucf.x;
                float y = (ucf.y.Equals(0)) ? unit.transform.position.y : ucf.y;
                float z = (ucf.z.Equals(0)) ? unit.transform.position.z : ucf.z;
                float direction = (ucf.direction.Equals(0)) ? unit.transform.eulerAngles.y : ucf.direction;

                unit.transform.position = new Vector3(x, y, z);
                unit.transform.eulerAngles = new Vector3(0, direction, 0);

                unit.correctionVector = unit.transform.position;
                unit.correctionDirection = unit.transform.eulerAngles.y;
            }
            continue;
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Welcome to CodeReview. Nice first post. Have you tried profiling your code? \$\endgroup\$ – Oscar Smith Oct 28 '17 at 22:52
  • \$\begingroup\$ @OscarSmith thank you, and I have never done that before. But I shall have to investigate. \$\endgroup\$ – inappropriateCode Oct 29 '17 at 11:26
3
\$\begingroup\$

Implementation

  • A unit's position can never be set to 0 on any of the cardinal axes: The client will just discard the change! I don't think this is intended. Maybe switch over ucf.type instead of comparing new values to 0 in UnitCorrections?
  • A unit's position will be updated wrongly if it moves on either the x-axis or the z-axis: The updates are only sent if both x- and z-coordinates have changed!
  • UnitCorrectionFloat.type should probably be an enum (possibly with [Flags] attribute to allow movements of any type).

Design / Scalability

  • PrepareUnitCorrections has to iterate over all the units. Since there probably already is a method to update the units anyways, why not add the units whose position changed into a HastSet<Unit> there and only iterate over that set of changed units in PrepareUnitCorrections? (Clear it afterwards to only record new changes!)
  • Every change in a unit's position is sent to every player - regardless whether they actually need that information. If the player can't see the unit (e.g. it's in the fog of war / outside of vision radius), the changes of those units don't need to be sent to that specific player.
  • Also, only changes in position are sent. It might be more efficient to only send changes in the velocity / movement target (i.e. the clients can move the units on their own and corrections only need to be sent in case the velocity / movement is changed instead of the position). After all, if the movement is constant, it's easy to calculate where a unit will be.
\$\endgroup\$
  • \$\begingroup\$ Thank you, I have implemented i1, i2, and i3. I didn't even notice i2 was an issue! As for d1, that is somewhere deeper in Unity and I don't have a general update function yet. d2, I haven't implemented fog yet, but it's definitely to consider. d3 is interesting, though I fear it may lead to desyncs since movement speed is based in floats, the guts of which are pretty deep in Unity. I can't guarantee determinism with velocity, but good idea if I could! \$\endgroup\$ – inappropriateCode Oct 29 '17 at 12:38

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.