Skip to content

justinleemans/uni-networking

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

83 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UniNetworking - Event based networking solution for Unity

A custom networking solution based around a signals architechture from my library justinleemans/signals. UniNetworking offers a solution to easily establish connections and offers an easy to use and understand messaging layer leaving you in control of what data you want to send back and forth.

UniNetworking is capable of being used in many different configurations like dedicated server and client applications as well as a host client implementation where a client hosts the server in the same application.

Although UniNetworking is designed to work with Unity specifically it is totally independant and does not rely on any Unity specific methods. As such this package could also theoretically be implemented in any other .Net platform of your choice.

Table of Contents

Installation

Currently the best way to include this package in your project is through the unity package manager. Add the package using the git URL of this repo: https://github.com/justinleemans/uni-networking.git

Quick Start

Note

The quick start guide is not finished and will be updated as the project progresses.

This is a very lightweight networking package. As such a lot of the setup is made easy for you. However the actual architecture of your networking implementation is up to you.

This package can be used to create a client-host setup where one of the clients will host the server on their machine from within the game. But this package could also be used to create dedicated client and server applications.

Running a server

To run a server you simply first have to create a server instance. When creating a server instance you have the option to choose a transport layer by passing a transport layer instance in the constructor. Currently the default transport layer is TCP. For more info on transports take a look at transports.

Server server = new Server();
Server server = new Server(new TcpServerTransport());

Once you have your server instance you can start the server. Simply call the method Start(). Before starting your server remember to set the correct connection details on your transport. For more info see transports.

If you want to stop the server again simply call Stop().

server.Start();
server.Stop();

The server also has two events that can be subscribed to for when a client either connects or disconnects to/from the server.

server.ClientConnected += OnClientConnected;
server.ClientDisconnected += OnClientDisconnected;

Both these events take a delegate with an integer as parameter which represents the connection id that has connected/disconnected.

Finally the server class comes with a method that can be used to close connections remotely, in other words kick the player. This can be done by calling the CloseConnection(connectionId) method with the id of the connection you want to close.

server.CloseConnection(connectionId);

Connecting a client

To connect a client to a server you will first need a client instance. This is practically the same as for the server.

Client client = new Client();
Client client = new Client(new TcpClientTransport());

Once you have your instance you can start connecting to a server. For this you can call the method Connect(). Before connecting the client remember to set the correct connection details on your transport. For more info see transports.

If you want to disconnect your client you can call Disconnect().

client.Connect();
client.Disconnect();

The client also has an event which can be subscribed to for when this client gets disconnected either by disconnecting themself or getting disconnected by server.

client.ConnectionClosed += OnConnectionClosed;

Running the update loop

To make sure your peer is receiving all communications and managing all connections you have to consistently update the peer by calling the Tick() method. This goes for both server and client. It is recommended to call this method from the FixedUpdate() method on a MonoBehaviour or through a similar approach. This is because you don't want you communications to be framerate dependant.

Creating messages

The message side of this system is heavily based on my signals library and because of that the process is mostly similar except for a few small changes. For more info see justinleemans/signals.

Note

In this library we use messages instead of signals. This is mostly a naming difference.

To create a message we make a new class which inherits from the abstract class Message, this class is used for all message instances you will be sending and receiving. This class is also where you will define all fields/properties that you want to pass through.

Furthermore all message classes need to include the Message attribute above the class with a unique id(int) to identify the message.

[Message(1)]
public class ExampleMessage : Message
{
}

Tip

Define all message ids as constants in a class to more easily keep track of what ids are used.

Just like with the signals library you have the option to override the method OnClear() which will be called whenever the message class get released back to the message pool. In this library it is extra important to override this method because you don't want to accidentally send values that were set in a different message call.

public override void OnClear()
{
    Foo = default;
}

Besides the OnClear() method two other overridable methods have been added. OnSerialize(IWriteablePayload payload) and OnDeserialize(IReadablePayload payload). These methods are used to read/write the values of fields and properties to/from the payload.

public override void OnSerialize(IWriteablePayload payload)
{
    payload.WriteBool(boolVariable);
    payload.WriteFloat(floatVariable);
    payload.WriteInt(intVariable);
    payload.WriteString(stringVariable);
}
public override void OnDeserialize(IReadablePayload payload)
{
    boolVariable = payload.ReadBool();
    floatVariable = payload.ReadFloat();
    intVariable = payload.ReadInt();
    stringVariable = payload.ReadString();
}

Sending messages

