Unity NGO로 구현한 Pong 멀티플레이 구조 정리

심지태
∙∙조회 8

이 프로젝트는 Host가 Server와 Player 역할을 동시에 수행하고, 다른 플레이어가 Client로 접속하는 Host-Client 구조로 구현했다.

구조를 이해하는 핵심은 각 데이터를 누가 변경하고, 게임의 최종 결과를 누가 결정하는지 구분하는 것이다.

기능담당
패들 입력과 이동각 패들의 Owner Client
공 이동과 충돌 판정Server
점수와 승패 판정Server
씬 전환과 오브젝트 생성Server
화면과 UI 표시각 컴퓨터에서 개별 처리

핵심 원칙: Client는 자신의 패들을 움직이고, Server는 게임의 최종 결과를 결정한다.

NGO의 기본 동작 방식

멀티플레이 게임이라고 해서 Host와 Client가 하나의 GameObject를 함께 사용하는 것은 아니다.

각 컴퓨터에는 Player, Ball, GameManager 등의 복사본이 따로 존재한다. NGO는 이 복사본들을 NetworkObjectId로 연결하고, 동기화가 필요한 데이터만 네트워크를 통해 전달한다.

주요 구성 요소는 다음과 같다.

  • NetworkManager: 연결, 접속자, Network Prefab, 네트워크 씬을 관리한다.
  • UnityTransport: IP와 포트를 이용해 실제 네트워크 데이터를 전송한다.
  • NetworkObject: 네트워크 오브젝트의 ID와 소유권을 관리한다.
  • NetworkBehaviour: RPC, NetworkVariable, IsServer, IsOwner 등을 사용할 수 있게 한다.
  • NetworkVariable: 위치처럼 일정 시간 동안 계속 유지되어야 하는 상태를 동기화한다.
  • ServerRpc: Client가 Server에 요청을 보낼 때 사용한다.
  • ClientRpc: Server가 Client들에게 결과나 명령을 전달할 때 사용한다.

NetworkBehaviour를 상속했다고 해서 일반 변수나 Transform이 자동으로 동기화되는 것은 아니다. 동기화할 데이터는 NetworkVariable이나 RPC를 이용해 직접 지정해야 한다.

RPC의 방향은 다음처럼 기억하면 된다.

  • ServerRpc: Client → Server
  • ClientRpc: Server → Clients

프로젝트 전체 실행 흐름

전체적인 실행 순서는 다음과 같다.

  1. Init 씬에서 NetworkManager를 생성한다.
  2. GameInitializer가 Menu 씬을 불러온다.
  3. Host는 StartHost(), Client는 StartClient()를 실행한다.
  4. Server가 모든 참가자를 Lobby 씬으로 이동시킨다.
  5. Lobby에서 Ready 상태를 RPC로 동기화한다.
  6. 두 플레이어가 모두 Ready 상태가 되면 Server가 InGame 씬을 로드한다.
  7. Server가 PlayerPaddle 두 개와 Ball을 네트워크 스폰한다.
  8. 각 Client가 자신이 소유한 패들을 움직인다.
  9. Server가 공 이동, 충돌, 점수, 승패를 계산한다.
  10. 계산 결과를 NetworkVariable과 ClientRpc를 통해 모든 화면에 반영한다.

일반 SceneManager와 Network SceneManager의 구분

Init에서 Menu로 이동할 때는 일반 SceneManager를 사용한다. 이 시점에는 아직 네트워크 세션이 시작되지 않았기 때문이다.

반면 Lobby에서 InGame으로 이동할 때는 NetworkManager.SceneManager를 사용한다. 그래야 Server와 연결된 모든 Client가 같은 씬으로 함께 이동한다.

컴포넌트 연결 구조

Network Manager

Init 씬의 Network Manager 오브젝트에는 다음 컴포넌트가 연결되어 있다.

  • NetworkManager
  • UnityTransport
  • GameInitializer

NetworkManager는 씬이 바뀌어도 사라지지 않고 전체 네트워크 세션을 관리한다.

Network Prefab 목록에는 PlayerPaddle과 Ball이 등록되어 있다. Server가 프리팹을 스폰하면 Client는 등록된 목록에서 같은 프리팹을 찾아 로컬에 생성한다.

Lobby Manager

Lobby Manager에는 다음 컴포넌트가 연결되어 있다.

  • NetworkObject
  • LobbyManager : NetworkBehaviour

LobbyManager는 Ready 버튼을 처리하고, ServerRpc와 ClientRpc를 이용해 플레이어들의 준비 상태를 맞춘다.

Game Manager

Game Manager에는 다음 컴포넌트가 연결되어 있다.

  • NetworkObject
  • GameManager : NetworkBehaviour

