[개발일지] Unity Multiplay #1 - Lobby

qweasfjbv·2025년 3월 2일

개발일지

목록 보기
1/8
post-thumbnail

개요


Steamworks SDK 는 cpp로 짜여있습니다.
따라서 이걸 Unity에서 사용하려면 추가적인 플러그인이 필요한데, 가장 유명한건 Steamworks.NET입니다.

하지만 Steamworks.NET은 관련 자료가 찾기 힘들기도 하고, FacePunch 라는 더 편한 플러그인이 있어서 저는 Netcode for GameobjectsFacePunch 를 사용해서 로비를 구현해보겠습니다.

참고한 유튜브 영상 링크 입니다.

구현


GameNetworkManager

		private void Start()
		{
			transport = GetComponent<FacepunchTransport>();

			SteamMatchmaking.OnLobbyCreated += SteamMatchmaking_OnLobbyCreated;
			SteamMatchmaking.OnLobbyEntered += SteamMatchmaking_OnLobbyEntered;
			SteamMatchmaking.OnLobbyMemberJoined += SteamMatchmaking_OnLobbyJoined;
			SteamMatchmaking.OnLobbyMemberLeave += SteamMatchmaking_OnLobbyLeaved;
			SteamMatchmaking.OnLobbyInvite += SteamMatchMaking_OnLobbyInvite;
			SteamMatchmaking.OnLobbyGameCreated += SteamMatchmaking_OnLobbyGameCreated;
			SteamFriends.OnGameLobbyJoinRequested += SteamFriends_OnGameLobbyJoinRequested;

		}

		private void OnDestroy()
		{
			SteamMatchmaking.OnLobbyCreated -= SteamMatchmaking_OnLobbyCreated;
			SteamMatchmaking.OnLobbyEntered -= SteamMatchmaking_OnLobbyEntered;
			SteamMatchmaking.OnLobbyMemberJoined -= SteamMatchmaking_OnLobbyJoined;
			SteamMatchmaking.OnLobbyMemberLeave -= SteamMatchmaking_OnLobbyLeaved;
			SteamMatchmaking.OnLobbyInvite -= SteamMatchMaking_OnLobbyInvite;
			SteamMatchmaking.OnLobbyGameCreated -= SteamMatchmaking_OnLobbyGameCreated;
			SteamFriends.OnGameLobbyJoinRequested -= SteamFriends_OnGameLobbyJoinRequested;

			if (NetworkManager.Singleton == null) return;

			NetworkManager.Singleton.OnServerStarted -= Singleton_OnServerStarted;
			NetworkManager.Singleton.OnClientConnectedCallback -= Singleton_OnClientConnectedCallback;
			NetworkManager.Singleton.OnClientDisconnectCallback -= Singleton_OnClientDisconnectedCallback;
		}

GameNetworkManagerServerManager, FacepunchTransport 와 같은 오브젝트에 붙어있어야합니다.
가능한 콜백부터 먼저 연결시켜줍니다.


		private void SteamMatchmaking_OnLobbyCreated(Result result, Lobby lobby)
		{
			if (result != Result.OK)
			{
				Debug.Log("Lobby was not created");
				return;
			}
			lobby.SetPublic();
			lobby.SetJoinable(true);
			lobby.SetGameServer(lobby.Owner.Id);
			Debug.Log($"Lobby created : {lobby.Owner.Name}");
		}

		private void SteamMatchmaking_OnLobbyEntered(Lobby lobby)
		{
			Debug.Log("LobbyEntered!");
			if (NetworkManager.Singleton.IsHost) return;
			StartClient(currentLobby.Value.Owner.Id);
		}

OnLobbyCreated 는 Host에서만 호출되는 함수입니다.
Host가 호출되면 Lobby의 Owner를 Server로 설정하는 등의 세팅을 합니다.

OnLobbyEntered 는 Host와 Client에서 전부 호출되는 함수입니다.
따라서, StartClient 함수를 호출하기 위해서는 Host를 걸러야합니다.


		public async void StartHost(int maxMembers)
		{
			NetworkManager.Singleton.OnServerStarted += Singleton_OnServerStarted;
			NetworkManager.Singleton.StartHost();
			GameManagerEx.Instance.MyClientId = NetworkManager.Singleton.LocalClientId;
			currentLobby = await SteamMatchmaking.CreateLobbyAsync(maxMembers);
		}

		public void StartClient(SteamId steamId)
		{
			NetworkManager.Singleton.OnClientConnectedCallback += Singleton_OnClientConnectedCallback;
			NetworkManager.Singleton.OnClientDisconnectCallback += Singleton_OnClientDisconnectedCallback;
			transport.targetSteamId = steamId;
			GameManagerEx.Instance.MyClientId = NetworkManager.Singleton.LocalClientId;
			if (NetworkManager.Singleton.StartClient())
			{
				Debug.Log("Client has started");
			}
		}

StartHost 는 버튼을 통해 실행합니다.
NetworkManager.StartHostCreateLobby 를 통해 로비를 만들 수 있도록 합니다.

