본문으로 건너뛰기

© 2026 Molayo

YouTube요약2026. 06. 15. 10:29

방법: Unity 온라인 멀티플레이어

요약

Unity의 Netcode for GameObjects 프레임워크를 사용하여 실시간 온라인 멀티플레이어 게임을 구현하는 방법을 설명합니다. 패키지 설치부터 Network Manager 설정, ParrelSync를 활용한 효율적인 테스트 방법까지 다룹니다.

핵심 포인트

  • Netcode for GameObjects를 통한 실시간 멀티플레이어 구현
  • ParrelSync를 활용하여 에디터 내에서 다중 클라이언트 테스트 가능
  • Network Manager와 Transport 레이어의 역할 이해
  • Network Object 컴포넌트를 통한 네트워크 오브젝트 동기화

동영상: 방법: Unity 온라인 멀티플레이어
채널: Tarodev
길이: 24분 46초
출처: 자막 (자동 생성, 영어)

전사(Transcript):
안녕하세요 친구들, 오늘은 Netcode for GameObjects에 대해 살펴보겠습니다. Netcode는 Unity에서 제공하는 프레임워크로, 실시간 멀티플레이어를 가능하게 해주며 사용법이 믿을 수 없을 정도로 간단하고 효율적입니다. 따라서 저는 여러분에게 이를 어떻게 시작하고 실행하는지 보여드릴 뿐만 아니라, 각 구성 요소가 어떻게 작동하는지, 그리고 가장 성능이 좋은 네트워크 코드 (Network code)를 얻기 위한 몇 가지 팁도 설명해 드릴 것입니다.

시작하려면 몇 가지 패키지가 필요합니다. Windows Package Manager로 가서 세 가지 다른 패키지를 추가할 것입니다. 이 패키지들은 화면과 설명란에 표시해 두겠습니다. 이를 추가하려면 먼저 '+' 버튼을 누르고 'Add package from git URL'을 클릭하세요. 첫 번째는 Netcode for GameObjects입니다. 다음은... 사실 이미 프로젝트에 가지고 있는 것인데, Multiplayer Samples Utility입니다. 하지만 설명란에 있는 것을 복사하여 git URL로 추가하면 됩니다. 마지막 하나는 Unity에서 권장하는 작은 헬퍼 라이브러리(helper Library)로, 여러 클라이언트 간의 테스트를 도와줄 것입니다. 멀티플레이어 게임을 테스트할 때는 당연히 에디터 하나만으로는 테스트할 수 없기 때문입니다. 이전에는 사람들이 Build Settings로 가서 게임을 빌드한 다음, 빌드된 화면을 띄워놓고 에디터에서 Play를 눌러 두 개의 클라이언트를 만드는 방식을 사용하곤 했습니다. 하지만 그것은 아주 끔찍한 방식입니다. 훨씬 더 나은 방법이 있습니다. 바로 ParrelSync를 추가하는 것입니다.

Netcode를 시작하려면 여기에 새 오브젝트를 생성하세요. Network Manager로 만들고, 중요하지는 않지만 위치를 0으로 리셋한 다음, Network Manager 컴포넌트를 추가하겠습니다. Network Manager가 무엇인지 설명하자면, 네트워크 게임의 오케스트레이터(orchestrator)와 같은 것이라고 볼 수 있습니다. 여기서 우리가 실제로 건드려야 할 것은 Transport뿐입니다. Transport 레이어(layer)는 여러분과 피어(peers) 사이의 초기 핸드셰이크(handshake)를 용이하게 하는 코드입니다. 이는 데이터의 무결성을 모니터링합니다...

