
こんにちは、Habrahabr!
私は、あなたの多くと同様、マルチプレイヤーゲームの大ファンです。 それらの中で、私は主に競争の精神と改善を獲得し、成果を蓄積する能力に魅了されています。 そして、このタイプのゲームをますます公開するというアイデアは、行動を促すことです。
最近、私自身が自分のプロジェクトを開発し始めました。 そして、このテーマに関する記事がHabrahabrで見つからなかったため、 Unity3Dエンジンに基づいたマルチプレイヤーゲームの作成経験を共有することにしました。 また、 NetworkおよびNetworkViewコンポーネント 、 RPC属性、組み込みイベントメソッドについても説明します。 記事の最後に、ゲームの例と、もちろんUnityのプロジェクトが記載されています。 だから...
ネットワーククラス
このクラスは、クライアント/サーバー接続を整理するために必要です。 主な機能:サーバーの作成、サーバーへの接続、プレハブのネットワークインスタンスの作成。
主な方法:
Network.Connect(string host、int remotePort、string password = "") -remotePortポートとパスワードpasswordを使用してホストサーバーに接続します 。 このメソッドは、NetworkConnectionError列挙を返します。
Network.InitializeServer(int connections、int listenPort、bool useNat) -接続接続の最大許容数でサーバーを作成します。 listenPortインバウンドポートとuseNat : NATを使用するかどうか。 NetworkConnectionError列挙も返します。
Network.InitializeSecurity() -不正行為から保護するためにNetwork.InitializeServer()の前に呼び出されます。 公式ドキュメントの詳細。 クライアントを呼び出さないでください!
Network.Instantiate(オブジェクトプレハブ、Vector3位置、クォータニオン回転、intグループ) -回転回転およびグループgroupの位置位置にあるネットワーク上のプレハブプレハブのインスタンスを作成します。 作成後に追加のアクションを実行できる作成済みオブジェクト全体を返します。 詳細は記事の後半で説明します。
主なプロパティ:
bool Network.isClientおよびbool Network.isServer-ゲームがサーバーかクライアントかを決定します。 サーバーが作成されなかった場合、またはサーバーへの接続がなかった場合、両方のプロパティはfalseです。
string Network.incomingPassword-このプロパティは、着信接続のパスワードを設定します。
NetworkPlayer Network.player-ローカルNetworkPlayerプレーヤーのインスタンスを返します。
NetworkPeerType Network.peerType-現在の接続ステータスを返します: 切断 (切断)、 サーバー (サーバーとして実行)、 クライアント (サーバーに接続 )、 接続 (試行、接続中)。
NetworkPlayer [] Network.connections-接続されているすべてのプレーヤーを返します。 クライアントでは、サーバープレーヤーのみを返します。
主なイベント(MonoBehaviourから継承された場合):
OnConnectedToServer() -サーバーへの接続が成功したときにクライアントで呼び出されます。
OnDisconnectedFromServer(NetworkDisconnection info) -サーバーから切断するときにクライアントで呼び出され、 Network.Disconnect ()接続が完了するとサーバーで呼び出されます。 情報には、切断の理由: LostConnection (接続の喪失)およびDisconnected (正常な切断時)が含まれます。
OnFailedToConnect(NetworkConnectionErrorエラー) -接続エラーが発生したときにクライアントで呼び出されます 。 errorにはタイプNetworkConnectionErrorの エラーが含まれます。
OnNetworkInstantiate(NetworkMessageInfo info)-Network.Instantiate( )メソッドを使用して新しいインスタンスが作成された場合、クライアントとサーバーで呼び出されます。 NetworkMessageInfoタイプの情報が含まれます。
OnPlayerConnected(NetworkPlayerプレーヤー) -クライアントが正常に接続し、タイプNetworkPlayerの プレーヤーが含まれている場合にサーバーで呼び出されます。
OnPlayerDisconnected(NetworkPlayerプレーヤー) -クライアントが切断され、タイプNetworkPlayerの プレーヤーが含まれている場合にサーバーで呼び出されます。
OnServerInitialized() -サーバーが正常に作成された後にサーバーで呼び出されます。
OnSerializeNetworkView(BitStreamストリーム、NetworkMessageInfo情報) -コンポーネントをネットワークと同期するための重要なイベント。 詳細は記事の後半で説明します。
クラスnetwokview
このクラスはUnityのコンポーネントとしても存在し、ネットワーク上のコンポーネントを同期してRPCを呼び出すように設計されています。
次のNetworkStateSynchronization同期プロパティがあります。
- オフ -オブジェクトを同期しませんが、リモートプロシージャを呼び出すことができます。
- ReliableDeltaCompressed-パケットを一度に1つずつ送信し、パケットが配信されたかどうかを確認します( TCPなど )。
- 信頼性の低い -配信を保証せずに高速パケット送信を実行します( UDPなど )。
主な方法:
networkView.RPC(文字列名、RPCModeモード、paramsオブジェクト[] args) -リモートプロシージャ名を呼び出し、モードは受信者を決定します、args-プロシージャに渡す引数。
networkView.RPC(文字列名、NetworkPlayerターゲット、paramsオブジェクト[] args) -前のメソッドと同じですが、特定のNetworkPlayerプレーヤーに送信します。
主なプロパティ:
bool networkView.isMine-オブジェクトがローカルかどうかを決定するプロパティ。 オブジェクトの所有者を確認するために頻繁に使用されます。
コンポーネントnetworkView.observed-同期されるコンポーネント。 これがスクリプトである場合、上記のOnSerializeNetworkView(BitStreamストリーム、NetworkMessageInfo情報)メソッドを含める必要があります。
NetworkPlayer networkView.owner-オブジェクトの所有者を返すプロパティ。
NetworkStateSynchronization networkView.stateSynchronization-同期のタイプ: Off 、 ReliableDeltaCompressed 、 Unreliable 。
NetworkViewID networkView.viewIDは、 NetworkViewのネットワーク上の一意の識別子です 。
RPC属性
ウィキペディアによると、 RPCは、コンピュータープログラムが別のアドレス空間(通常はリモートコンピューター上)で関数またはプロシージャを呼び出すことを可能にするテクノロジーのクラスです。
この属性は、ネットワークから呼び出されるメソッドを割り当てるために使用されます。 機能するためには、 NetworkViewコンポーネントを追加する必要があります。
OnSerializeNetworkViewメソッド(BitStreamストリーム、NetworkMessageInfo情報)
このメソッドは、ネットワーク上のコンポーネントを同期するために使用されます。 データがネットワークを介して送受信されるたびに呼び出されます。
Serializeメソッドを使用して送受信できるデータの種類は次のとおりです。bool、char、short、int、float、Quaternion、Vector3、NetworkPlayer、NetworkViewID。
受信または送信が進行中かどうかを確認するには、isReadingまたはisWritingプロパティが使用されます。
使用例を示します。
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; // . } }
この例は理想的ではありません。なぜなら、その動作中にオブジェクトが「ひきつる」ためです。 これを回避するには、補間を使用する必要があります。 詳細については、この記事の後半で説明します。
補間
補間の本質は、ネットワークから位置を読み取る間に、画面の更新中にLerp関数を介してオブジェクトをスムーズに移動することです。
現在の位置を開始点として、同期をとります-終了点として、フレームが更新されると、オブジェクトを移動します。

