
Steamworks SDK 는 cpp로 짜여있습니다.
따라서 이걸 Unity에서 사용하려면 추가적인 플러그인이 필요한데, 가장 유명한건 Steamworks.NET입니다.
하지만 Steamworks.NET은 관련 자료가 찾기 힘들기도 하고, FacePunch 라는 더 편한 플러그인이 있어서 저는 Netcode for Gameobjects 와 FacePunch 를 사용해서 로비를 구현해보겠습니다.
참고한 유튜브 영상 링크 입니다.
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;
}
GameNetworkManager 는 ServerManager, 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.StartHost 와 CreateLobby 를 통해 로비를 만들 수 있도록 합니다.
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들을 정리하고 다른 클라이언트들의 연결을 끊습니다.
[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 될 때 호출해서 정보를 갱신합니다.
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를 만들면 호스트로 서버를 열고, 초대를 통해 해당 서버에 접속할 수 있게 됩니다.
해당 부분과 씬 관리에 대한 부분은 다음에 다루도록 하겠습니다.