요약
Player Data는 플레이어가 연결할 때, 그리고 재접속할 때 필요합니다!
ClientID는 연결될 때 생기지만 연결 해제될 때 사라지므로, ClientID를 Player Data로 쓰지 못합니다
따라서, 플레이어만의 고유한 ID를 이용하여 Player Data로 사용해야 합니다!
고유한 ID를 만들어 내기 위해선, 다음 옵션을 포함해야합니다!
- 고유한 유저 계정을 이용한 로그인 시스템
- 글로벌한 고유한 id, 예를 들어, System.Guid.NewGuid를 통해 생성된 GUID나 클라이언트 측에서 PlayerPrefs로 저장한 데이터들이 있습니다!
고유한 ID를 가지고, 각 플레이어의 Data(플레이어의 현재 상태)를 매핑시킬 수 있습니다
플레이어가 다시 연결할 때를 위해 호스트 측에서 데이터를 유지합니다!
물론, 세션이 완료돼서 데이터를 초기화할 수 있으며, 지정된 시간이 경과한 후에 초기화 할 수 있습니다!
Reconnection
유저를 재연결하는데 가장 좋은 방법은 Player Object가 생성된 후 부터 데이터를 저장(Transform Position 등 중요하다고 생각하는 data들)하여 보관하고 있다가
연결이 끊겨서 재접속할 때 OnNetworkSpawn에 데이터를 매핑시키는 것이랍니다!
이 맵핑 방식으로는 Dictionary 방식을 선택하면 쉽게 구현할 수 있습니다!
아래는 예시코드를 첨부하였으니 참고하실분은 참고하시면 좋겠습니다
코드는, Unity Boss Room 일부입니다
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Multiplayer.Samples.BossRoom
{
public interface ISessionPlayerData
{
bool IsConnected { get; set; }
ulong ClientID { get; set; }
void Reinitialize();
}
/// <summary>
/// This class uses a unique player ID to bind a player to a session. Once that player connects to a host, the host
/// associates the current ClientID to the player's unique ID. If the player disconnects and reconnects to the same
/// host, the session is preserved.
/// </summary>
/// <remarks>
/// Using a client-generated player ID and sending it directly could be problematic, as a malicious user could
/// intercept it and reuse it to impersonate the original user. We are currently investigating this to offer a
/// solution that handles security better.
/// </remarks>
/// <typeparam name="T"></typeparam>
public class SessionManager<T> where T : struct, ISessionPlayerData
{
SessionManager()
{
m_ClientData = new Dictionary<string, T>();
m_ClientIDToPlayerId = new Dictionary<ulong, string>();
}
public static SessionManager<T> Instance => s_Instance ??= new SessionManager<T>();
static SessionManager<T> s_Instance;
/// <summary>
/// Maps a given client player id to the data for a given client player.
/// </summary>
Dictionary<string, T> m_ClientData;
/// <summary>
/// Map to allow us to cheaply map from player id to player data.
/// </summary>
Dictionary<ulong, string> m_ClientIDToPlayerId;
bool m_HasSessionStarted;
/// <summary>
/// Handles client disconnect."
/// </summary>
public void DisconnectClient(ulong clientId)
{
if (m_HasSessionStarted)
{
// Mark client as disconnected, but keep their data so they can reconnect.
if (m_ClientIDToPlayerId.TryGetValue(clientId, out var playerId))
{
if (GetPlayerData(playerId)?.ClientID == clientId)
{
var clientData = m_ClientData[playerId];
clientData.IsConnected = false;
m_ClientData[playerId] = clientData;
}
}
}
else
{
// Session has not started, no need to keep their data
if (m_ClientIDToPlayerId.TryGetValue(clientId, out var playerId))
{
m_ClientIDToPlayerId.Remove(clientId);
if (GetPlayerData(playerId)?.ClientID == clientId)
{
m_ClientData.Remove(playerId);
}
}
}
}
/// <summary>
///
/// </summary>
/// <param name="playerId">This is the playerId that is unique to this client and persists across multiple logins from the same client</param>
/// <returns>True if a player with this ID is already connected.</returns>
public bool IsDuplicateConnection(string playerId)
{
return m_ClientData.ContainsKey(playerId) && m_ClientData[playerId].IsConnected;
}
/// <summary>
/// Adds a connecting player's session data if it is a new connection, or updates their session data in case of a reconnection.
/// </summary>
/// <param name="clientId">This is the clientId that Netcode assigned us on login. It does not persist across multiple logins from the same client. </param>
/// <param name="playerId">This is the playerId that is unique to this client and persists across multiple logins from the same client</param>
/// <param name="sessionPlayerData">The player's initial data</param>
public void SetupConnectingPlayerSessionData(ulong clientId, string playerId, T sessionPlayerData)
{
var isReconnecting = false;
// Test for duplicate connection
if (IsDuplicateConnection(playerId))
{
Debug.LogError($"Player ID {playerId} already exists. This is a duplicate connection. Rejecting this session data.");
return;
}
// If another client exists with the same playerId
if (m_ClientData.ContainsKey(playerId))
{
if (!m_ClientData[playerId].IsConnected)
{
// If this connecting client has the same player Id as a disconnected client, this is a reconnection.
isReconnecting = true;
}
}
// Reconnecting. Give data from old player to new player
if (isReconnecting)
{
// Update player session data
sessionPlayerData = m_ClientData[playerId];
sessionPlayerData.ClientID = clientId;
sessionPlayerData.IsConnected = true;
}
//Populate our dictionaries with the SessionPlayerData
m_ClientIDToPlayerId[clientId] = playerId;
m_ClientData[playerId] = sessionPlayerData;
}
/// <summary>
///
/// </summary>
/// <param name="clientId"> id of the client whose data is requested</param>
/// <returns>The Player ID matching the given client ID</returns>
public string GetPlayerId(ulong clientId)
{
if (m_ClientIDToPlayerId.TryGetValue(clientId, out string playerId))
{
return playerId;
}
Debug.Log($"No client player ID found mapped to the given client ID: {clientId}");
return null;
}
/// <summary>
///
/// </summary>
/// <param name="clientId"> id of the client whose data is requested</param>
/// <returns>Player data struct matching the given ID</returns>
public T? GetPlayerData(ulong clientId)
{
//First see if we have a playerId matching the clientID given.
var playerId = GetPlayerId(clientId);
if (playerId != null)
{
return GetPlayerData(playerId);
}
Debug.Log($"No client player ID found mapped to the given client ID: {clientId}");
return null;
}
/// <summary>
///
/// </summary>
/// <param name="playerId"> Player ID of the client whose data is requested</param>
/// <returns>Player data struct matching the given ID</returns>
public T? GetPlayerData(string playerId)
{
if (m_ClientData.TryGetValue(playerId, out T data))
{
return data;
}
Debug.Log($"No PlayerData of matching player ID found: {playerId}");
return null;
}
/// <summary>
/// Updates player data
/// </summary>
/// <param name="clientId"> id of the client whose data will be updated </param>
/// <param name="sessionPlayerData"> new data to overwrite the old </param>
public void SetPlayerData(ulong clientId, T sessionPlayerData)
{
if (m_ClientIDToPlayerId.TryGetValue(clientId, out string playerId))
{
m_ClientData[playerId] = sessionPlayerData;
}
else
{
Debug.LogError($"No client player ID found mapped to the given client ID: {clientId}");
}
}
/// <summary>
/// Marks the current session as started, so from now on we keep the data of disconnected players.
/// </summary>
public void OnSessionStarted()
{
m_HasSessionStarted = true;
}
/// <summary>
/// Reinitializes session data from connected players, and clears data from disconnected players, so that if they reconnect in the next game, they will be treated as new players
/// </summary>
public void OnSessionEnded()
{
ClearDisconnectedPlayersData();
ReinitializePlayersData();
m_HasSessionStarted = false;
}
/// <summary>
/// Resets all our runtime state, so it is ready to be reinitialized when starting a new server
/// </summary>
public void OnServerEnded()
{
m_ClientData.Clear();
m_ClientIDToPlayerId.Clear();
m_HasSessionStarted = false;
}
void ReinitializePlayersData()
{
foreach (var id in m_ClientIDToPlayerId.Keys)
{
string playerId = m_ClientIDToPlayerId[id];
T sessionPlayerData = m_ClientData[playerId];
sessionPlayerData.Reinitialize();
m_ClientData[playerId] = sessionPlayerData;
}
}
void ClearDisconnectedPlayersData()
{
List<ulong> idsToClear = new List<ulong>();
foreach (var id in m_ClientIDToPlayerId.Keys)
{
var data = GetPlayerData(id);
if (data is { IsConnected: false })
{
idsToClear.Add(id);
}
}
foreach (var id in idsToClear)
{
string playerId = m_ClientIDToPlayerId[id];
if (GetPlayerData(playerId)?.ClientID == id)
{
m_ClientData.Remove(playerId);
}
m_ClientIDToPlayerId.Remove(id);
}
}
}
}
위 경우는, 게임 세션이 끝났을 때와 세션이 시작하기 전에 연결해제된 플레이어들의 데이터를 모두 초기화하기 때문에 짧습니다
다음 게임 세션에 영향을 주지 않는 게임 같은 경우에 이렇게 진행하면 됩니다!(즉, 새로운 연결/시작을 다뤄야합니다!)(모든 유저들이 로비로 돌아가는 경우!!)
상황에 맞게 옵션을 컨트롤하면 됩니다!