ネットワーク同期を最適化する方法の詳細については、開発者向け Webサイトを参照してください。ValveDeveloper Community-Source Multiplayer Networking
マルチプレイヤーゲームの例
そのため、基本を理解してから、小さなマルチプレイヤーゲームの作成を開始できます。 例として、 NetworkViewのさまざまな使用方法を使用します 。 自分に最も便利な方法を選択するだけです。
ServerSide.csスクリプトを作成し、そこに以下を記述します。
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 ); // } }
クライアントスクリプト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 ); // } }
したがって、クライアントとサーバーのロジックがあり、 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" ); // } }
ネットワーク管理が作成されました。 次に、プレーヤーコントロールPlayerControls.csを記述します。 この例では、 NetworkViewコンポーネントを使用する別の方法を使用します。
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; // } }
同期と制御は分離する必要があることは知っていますが、例として、それらを組み合わせることにしました。 お気づきのように、ここではスクリプトの初期化中にNetworkViewが作成されます。 私の意見では、これは「追加するのを忘れる」可能性(もちろん、 RequireComponent(typeof(Rigidbody))が記述されていない場合)から保護するためのより便利な方法であり、インスペクター内のオブジェクトのコンポーネント数も減らします。
たとえば、一見、すべてが正しく行われたが、スクリプトが補間せず、同期中のすべてのアクションを無視した場合がありました。 そのため、エラーは、 Observedが私のスクリプトではなく、オブジェクト変換であることが判明しました。
これで、ミニゲームを作成するために必要なスクリプトがすべて揃いました。
空のオブジェクトを作成し、 MultiplayerMenu 、 ServerSide 、 ClientSideスクリプトを割り当てます 。
平面を作成し、少し下げます。
プレーヤーのプレハブを作成します(私の例では、これらはボールになります)。 sphereオブジェクトを作成し、 それにPlayerControlsスクリプトを割り当てて、プレハブに追加します。 Player PrefabフィールドのClientSideでのプレハブドラッグ。
以上で、プロジェクトをコンパイルし(プレーヤーの設定で[バックグラウンドで実行]を有効にすることを忘れないでください)、数回実行します。 ウィンドウの1つで、残りのサーバー(クライアント)をクリックして、結果を確認します。
プロジェクトへのリンク 。
*プロジェクトには論理的なエラーがあるかもしれませんが、この記事の本質には影響しません。
ご清聴ありがとうございました!
マルチプレイヤーゲームの作成に成功することを願っています。