Hello! Today I would like to tell you about one of the ways how to create a local multiplayer in Unity. This solution is suitable for showcases, dough features or local multiplayer. For example, if you want to see what the player is doing, but you don’t want to say that you spend extra resources on an android and take a screencast using ADB, then you can simply raise the server on some kind of machine as a copy of the application that runs on the phone and send there information about the actions of the player.

I will briefly describe how this can be done with HLAPI on Unet, but not through NetworkingManager, but a bit more low-level. By a good tradition I will attach an example of my own implementation of such a client-server interaction. I ask you not to judge an example strictly, as I understand perfectly well that the architecture of this solution is worthless and creates a lot of problems in the future. My goal was as quickly as possible (over the weekend) to write a system in which you can show the principle of working with the network. Also I will say what problems I had to face. In this example implementation, it is assumed that the server is also a Unity application.
On the Internet, the most frequent example of how to make multiplayer is chat, but I love games, and it seemed boring to make chat. Therefore, I decided to make out how to make multiplayer using the example of daggers. Assume that all the logic of the game, determining the winner, changing moves, etc., are written, and we have to tie the server. In the simplest case, we need to process 2 messages in tic-tac-toe. Determining the order of the course (distribution of aydishnikov) and the capture of the cell.
')

In general, the game in this example works very simply. There are 2 scenes. Boot and gameplay. When loading the gameplay scene, we generate a field on which players play. There is also a check of the victory conditions, there are classes responsible for the work of the UI, as well as the order of moves and the overall logic of the game, but we are not particularly interested. The main classes that are responsible for the grid are Server, Client, NetManager and a separate file for NetMessages messages and the enum MyMsgType defined in it. They are a wrapper over Unet tools. From the point of view of Unet, the main classes that we will use are NetworkClient, NetworkServer, MessageBase and MsgType. What are these classes?
The simplest classes are MessageBase and MsgType. The first is an abstract class from which all our messages must be inherited in order to forward them between the client and the server. MsgType is just a class that stores constants that are responsible for a certain set of messages embedded in Unet.

NetworkServer is a singleton that provides the ability to handle communication with remote clients. Under the hood, he uses an instance of NetworkServerSimple and is essentially a convenient wrapper over it. First we need to start the server on a specific port. To do this, call the
Listen (int serverPort) method — this method starts the server on the serverPort port. (I recall that all ports in the range from 0 to 1023 are system ports and should not be used as a parameter of this method)
Excellent server is running and listening to some port. Now we need him to respond to messages. To do this, register the handler using the
RegisterHandler method
(short msgType, Networking.NetworkMessageDelegate handler) . This method accepts a message type and a delegate. The delegate, in turn, must accept the NetworkMessage input parameter. Suppose we want to, at the moment when a player joins him on the server, the gameplay scene starts to load, as well as aydishniki players. Then you need to register the handler for the corresponding message, as well as implement the method that we will pass as a delegate for registration.
It looks like this:
An example of handler and delegate registrationNetworkServer.RegisterHandler(MsgType.Connect, OnConnect); private void OnConnect(NetworkMessage msg) { Debug.Log(string.Concat("Connected: ", msg.conn.address)); var connId = msg.conn.connectionId; if (NetworkServer.connections.Count > Constants.PLAYERS_COUNT) { SendPlayerID(connId, -1); } else { int index = Random.Range(0, Constants.PLAYERS_IDS.Length); SendPlayerID(connId, Constants.PLAYERS_IDS[index]); _CurrentUser.PlayerID = Constants.PLAYERS_IDS[(index + 1) % Constants.PLAYERS_COUNT]; SceneManager.LoadScene(1); } }
Now the OnConnect method will be called at each connection of some user. It should be clarified that in this implementation, the IDs determine the order of the move, so when the first connection, IDs for the client and server are selected. Other clients automatically get an id equal to -1, which means that this client is a spectator.
Simple server is. Now customers would not interfere. To do this, we use the NetworkClient class. In order to join the server, simply call the
Connect method
(string serverIp, int serverPort) . As a port, we set the port that our server listens to, set localhost as an ip if we test our application on the same machine, or ip a computer on the local network that we use as a server (you can find it in the network settings, or in the console using the ipconfig command on the computer that will act as the server).
Great, we can connect. Earlier it was said that the server is distributed by aydishniki. So we first need to send a message about the ID, as well as register the handler of this message on the client. As mentioned earlier, all messages must be inherited from MessageBase. First we define them (I prefer to do this in a separate file):
Message and its type public class PlayerIDMessage : MessageBase { public int PlayerID; } public enum MyMsgType : short { PlayerID = MsgType.Highest + 1, }
To send this message, call the
Send method
(short msgType, Networking.MessageBase msg) on the client, which will send the msg message of “type” msgType to the server, or on the server one of the methods depending on the
SendToAll target
(short msgType, Networking.MessageBase msg) or
SendToClient (int connectionId, short msgType, Networking.MessageBase msg) , where connectionId is the id of a specific client.
To read our custom messages that came to us from another application, we use
reader.ReadMessage () . For example:
Processing client came IT private void OnPlayerID(NetworkMessage msg) { PlayerIDMessage message = msg.reader.ReadMessage<PlayerIDMessage>(); _CurrentUser.PlayerID = message.PlayerID; SceneManager.LoadScene(1); }
In principle, everything. Further use depends on the specifics. We create the messages we need depending on the gameplay and send them. In the tic-tac-toe, I also identified another message that is responsible for capturing the point. The full implementation of the project can be found
here .
What else you want to say, and what you had to spend time on, is a couple of things that you may not be obvious if you have not worked with networks.
1. Check that the unit editor is not blocked by your firewall for communication using TCP and UDP protocols. Once I spent some time on it, despite the fact that I got to the firewall and set the required port as an exception, but I did not check that the editor is unlocked.
2. Send value-type or serializable reference-type in messages, as well as don’t pass MonoBehavior heirs. It is also important to understand that the reference-type in this case will transmit a copy of the object, and not the object itself, and it must be processed accordingly.
In the case when we are talking about a local network and previously known hardware, there are not many problems arising in the case of a “real” multiplayer. Virtually no need to think about delays, fluctuations, packet loss and so on. Therefore, such a solution can be useful, although these problems can be solved with such an approach to a certain extent. Compared to the higher level of abstraction in Unet, via NetworkManager, NetworkBehavior, etc., it provides more understandable and obvious flexibility (in my opinion) if clients need to load different scenes, etc., and say the server is used , as streaming, and shows what is displayed on the player's device, taking its position and rotation + duplicating what is happening on its side. In other cases, when you need a solution faster and you can work with the network, Unet provides the ability to write at the transport level.
I would also like to clarify the decision on the githab (not in terms of tools, but in terms of approach to architecture) in my opinion, this is an example of how not to do it. Not to mention the architecture of the game itself, the problem is that the logic on the client and on the server is considered independently. When implementing an adequate multiplayer with a client-server architecture, it is better when the game state is stored on the server and replicated to the clients, and the clients send commands that change the game state on the server. This of course also depends on many factors, but on average this approach.
I will duplicate here the
link to the solution .
Thanks for attention!