당신과 피어(peers) 사이의 연결을 관리하므로, 만약 패킷(packets)이 누락된다면 시스템이 우리를 대신해 패킷을 다시 제출할 것입니다. 우리는 새로운 Unity Transport를 사용할 예정인데, 이는 기본적으로 현재는 사용되지 않는(deprecated) 이전 Unity Transport의 버전 2라고 할 수 있습니다. Unity Transport를 클릭하자마자 여기에 새로운 스크립트가 생성되는 것을 볼 수 있습니다. 여기서는 아무것도 확인할 필요가 없습니다. 실제로 온라인에서 플레이하려면 제어할 캐릭터가 필요하겠죠? 여기 작은 플레이어(player)가 있습니다. 단순한 캡슐(capsule) 형태이며, 플레이어 컨트롤러(player controller) 스크립트와 리지드바디(RigidBody)가 붙어 있습니다. 플레이(Play)를 누르면 캐릭터가 약간 미끄러지듯 움직이며 제 마우스를 따라다니는 것을 볼 수 있습니다. 네트워크 매니저(Network Manager)에게 이것이 실제 네트워크 오브젝트(Network Object)임을 알려주기 위해 우리가 해야 할 일은, 플레이어 스크립트에 네트워크 오브젝트(Network Object)를 추가하는 것뿐이며 여기서는 다른 것을 변경할 필요가 없습니다. 이제 이 변경 사항을 프리팹(prefab)에 적용하고 여기서는 제거하겠습니다. 제가 사용해 본 다른 네트워크 프레임워크(network frameworks)와 비교했을 때 Netcode의 아름다운 점은, Unity가 거의 모든 게임에서 각 플레이어가 하나의 메인 플레이어 오브젝트(player object)를 가진다는 것을 알 정도로 똑똑하다는 것입니다. 그래서 Unity는 플레이어 프리팹(player prefab)이라는 개념을 도입했습니다. 여기에 플레이어 오브젝트를 드래그하여 넣기만 하면, 우리가 스폰(spawn)하는 다른 네트워크 오브젝트(Network Object)와 동일하게 작동합니다. 다만 Unity는 각 플레이어가 이 특정 오브젝트를 단 하나만 가지도록 제한합니다. 이를 통해 Unity는 플레이어 오브젝트에 특화된 추가 기능들을 제공할 수 있게 되었습니다. 예를 들어, 게임에 처음 진입할 때 Unity는 이 플레이어 프리팹(player prefab)을 자동으로 스폰하며, 우리가 수많은 참조(reference) 코드를 작성할 필요 없이 이 플레이어 오브젝트들에 대한 참조를 항상 가질 수 있게 해주는 등의 헬퍼 함수(helper functions)들을 함께 제공합니다. 이는 Unity가 다른 네트워크 프레임워크(network frameworks)에 비해 제공하는 아주 좋은 편의성(quality of life) 개선 사항입니다. 동기화된 오브젝트(synced object)를 만들기 위해 필요한 작업은 사실 이것이 전부입니다. 이를 테스트하기 위해, 이전에 ParrelSync를 다운로드했다면 그냥 열어보세요.

클릭하여 'create new clone(새 클론 생성)'을 누르면 어떻게 될까요? 이렇게 하면 프로젝트 폴더 전체를 실제로 복사하여 복제본을 만들게 됩니다. 클라이언트를 클론했다면, 새로운 에디터에서 바로 열 수 있습니다. 좋습니다. 여기 두 번째 클라이언트가 준비되었습니다. 메인 클라이언트에서 Play를 누르고 Network Manager(네트워크 매니저)로 이동하면, Start Host(호스트 시작), Server(서버), Client(클라이언트)라는 세 개의 버튼이 나타나는 것을 볼 수 있습니다. 여기서 Server(서버)는 권한(Authority) 역할을 하는 컴퓨터입니다. 설정을 어떻게 하느냐에 따라 클라이언트로부터 오는 모든 트래픽은 실제로 서버를 거쳐 다시 클라이언트로 라우팅(Routing)됩니다. 항상 그런 것은 아닙니다. 예를 들어, Dedicated Server(전용 서버)를 사용하고 비용을 지불하고 있다면, 클라우드 어딘가에 Linux(리눅스) 박스를 두고 그것이 서버 역할을 하게 하며, 모든 사람이 그곳에 연결하게 됩니다. Client(클라이언트)는 단순히 그 안에 있는 실제 플레이어들을 의미하며, 서버에 연결된 모든 개별 컴퓨터는 클라이언트가 됩니다. 만약 Dedicated Server(전용 서버)를 사용하지 않는다면, 클라이언트 중 하나가 서버 역할도 겸하게 됩니다. 즉, 두 가지 역할을 모두 수행할 수 있습니다. 그리고 Start Host(호스트 시작)는 이 두 가지를 모두 처리해 줍니다. 여기서 Start Host를 클릭하면, 당연히 로컬에서 서버를 실행함과 동시에 저를 클라이언트로 연결해 줄 것입니다. 그렇게 하겠습니다. Start Host를 누르고, 저는 이쪽으로 이동하겠습니다. 그리고 다른 Unity(유니티) 창에서는... 아, 제가 여기서 실제 바(Bar)를 제거했기 때문에 버튼이 보이지 않네요. 그래서 'Network buttons'라는 작은 스크립트를 작성했는데, 이 스크립트가 하는 일은 화면에 버튼을 배치하여 우리가 화면에서 직접 클릭할 수 있게 해주는 것입니다. 이제 Network Manager(네트워크 매니저)에 바로 적용하겠습니다. 이제 Play를 누르면 버튼들이 여기 나타나서 조금 더 쉬울 것입니다. 보시다시피 첫 번째 클라이언트에서 씬(Scene)을 저장하면 다음 클라이언트로 전파(Propagate)되는데, 이는 매우 매우 유용합니다. 이제 연결해 보겠습니다.

