[Unity6] Netcode 멀티 게임 구현

a-a·2025년 1월 28일

알쓸신잡

목록 보기
13/26

Introduction


NGO가 어떤식으로 작동하는지 예제를 만들어보며 학습해보ㅈr

  1. 고라니님의 학습 패턴을 이용해 보고자 포스팅합니다.
  2. Unity6로 Netcode를 이용한 멀티 게임을 만들기 위한 사전 지식을 정리 & 공유하고자 함에 의의가 있습니다.
  3. 모든 패키지는 무료입니다!
  4. 유니티6를 사용한 이유는, 멀티플레이 구현이 정말 쉬워졌기 때문입니다.

Netcode?


넷코드는 많은 엔지니어가 데이터 동기화나 지연 보상 등 네트워크 게임플레이의 특정 측면을 더 쉽게 구축할 수 있도록 특별히 고안된 프레임워크를 지칭할 때 사용하는 주요 용어입니다. 출처:유니티 블로그

넷코드의 특징으로는

  1. 데디케이티드 서버, 호스트, 클라이언트로 연결할 수 있습니다.
  2. 변수 동기화(NetworkVariable)인데 중간에 들어와도 유지됩니다.
  3. 함수 동기화(Rpc)인데 대상 설정이나 보낸 정보를 세밀히 할 수 있습니다.
  4. 기본적으로 서버쪽 권한으로 네트워크 객체들의 동기화를 결정하여 보안이 됩니다.
  5. Unity6에 들어오면서 NGO(Network GameObject)에 힘을 더 쏟아 강력해졌습니다.

NGameobjects vs NEntities

만들고자하는 게임에 따라서 선택해야 하는 패키지가 다릅니다!
간략하게 설명하자면,

  1. NGameobjects는 모든 플레이어 상태를 완벽하게 동기화할 필요가 없는 캐주얼 게임이나 협동 게임을 제작하는 경우입니다.
  2. NEntities는 물리적 기술로 서로 경쟁하는 빠른 속도감 및 액션 중심의 게임을 제작하는 경우에 추천합니다.

NEntities는 개발 난이도가 높다고 합니다. (나중에 기회가 된다면 포스팅 하겠습니다.)



Body (본론)


Package Setting

패키지 매니저에서 아래의 패키지를 설치해주세요.

  1. Multiplayer Center은 필수는 아니나, 시작 셋팅을 빠르게 할 수 있고, 다양한 예제 및 학습 방법을 제시합니다.
  2. Multiplayer Play Mode은 필수입니다! 원래는 멀티플레이를 디버깅하기 위해서는 빌드를 한 이후에 작업을 했어야 했으나, 이제는 에디터에서 제공합니다.
  3. Multiplayer Tools도 자동으로 설치됩니다.
  4. Multiplayer for GameObjects가 핵심 패키지입니다.

Multiplayer Play Mode와 Multiplayer for GameObjects만 설치하면 됩니다.


NetworkManager


패키지를 전부 다 설치했다면, SampleScene에서 아래와 같이 셋팅해 주세요.

NetworkManager Object

  1. Ceate Empty로 생성합니다.
  2. Add Component에서 NetworkManager를 추가합니다! Log-Level은 Developer로 지정해 주세요.
  3. 똑같이 NetworkTransport도 추가합니다. Protocol Type을 Unity Transport로 지정합니다.

Unity Transport란? Protocol을 모른다면 여기로!

Unity Engine에서 지원하는 모든 플랫폼은 UDP 소켓이나 WebSocket을 통해 제공하는 연결 기반 추상화 계층( 내장 네트워크 드라이버 ) 덕분에 Unity Transport에서 완벽하게 지원됩니다. 둘 다 암호화 여부에 관계없이 설정할 수 있으며, 위의 블록 다이어그램에서 닫힌 자물쇠와 열린 자물쇠로 구현됩니다. 프로젝트에서 메모리 최적화의 이유로 커스텀 프로토콜을 구현해서 사용하는 것이 아니라면, Unity Transport를 사용하면 됩니다!

Plane Object

Player Prefab for Network


게임에서 보여질 Player을 만들어 보겠습니다.
NGO는 기본적으로 모든 생성과 삭제가 Server에서 이루어져야 합니다.
ClientHost의 경우, 내가 Client이면서 Server입니다.

그리고 Network를 사용하는 오브젝트는 Prefab으로 생성이 돼있어야 합니다.

