Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!

Dzisiejszy odcinek: Komponenty w UNet

Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zatem zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy

Teoria

Jakiś czas temu opisywałem jak tworzyć gry multiplayer z wykorzystaniem UNet na podstawie prostego sterowania. Taki projekt, był trochę wynajdywaniem koła na nowo, bo o ile chodzi o przemieszczanie graczy, Unity daje nam gotowe narzędzia. Więc żeby nie wynajdywać innych kół, postanowiłem opisać najciekawsze komponenty dostępne w UNet.

NetworkTransform

Jeśli przerabiałeś sobie moje poprzednie tutoriale odnośnie UNeta to wiesz, że robiliśmy tam system sterowania postacią dla gry multiplayer. Przy czym stworzony tam kod można prosto zastąpić komponentem: NetworkTransform.

NetworkTransform zapewnia nam synchronizację parametru transform obiektu na wszystkich komputerach.Tzn. jeśli przesuniemy obiekt o 10 i posiada on ten komponent, obiekt zostanie przesunięty na wszystkich klientach i serwerze.

Ważne jest tutaj, aby ustawić sobie odpowiednio NetworkIdentity. Ponieważ system dla klienta będzie dobrze działał tylko wtedy, gdy ustalimy mu bycie obiektem klienta. Jeśli UNet nie zostanie o tym poinformowany, to nie dokona synchronizacji.

Teraz aby zrobić to samo, co w poprzednich tutorialach, wystarczy dodać do obiektu gracza ten komponent i wzbogacić go o taki skrypt:

using UnityEngine;
using UnityEngine.Networking;
public class Moving : NetworkBehaviour
{
	void Update()
	{
		if (isLocalPlayer) {
			KeyCode[] arrowKeys = { KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.RightArrow, KeyCode.LeftArrow };
			foreach (KeyCode arrowKey in arrowKeys) {
				if (!Input.GetKey (arrowKey))
					continue;
				switch (arrowKey) {
				case KeyCode.UpArrow:
					transform.Translate (0, 0, Time.deltaTime);
					break;
				case KeyCode.DownArrow:
					transform.Translate (0, 0, -Time.deltaTime);
					break;
				case KeyCode.RightArrow:
					transform.Translate (Time.deltaTime, 0, 0);
					break;
				case KeyCode.LeftArrow:
					transform.Translate (-Time.deltaTime, 0, 0);
					break;
				}
			}
		}
	}
}

Jak widać, skrypt nie robi nic, po za dokonaniem lokalnego przesunięcia, po naciśnięciu przycisku. NetworkTransform synchronizuje pozycję gracza, ale nie tworzy kodu sterowania!

Zaznaczamy, że obiekt jest obiektem lokalnym (nie serwerowym).
Zaznaczamy, że obiekt jest obiektem lokalnym (nie serwerowym).

NetworkIdentity zostanie dodane automatycznie po dodaniu NetworkTransform. Natomiast parametry samego NetworkTransform to:

  • Network Send Rate – Czyli jak często w trakcie sekundy dokonywana jest synchronizacja pozycji. Dla obiektów, które nie muszą być synchronizowane po utworzeniu, takie jak pociski, dokumentacja zaleca ustalenie wartości tego parametru na 0,
  • Tranform Sync Method – Jaką metodę synchronizacji wykorzystać,
  • Movement Thresh – Jest to wartość, którą musi przekroczyć przesunięcie postaci, żeby nastąpiła synchronizacja,
  • Snap Threshold – Jeśli obiekt zostanie jednorazowo przesunięty o wartość większą od tego parametru, obiekt zostanie natychmiastowo przeniesiony na nową pozycję, zamiast powoli przesunięty,
  • Interpolate Movement Factor – Umożliwia interpolację przesunięcia,
  • Rotation Axis – Pozwala wybrać, w okół których osi ma być synchronizowana rotacja,
  • Interpolate Rotation Factor – Umożliwia interpolację rotacji,
  • Compress Rotation – Jak bardzo skompresować rotację (prawdopodobnie utrata dokładności, w zamian za szybkość przesyłu danych),
  • Sync Angular Velocity – Do interpolacji rotacji przesyła również velocity (prędkość).

NetworkStartPosition

Ten komponent jest prosty i bezparametrowy, a jego nazwa mówi wszystko. Zamiast kombinować gdzie gracz ma się domyślnie pojawiać, ustawiamy gdzieś NetworkStartPosition. Spawnowany gracz, pojawi się dokładnie w lokalizacji tego komponentu. Najlepiej przypisać go do pustego GameObjectu.

Przykładowe użycie NetworkStartPoint
Przykładowe użycie NetworkStartPoint

NetworkProximityChecker