To send a message over the network you can either get a message and populate any fields or properties you have on your message class by getting a message with GetMessage<TMessage>() and sending it with SendMessage(message).

These methods are available on either your server or client instance depending on which side is sending the message. Keep in mind that when a client sends a message it is the server that will receive it and if server is the one sending a message, all connected clients will receive it.

var message = client.GetMessage<ExampleMessage>();
message.Foo = "bar";
client.SendMessage(message);

Or send it directly with SendMessage<TMessage>() without populating fields or properties.

client.SendMessage<TMessage>();

Warning

It is recommended to use the included GetMessage<TMessage>() method to retrieve a message instance to avoid filling up the pool and never retrieving from it.

In the case of the server you get an extra set of methods so you can send to specific connections. These methods are the same as the other methods but with an extra parameter.

server.SendMessage<TMessage>(connectionId);
server.SendMessage(message, connectionId);

Receiving messages

You can subscribe to a message using either the server or client instance and calling Subscribe<TMessage>(OnMessage) where the handler is a delegate with a message instance of the given message type as parameter.

client.Subscribe<ExampleMessage>(OnMessage);
private void OnExampleMessage(ExampleMessage message)
{
}

To unsubscribe from a message you make a call similar to subscribing by calling Unsubscribe<TMessage>(OnMessage) with the same method as you used to subscribe earlier.

client.Unsubscribe<ExampleMessage>(OnMessage);

Same as with sending message, the server class has a set of extra methods that allow you to see which connection has sent a message. This simply changes the delegate to include a connection id.

private void OnExampleMessage(ExampleMessage message, int connectionId)
{
}

Transports

The currently implemented and available transports are:

Tcp transport

The tcp transport uses a tcp protocol for connecting and sending data across a network.

These connection details are part of the transport and can be set through properties when initializing or before starting/connection the peer.

IServerTransport transport = new TcpServerTransport()
{
    Port = 7777,
    MaxConnections = 10,
}

transport.Port = 7777;
transport.MaxConnections = 10;
IClientTransport transport = new TcpClientTransport()
{
    IpAddress = "127.0.0.1",
    Port = 7777,
}

transport.IpAddress = "127.0.0.1";
transport.Port = 7777;

Creating a custom transport

If you want to implement your own transport there is a few things you will have to do. You can take a look at the included Tcp transport as an example.

You will have to create a class implementing the IServerTransport interface for the server implementation. This will require you to implement 4 events, properties or methods.

  • Action<Connection> NewConnection which is an event that should be called when a new connection is made. Should return this new connection.
  • void Start() which is to start the server.
  • void Stop() which is to stop the server.
  • void Tick() this method is the update loop for your transport, you will use this for checking wether you are able to receive a message.

Next you will have to create a class implementing the IClientTransport interface. This will also require you to implement 3 methods.

  • Connection Connect(string remoteAddress, ushort port) which is used to connect this client to a server. Should return an instance of Connection or null if failed to connect.
  • void Disconnect() which is to disconnect this client from the server.
  • void Tick() this method is the update loop for your transport, you will use this for checking wether you are able to receive a message.

And lastly you will have to create a class deriving from Connection. This is the connection representing your peer.

Next there is 3 methods you will have to implement.

  • OnSend(byte[] dataBuffer) which is used for sending the data. You get a byte array which you will have to send through your method of choice. Further manipulation of this byte array is generally not needed.
  • OnReceive(out byte[] dataBuffer) which is used to check if you have data that you can read. You will have to set the dataBuffer variable before exiting the method. If you don't have enough bytes for a full message or the data is not ready to be send through you can leave this at null. The dataBuffer will always come prefixed with a length int and a message id int. You have to read the length data and remove it before passing it on. You need to leave the message id in there.
  • OnClose() which will execute the code to close this connection.

Logging

UniNetworking comes with a custom logger. This is one of the additions made to keep this system independant from whatever platform it is used on.

To enable logging simply register your preferred log methods by calling the SetLogMethod() method on the NetworkLogger class. This method takes two arguments. First an enum of type LogLevel which determines at which method should be used for which severity. And also a method that takes a string argument for the actual log method. The available levels are Log, Warning and Error and ideally should all be provided their own methods.

NetworkLogger.SetLogMethod(LogLevel.Log, Debug.Log);

If you need to disable logging for some reason than you can do that by toggling the IsEnabled property on the NetworkLogger class.

Contributing

Currently I have no set way for people to contribute to this project. If you have any suggestions regarding improving on this project you can make a ticket on the GitHub repository or contact me directly.