GameManager는 플레이어와 공을 생성하고, 점수와 승패를 관리한다.

PlayerPaddle

PlayerPaddle 프리팹은 다음과 같이 구성되어 있다.

  • NetworkObject
  • PlayerPaddle
  • PositionSync
  • SpriteRenderer
  • BoxCollider2D

PlayerPaddle은 입력과 로컬 이동을 담당하고, PositionSync는 이동 결과를 다른 참가자에게 전달한다.

Ball

Ball 프리팹은 다음과 같이 구성되어 있다.

  • NetworkObject
  • Ball
  • PositionSync
  • SpriteRenderer

Ball 프리팹 자체에는 Collider가 없다. Server에서 실행되는 Ball 스크립트가 Physics2D.Raycast()를 이용해 Wall, Paddle, ScoringZone의 Collider를 직접 검사한다.

연결과 플레이어 생성

Host와 Client의 연결 과정

  1. UnityTransport의 포트를 7777로 설정한다.
  2. Host가 NetworkManager.StartHost()를 호출한다.
  3. Host가 Server이자 Local Client로 동작하기 시작한다.
  4. Server가 Lobby 씬을 네트워크 방식으로 로드한다.
  5. Client는 Host의 IP와 포트를 입력한다.
  6. Client가 NetworkManager.StartClient()를 호출한다.
  7. 연결 승인이 완료되면 Client도 Lobby 씬으로 이동한다.

Host의 ApprovalCheck()는 최대 참가 인원을 두 명으로 제한한다.

이때 CreatePlayerObject를 false로 설정해 접속 순간에는 플레이어 오브젝트를 자동으로 만들지 않는다. 플레이어는 실제 게임이 시작되는 InGame 씬에서 GameManager가 직접 생성한다.

PlayerPaddle 생성 과정

  1. Server가 PlayerPaddle 프리팹을 Instantiate한다.
  2. SpawnAsPlayerObject(clientId)를 호출한다.
  3. 모든 참가자의 화면에 해당 패들이 생성된다.
  4. 지정된 Client가 해당 패들의 Owner가 된다.

Ball은 일반 NetworkObject.Spawn()으로 생성하기 때문에 Server가 Owner가 된다.

즉, 패들은 지정된 Client에게 소유권이 부여되지만 공은 Server가 소유하고 관리한다.

Lobby의 RPC 흐름

Lobby의 준비 상태는 일반 Dictionary에 저장된다.

일반 Dictionary는 NGO가 자동으로 동기화하지 않기 때문에 RPC를 이용해 각 참가자의 상태를 직접 맞춰야 한다.

Client가 Ready 버튼을 누르면 다음 순서로 처리된다.

  1. Client가 자신의 준비 상태를 변경한다.
  2. SetClientIsReadyServerRpc()를 호출한다.
  3. Server가 준비 상태의 원본 Dictionary를 갱신한다.
  4. Server가 SetClientIsReadyClientRpc()를 호출한다.
  5. 각 Client가 로컬 목록과 Ready UI를 갱신한다.
  6. Server가 두 플레이어 모두 Ready 상태인지 확인한다.
  7. 모두 Ready라면 Server가 InGame 씬을 로드한다.

RequireOwnership = false가 필요한 이유

LobbyManager의 Owner는 Server다.

그러나 일반 Client도 이 오브젝트의 ServerRpc를 호출해야 한다. 따라서 Ready 요청 RPC에는 RequireOwnership = false가 사용된다.

Client가 Ready를 요청하면 Server가 상태의 원본을 변경하고, 그 결과를 다시 모든 Client에게 전달한다.

패들 위치 동기화

PlayerPaddle.Update()는 모든 컴퓨터에서 실행된다.

하지만 IsOwner 검사를 사용해 해당 패들의 Owner만 입력을 처리하도록 제한한다.

패들 이동 흐름은 다음과 같다.

  1. Owner가 키보드 입력을 받는다.
  2. Owner 측의 패들 Transform을 이동시킨다.
  3. PositionSync가 위치를 NetworkVariable에 기록한다.
  4. NGO가 변경된 위치를 Server에 전달한다.
  5. Server와 다른 Client가 최신 위치를 받는다.
  6. 비소유자 측 PositionSync가 전달받은 위치를 Transform에 적용한다.

PositionSync에서 사용하는 NetworkVariable의 권한은 다음과 같다.

권한대상
읽기모든 참가자
쓰기해당 NetworkObject의 Owner

따라서 각 Client는 자신이 소유한 패들의 위치만 변경할 수 있다.

이 프로젝트는 NetworkTransform 대신 직접 만든 PositionSync를 이용해 위치를 동기화한다.

공, 충돌, 점수 동기화

