📜 ⬆️ ⬇️

Unity3D multiplayer basics



Hi, Habrahabr!

I, like many of you, are a big fan of multiplayer games. In them, I am attracted mainly by the spirit of competition and the opportunity to acquire improvements, accumulating achievements. And the very idea of ​​the release of an increasing number of games of this type prompts to action.
Recently, I myself took up the development of my own project. And since I did not find any articles on this topic on Habrahabr, I decided to share my experience of writing a multiplayer game on the Unity3D engine. I also want to talk about the Network and NetworkView components , the RPC attribute, and the embedded event methods. At the end of the article an example of the game and, of course, the project for Unity is presented. So…

Network class

This class is needed for the organization of the client-server connection. The main functions: creating a server, connecting to a server, creating a network instance of the prefab.
')
Basic methods:

Network.Connect (string host, int remotePort, string password = "") - connects to the host server with a remotePort port and password password . The method returns a NetworkConnectionError enumeration.

Network.InitializeServer (int connections, int listenPort, bool useNat) - creates a server with the maximum allowed number of connections ; inbound listenPort port , and useNat : use or no NAT . Also returns a NetworkConnectionError enumeration.

Network.InitializeSecurity () - called before Network.InitializeServer () to protect against cheating. Details in the official documentation . Do not call on the client!

Network.Instantiate (Object prefab, Vector3 position, Quaternion rotation, int group) - creates an instance of the prefab prefab on the network at the position position with rotation and group . Returns the entire created object, with which after creation you can perform additional actions. Details - further in the article.

Basic properties:

bool Network.isClient and bool Network.isServer - determine whether your game is a server or a client. Both properties are false if no server was created or there was no connection to the server.

string Network.incomingPassword - property sets the password for incoming connections.

NetworkPlayer Network.player - returns an instance of the local NetworkPlayer player.

NetworkPeerType Network.peerType - returns the current connection status: Disconnected (disconnected), Server (started as a server), Client (connected to the server), Connecting (attempted, in the process of connection).

NetworkPlayer [] Network.connections - returns all connected players. On the client returns only the server player.

Main events (for inherited from MonoBehaviour):

OnConnectedToServer () - called on the client upon successful connection to the server.

OnDisconnectedFromServer (NetworkDisconnection info) - called on the client when disconnected from the server and on the server when Network.Disconnect () terminates connections. The info contains the reason for the disconnection: LostConnection (loss of communication) and Disconnected (if disconnected successfully).

OnFailedToConnect (NetworkConnectionError error) - called on the client when the connection fails . error contains an error of type NetworkConnectionError .

OnNetworkInstantiate (NetworkMessageInfo info) - called on the client and server if a new instance was created using the Network.Instantiate () method. Contains info of type NetworkMessageInfo .

OnPlayerConnected (NetworkPlayer player) - called on the server when the client connects successfully and contains a player of the NetworkPlayer type.

OnPlayerDisconnected (NetworkPlayer player) - is called on the server when the client is disconnected and contains a player of the NetworkPlayer type.

OnServerInitialized () - called on the server after the server has been successfully created.

OnSerializeNetworkView (BitStream stream, NetworkMessageInfo info) is an important event for synchronizing a component with a network. Details - further in the article.

NetwokView class

This class also exists as a component for Unity, and it is designed to synchronize components on the network and to call RPC .
It has the following NetworkStateSynchronization synchronization properties:



Basic methods:

networkView.RPC (string name, RPCMode mode, params object [] args) - calls the remote procedure name, mode specifies the recipients, args - the arguments to pass to the procedure.

networkView.RPC (string name, NetworkPlayer target, params object [] args) is the same as the previous method, but sends it to a specific NetworkPlayer player.

Basic properties:

bool networkView.isMine - the property that determines whether the object is local. Very often used to verify the owner of an object.

Component networkView.observed - the component to be synchronized. If this is a script, then it should contain the OnSerializeNetworkView method (BitStream stream, NetworkMessageInfo info) mentioned above.

NetworkPlayer networkView.owner - a property that returns the owner of the object.

NetworkStateSynchronization networkView.stateSynchronization - synchronization type: Off , ReliableDeltaCompressed , Unreliable .

NetworkViewID networkView.viewID is a unique network identifier for NetworkView .

RPC attribute

According to Wikipedia, RPC is a class of technologies that allow computer programs to call functions or procedures in another address space (usually on remote computers).
The attribute is used to assign a method called from the network. For its operation, you must add the NetworkView component.

OnSerializeNetworkView method (BitStream stream, NetworkMessageInfo info)