아래의 순서를 따라주세요!

  1. 캡슐 오브젝트 생성, Y의 좌표만 1로 올려주세요.
  2. NetworkObject 컴포넌트를 추가해 주세요.
  3. NetworkTransform 컴포넌트를 추가해 주세요.

NetworkObject는 네트워크의 동기화에 사용되고, NetworkTransform는 Transform의 동기화에 사용됩니다.
Transform의 Auhoirty는 스크립트의 제어권의 주체를 누구로 둘 지를 정합니다.
Server와 Owner로 2개의 옵션이 있습니다. Player같은 경우는 Owner로 두는 것이 좋겠죠.

이렇게 생성했다면, 해당 오브젝트는 Prefab으로 만들어 주고, 씬에서는 삭제하시면 됩니다.

그 다음, NetworkManager에 Default Player Prefab에 등록해 줍시다.
이렇게 하면, 생선된 세션에 입장을 할 때, 해당 Prefab이 자동으로 Spawn 됩니다.

NetworkUI


이제 방을 만들고, 입장을 하는 기능을 구현하겠습니다.
놀랍게도, 아주 쉽게 세션을 생성하고 입장할 수 있습니다.
Netcode에서는, Server, Host, Client로 구분이 되어 있습니다.
Server는 Client와 Server가 아예 분리된 형태입니다.
Host는 Client와 Server가 결합된 상태로, Client이면서 Server의 기능도 하게 됩니다.
Client는 말 그대로 Client입니다.

Canvas를 생성해 주시고, NetworkMangerUI라는 이름으로 UI를 만들어 주세요.

Network Manager UI Script 코드는 아래와 같습니다.
NetworkManager 싱글톤 객체를 가져와 버튼 콜백 함수를 등록하는 간단한 코드입니다!

using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

public class NetworkMangerUI : MonoBehaviour
{
    [SerializeField] private Button serverBnt;
    [SerializeField] private Button hostBnt;
    [SerializeField] private Button clientBnt;

    private void Start()
    {
        serverBnt.onClick.AddListener(() => NetworkManager.Singleton.StartServer());
        hostBnt.onClick.AddListener(() => NetworkManager.Singleton.StartHost());
        clientBnt.onClick.AddListener(() => NetworkManager.Singleton.StartClient());
    }
}

씬 화면은 아래와 같이 구성했습니다.

Multiplayer Mode!

이제 테스트를 한번 해봅시다!
Window -> Multiplayer -> Multiplayer Play Mode를 눌러주세요.
아래의 이미지처럼 가상의 플레이어를 추가할 수 있습니다.

Player2에 체크를 해주시고, 실행을 한번 해봅시다.

아래 gif는 Host로 세션을 생성 및 참가를 해주고, 두번째 클라이언트로 Enter Client를 하는 모습입니다. Transform도 동기화를 해주고 있어서 Scene에서 좌표를 바꿔보면 동기화가 되는 모습을 볼 수 있습니다!

Network Behaviour


NetworkBehaviour는 NetworkIdentity 컴포넌트가 있는 오브젝트와 함께 작동하는 특별한 스크립트입니다. 이 스크립트는 Command, ClientRPC, SyncEvent 및 SyncVar 같은 HLAPI 함수를 수행할 수 있습니다.

Unity 네트워크 시스템의 서버 권한이 있는 시스템의 경우, NetworkIdentities가 있는 네트워크로 연결된 오브젝트는 NetworkServer.Spawn()을 사용하여 서버에서 “스폰”되어야 합니다. 그러면 오브젝트가 NetworkInstanceId를 할당 받고 서버에 연결된 클라이언트에서 생성됩니다.

프로퍼티

  1. NetworkManager NetworkManager: 싱글톤으로 자신의 인스턴스를 가져옵니다.
  2. RpcTarget RpcTarget: Rpc 대상을 특정해 전달합니다.
  3. bool IsLocalPlayer: 현재 클라이언트가 해당 네트워크 오브젝트를 제어하고 있는지
  4. bool IsOwner: 네트워크 오브젝트를 제어할 권한, 기본적으로는 LocalPlayer와 같지만 다른 클라이언트에게 권한을 부여한다면 제어할 수 있음
  5. bool IsServer: 서버인지
  6. bool ServerIsHost: 서버가 호스트인지
  7. bool IsClient: 클라이언트인지
  8. bool IsHost: 호스트인지
  9. bool IsOwnedByServer: 서버에게 소유권이 있는지
  10. bool IsSpawned: 씬에 네트워크 오브젝트가 생성이 됐는지
  11. NetworkObject NetworkObject: 네트워크 오브젝트
  12. bool HasNetworkObject: 네트워크 오브젝트를 가지고 있는지
  13. ulong NetworkObjectId: 세션 단위로 고유한 네트워크 오브젝트 식별 아이디, 생성 순서대로 1부터 부여
  14. ushort NetworkBehaviourId: 네트워크 오브젝트 아이디의 소유자인 NetworkBehaviour 아이디
  15. ulong OwnerClientId: 소유자의 클라이언트 아이디, 클라이언트 접속 순서대로 0부터 부여