클라이언트로서 확인해 보면, 이제 데이터가 양쪽 클라이언트에 모두 전달되어 실제로 동기화(Syncing)되고 있음을 알 수 있습니다. 하지만 지금 제가 이 클라이언트에서 이동을 시도하면, 두 클라이언트를 동시에 제어하고 있는 것을 볼 수 있을 것입니다. 이는 분명히 좋지 않은 상황입니다. 또한, 움직임이 이 클라이언트에는 표시되지 않고 있습니다. 그 이유는 우리가 이들을 네트워크 오브젝트 (Network Object)로 만들었고, 네트워크 매니저 (Network Manager)도 이들이 네트워크 오브젝트라는 것을 알고 있지만, Unity에게 어떤 종류의 데이터를 동기화해야 하는지는 말해주지 않았기 때문입니다. 즉,

실제로 무엇을 동기화해야 하는지 알아야 합니다. 만약 제가 처음에 말씀드렸던, 몇 가지 Unity 헬퍼 함수(helper functions)를 제공하는 두 번째 스크립트를 다운로드했다면, 여기 플레이어(player)로 가서 Client Network Transform을 추가해 보겠습니다. 보시다시피 우리가 네트워크를 통해 전송할 수 있는 다양한 항목들이 주어집니다. 당연히 게임의 핵심은 항상 최소한의 데이터를 전송하는 것입니다. 예를 들어, 저는 우리가 항상 Y와 Z축만 사용한다는 것을 알고 있으므로 Y 위치를 동기화할 필요가 없습니다. 또한 우리는 Y축으로만 회전한다는 것을 알고 있으므로 X와 Z축을 끌 수 있습니다. 그리고 스케일(scaling)은 전혀 사용하지 않는다는 것도 알고 있으므로 그것도 끌 수 있습니다. 이제 이 설정은 오직 세 개의 float 값만 동기화하게 됩니다. 좋습니다. 이제 여기로 돌아와서 하나는 호스트(host)로, 다른 하나는 클라이언트(client)로 실행해 보면, 우리가 자신의 캐릭터만 제어하고 있으며 회전과 위치가 전송되고 있는 것을 볼 수 있습니다. 이것은 Unity에서 제공하는 Client Transform 스크립트를 사용한 쉬운 방법이었습니다. 이제 여러분에게 여러분만의 데이터를 직렬화(serialize)하는 방법을 보여드릴 것입니다. 이는 필요한 정확한 데이터만 전송할 수 있고 데이터에 대한 완전한 제어권을 가질 수 있기 때문에 확실히 권장되는 방식입니다. 그럼 이것을 중지하고, 여기에 PlayerNetwork라는 이름의 새 스크립트를 만들어 보겠습니다. Unity가 데이터의 네트워크 직렬화(network serialization)를 처리하는 방식은 Network Variable이라고 불리는 것을 사용하는 것입니다. 위치를 위해 Vector3를 만들고, 이를 netPosition이라고 부른 뒤, new NetworkPosition으로 설정하겠습니다. 이 부분에 대해 깊게 들어가겠지만, 지금은 두 가지 다른 권한(Authority) 유형이 있다는 것만 알면 됩니다. 바로 클라이언트 권한(Client Authority)과 서버 권한(Server Authority)입니다. 이 내용은 나중에 자세히 다루겠습니다. 지금은 쓰기 권한을 소유자(owner)로 설정하겠습니다. 즉, 이 오브젝트의 소유자가 이 netPosition을 업데이트할 수 있도록 하는 것입니다. 새로운 C# 버전에서는 상용구(boilerplate) 코드를 제거할 수 있습니다. 이제 이것을 복제하여 일단은 회전(rotation)을 전송하도록 하겠습니다.

