[Unity] 멀티플레이(1)
![[Unity] 멀티플레이(1)](/_next/image?url=https%3A%2F%2Fdata.develog.develrocket.com%2Fupload%2Fdevelog%2Fuser_1777093328305%2F1783691652702-ozcffs%2F_____2026-07-10_225118.png&w=3840&q=75)
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!
Netcode for GameObjects(NGO)로 멀티플레이 연습을 진행하면서, Player 프리팹에 붙어 있는 두 스크립트 — MyPlayerMovement와 PlayerStats — 를 통해 "여러 클라이언트가 접속했을 때 플레이어가 어떻게 움직이고, 어떻게 상태를 공유하는지"를 정리해봤다.
Player 프리팹에는 다음 컴포넌트들이 붙어 있다.

멀티플레이에서 가장 먼저 부딪히는 문제는 "내 입력이 왜 화면에 있는 모든 플레이어를 움직이는가"다. NetworkBehaviour를 상속받는 것만으로는 입력 처리가 자동으로 분리되지 않기 때문에, Update() 초반에 직접 소유권을 체크해줘야 한다.
private void Update()
{
if (!IsOwner)
{
return;
}
Keyboard keyboard = Keyboard.current;
if (keyboard == null)
{
return;
}
float move = 0f;
float turn = 0f;
...
}
IsOwner는 NetworkBehaviour가 제공하는 프로퍼티로, "이 클라이언트가 이 오브젝트의 소유자인가"를 알려준다. 이 한 줄이 없으면 클라이언트 A의 키보드 입력이 화면에 스폰되어 있는 모든 NetworkObject의 **Update()**에서 동시에 실행되어, 결과적으로 다른 사람의 캐릭터까지 조종하게 된다.
즉 로직은 "각 클라이언트는 자기 소유 오브젝트의 입력만 처리한다"는 원칙으로 짜여 있다.
MyPlayerMovement를 보면 이동 처리 자체는 그냥 평범한 transform.Translate / transform.Rotate이다.
transform.Rotate(0f, turn * m_RotateSpeed * Time.deltaTime, 0f);
transform.Translate(0f, 0f, move * currentSpeed * Time.deltaTime);
RPC도, NetworkVariable도 없다. 그 이유는 프리팹에 같이 붙어 있는 NetworkTransform 컴포넌트가 알아서 위치/회전 변화를 감지해서 다른 클라이언트에게 전송해주기 때문이다.


프리팹 설정을 보면 Interpolate = 1, PositionThreshold = 0.001 같은 값들이 있는데, 이게 바로 "얼마나 자주/부드럽게 동기화할지"를 결정하는 파라미터다. 덕분에 개발자는 이동 로직 자체는 싱글플레이 게임처럼 짜고, 동기화는 컴포넌트에 위임할 수 있다.
이동은 Transform 자동 동기화로 해결됐지만, 체력·이름·점수 같은 "게임 로직 데이터"는 다른 방식이 필요하다. PlayerStats에서는 두 가지 패턴으로 구현해봤다.
private readonly NetworkVariable<int> m_Hp = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private readonly NetworkVariable<FixedString32Bytes> m_DisplayName =
new NetworkVariable<FixedString32Bytes>(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner
);
값이 바뀌면 OnValueChanged 이벤트가 호출되어 각 클라이언트에서 UI 갱신 등을 처리할 수 있다.
m_Hp.OnValueChanged += HandleHealthChanged;
...
if (IsServer)
{
m_Hp.Value = m_StartHealth;
}
이름(m_DisplayName)은 반대로 쓰기 권한이 Owner다. 즉 "내 이름은 내가 정하고, 다른 사람은 읽기만 가능"한 구조로, 권한 조합만 바꿔서 완전히 다른 동기화 정책을 만들 수 있다는 걸 보여준다.
점수는 NetworkVariable 대신 RPC로 처리하도록 구현해봤다.
[Rpc(SendTo.Server)]
private void RequestScoreRpc(RpcParams rpcParams = default)
{
ulong senderClientId = rpcParams.Receive.SenderClientId;
if (senderClientId != OwnerClientId)
{
return;
}
m_Score += m_ScorePerpress;
BroadcastScoreRpc(m_Score);
}
여기서 눈여겨볼 부분은 senderClientId != OwnerClientId 체크다. RPC는 SendTo.Server로 지정해도 "이 오브젝트의 소유자만 요청을 보낼 수 있다"는 걸 자동으로 보장해주지 않는다. 악의적인 클라이언트가 남의 PlayerStats에 RPC를 직접 호출하는 걸 막으려면, 서버 쪽에서 요청자와 소유자가 일치하는지 반드시 검증해야 한다. 이게 바로 서버 권위(Server Authoritative) 모델의 핵심이다 — 클라이언트는 "요청"만 하고, 실제 값 변경과 검증은 항상 서버가 한다.
값이 바뀐 뒤에는 **BroadcastScoreRpc(SendTo.ClientsAndHost)**로 모든 클라이언트에 결과를 다시 뿌려준다. 흐름을 정리하면:
클라이언트 → (RequestScoreRpc) → 서버가 검증 후 점수 갱신
서버 → (BroadcastScoreRpc) → 모든 클라이언트에 새 점수 전파
또한 늦게 접속한 클라이언트를 위해 RequestCurrentScoreRpc로 "현재 점수 좀 알려줘"라고 서버에 물어보고, 서버는 RpcTarget.Single로 그 클라이언트에게만 답장하는 패턴도 들어 있다.

Window - Multiplayer - Multiplayer Play Mode 들어가면

이런 창이 뜨고 Player2를 체크하면

다른 게임 뷰가 만들어진다

코드를 실행하면 멀티플레이를 실행할 수 있고

각 게임 씬을 활성화 하고 키보드를 움직이면

각각 따로 움직이는 것을 볼 수 있다
두 스크립트를 나란히 보면 멀티플레이 동기화의 기본 감각이 잡힌다.
| 구분 | 대상 | 동기화 방식 | 이유 |
|---|---|---|---|
| 이동 | Transform | NetworkTransform 자동 동기화 | 매 프레임 바뀌는 연속적인 값이라 별도 RPC 관리가 비효율적 |
| 체력/이름 | 상태 값 | NetworkVariable + 읽기/쓰기 권한 | 값 변경이 드물고, 권한 규칙(누가 쓰나)이 명확 |
| 점수 | 상태 값 | RPC(Request → Server 검증 → Broadcast) | 서버 검증 로직이 필요하거나, 이벤트성 처리에 적합 |
공통적으로 관통하는 원칙은 "입력과 판정은 로컬(Owner) 또는 서버에서, 결과 전파는 네트워크 계층에 맡긴다" 는 것. IsOwner / IsServer 체크와 소유자 검증(OwnerClientId)만 잘 지켜도 대부분의 치팅 문제와 "남의 캐릭터가 조종되는" 버그를 막을 수 있다는 걸 실습을 통해 체감했다.