StartClient 에서는 Host의 steamId를 받고 facepunchTransport 에 targetSteamId 를 설정합니다.


		public async void Disconnected()
		{
			if (NetworkManager.Singleton.IsHost)
			{
				NetworkTransmission.instance.DisconnectAllClientRPC();
				await Task.Delay(500);
			}

			currentLobby?.Leave();
			if (NetworkManager.Singleton == null) return;

			if (NetworkManager.Singleton.IsHost)
			{
				NetworkManager.Singleton.OnServerStarted -= Singleton_OnServerStarted;
			}
			else
			{
				NetworkManager.Singleton.OnClientConnectedCallback -= Singleton_OnClientConnectedCallback;
			}
			NetworkManager.Singleton.Shutdown(true);
			GameManagerEx.Instance.Disconnected();
		}

연결을 끊는 Disconnected() 함수입니다. 연결이 끊겼거나, 끊고 싶을때 사용합니다.
해당 함수에서는 등록한 Callback들을 정리하고 다른 클라이언트들의 연결을 끊습니다.


NetworkTransmission

		[ServerRpc(RequireOwnership = false)]
		public void IWishToSendAChatServerRPC(string message, ulong fromwho)
		{
			ChatFromServerClientRPC(message, fromwho);
		}

		[ClientRpc]
		private void ChatFromServerClientRPC(string message, ulong fromwho)
		{
			GameManagerEx.Instance.SendMessageToChat(message, fromwho, false);
		}

모든 사람에게 채팅을 띄우는 RPC 함수입니다.



		[ServerRpc(RequireOwnership = false)]
		public void AddMeToDictionayServerRPC(ulong steamId, string steamName, ulong clientId)
		{
			GameManagerEx.Instance.SendMessageToChat($"{steamName} has joined", clientId, true);
			GameManagerEx.Instance.AddPlayerToDictionary(clientId, steamName, steamId);
			GameManagerEx.Instance.UpdateClients();
		}

		[ClientRpc]
		public void UpdateClientsPlayerInfoClientRPC(ulong steamId, string steamName, ulong clientId)
		{
			GameManagerEx.Instance.AddPlayerToDictionary(clientId, steamName, steamId);
		}

		[ServerRpc(RequireOwnership = false)]
		public void RemoveMeFromDictionaryServerRPC(ulong steamId)
		{
			RemovePlayerFromDictionaryClientRPC(steamId);
		}

		[ClientRpc]
		public void RemovePlayerFromDictionaryClientRPC(ulong steamId)
		{
			Debug.Log("removing client");
			GameManagerEx.Instance.RemovePlayerFromDictionary(steamId);
		}

GameManagerEx 에서 갖고있는 PlayerDictionary에 player를 추가/삭제하는 RPC함수들입니다.
Dictionary<clientId, PlayerInfoClass> 를 Entry로 가집니다.


		[ServerRpc(RequireOwnership = false)]
		public void IsTheClientReadyServerRPC(bool ready, ulong clientId)
		{
			AClientMightBeReadyClientRPC(ready, clientId);
		}

		[ClientRpc]
		private void AClientMightBeReadyClientRPC(bool ready, ulong clientId)
		{
			GameManagerEx.Instance.UpdatePlayerIsReady(ready, clientId);
		}

Player들의 Ready 상태를 변경하는 RPC 함수들입니다.
UI에서 Ready/Notready 버튼을 클릭할 때나, 처음 Connected 될 때 호출해서 정보를 갱신합니다.


GameManagerEx

		public Action<string, string> OnSendMessageAction { get; set; }
		public Action <PlayerInfo> OnAddPlayerAction { get; set; }
		public Action <PlayerInfo> OnRemovePlayerAction { get; set; }
		public Action<bool, ulong> OnUpdatePlayerReadyAction { get; set; }

해당 GameManagerEx 는 싱글톤 오브젝트이고, Network 를 안정적으로 유지하기 위해서 BaseScene에서 새로운 씬을 로드/언로드 하는 방식으로 구현했기 때문에 Action으로 UI가 콜백함수를 등록/해제할 수 있도록 했습니다.

		public void SendMessageToChat(string text, ulong fromwho, bool server)
		{
			string name = Constants.NAME_SERVER;

			if (!server && playerInfo.ContainsKey(fromwho))
			{
				name = playerInfo[fromwho].steamName;
			}

			OnSendMessageAction(name, text);
		}

예를 들어, SendMessageToChat 같은 경우에는, UI에서 출력할 변수만 넘겨서 처리할 수 있도록 했습니다.

		public void AddPlayerToDictionary(ulong clientId, string steamName, ulong steamId)
		{
			if (!playerInfo.ContainsKey(clientId))
			{
				PlayerInfo pi = new PlayerInfo(steamName, steamId);
				playerInfo.Add(clientId, pi);
				OnAddPlayerAction?.Invoke(pi);
			}
		}

AddPlayerToDictionary 또한 마찬가지로, PlayerCard를 구성하기 위한 최소한의 정보만 담은 PlayerInfo 자료형으로 UI에 정보를 넘겨서 처리하도록 했습니다.

마무리


여기까지하고 UI를 만들면 호스트로 서버를 열고, 초대를 통해 해당 서버에 접속할 수 있게 됩니다.
해당 부분과 씬 관리에 대한 부분은 다음에 다루도록 하겠습니다.

참고자료


https://www.youtube.com/watch?v=kBgnIJUfQak

0개의 댓글