Marek Winiarski

Unity3d QuickTip #41 – Komponenty w UNet

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:

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

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

Co dają parametry?

Ż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…

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

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.

Exit mobile version