이것을 net rotation이라고 부르겠습니다. 자, 이제부터 흥미로운 부분이 시작됩니다. 여러분이 해당 오브젝트의 소유자(owner)인지 아닌지에 따라, 네트워크 변수(network variable)에 값을 쓰거나(writing) 혹은 네트워크 변수로부터 값을 읽게(reading) 될 것입니다. 우선, NetworkBehavior를 상속(inherit)받아야 합니다. 이제 우리가 소유자인지 확인하겠습니다. 만약 우리가 소유자라면, 다른 모든 사람이 읽을 수 있도록 여기에 값을 써야 합니다. 따라서 net position.value = transform.position이라고 작성하고, net rotation.value = transform.rotation이라고 작성하겠습니다. 하지만 만약 우리가 이 오브젝트의 소유자가 아니라면, 이제 우리의 변수들을 그 값으로 설정하기만 하면 됩니다. 즉, transform.position = net position.value라고 하고, transform.rotation = net rotation.value라고 하면 됩니다. 아주 간단하죠. 우리가 소유하고 있다면 쓰고, 소유하고 있지 않다면 읽는 것입니다. 실제로 이 변수들을 읽기 전용(read-only)으로 만들 수도 있습니다. 좋습니다. 이제 플레이어(player)를 열어서, 기존의 Unity 스크립트인 Client Network Transform을 제거하고 우리가 만든 Player Network를 추가하겠습니다. 이제 여기서 호스트(host)로 시작하고, 다른 하나를 클라이언트(client)로 시작하면, 실제로 우리의 위치(position)와 회전(rotation)이 동기화(syncing)되는 것을 볼 수 있습니다. 하지만 약간 끊기는(choppy) 느낌이 들 것입니다. 맞죠? 그 이유는 부드러웠던 Unity의 방식에는 보간(interpolation) 로직이 잔뜩 들어있었기 때문입니다. 지금 상황을 상상해 본다면, 우리는 네트워크 위치(Network position)를 상당히 빈번하게 보내고 있지만, 다른 사람들의 컴퓨터에서는 기본적으로 그 새로운 위치를 가져와서 Transform에 직접 설정하고 있는 것입니다. 당연히 놓치는 프레임(frames)이 아주 많을 수밖에 없습니다. 실제로 해야 할 일은 부드럽게 만들기 위해 여기에 보간(interpolation) 코드를 작성하는 것인데, 이는 곧 보여드리겠습니다. 하지만 앞서 말했듯이, Unity 방식에서는 수많은 float 값들을 체크했었죠? 그 이유는 사실 그 값들이 필요하지 않았기 때문입니다. 예를 들어, 이 Vector3는 세 개의 float를 가지고 있습니다.

실제로 우리는 두 개의 float만 필요합니다.
반면 이 Quaternion은 네 개의 float를 가지고 있지만,
우리는 실제로 오일러 각도 (Euler angle)의 Y 값만 필요합니다. 그렇죠? 그렇다면 이 바이트 (bytes)들을 어떻게 하면 가능한 한 효율적으로, 그리고 가능한 한 최소한의 데이터로 패킹 (pack)할 수 있을까요? 그 방법은 바로 여러분만의 직렬화 (serialization) 버전을 사용하는 것입니다. 그래서 여기에 구조체 (struct)를 하나 만들겠습니다. 이름을 playerNetworkData라고 부르죠. 그리고 여기에서 몇 가지 사항들을 추적할 것입니다. private float를 선언하고, 이것은 우리의 X 위치 (X position)가 될 것이며, Z 위치 (Z position)도 될 것입니다. 그리고 여기 또 다른 float를 가질 것이고, 이것은 우리의 Y 회전 (Y rotation)이 될 것입니다. 하지만 Unity는 여전히 이것을 어떻게 적절히 직렬화 (serialize)해야 하는지 모릅니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 YouTube Tarodev (Unity 팁)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0