Bardzo fajne narzędzie. NetworkProximityChecker określa czy gracz powinien widzieć inne obiekty sieciowe. Jako obiekt sieciowy rozumiemy taki, który jest synchronizowany pomiędzy komputerami, np. obiekty graczy. NetworkPorximityChecker posiada kilka parametrów, w tym najistotniejszy jest zasięg. Jeśli jakiś obiekt sieciowy jest w zasięgu, to zostaje wyrenderowany.

Taka rzecz jest dla nas przydatna z dwóch powodów. Po pierwsze możemy bardzo prosto zrobić sobie zasięg widzenia postaci. Np. możemy szybko określić, które jednostki w grze są widziane przez nasze jednostki. Druga sprawa, to optymalizacja łącza. Jeśli gracz nie widzi jakiejś jednostki, to nie musimy mu przesyłać danych o jego położeniu, rotacji etc. Tym samym oszczędzamy łącze. Jak wiemy z poprzedniej części kursu, Unity liczy nas za transfer, więc taka oszczędność łącza, to też oszczędność pieniędzy.

NetworkProximityChecker w UNet
NetworkProximityChecker w UNet

Co dają parametry?

  • Vis Range – Czyli jaki mamy zasięg widzenia,
  • Vis Update Interval – Jak często (w sekundach) ma być sprawdzane pole widzenia,
  • Check Method – Metoda sprawdzania zasięgu widzenia,
  • Force Hidden – Nie mam pojęcia, a dokumentacja tego nie opisuje.

Żeby go przetestować wystarczy dodać komponent do obiektu gracza i pochodzić sobie postacią. W pewnej odległości drugi gracz zniknie. Polecam tutaj ustawić Vis Range na 1, żeby szybko zaobserwować efekt.

NetworkMigrationManager

Kolejny bardzo fajny komponent wprowadzony wraz z wersją Unity 5.3. Załatwiam nam… migrację hosta. Jeśli gramy sobie w grę i gracz X jest hostem, ale z jakiegoś powodu straci połączenie, to wszyscy inni są wyrzucani z gry. Tak było do tej pory. Proste dodanie NetworkMigrationManagera, sprawia, że UNet orientuje się, że straciliśmy hosta. Wtedy podtrzymuje rozgrywkę i próbuje przywrócić połączenie z hostem. Ale każdy z graczy ma wybór opuszczenia rozgrywki, lub zgłoszenie się na hosta. Ze zgłoszonych osób UNet wybiera hosta i gra toczy się dalej. Co prawda bez tej jednej pechowej osoby…

Network Migration Manager
NetworkMigrationManager

NetworkMigrationManager oferuje własne GUI. Nie jest atrakcyjne, ale działa. Żeby go przetestować, wystarczy dodać komponent do jakiegoś obiektu, uruchomić grę dla 2 graczy i rozłączyć hosta.

NetworkTransformChild

Bardzo podobny do NetworkTransform z taką różnicą że dotyczy obiektów podrzędnych (childów). Jeśli nasz obiekt ma childy, to możemy do niego (nie do childów!) dodać NetworkTransformChild, który przypilnuje za nas synchronizację obiektów podrzędnych danego obiektu w całej grze.

NetworkTransformChild w UNet
NetworkTransformChild w UNet
  • Network Send Rate – Jak często synchronizujemy (w sekundach),
  • Target – Obiekt child, który mamy synchronizować,
  • Movement Treshold – Jest to wartość, którą musi przekroczyć przesunięcie postaci, żeby nastąpiła synchronizacja,
  • Interpolate MOvement i Interpolate Rotation – Współczynniki wykorzystane do kontroli interpolacji przesunięcia i rotacji,
  • Rotation Axis – Po jakich osiach można wykonywać rotację,
  • Compress Rotation – Dokumentacja tego nie opisuje, ale domyślam się, że możemy skompresować dane, zmniejszajć dokładność rotacji, ale oszczędzając transfer i przyspieszając przesyłanie danych.

Własny HUD

To już nie jest komponent, ale coś o co byłem pytany i uznałem, że warto o tym wspomnieć. Mamy co prawda NetworkHUD, ale jak on wygląda każdy widzi. Więc nasuwa się pytanie, jak zrobić własny HUD? Odpowiedź jest prosta, ale pewnie wielu osób nie ucieszy. Trzeba go napisać samemu. Unity udostępnia kawałek kodu, który ma mniej więcej funkcjonalność HUDa. Jest tam sporo kodu niskopoziomowego, do tego funkcję onGUI, które już znamy. Więc taki kod łatwo można dostosować do swoich potrzeb. No cóż, nie mogli za nas odwalić wszystkiego.

Poniżej kod, który rozwiązuje MatchMaking z prostym przesyłaniem komunikatów.

using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.Types;
using UnityEngine.Networking.Match;
using System.Collections.Generic;