함수

  1. NetworkBehaviour GetNetworkBehaviour(ushort behaviourId): NetworkBehaviour 아이디로 다른 NetworkBehaviour 객체를 가져옵니다.
  2. NetworkObject GetNetworkObject(ulong networkId): 네트워크 오브젝트 아이디로 다른 네트워크 오브젝트 객체를 가져옵니다.
  3. virtual void OnNetworkPreSpawn(ref NetworkManager networkManager): 네트워크 오브젝트의 Spawn 전에 호출, Awake 같은
  4. virtual void OnNetworkSpawn(): 네트워크 오브젝트가 생성시 호출, Start 같은
  5. virtual void OnNetworkPostSpawn(): 네트워크 오브젝트의 Spawn 후에 호출
  6. virtual void OnNetworkDespawn(): 네트워크 오브젝트가 제거시 호출, OnDestroy 같은
  7. virtual void OnGainedOwnership(): 클라이언트가 네트워크 오브젝트의 소유권 획득시 호출
  8. virtual void OnLostOwnership(): 클라이언트가 네트워크 오브젝트의 소유권 잃을시 호출
  9. virtual void OnOwnershipChanged(ulong previous, ulong current): 네트워크 오브젝트의 소유권이 다른 클라이언트한테 전환시 호출
  10. virtual void OnNetworkSessionSynchronized(): 네트워크 세션이 동기화 되었을 때 호출, 게임상태 초기화, 시간 동기화 등의 작업을 할 수 있습니다.
  11. virtual void OnInSceneObjectsSpawned(): 씬에 네트워크 오브젝트가 생성되었을 때 호출

Network Variable


변수 동기화 파트입니다!
Player Prefab에 NetworkPlayer 스크립트를 생성합니다.

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    NetworkVariable<int> num = new(
        0,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Owner);

    public override void OnNetworkSpawn()
    {
        Debug.Log($"isOwner? {IsOwner}, ClientID: {OwnerClientId}");
        num.OnValueChanged += (int preValue, int newValue) =>
        {
            Debug.Log($"OwnerClientID: {OwnerClientId}, num {num.Value}");
        };
    }

    private void Update()
    {
        if (!IsOwner)
            return;

        if(Input.GetKeyUp(KeyCode.A))
        {
            num.Value = Random.Range(0, 100);
        }
    }
}

동기화 타입

변수 동기화 할 수 있는 타입

  • C#의 struct 타입 (string은 class 타입이므로 제외)
    bool, byte, sbyte, char, decimal, double, float, int, uint, long, ulong, short ,ushort
  • 유니티의 struct 타입
    Vector2, Vector3, Vector2Int, Vector3Int, Vector4, Quaternion, Color, Color32, Ray, Ray2D
  • 고정된 string 타입 (Unity.Collections 네임스페이스)
    FixedString32Bytes, FixedString64Bytes, FixedString128Bytes, FixedString512Bytes, FixedString4096Bytes
  • 모든 enum 타입
  • INetworkSerializable을 구현하는 struct 타입

커스텀 구조체를 다음과 같이 만들고 동기화할 수 있습니다.

Rpc


Rpc는 원격 프로시저 호출로 함수를 동기화 합니다.
대상을 지정할 수 있고 일시적이거나 지속적으로 동기화합니다.

Rpc를 할 함수는 반드시 이름이 Rpc로 끝나야 하고 [Rpc(SendTo.대상)] 어트리뷰트가 필요합니다.