This method is used to synchronize the component on the network. It is called whenever it is received or sent over the network.
Here are the data types that can be received / sent by the Serialize method : bool, char, short, int, float, Quaternion, Vector3, NetworkPlayer, NetworkViewID.
To check whether the reception is either transmitted, the isReading or isWriting properties are used.

I give an example of use:
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { Vector3 syncPosition = Vector3.zero; //       if (stream.isWriting) { syncPosition = rigidbody.position; //    stream.Serialize(ref syncPosition); //    } else { stream.Serialize(ref syncPosition); //     rigidbody.position = syncPosition; //     . } } 


This example is not perfect, because during its operation our objects will “twitch”. To avoid this, you need to use interpolation. More details - further in the article.

Interpolation

The essence of the interpolation lies in the fact that between reading the position from the network, we smoothly move our object through the Lerp function during the screen update.
We take the current position as the beginning and synchronized - as the end, and as the frames are updated we move our object.

For more information about how to optimize network synchronization, see the developers site: Valve Developer Community - Source Multiplayer Networking

An example of a multiplayer game

So, having an idea of ​​the basics, you can start writing a small multiplayer game. As examples, I use different ways to use NetworkView . You just have to choose for yourself the most convenient way.

Create a ServerSide.cs script and write the following there:
 using UnityEngine; using System.Collections; [RequireComponent( typeof( NetworkView ) )] //  Unity  ,     NetworkView.   NetworkStateSynchronization   Off. public class ServerSide : MonoBehaviour { private int playerCount = 0; //     public int PlayersCount { get { return playerCount; } } //           void OnServerInitialized() { SendMessage( "SpawnPlayer", "Player Server" ); //     } void OnPlayerConnected( NetworkPlayer player ) { ++playerCount; //          networkView.RPC( "SpawnPlayer", player, "Player " + playerCount.ToString() ); //        } void OnPlayerDisconnected( NetworkPlayer player ) { --playerCount; //    Network.RemoveRPCs( player ); //     Network.DestroyPlayerObjects( player ); //     } } 


Now create client script ClientSide.cs :
 using UnityEngine; using System.Collections; [RequireComponent( typeof( NetworkView ) )] //  Unity  ,     NetworkView.   NetworkStateSynchronization   Off. public class ClientSide : MonoBehaviour { public GameObject playerPrefab; //  ,       public Vector2 spawnArea = new Vector2( 8.0f, 8.0f ); //   private Vector3 RandomPosition { //      get { return transform.position + transform.right * ( Random.Range( 0.0f, spawnArea.x ) - spawnArea.x * 0.5f ) + transform.forward * ( Random.Range( 0.0f, spawnArea.y ) - spawnArea.y * 0.5f ); } } [RPC] //  Unity  ,        private void SpawnPlayer( string playerName ) { Vector3 position = RandomPosition; //      GameObject newPlayer = Network.Instantiate( playerPrefab, position, Quaternion.LookRotation( transform.position - position, Vector3.up ), 0 ) as GameObject; //      newPlayer.BroadcastMessage( "SetPlayerName", playerName ); //     (     ) } void OnDisconnectedFromServer( NetworkDisconnection info ) { Network.DestroyPlayerObjects( Network.player ); //    } } 


Thus, the client and server logic is, now for it you need to make the management of MainMenu.cs :
 using UnityEngine; using System.Collections; public class MultiplayerMenu : MonoBehaviour { const int NETWORK_PORT = 4585; //   const int MAX_CONNECTIONS = 20; //     const bool USE_NAT = false; //  NAT? private string remoteServer = "127.0.0.1"; //   (  localhost) void OnGUI() { if ( Network.peerType == NetworkPeerType.Disconnected ) { //    if ( GUILayout.Button( "Start Server" ) ) { //  « » Network.InitializeSecurity(); //   Network.InitializeServer( MAX_CONNECTIONS, NETWORK_PORT, USE_NAT ); //   } GUILayout.Space(30f); //  remoteServer = GUILayout.TextField( remoteServer ); //    if ( GUILayout.Button( "Connect to server" ) ) { //  «» Network.Connect( remoteServer, NETWORK_PORT ); //    } } else if ( Network.peerType == NetworkPeerType.Connecting ) { //    GUILayout.Label( "Trying to connect to server" ); //   } else { //    ( NetworkPeerType.Server, NetworkPeerType.Client) if ( GUILayout.Button( "Disconnect" ) ) { //  «» Network.Disconnect(); //        } } } void OnFailedToConnect( NetworkConnectionError error ) { Debug.Log( "Failed to connect: " + error.ToString() ); //         } void OnDisconnectedFromServer( NetworkDisconnection info ) { if ( Network.isClient ) { Debug.Log( "Disconnected from server: " + info.ToString() ); //        } else { Debug.Log( "Connections closed" ); //      Network.Disconnect() } } void OnConnectedToServer() { Debug.Log( "Connected to server" ); //        } } 


Network management created. Next, write the player control PlayerControls.cs . In this example, I use another way to use the NetworkView component:
 using UnityEngine; using System.Collections; [RequireComponent( typeof( Rigidbody ) )] //      Rigidbody public class PlayerControls : MonoBehaviour { /*   */ private float lastSynchronizationTime; //    private float syncDelay = 0f; //        private float syncTime = 0f; //   private Vector3 syncStartPosition = Vector3.zero; //   private Vector3 syncEndPosition = Vector3.zero; //    private Quaternion syncStartRotation = Quaternion.identity; //    private Quaternion syncEndRotation = Quaternion.identity; //    private NetworkView netView; //  NetworkView private string myName = ""; //   ( ,    ) public string MyName { get { return myName; } } //     public float power = 20f; void Awake () { netView = gameObject.AddComponent( typeof( NetworkView ) ) as NetworkView; //   NetworkView    netView.viewID = Network.AllocateViewID(); //      netView.observed = this; //    ()   netView.stateSynchronization = NetworkStateSynchronization.Unreliable; //       ,     lastSynchronizationTime = Time.time; //    } void FixedUpdate () { if ( netView.isMine ) { //    ,    ,       float inputX = Input.GetAxis( "Horizontal" ); float inputY = Input.GetAxis( "Vertical" ); if ( inputX != 0.0f ) { rigidbody.AddTorque( Vector3.forward * -inputX * power, ForceMode.Impulse ); } if ( inputY != 0.0f ) { rigidbody.AddTorque( Vector3.right * inputY * power, ForceMode.Impulse ); } } else { syncTime += Time.fixedDeltaTime; rigidbody.position = Vector3.Lerp( syncStartPosition, syncEndPosition, syncTime / syncDelay ); //   rigidbody.rotation = Quaternion.Lerp( syncStartRotation, syncEndRotation, syncTime / syncDelay ); //   } } void OnSerializeNetworkView( BitStream stream, NetworkMessageInfo info ) { Vector3 syncPosition = Vector3.zero; //    Vector3 syncVelocity = Vector3.zero; //     Quaternion syncRotation = Quaternion.identity; //    if ( stream.isWriting ) { //    ,       syncPosition = rigidbody.position; stream.Serialize( ref syncPosition ); syncPosition = rigidbody.velocity; stream.Serialize( ref syncVelocity ); syncRotation = rigidbody.rotation; stream.Serialize( ref syncRotation ); } else { //       stream.Serialize( ref syncPosition ); stream.Serialize( ref syncVelocity ); stream.Serialize( ref syncRotation ); syncTime = 0f; //    syncDelay = Time.time - lastSynchronizationTime; //     lastSynchronizationTime = Time.time; //      syncEndPosition = syncPosition + syncVelocity * syncDelay; //  ,     syncStartPosition = rigidbody.position; //      syncEndRotation = syncRotation; //   syncStartRotation = rigidbody.rotation; //   } } void SetPlayerName( string name ) { myName = name; //    } } 


I know that synchronization and control should be separate, but for example, I decided to combine them. As you noticed, here NetworkView is created during script initialization. In my opinion, this is a more convenient way to protect against the possible “forgot to add” (of course, if RequireComponent (typeof (Rigidbody)) is not written ) ), and also reduces the number of components on the object in the inspector.
For example, I had a case: when, at first glance, everything was done correctly, however, my script did not interpolate, and ignored all my actions in sync. So the mistake was that Observed was not my script, but a transform object.

So, now we have all the necessary scripts for writing a mini-game.
Create an empty object and assign it the MultiplayerMenu , ServerSide , ClientSide scripts .
Create a plane and lower it a bit.
Create a player prefab (in my example these will be balls). Create a sphere object, assign the PlayerControls script to it and add it to the prefab. Prefab we drag on ClientSide in the field Player Prefab .
That's it, compile the project (remembering to enable Run in background in the player’s settings) and run it several times. In one of the windows, click the server, on the rest - the client, and look at the result.

Link to the project .
* The project may be logical errors, but they do not affect the essence of this article.

Thank you all for your attention!
I wish you success in creating multiplayer games!

Source: https://habr.com/ru/post/211202/


All Articles