public class SimpleSetup : MonoBehaviour {

	// Matchmaker
	List<MatchDesc> m_MatchList = new List<MatchDesc>();
	bool m_MatchCreated;
	bool m_MatchJoined;
	MatchInfo m_MatchInfo;
	string m_MatchName = "NewRoom";
	NetworkMatch m_NetworkMatch;

	// Connection/Communication
	int m_HostId = -1;
	List<int> m_ConnectionIds = new List<int>();
	byte[] m_ReceiveBuffer;
	string m_NetworkMessage = "Hello World";
	string m_LastReceivedMessage = "";
	NetworkWriter m_Writer;
	NetworkReader m_Reader;
	bool m_ConnectionEstablished;

	const int k_ServerPort = 25000;
	const int k_MaxMessageSize = 65535;

	void Awake() {
		m_NetworkMatch = gameObject.AddComponent<NetworkMatch> ();
	}

	void Start() {
		m_ReceiveBuffer = new byte[k_MaxMessageSize];
		m_Writer = new NetworkWriter ();

		// For mutliple players on one machine
		Application.runInBackground = true;
	}

	void OnApplicationQuit() {
		NetworkTransport.Shutdown ();
	}

	void OnGUI() {
		if (string.IsNullOrEmpty (Application.cloudProjectId)) {
			GUILayout.Label ("You must set up the project first. See the Multiplayer tab in the Service Window");
		} else {
			GUILayout.Label ("Cloud Project ID: " + Application.cloudProjectId);
		}

		if (m_MatchJoined) {
			GUILayout.Label ("Match joined '" + m_MatchName + "' on Matchmaker server");
		} else if (m_MatchCreated) {
			GUILayout.Label("Match '" + m_MatchName + "' created on Matchmaker server");
		}

		GUILayout.Label("Connection Established: " + m_ConnectionEstablished);

		if (m_MatchCreated || m_MatchJoined) {
			GUILayout.Label ("Relay Server: " + m_MatchInfo.address + ":" + m_MatchInfo.port);
			GUILayout.Label ("NetworkID: " + m_MatchInfo.networkId + " NodeID: " + m_MatchInfo.nodeId);
			GUILayout.BeginHorizontal ();
			GUILayout.Label ("Outgoing message:");
			m_NetworkMessage = GUILayout.TextField (m_NetworkMessage);
			GUILayout.EndHorizontal ();
			GUILayout.Label ("Last incoming message: " + m_LastReceivedMessage);

			if (m_ConnectionEstablished && GUILayout.Button ("Send message")) {
				m_Writer.SeekZero ();
				m_Writer.Write (m_NetworkMessage);
				byte error;
				for (int i = 0; i < m_ConnectionIds.Count; i++) {
					NetworkTransport.Send (m_HostId, m_ConnectionIds [i], 0, m_Writer.AsArray (), m_Writer.Position, out error);
					if ((NetworkError)error != NetworkError.Ok) {
						Debug.LogError ("Failed to send message: " + (NetworkError)error);
					}
				}
			}

			if (GUILayout.Button ("Shutdown")) {
				m_NetworkMatch.DropConnection (m_MatchInfo.networkId, m_MatchInfo.nodeId, OnConnectionDropped);
			}
		} else {
			if (GUILayout.Button ("Create Room")) {
				m_NetworkMatch.CreateMatch (m_MatchName, 4, true, "", OnMatchCreate);
			}

			if (GUILayout.Button ("Join first found match")) {
				m_NetworkMatch.ListMatches (0, 1, "", (response) => {
					if(response.success && response.matches.Count > 0) {
						m_NetworkMatch.JoinMatch(response.matches[0].networkId, "", OnMatchJoined);
					}
				});
			}

			if (GUILayout.Button ("List rooms")) {
				m_NetworkMatch.ListMatches (0, 20, "", OnMatchList);
			}

			if (m_MatchList.Count > 0) {
				GUILayout.Label ("Current rooms:");
				foreach (var match in m_MatchList) {
					if (GUILayout.Button (match.name)) {
						m_NetworkMatch.JoinMatch (match.networkId, "", OnMatchJoined);
					}
				}
			}
		}
	}

	public void OnConnectionDropped(BasicResponse callback) {
		Debug.Log ("Connection has been dropped on matchmaker server");
		NetworkTransport.Shutdown ();
		m_HostId = -1;
		m_ConnectionIds.Clear ();
		m_MatchInfo = null;
		m_MatchCreated = false;
		m_MatchJoined = false;
		m_ConnectionEstablished = false;
	}