SendTo

  • Server: 소유권과 관계없이 서버로 전송
  • NotServer: 서버를 제외한 모든 사람에게 전송, 호스트에겐 전송안됨
  • Authority: 네트워크 오브젝트에서 권한이 있는 사람(여러 클라이언트 일 수 있음)에게 전송
  • NotAuthority: 네트워크 오브젝트에서 권한이 없는 사람에게 전송
  • Owner: 네트워크 오브젝트에서 소유자(항상 하나의 클라이언트)에게 전송
  • NotOwner: 네트워크 오브젝트에서 소유자가 아닌 사람에게 전송
  • Me: 로컬에서 실행, 일반함수와 같음
  • NotMe: 로컬이 아닌 모든 사람에게 전송
  • Everyone: 모든 사람에게 전송
  • ClientsAndHost: 호스트모드의 경우 호스트와 클라이언트한테 전송
  • SpecifiedInParams: RpcTarget이 정해진 대상에게 전송

Rpc Target


기본적으로 SendTo의 SpecifiedInParams을 제외하고 SendTo와 대상은 같습니다.

RpcParams의 매개변수로 받으며 이를 호출할 때 대상을 정해줍니다.

RpcTargetUse에는 Temp 임시적으로 한 번만 전송과 Persistent 지속적으로 전송하는 방법이 있습니다.

  • Single(ulong clientId, RpcTargetUse use): 한 클라이언트 아이디에게만 전송
  • Group(ulong[] clientIds, RpcTargetUse use): 특정 클라이언트 아이디들에게 전송
  • Not(ulong excludedClientId, RpcTargetUse use): 특정 클라이언트 아이디를 제외하고 전송
  • Not(ulong[] excludedClientIds, RpcTargetUse use): 특정 클라이언트 아이디들을 제외하고 전송

Network 객체 생성하기


네트워크로 동기화할 모든 게임오브젝트는 반드시 NetworkObject를 거쳐야만 동기화가 일어납니다.

플레이어는 접속하자마자 바로 스폰이 되었으나 런타임중에 동적으로 생성하고 파괴하는 법을 알아보겠습니다.

Player와 비슷하게 빈 게임오브젝트 Bullet으로 이름을 변경하고, NetworkObject와 NetworkBullet 스크립트를 만들어 넣습니다. 자식으로는 Sphere를 만들고 크기를 0.5로 한 뒤 Bullet을 프리팹으로 만들고 씬에 있는 건 지웁니다. Bullet 스크립트는 아래와 같습니다.

//NetworkBullet.cs
using Unity.Netcode;
using UnityEngine;

public class NetworkBullet : NetworkBehaviour
{
    public override async void OnNetworkSpawn()
    {
        if (!IsOwner) return;

        await Awaitable.WaitForSecondsAsync(3);

        DespawnObjectRpc(NetworkObject);
    }


    void Update()
    {
        transform.Translate(Vector3.up * Time.deltaTime * 10);
    }
	
    //서버에게 오브젝트 삭제 요청을 한다.
    [Rpc(SendTo.Server)]
    void DespawnObjectRpc(NetworkObjectReference target)
    {
        if (target.TryGet(out NetworkObject targetObject))
        {
            targetObject.Despawn();
        }
    }
}

구체인 오브젝트의 NetworkPlayer 스크립트를 아래와 같이 수정하면 됩니다.
그럼 A키를 누르면 이제 하늘을 향해 총알을 발사합니다.

	// NetworkPlayer.cs
    private void Update()
    {
        if (!IsOwner)
            return;

        if (Input.GetKeyUp(KeyCode.A))
        {
            SpawnObjectRpc(transform.position, Quaternion.identity);
        }
    }

    /// <summary>
    /// 인스펙터에는 bulletPrefab에 총알 프리팹을 넣습니다. Rpc는 서버에게 전송이 되며 Instantiate로 일반적인 게임오브젝트를 생성합니다. 그리고 NetworkObject 컴포넌트에서 생성과 함께 소유권을 자신의 클라이언트 아이디로 부여합니다.
    /// </summary>
    /// <param name="pos"></param>
    /// <param name="rotation"></param>
    [Rpc(SendTo.Server)]
    void SpawnObjectRpc(Vector3 pos, Quaternion rotation)
    {
        Instantiate(bulletPrefab, pos, rotation)
            .GetComponent<NetworkObject>().SpawnWithOwnership(OwnerClientId);
    }



Conclusion


기본적으로 필요한 지식을 정리해 봤습니다!
이제 복잡한 로직 속에서 멀티 게임은 어떻게 작동하는지 틈틈이 공부한 것을 정리해 보겠습니다!

profile
"게임 개발자가 되고 싶어요."를 이뤄버린 주니어 0년차

3개의 댓글

comment-user-thumbnail
2025년 1월 30일

호오 엄청 기네요 성공하면 저 포르쉐 타이칸 한대 부탁드립니다

1개의 답글