공 이동과 충돌

공의 이동과 충돌 계산은 Server에서만 실행한다.

  1. Server가 공의 방향과 속도를 계산한다.
  2. Physics2D.Raycast()로 Wall과 Paddle 충돌을 검사한다.
  3. 공이 ScoringZone에 닿으면 GameManager에 점수 처리를 요청한다.
  4. 공의 위치를 PositionSync의 NetworkVariable로 전달한다.
  5. Client는 공의 물리를 계산하지 않고 전달받은 위치만 화면에 표시한다.

점수와 게임 종료

점수의 원본도 Server에만 존재한다.

  1. Server가 playerScores 배열을 변경한다.
  2. UpdateScoreTextClientRpc()로 점수를 전달한다.
  3. 각 컴퓨터가 자신의 Score UI를 갱신한다.
  4. 한 플레이어가 11점에 도달하면 Server가 승자를 결정한다.
  5. Ball의 NetworkObject를 Despawn한다.
  6. EndGameClientRpc()로 승자의 Client ID를 전달한다.
  7. 각 Client가 자신의 ID와 비교해 승리 또는 패배 UI를 표시한다.

이 프로젝트의 동기화 방식 요약

대상동기화 방식
네트워크 씬 전환NetworkManager.SceneManager
패들 생성SpawnAsPlayerObject(clientId)
공 생성NetworkObject.Spawn()
공 제거NetworkObject.Despawn()
Ready 요청ServerRpc
Ready 결과 전달ClientRpc
패들 위치Owner가 쓰는 NetworkVariable
공 위치Server가 쓰는 NetworkVariable
패들 색상과 시작 위치ClientRpc
점수와 게임 종료 UIClientRpc
공 충돌과 승패 판정Server에서만 실행

가장 중요한 기준은 다음과 같다.

  • 지속적으로 유지되어야 하는 상태에는 NetworkVariable을 사용한다.
  • 버튼 입력이나 게임 종료처럼 특정 시점에 발생하는 사건에는 RPC를 사용한다.
  • 게임 결과에 영향을 주는 최종 판정은 Server가 담당한다.
  • 로컬 플레이어의 입력은 IsOwner로 제한한다.

현재 구현에서 주의할 점

연결 종료 콜백 등록

GameManager의 연결 종료 이벤트 등록에는 오타가 있다.

OnClientDisconnected 핸들러는 OnClientConnectedCallback이 아니라 OnClientDisconnectCallback에 등록해야 한다.

현재 상태에서는 게임 도중 상대가 나가더라도 종료 처리가 실행되지 않을 수 있다.

Server 전용 RPC 호출 조건

OnNetworkSpawn()은 Server뿐만 아니라 Client에서도 실행된다.

따라서 UpdateScoreTextClientRpc()처럼 Server만 호출해야 하는 RPC는 반드시 IsServer 조건 안에서 호출해야 한다.

Client 위치를 그대로 신뢰하는 구조

현재 구현에서는 Owner Client가 패들의 위치를 직접 결정한다.

입력 반응성은 좋지만 Server가 Client의 위치를 그대로 신뢰하기 때문에 속도 조작이나 순간이동에 취약하다.

학습용 프로젝트에는 적합하지만, 실제 서비스에서는 이동 속도와 위치 변화량을 Server가 검증하거나 Server 권한 이동 구조를 적용할 필요가 있다.

위치 보간 부재

현재 PositionSync에는 보간 처리가 없다.

따라서 네트워크 상태에 따라 상대 패들과 공이 끊겨 보일 수 있다. 이후에는 NetworkTransform, Lerp 또는 별도의 보간 처리를 적용해 움직임을 부드럽게 만들 수 있다.

마무리

이 Pong 프로젝트의 핵심 네트워크 흐름은 다음 문장으로 정리할 수 있다.

Client가 자신의 패들을 움직여 위치를 전송하고, Server가 그 위치를 기준으로 공의 충돌과 점수, 승패를 결정한 뒤 결과를 모든 Client에게 전달한다.

이 구조를 이해하면 NGO의 핵심 개념인 NetworkObject, 소유권, 네트워크 스폰, RPC, NetworkVariable, Server 권한, 네트워크 씬 전환을 하나의 프로젝트 안에서 함께 정리할 수 있다.

오른쪽 입력란 추천값

  • 태그: Unity, Netcode for GameObjects, NGO, 멀티플레이, Pong, C#
  • URL 슬러그: unity-ngo-pong-multiplayer-structure
  • 요약: Unity Netcode for GameObjects로 구현한 Host-Client Pong의 씬 전환, 네트워크 스폰, 소유권, RPC, NetworkVariable, 서버 권한과 동기화 흐름을 정리합니다.