	public void OnMatchCreate(CreateMatchResponse matchResponse) {
		if (matchResponse.success) {
			Debug.Log ("Crate match succeeded");

			Utility.SetAccessTokenForNetwork (matchResponse.networkId, new NetworkAccessToken (matchResponse.accessTokenString));

			m_MatchCreated = true;
			m_MatchInfo = new MatchInfo (matchResponse);

			StartServer (matchResponse.address, matchResponse.port, matchResponse.networkId, matchResponse.nodeId);
		} else {
			Debug.LogError ("Create match faild");
			Debug.Log (matchResponse.extendedInfo);
		}
	}

	public void OnMatchList(ListMatchResponse matchListResponse) {
		if (matchListResponse.success && matchListResponse.matches != null) {
			m_MatchList = matchListResponse.matches;
		}
	}

	public void OnMatchJoined(JoinMatchResponse matchJoin) {
		if (matchJoin.success) {
			Debug.Log ("Join match succeeded");

			Utility.SetAccessTokenForNetwork (matchJoin.networkId, new NetworkAccessToken (matchJoin.accessTokenString));

			m_MatchJoined = true;
			m_MatchInfo = new MatchInfo (matchJoin);

			Debug.Log ("Connecting to Address: " + matchJoin.address +
			" Port: " + matchJoin.port +
			" NetworkID: " + matchJoin.networkId +
			" NodeID: " + matchJoin.nodeId);
			
			ConnectThroughRelay (matchJoin.address, matchJoin.port, matchJoin.networkId, matchJoin.nodeId);
		} else {
			Debug.LogError ("Join match failed");
		}
	}

	private void SetupHost(bool isServer) {
		Debug.Log ("Initializing network transport");
		NetworkTransport.Init ();
		var config = new ConnectionConfig ();
		config.AddChannel (QosType.Reliable);
		config.AddChannel (QosType.Unreliable);
		var topology = new HostTopology (config, 4);

		if (isServer) {
			m_HostId = NetworkTransport.AddHost (topology, k_ServerPort);
		} else {
			m_HostId = NetworkTransport.AddHost (topology);
		}
	}

	private void StartServer(string relayIp, int relayPort, NetworkID networkId, NodeID nodeId) {
		SetupHost (true);

		byte error;
		NetworkTransport.ConnectAsNetworkHost (m_HostId, relayIp, relayPort, networkId, Utility.GetSourceID(), nodeId, out error);
	}

	private void ConnectThroughRelay(string relayIp, int relayPort, NetworkID networkId, NodeID nodeId) {
		SetupHost (false);

		byte error;
		NetworkTransport.ConnectToNetworkPeer (m_HostId, relayIp, relayPort, 0, 0, networkId, Utility.GetSourceID(), nodeId, out error);
	}

	void Update() {
		if (m_HostId == -1) {
			return;
		}

		var networkEvent = NetworkEventType.Nothing;
		int connectionId;
		int channelId;
		int receivedSize;
		byte error;

		// Get events from connection
		networkEvent = NetworkTransport.ReceiveRelayEventFromHost(m_HostId, out error);
		if (networkEvent == NetworkEventType.ConnectEvent) {
			Debug.Log ("Relay server connected");
		} 
		if (networkEvent == NetworkEventType.DisconnectEvent) {
			Debug.Log ("Relay server disconnected");
		}

		do {
			//Get events from server/client
			networkEvent = NetworkTransport.ReceiveFromHost (m_HostId, out connectionId, out channelId, m_ReceiveBuffer, (int)m_ReceiveBuffer.Length, out receivedSize, out error);
			if ((NetworkError)error != NetworkError.Ok) {
				Debug.LogError ("Error while receiveing network message: " + (NetworkError)error);
			}

			switch (networkEvent) {
			case NetworkEventType.ConnectEvent:
				{
					Debug.Log ("Connected through relay, ConnectionID: " + connectionId + " ChannelID: " + channelId);
					m_ConnectionEstablished = true;
					m_ConnectionIds.Add (connectionId);
					break;
				}
			case NetworkEventType.DataEvent:
				{
					Debug.Log ("Data event, ConnectionID: " + connectionId +
					" ChanneldID: " + channelId +
					" Received Size: " + receivedSize);
					m_Reader = new NetworkReader (m_ReceiveBuffer);
					m_LastReceivedMessage = m_Reader.ReadString ();
					break;
				}
			case NetworkEventType.DisconnectEvent:
				{
					Debug.Log ("Connection disconnected, ConnectionID: " + connectionId);
					break;
				}
			case NetworkEventType.Nothing:
				{
					break;
				}
			}
		} while(networkEvent != NetworkEventType.Nothing);
	}
}

Podsumowanie

To już chyba ostatnia część dotycząca UNet. W najbliższym czasie raczej nie będę do tematu wracał, chyba że coś znaczącego się w temacie zmieni. Jeśli macie pytania, zachęcam do zostawienia ich w komentarzu, na pewno odpowiem.