멀티플레이 게임에서 하나의 물건을 동시에 접근하면 안되는 경우가 있습니다.
예를 들어 한 사람이 들고있다면, 다른 사람은 해당 물건과 상호작용 할 수 없는 것이 일반적입니다.
따라서, 해당 물체가 현재 어떤 물체와 상호작용 하고있는지를 알 수 있도록 정보를 저장하고, 이와 관련된 함수 호출은 서버에서만 처리하여 RPC끼리 어긋나는 경우가 없도록 구현해보도록 하겠습니다.
Prop의 기반 클래스부터 차근차근 작성해보도록 하겠습니다.
// PropBase.cs
// 모든 Prop들의 기반이 되는 클래스
public class PropBase : NetworkBehaviour
{
private ItemData itemData;
public ItemData ItemData => itemData;
protected Rigidbody rigid;
public virtual void Awake()
{
rigid = GetComponent<Rigidbody>();
}
#region Transform RPC
/*
* 위치/물리 관련 RPC
* ...
*/
#endregion
}
우선 PropBase.cs 에서는 가장 기본이 되는 것들만 선언해줍니다.
이제 이 Base를 상속받아서 한 사람이 소유할 수 있는 Prop을 구현해보겠습니다.
// OwnableProp.cs
// 한 사람이 소유가능한 Prop
public class OwnableProp : PropBase
{
private NetworkVariable<ulong> ownerClientId = new NetworkVariable<ulong>(ulong.MaxValue);
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
ownerClientId.OnValueChanged += OnClientIDChanged;
}
private void OnClientIDChanged(ulong prev, ulong clientId)
{
if (clientId == ulong.MaxValue)
{
controller = null;
return;
}
controller = NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject.GetComponent<PlayerController>();
}
//
// ...
//
}
우선 해당 물건이 누구의 소유인지를 저장해두기 위해서 NetworkVariable<ulong> 를 사용하였습니다.
또한, OnClientIDChanged 콜백함수를 사용하여 해당 ID가 Connected되어있으면 controller 에 해당 플레이어의 컨트롤러 정보를 저장하여 자식 클래스들이 사용할 수 있도록 하였습니다.
// 외부에서 Interact 시작하는 함수
public void TryInteract(ulong clientId)
{
RequestOwnershipServerRpc(clientId);
}
// Ownership이 가능한지 판단 및 소유
[ServerRpc(RequireOwnership = false)]
private void RequestOwnershipServerRpc(ulong requestingClientId)
{
if (ownerClientId.Value == ulong.MaxValue)
{
ownerClientId.Value = requestingClientId;
GetComponent<NetworkObject>().ChangeOwnership(requestingClientId);
GrantInteractionClientRPC(requestingClientId);
}
else
{
// 누가 소유하고 있는 경우
}
}
// 소유하기 시작했을 때 Client측에서 실행할 함수
[ClientRpc]
private void GrantInteractionClientRPC(ulong clientId)
{
if (clientId != NetworkManager.Singleton.LocalClientId) return;
StartInteraction(clientId);
}
protected virtual void StartInteraction(ulong newOwnerClientId)
{
NetworkManager.Singleton.LocalClient.PlayerObject.GetComponent<PlayerController>().OnInteractionGranted(this);
}
일반적으로 NetworkVariable의 WritePermission은 NetworkVariableWritePermission.Server 에 있기 때문에 ServerRPC를 통해 접근해야 합니다.
또한, ServerRPC를 통해 접근함으로써 RPC가 엇갈리는 문제를 해결할 수 있습니다.
ServerRPC를 통해 소유권을 받았다면 GrantInteractionClientRPC 를 통해서 해당 클라이언트가 Interaction을 시작할 수 있도록 알립니다.
Interaction을 끝낼 때에는 더 간단하게 구현할 수 있습니다.
// 외부에서 호출하는 함수
public virtual void OnEndInteraction(Transform transform)
{
RemoveOwnershipServerRpc();
}
// Ownership을 뺏는 함수
[ServerRpc(RequireOwnership = false)]
private void RemoveOwnershipServerRpc()
{
ownerClientId.Value = ulong.MaxValue;
}
위 코드들에서 상호작용을 시작/끝내는 함수는 virtual함수로 만들었기 때문에 자식 클래스들이 각자에 맞는 기능을 구현할 수 있습니다.
예를 들어, 소화기 같은 경우는 다음과 같이 구현가능합니다.
protected override void StartInteraction(ulong newOwnerClientId)
{
base.StartInteraction(newOwnerClientId);
// 물리 상태 동기화
transform.GetComponent<Rigidbody>().useGravity = false;
rigid.isKinematic = true;
transform.GetComponent<Collider>().isTrigger = true;
SyncStateServerRPC(true);
}
public override void OnEndInteraction(Transform controller)
{
// 물리 상태 동기화
rigid.isKinematic = false;
transform.GetComponent<Rigidbody>().useGravity = true;
transform.GetComponent<Collider>().isTrigger = false;
SyncStateServerRPC(false);
base.OnEndInteraction(controller);
}
소화기는 평소에 플레이어/다른 물체들과 물리연산을 하다가, 상호작용이 시작되면 플레이어의 손에 들려서 움직여야 합니다.
따라서, useGravity, isKinematic, isTrigger 등을 적절하게 설정해 주었습니다.
추가적으로, 위치를 따라가는 부분은 모든 클라이언트가 해당 Prop의 주인이 누구인지 알고 있으므로 해당 Transform을 따라가도록 구현할 수 있습니다.

소화기를 쥐고있을 때에는 다른사람이 상호작용하려해도 상호작용이 되지 않는 모습을 볼 수 있습니다.
특히 NetworkVariable을 사용하지 않으면 동시에 한 물체에 접근했을 때 누가 먼저인지 판단하기 어렵기 때문에, 항상 ServerRPC를 사용하여 NetworkVariable에 접근함으로써 해당 문제를 해결하였습니다.
이제 네트워크 연결을 안정적으로 유지하기 위한 기법을 구현해보겠습니다.