Simple Authoritative Server Part 2

This is part 2 of the authoritative server. In this example the client will tell the server where it wants to go (up, down, left, right), and the server in turn tells the client where its position is.

clientinput

This is written using Unity’s NetworkTransport API.

First the server will create itself and set up a routine to send out client position information every tenth of a second.

void Start()
{
    Application.runInBackground = true;
    NetworkTransport.Init();

    ConnectionConfig config = new ConnectionConfig();
    reliableChannel = config.AddChannel(QosType.Reliable);
    HostTopology topology = new HostTopology(config, 5);
#if UNITY_EDITOR
    // Listen on port 25000
    m_hostId = NetworkTransport.AddHostWithSimulator(topology, 200, 400, 25000);
#else
    m_hostId = NetworkTransport.AddHost(topology, 25000);
#endif

    // Send client position data every so often to those connected
    StartCoroutine(SendPositionCoroutine());
}

Here is how we send out each clients’ position as calculated by Unity’s physics engine. clientList is a list we keep of all clients that are connected to us and which GameObject is theirs. We send the server GameObject’s GetInstanceID() so that the clients will know who we’re talking about. Note we include the PacketTypeEnum as the first byte so that the client will know if this is a “position of all clients” packet or about the clients that are connected (name, color).

enum PacketTypeEnum
{
    Unknown = (1 << 0),
    Position = (1 << 1),
    Information = (1 << 2)
}

// Send client position data every so often to those connected
IEnumerator SendPositionCoroutine()
{
    NetworkWriter nr = new NetworkWriter();

    while (true)
    {
        yield return new WaitForSeconds(0.1f);

        // Anything to do?
        if (clientList.Count == 0)
            continue;

        // Reset stream
        nr.SeekZero();

        nr.Write((byte)PacketTypeEnum.Position);
        foreach (var item in clientList)
        {
            nr.Write(item.obj.GetInstanceID());
            nr.Write(item.obj.transform.position);

            // Don't pack too much
            // Fix me! Send more than one packet instead
            if (nr.Position > 1300)
                break;
        }

        // Send data out
        byte[] buffer = nr.ToArray();
        byte error;
        //Debug.Log(string.Format("Sending data size {0}", buffer.Length));
        foreach (var item in clientList)
        {
            NetworkTransport.Send(m_hostId, item.connectionId, reliableChannel, buffer, buffer.Length, out error);
        }
    }
}

Then the server has an Update routine where it listens for incoming traffic. It will create a GameObject when clients connect, remove one when they disconnect, and process client input here.

void Update()
{
    // Remember who's connecting and disconnecting to us
    if (m_hostId == -1)
        return;
    int connectionId;
    int channelId;
    int receivedSize;
    byte error;
    byte[] buffer = new byte[1500];
    NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(m_hostId, out connectionId, out channelId, buffer, buffer.Length, out receivedSize, out error);
    switch (networkEvent)
    {
        case NetworkEventType.Nothing:
            break;
        case NetworkEventType.ConnectEvent:
            ClientConnected(connectionId);
            break;
        case NetworkEventType.DisconnectEvent:
            ClientData cd = clientList.FirstOrDefault(item => item.connectionId == connectionId);
            if (cd != null)
            {
                Destroy(cd.obj);
                clientList.Remove(cd);
                Debug.Log("Client disconnected");
                // Send all clients new info
                SendClientInformation();
            }
            else
            {
                Debug.Log("Client disconnected that we didn't know about!?");
            }
            break;
        case NetworkEventType.DataEvent:
            //Debug.Log(string.Format("Got data size {0}", receivedSize));
            Array.Resize(ref buffer, receivedSize);
            ProcessClientInput(connectionId, buffer);
            break;
    }
}

To process client input we do this.

enum InputTypeEnum
{
    KeyNone = (1 << 0),
    KeyUp = (1 << 1),
    KeyDown = (1 << 2),
    KeyLeft = (1 << 3),
    KeyRight = (1 << 4),
    KeyJump = (1 << 5)
}

void ProcessClientInput(int connectionId, byte[] buffer)
{
    ClientData cd = clientList.FirstOrDefault(item => item.connectionId == connectionId);
    if (cd == null)
    {
        Debug.Log("Client that we didn't know about!?");
        return;
    }

    InputTypeEnum input = (InputTypeEnum)buffer[0];
    float deltaX = 0.0f;
    float deltaZ = 0.0f;
    if ((input & InputTypeEnum.KeyUp) == InputTypeEnum.KeyUp)
        deltaX = 1.0f;
    if ((input & InputTypeEnum.KeyDown) == InputTypeEnum.KeyDown)
        deltaX = -1.0f;
    if ((input & InputTypeEnum.KeyRight) == InputTypeEnum.KeyRight)
        deltaZ = 1.0f;
    if ((input & InputTypeEnum.KeyLeft) == InputTypeEnum.KeyLeft)
        deltaZ = -1.0f;
    Vector3 movement = new Vector3(deltaX, 0, deltaZ);
    movement = transform.TransformDirection(movement);
    movement *= 10.0f;
    cd.obj.GetComponent<CharacterController>().Move(movement * Time.deltaTime);
}

On the client side, our Update script will look like this:

void Update()
{
    if (m_hostId == -1)
        return;
    int connectionId;
    int channelId;
    int receivedSize;
    byte error;
    byte[] buffer = new byte[1500];
    NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(m_hostId, out connectionId, out channelId, buffer, buffer.Length, out receivedSize, out error);
    switch (networkEvent)
    {
        case NetworkEventType.Nothing:
            break;
        case NetworkEventType.ConnectEvent:
            m_serverConnectionId = connectionId;
            break;
        case NetworkEventType.DisconnectEvent:
            m_serverConnectionId = -1;
            break;
        case NetworkEventType.DataEvent:
            if (connectionId != m_serverConnectionId)
            {
                Debug.Log("Data not from server!?");
            }
            else
            {
                Array.Resize(ref buffer, receivedSize);
                ProcessServerData(buffer);
            }
            break;
    }
}

The client’s ProcessServerData is a big routine where we process PacketTypeEnum.Position:

class PositionData
{
    public int objectId;
    public Vector3 pos;
}

NetworkReader nr = new NetworkReader(buffer);
List<PositionData> posList = new List<PositionData>();
PositionData p;
while (nr.Position != buffer.Length)
{
    p = new PositionData();
    p.objectId = nr.ReadInt32();
    p.pos = nr.ReadVector3();
    posList.Add(p);
}

// Update game objects
foreach (var item in clientList)
{
    if (item.obj == null)
        continue;
    p = posList.FirstOrDefault(x => x.objectId == item.objectId);
    if (p == null)
        Debug.Log("Cannot find game object");
    else
        item.obj.transform.position = p.pos;
}

…we also process PacketTypeEnum.Information which is sent out from the server whenever a client connects or disconnects:

class InformationData
{
    public int objectId;
    public string name;
    public Vector3 pos;
    public float r;
    public float g;
    public float b;
}

NetworkReader nr = new NetworkReader(buffer);
List<InformationData> infoList = new List<InformationData>();
InformationData info;
while (nr.Position != buffer.Length)
{
    info = new InformationData();
    info.objectId = nr.ReadInt32();
    info.name = nr.ReadString();
    info.pos = nr.ReadVector3();
    info.r = nr.ReadSingle();
    info.g = nr.ReadSingle();
    info.b = nr.ReadSingle();
    infoList.Add(info);
}

// Remove clients that aren't listed
foreach (var item in clientList)
{
    if (item.obj == null)
        continue;
    info = infoList.FirstOrDefault(x => x.objectId == item.objectId);
    if (info == null)
        Destroy(item.obj);
}
clientList.RemoveAll(x => x.obj == null); // Note items are set to null only after Update!

foreach (var item in infoList)
{
    ClientData cd = clientList.FirstOrDefault(x => x.objectId == item.objectId);
    // Is this new client info?
    if (cd == null)
    {
        // Create new object
        GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
        // No CharacterController here!
        // Set position
        obj.transform.position = item.pos;
        // Set color
        obj.GetComponent<Renderer>().material.color = new Color(item.r, item.g, item.b);

        cd = new ClientData();
        cd.objectId = item.objectId;
        cd.name = item.name;
        cd.obj = obj;
        clientList.Add(cd);
        Debug.Log(string.Format("New client info for {0}", cd.name));
    }
}

The client runs a coroutine where it sends input information every tenth of a second:

// Send input data every so often
IEnumerator SendInputCoroutine()
{
    while (true)
    {
        yield return new WaitForSeconds(0.1f);

        // Anything to do?
        if (m_hostId == -1 || m_serverConnectionId == -1)
            continue;

        InputTypeEnum input = InputTypeEnum.KeyNone;
        float f;
        f = Input.GetAxis("Horizontal");
        if (f > 0.0f)
            input |= InputTypeEnum.KeyUp;
        if (f < 0.0f)
            input |= InputTypeEnum.KeyDown;
        f = Input.GetAxis("Vertical");
        if (f > 0.0f)
            input |= InputTypeEnum.KeyRight;
        if (f < 0.0f)
            input |= InputTypeEnum.KeyLeft;
        if (Input.GetKey(KeyCode.Space))
            input |= InputTypeEnum.KeyJump;

        if (input == InputTypeEnum.KeyNone)
            continue;

        // Send data out
        byte[] buffer = new byte[1];
        buffer[0] = (byte)input;
        byte error;
        NetworkTransport.Send(m_hostId, m_serverConnectionId, reliableChannel, buffer, buffer.Length, out error);
    }
}

That’s basically it! We are still not doing interpolation which is needed. But I think this is a good start.

The two main scripts are Server.cs and Client.cs.

Both projects at GitHub

Leave a Reply

Your email address will not be published. Required fields are marked *