반응형

UNet 

UNet 지원 중단과 새로운 네트워크 지원 예정


이전 포스트에서 유니티에서 지원하는 네트워크 게임 구현 API인 UNet에 관한 내용을 다룬 적이 있으며, 추후에 추가적인 내용을 다룰 예정이었지만 지난 2018년 8월 2일에 유니티 공식 블로그에 게시된 글에 의하면 UNet은 deprecated 되었으며 새로운 네트워크 API를 지원하기 위한 준비를 하고 있다고 이야기하고 있다.


 

UNet 튜토리얼 (1) - 개요 글에서 이야기하였듯이 UNet의 구조는 위의 이미지처럼 전송 계층에 가까운 LLAPI(Low Level API)와 게임에 필요한 기능을 제공하는 HLAPI(High Level API)로 나누어져 있다. 이 LLAPI와 HLAPI는 유니티의 계획에 의하면 :


1. HLAPI는 2018.4(LTS) 이후 더 이상 유니티와 함께 제공되지 않음. 2018.4(LTS) 출시일 이후 2년 동안 유니티의 장기 지원 정책에 따라 중요한 수정 사항 제공.

2. LLAPI는 2019.4(LTS) 이후 더 이상 유니티와 함께 제공되지 않음. 2019.4(LTS) 출시일  이후 2년 동안 중요한 수정 사항 제공.

3. 유니티 릴레이 서버 및 레거시 매치 메이커 서비스는 2018.4(LTS) 제공 이후 3년 이상 계속 운영하며 다음 서비스로 전환 계획.


UNet은 더 이상 사용되지 않지만 ECS(Entity Component System)와 대응이 되는 차세대 네트워크 기능을 곧 제공할 예정이다. 유니티가 제공할 예정인 기능은 다음과 같다.


1. 기존 UNet의 HLAPI 및 LLAPI를 대체하며 DOTS와 호환되는 새로운 네트워킹 계층.

2. P2P 지원 릴레이 서버를 대체하는 멀티 플레이 게임 서버 호스팅 서비스.

3. 레거시 매치 메이커 서비스를 대체하는 새로운 매치 메이킹 서비스.


유니티의 네트워크 API 제공 계획은 다음과 같습니다.




반응형
반응형

네트워크 애니메이터(Network Animator)


네트워크 게임에서는 게임이나 유닛, 캐릭터 등의 상태를 동기화하는 것도 중요하지만 눈에 보이는 캐릭터들의 움직임, 즉 애니메이션 역시 동기화가 필요하다. 아무리 다른 동기화가 잘 되고 있다고 하더라도, 애니메이션 동기화가 진행되지 않아서 가만히 서있는 자세로 이동하거나 공격한다면 문제가 많을 것이다.


유니티 네트워크 시스템에서는 이러한 애니메이션 동기화를 위한 기본적인 기능을 제공하는데 그것이 바로 네트워크 애니메이터(Network Animator)다.


이번 섹션을 진행하기 위해서는 기본적인 유니티의 애니메이션 시스템과 애니메이터 컨트롤러에 대한 지식이 필요하다. 기반 지식이 부족하다면 유니티의 애니메이션 문서를 참조하여 공부를 해두는 것이 좋다.


그럼 이제부터 네트워크 애니메이터를 사용하는 방법을 알아보도록 하자.



네트워크 애니메이터 컴포넌트의 기본적인 요구사항



네트워크 애니메이터를 사용하기 위해서는 네트워크 애니메이터 컴포넌트가 있는 오브젝트와 같은 오브젝트에 에니메이터 컨트롤러와 네트워크 아이덴티티 컴포넌트가 있어야 한다.



네트워크 애니메이터의 작동 방식



네트워크 애니메이터의 작동 방식은 기본 애니메이터 컨트롤러의 파라메터가 변경되면 그 변경된 파라메터의 값을 네트워크 애니메이터를 통해서 클라이언트의 해당 네트워크 애니메이터로 전송하고 받은 측의 네트워크 애니메이터가 자신이 소유한 애니메이터 컨트롤러에 변경된 파라메터와 값을 알려서 애니메이션을 동작하게 한다.






네트워크 애니메이터를 사용하기 위한 준비


앞에서 네트워크 애니메이터의 기본적인 요구 사항과 작동 방식을 알아보았으니 이제 네트워크 애니메이터를 추가하고 사용하는 방법에 대해서 알아보도록 하자.



애니메이션 섹션의 단골인 박스맨이 이번 네트워크 애니메이터 섹션에서도 도움을 줄 것이다.



위의 이미지에 맞춰서 애니메이션 스테이트를 구성해보자. 박스맨 캐릭터는 대기(Idle) - 이동(Walk) - 공격(Attack), 세 가지 상태를 가지며, IsMove 파라메터의 상태에 따라서 대기와 이동 상태를 오가며, Attack 트리거를 받으면 공격 애니메이션을 재생하고 대기 상태로 돌아가는 아주 간단한 형태다.



위와 같이 게임 오브젝트와 컴포넌트를 세팅해주면 준비는 끝이다.





네트워크 애니메이터 추가하기



네트워크 애니메이터를 추가하는 방법은 간단하다. 애니메이션을 동기화하고자 하는 오브젝트에(애니메이터 컨트롤러 컴포넌트를 가지고 있어야 한다) 네트워크 애니메이터를 Add Component 해주면 된다. 그러면 네트워크 애니메이터와 함께 네트워크 통신에 필요한 Network Identity 컴포넌트가 자동으로 함께 추가된다.



네트워크 애니메이터를 추가한 후에는 네트워크 애니메이터 컴포넌트의 애니메이터 프로퍼티에 동기화되어야할 애니메이터를 추가해주면 된다.



그렇게하면 네트워크 애니메이터 컴포넌트에 동기화될 애니메이터의 파라메터들이 표시된다. 이 다음에는 네트워크 애니메이터를 컨트롤할 스크립트를 작성해야 한다.


using UnityEngine;
using UnityEngine.Networking;

public class PlayerCharacter : MonoBehaviour
{
    private NetworkAnimator netAnimator;

    void Start ()
    {
        netAnimator = GetComponent<NetworkAnimator>();
    }
   
    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            netAnimator.animator.SetBool("IsMove", true);
        }
        else if (Input.GetKeyUp(KeyCode.W))
        {
            netAnimator.animator.SetBool("IsMove", false);
        }

        if (Input.GetMouseButtonDown(0))
        {

            netAnimator.animator.SetTrigger("Attack");
            netAnimator.SetTrigger("Attack");
        }
    }
}


위와 같이 스크립트를 작성한 이후에 빌드하고 실행해서 한 쪽에서 서버를 열고 다른 쪽에서 클라이언트로 접속한 뒤 서버 측에서 W를 누르거나 마우스를 클릭했을때 애니메이션이 동기화됨을 확인할 수 있다.



위의 예시 코드에서 몇 가지 의문점이 있을 수 있는데, SetBool을 호출할 때는 networkAnimator.animator를 통해서 호출하고 SetTrigger를 호출할 때는 networkAnimator.SetTrigger()로 바로 호출하는가와 트리거를 사용하는 부분에서 왜 networkAnimator.animator.SetTrigger()와 networkAnimator.SetTrigger()를 중복해서 사용했는지가 그것이다.


첫 번째 의문점의 경우에는 Trigger는 NetworkAnimtor 클래스에서 호출할 수 있는 SetTrigger() 함수가 있지만, 다른 애니메이터의 파라메터(Int, Float, Bool)는 NetworkAnimator 클래스에서 바로 호출해서 사용하는 메서드가 따로 없고, NetworkAnimator의 멤버 변수인 animator를 통해서 SetBool(), SetInt(), SetFloat() 함수를 호출하도록 만들어져 있기 때문이다.


두 번째 문제는 트리거를 사용하는 부분에서 왜 networkAnimator.animator.SetTrigger()와 networkAnimator.SetTrigger()를 중복해서 사용했는지 인데, 이것은 networkAnimator.animator.SetTrigger() 함수를 통해서 동작하는 애니메이션은 서버에서만 재생되고, networkAnimator.SetTrigger() 함수를 통해서 동작하는 애니메이션은 클라이언트에서만 재생되기 때문이다. 즉, 서버에서도 애니메이션을 재생하고 클라이언트에서도 애니메이션을 재생하기를 원한다면 위의 예시 코드와 같이 작성되어야 한다. 특히 서버 측에서 애니메이션 이벤트를 통해서 애니메이션이 진행되는 도중에 특정한 동작이 발생되도록 로직이 짜여있다면 서버에서도 애니메이션이 동작되어야 하기 때문에 반드시 위의 코드처럼 작성되어야만 한다. 하지만 이후에도 설명하겠지만, 애니메이션을 재생하는 처리는 상당히 무거운 작업에 속하기 때문에, 클라이언트가 서버의 역할을 함께하는 P2P 방식이 아닌 순수한 서버라면 애니메이션 재생 도중 호출되는 애니메이션 이벤트는 반드시 배제해야 한다. 그리고 서버에서는 애니메이션이 완전히 동작하지 않도록 하는 것이 좋다.





추가로 : 네트워크 애니메이터에 대한 중요한 사실


유니티에서 기본적으로 제공하는 네트워크 애니메이터가 있기 때문에 우선은 소개하고 사용법에 대해서 알려주지만, 사실 기본 네트워크 애니메이터를 사용하는 것은 추천하지 않는다. 오히려 직접 커스텀 네트워크 애니메이터를 따로 구현해서 사용하도록 권장하고 싶다. 지금은 주요 작업을 2017.3.03f 버전의 유니티로 하고 있기 때문에 이후의 버전에서는 더 좋게 바뀌었는지 모르겠지만, 이 버전과 이전 버전에서는 기본 네트워크 애니메이터에 상당한 문제가 있기 때문이다.


첫 번째 문제는, 기본 네트워크 애니메이터가 소모하는 데이터량이 너무 많다. 커스텀 애니메이터를 잘 구현한다면 기본 네트워크 애니메이터를 사용할 때보다 훨씬 데이터 소모량을 많이 줄일 수 있다.


두 번째 문제는, 꽤나 심각한 문제인데, 기본 네트워크 애니메이터가 굉장히 많은 가비지(Garbage)를 발생시켜서 GC로 인한 프레임드랍이 심각하다고 여겨질 만큼 발생한다는 것이다. 이것에 대한 이슈는 구글링해보면 해외 개발자들도 상당히 심각하게 느끼도 있다는 것을 알 수 있다. 실제 개발에서 네트워크 애니메이터로 애니메이션을 동기화 했다가 이 문제 때문에 네트워크 애니메이터를 모두 제거하고 커스텀 네트워크 애니메이터를 구현해서 사용해야 했었다.



반응형
  1. modernator 2018.09.30 01:21

    잘보고 갑니다. UNet의 NetworkAnimator는 잘모르겠지만, 예전에 Photon Unity Networking의 NetworkAnimator 코드를 분석한 적이 있었는데요, 동기화를 위해서 어마어마한 작업들을 하더군요. 간단한 데모 구현이나 가벼운 게임이 아니라면 말씀하신대로 직접 스크립트로 필요한 경우에만 동기화하는게 제일 좋은것 같습니다.

    • wergia 2018.10.01 11:08 신고

      유넷이 기본 제공하는 네트워크 애니메이터는 사실상 프로토타이핑용인 것 같습니다.

  2. mimus 2018.11.15 23:07

    안녕하세요 unet 관련 설명을 잘봤습니다 ㅎㅎ..
    다름이 아니라 이번에 혼자서 unet으로 VR 게임을 제작 하는데 문제가 생겨서 질문을 좀드립니다ㅠㅠ..
    멀티플레이 방식을 internt 방식을 사용해서 제작하니 방을 만든 사용자의 움직임은 방에 접속한 사람에게 보이는데
    방에 접속한 사람의 움직임은 방을 만든 사용자에게 보이지가 않습니다..
    동기화 방식은 networkTransform 을 사용했습니다..

    혼자서 아무리 생각을 해봐도 이유를 모르겠습니다 ㅠㅠ

    아그리고 추가 로 질문을 하면 혹시 internet 방식으로 멀티플레이를 하게되면 방을 만든 사람 만 local player로 인식 되는건가요?..
    답변 기다리겠습니다 ㅠ

    • wergia 2018.11.16 11:13 신고

      움직임이 보이지 않는다는 것은 애니메이션이 보이지 않는다는 의미인가요? 아니면 위치 동기화가 되지 않는다는 의미인가요?

      1. 만약 애니메이션이 보이지 않는다는 뜻이라면 방을 만들때 StartServer로 하셨나요?
      방을 만드는 사람이 플레이어로 참여하려면 StartServer로 방을 만들기보다 StartHost로 방을 만들어야 합니다. Server의 경우 기본적으로 서버로서의 계산만을 하기 때문에 기본 NetworkAnimator의 경우 애니메이션 재생을 배재하기때문에 애니메이션 동기화가 되지 않을 수 있습니다.

      2. 만약 위치 동기화가 되지 않는다면 authority 문제일 확률이 제일 높습니다. 일반적으로 네트워크 게임에서 동기화하는 권한은 서버 역할을 하는 호스트에게 있어야만 합니다. 그런데 만약 권한이 로컬 플레이어가 가지게 된다면 호스트 역할을 하는 플레이어의 경우 서버역을 겸하기 때문에 방에 접속한 다른 사용자에게 위치 동기화가 되지만, 클라이언트로서 접속한 플레이어의 위치 동기화가 이루어지지 않는 문제라고 생각됩니다.
      플레이어 프리팹 오브젝트의 NetworkIdentity 컴포넌트에서 Local Player Authority를 확인해보세요.

    • mius 2018.11.16 15:12

      감사합니다! 덕분에 해결 했습니다!

  3. myouzing 2019.08.05 23:09

    안녕하세요 unity를 공부하고 있는 학생입니다.
    저는 이제까지 single fps 게임을 개발하였습니다. 그 후에 multi player로 발전시키는 과정에서 문제가 생겨 여쭤보고 싶습니다. ㅠㅠ
    우선, 문제점은 제가 single에서 넣어놓은 적 애니메이션은 player와 관련이 있었습니다.
    player가 존재하면 걷고, 가까워지면 player를 공격하고 하는 등등의 애니메이션을 적용한 것입니다
    그러나, 제가 player에게 networkmanager를 적용시켰을 때 player를 인식하지 못하는 문제점이 있는 것 같습니다.
    이 문제에 대해 혹시 조언을 구할 수 있을까요..?
    답변 주시면 정말 감사하겠습니다 !

    • wergia 2019.08.19 17:53 신고

      상황을 정확히 알 수 없어서 자세한 답변은 어렵지만 대강 짐작가는 부분에 대한 답변을 드립니다.

      https://wergia.tistory.com/176

반응형

QoS 채널(Quality of Service)


유니티 네트워크에서는 메시지의 전송 품질을 QoS(Quality of Service)라고 하는데, 네트워크 매니저에서 채널을 추가하고, 그 채널의 전송 품질을 설정할 수 있다. 그리고 NetworkBehaviour를 상속받는 클래스를 정의할 때, NetworkSetting 어트리뷰트를 통해서 이 클래스의 객체가 네트워크 메시지를 보낼 때, 어떤 채널을 통해서 메시지를 보낼지, 그 채널이 어떤 수준의 전송 품질로 메시지를 전송할 것인지를 설정할 수 있다.





전송 품질 타입(QosType)


우선은 유니티 네트워크에서는 어떤 종류의 전송 품질의 정의하고 지원하는지 알아야 하는데, 이러한 전송 품질에 대한 타입을 QosType이라는 열거형으로 정의하고 있다.


유니티 네트워크에서 정의하는 전송 품질의 종류는 다음과 같다.


  • Unreliable - 전송되는 메시지의 도착이나 순서를 보장하지 않는다.
  • UnreliableFragmented - 전송되는 메시지의 도착이나 순서를 보장하지 않지만 메시지를 최대 32개로 분할된 메시지를 허용한다.
  • UnreliableSequenced - 메시지의 도착은 보장하지 않지만 순서는 보장한다. 만약 지금 도착한 메시지보다 이전에 전송된 메시지는 무시한다.
  • Reliable - 메시지의 도착은 보장하지만 순서는 보장하지 않는다.
  • ReliableFragmented - 메시지의 도착을 보장하며, 메시지 당 최대 32개로 분할된 메시지를 허용한다.
  • ReliableSequenced - 메시지의 도착과 순서를 보장한다.
  • StateUpdate - 기본적으로 메시지의 도착이나 순서를 보장하지 않고, 전송 버퍼에 쌓인 메시지 중에 가장 최근의 마지막 메시지만 전송한다.
  • ReliableStateUpdate - 메시지의 도착을 보장하며, 전송 버퍼에 쌓인 메시지 중에 가장 최근의 마지막 메시지만 전송한다.
  • AllCostDelivery - 상대가 수신을 받았다는 확인을 받을 때까지 높은 빈도로 재전송하는 가장 신뢰성이 높은 메시지.
  • UnreliableFragmentedSequenced - 전송되는 메시지의 도착은 보장하지 않지만, 순서를 보장하며, 최대 32개로 분할된 메시지를 허용한다.
  • ReliableFragmentedSequenced - 전송되는 메시지의 도착과 순서를 보장하며, 최대 32개로 분할된 메시지를 허용한다.


제일 기본적인 전송 품질은 Reliable과 Unreliable인데, 기본 네트워크로 따지면 Reliable은 TCP, Unreliable은 UDP와 같다. 그리고 여기에 추가적으로 순서를 보장할 것인지, 메시지의 분할을 허용할 것인지, 버퍼에 쌓인 메시지 중에 가장 마지막 메시지만 보낼 것인지에 따라서 전송 품질의 종류가 나누어진다.





채널 추가하기(Add Channel)


원하는 채널을 사용하기 위해서는 우선 네트워크 매니저에 채널을 추가 해주어야 한다.


Inspector 뷰에서 채널 추가하기




Inspector 뷰에서 추가하는 방법은 매우 간단하다. Inspector 뷰에서 네트워크 매니저의 내용들을 살펴보면 Advenced Configuration에 체크를 해주면 Qos Channels라는 것이 생기는데, 여기서 + 버튼을 눌러서 채널을 추가하고 드롭다운 메뉴에서 QosType을 선택하면 된다.


주의사항


네트워크 매니저에 채널을 추가할 때, 주의해야할 사항이 있다. 특히 서버와 클라이언트를 나누어서 빌드하는 경우에는, 서버와 클라이언트 간에 채널이 일치하지 않는 일이 발생하지 않도록 주의를 기울여야 한다.


만약에 서버와 클라이언트의 채널이 일치하지 않는다면, 클라이언트는 CRC Mismatch 오류를 발생시키며, 서버에 접속할 수 없게 된다.





채널 사용하기


채널을 사용하는 방법은 추가하는 것보다 더 간단하다.


using UnityEngine.Networking;

[NetworkSettings(channel = 0, sendInterval = 0.1f)]
public class Player : NetworkBehaviour
{
}


NetworkBehaviour를 상속받는 클래스에 NetworkSettings 어트리뷰트를 이용해서 channel 값에 그 네트워크 오브젝트가 사용하고자 하는 채널의 번호를 넣어주면 된다. 아무것도 넣지 않았을 경우에는 기본으로 0번 채널을 사용하고, 채널을 추가하지 않았다면 Reliable Sequenced 통신 채널을 기본으로 사용한다.





Qos 채널을 사용한 네트워크 사용량 최적화


각기 다른 전송 품질을 가진 전송 채널을 여러 개를 두고 각 메시지 타입의 특성에 알맞은 전송 품질을 가진 채널로 메시지를 전송하는 것만으로도 네트워크 전송량 최적화에 상당히 많은 도움이 된다.


네트워크 메시지 중에서 Rpc나 Command 같은 원격 액션은 무관하지만 SyncVar로 동기화 되는 멤버 변수의 경우, StateUpdate나 ReliableStateUpdate 채널을 사용하지 않는다면, 값이 업데이트되는 속도가 Send Interval보다 짧을 때, 변경된 메시지를 전송 버퍼에 쌓아두었다가 Send Interval이 끝나서 메시지를 전송하는 순간에 버퍼에 쌓여있던 메시지를 한꺼번에 전송해버린다. 즉, StateUpdate가 아닌 Reliable이나 Unreliabe 채널을 통해서 전송되는 SyncVar 메시지는 전송량이 StateUpdate 채널을 통해 전달되는 메시지보다 많을 수 밖에 없다.


그렇기 때문에 SyncVar를 사용할 때는 이 변수를 어떤 전송 품질로 전송하는게 적절한 지 충분히 고민한 후에 채널을 정해서 전송하는 것이 좋다.



참고

유니티 스크립트 API 레퍼런스 - QoS Type (https://docs.unity3d.com/ScriptReference/Networking.QosType.html)

반응형
  1. 2018.10.05 15:32

    감사합니다. 많은 도움을 받고 있습니다. 유니티 네트워크는 한글 자료가 많이 부족한데 덕분에 배워갑니다^^

반응형

기본적인 네트워크 전송량 최적화


일반적으로 게임을 제작할 때 최적화라는 요소는 매우 중요하다. 적당한 성능의 컴퓨터에서 훌륭한 퍼포먼스를 보여주는 것은 얼마나 좋은 일인가. 하지만 고사양의 컴퓨터에서도 모자란 퍼포먼스를 보여주게 된다면 그 게임은 유저들에게 상당한 비평을 받게 될 것이다. 그와 마찬가지로 멀티플레이를 지원하는 게임의 경우에는 네트워크 전송량의 최적화가 필요하다. 월정액으로 사용되는 국내 인터넷 환경상 PC 멀티플레이 게임의 경우에는 그 중요성이 조금은 덜하겠지만, 데이터 사용량에 따라 요금이 달라지는 환경이나, 매월 사용할 수 있는 데이터량이 제한되어 있는 3G/LTE 같은 경우에 게임 중에 네트워크 전송량이 너무 많다면 성능이 발적화된 게임만큼이나 많은 유저들의 불만을 불러올 것이 틀림이 없다.


최근에 간단한 네트워크 게임을 프로토타입으로 만들면서 네트워크 전송량을 거의 최적화 하지 않은 상태로 테스트를 한 적이 있었다. 그 때 10분간 진행된 게임으로 무려 24MB나 되는 데이터를 사용했었다. 실시간으로 많은 수의 캐릭터가 움직이는 게임인 것을 감안하더라도 상당히 많은 데이터 소모였다. 그렇다면 이번 섹션에서는 네트워크를 사용하는 게임이 데이터를 과식하지 않도록 네트워크 사용량을 다이어트 하는 방법에 대해서 알아보자.





너무 자주 전송하고 있지는 않은가?


첫 번째로 확인해보아야 할 것은 전송 빈도가 너무 짧지 않은가 하는 것이다.


using UnityEngine.Networking;

[NetworkSettings(channel = 0, sendInterval = 0.1f)]
public class Player : NetworkBehaviour
{
    // Player 클래스 코드
}


유니티 네트워크의 네트워크 매니저에 의해 관리되는 NetworkBehaviour를 상속받는 모든 클래스는 NetworkSetting이라는 어트리뷰트를 통해서 동기화나 원격액션을 보내는 채널과 전송 빈도를 설정할 수 있는데, 기본적으로 이 전송 빈도는 0.1초당 한 번씩 전송되게 되어 있다. 0.1초라는 기본값은 얼핏 보기에는 나쁘지 않은 빈도로 보이는데, 여기서 사람의 욕심이 모든 문제를 발생시킨다.


1초에 10번을 전송하는 것은, 캐릭터의 상태나 체력, 공격력 같은 스탯을 전송하는데에는 나쁘지 않은 값이지만, 위치 동기화에는 조금 부족해 보일 것이다. 유니티 네트워크에서 제공하는 기본 NetworkTransform 클래스를 사용해보면 기본 전송 빈도가 초당 9회로 설정되어 있는데, 테스트를 해보면 동기화를 해주는 측에서는 부드러운 움직임을 보이겠지만, 동기화를 받는 측에서는 움직임이 뚝뚝 끊어져서 보이게 될 것이다. 그리고 기본 NetworkTransform에는 최대 전송 빈도가 초당 29회로 제한되어 있는데 이것 역시 테스트를 해보면 움직임이 미세하게 끊어져서 보이는 것을 확인할 수 있다.


이러한 문제를 해결하기 위해서, 단순한 해결책을 동원하게 되는데, 그것이 바로 전송 빈도를 매우 짧게 잡는 것이다. 초당 30프레임을 맞추기 위해서 sendInterval을 0.03333초로 잡거나, 더 과한 욕심으로 초당 60프레임의 동기화를 하기 위해 0.01666초로 맞추게 되는 것이다.


단순하게 계산해봐도, 0.1초의 전송 빈도에 비해서 0.03333초는 3배, 0.01666초는 6배로 네트워크 전송량이 증가하게 되는 것이다. 일반적으로 위치 동기화에는 Vector3 타입의 변수를 사용하게 되는데 4byte인 float 타입의 x, y, z 3개의 변수가 들어 있으니 1회 전송에 최소 12byte가 사용되는 것이니, 초당 120byte(사실 이것도 많은 편이다)면 되는 것이 360byte, 1080바이트까지 늘어나게 되는 것이다. 무려 1초에 1KB나 되는 데이터를 소모하게 된다.


무작정 전송 빈도를 짧게 설정하는 것은 좋지 않은 선택이라는 것을 알 수 있다. 그리고 전송 빈도의 마지노선인 30프레임도 아무런 처리 없이는 움직임이 미세하게 끊어져 보이는 현상이 있다.


그렇다면 전송 빈도를 길게 하면서 움직임을 부드럽게 하는 방법은 무엇인가?





추측항법(Dead Reckoning)


첫 번째로 제시되는 방법은 추측항법이다. 추측항법이란 최근에 확인한 실제 위치에 현재 움직이는 방향과 속력, 즉 속도를 이용해서 현재 위치를 추정해서 움직이는 것이다. 서버에선 새로 동기화 되어야할 위치와 속도(움직이는 방향과 속도)를 전송해주고 클라이언트에선 위치를 적용한 뒤 다음 동기화가 오기 전까지 오브젝트를 속도에 맞춰 이동시켜야 한다.



이렇게 서버가 다음 위치를 보내주기 전까지 그 사이의 움직임을 클라이언트가 계산해서 처리해주기 때문에 비교적 전송되는 텀이 길더라도 부드러운 움직임을 구현할 수 있게 된다. 위의 그림을 보면 첫 번째 빨간 원이 서버로부터 동기화 받은 위치이고 화살표가 오브젝트가 이동하는 방향과 속력을 의미한다. 처음 위치와 이동할 방향과 속력을 알고 있다면 클라이언트는 오브젝트가 이동해야할 위치를 알 수 있기 때문에 동기화 신호가 오지 않은 구간에서 클라이언트가 오브젝트를 이동시켜서 부드럽게 움직이는 것처럼 보이게 만든다.


이 방법의 경우에는, 처음 위치와 속도가 정확하게 동기화되었다면, 서버와 클라이언트 양쪽에 존재하는 오브젝트의 위치가 매우 정확한 수준으로 동기화 될 수 있다는 장점이 있다.


하지만 단점도 있는데, 이 섹션이 네트워크 전송량 최적화라는 것을 생각해본다면, 일반 위치 동기화와 같은 전송 빈도라고 비교했을 때, 더 많은 데이터를 소모한다는 것이다. 일반 위치 동기화라면 위치, 즉 Vector3 하나만 전송하면 되지만, 추측항법은 위치와 속도, Vector3 두 개를 전송해야 한다. 데이터 소모가 2배로 늘어난다는 뜻이다. 그렇기 때문에 추측항법을 사용하기 위해서는 일반 위치 동기화를 사용할 때와 비교해서 적절한 수준의 전송 빈도를 잘 계산해서 사용해야만 한다.





보간법(Interpolation)


네트워크 전송량을 줄이면서 부드러운 움직임을 보이는 두 번째 방법은 보간법이다. 보간법이란 두 위치 사이의 비어있는 위치를 알고 있는 두 위치를 이용하여 채워넣는 것이다. 일반적으로 두 위치 사이를 직선으로 채워넣는 선형 보간법이 사용된다.


보간법의 경우, 다음 이동할 위치를 받은 즉시 오브젝트를 그 위치로 이동시키지 않고 그 다음 위치 동기화가 오는 시간동안 현재 위치에서 동기화 받은 위치로 Lerp를 통해서 이동시킨다. 이동시키는 도중이나 그 다음에 다음 위치가 동기화 된다면 다시 현재 있는 위치에서부터 새로 동기화 받은 위치를 향해서 Lerp를 시키는 것이다.


보간법은 일반 위치 동기화와 같이 동기화할 위치만 전송하면 되기 때문에 전송 빈도만 일반 위치 동기화보다 길게 잡으면 데이터 전송량이 쉽게 줄어든다는 장점이 있지만, 보간법의 경우에는 서버가 위치를 알려주면 클라이언트의 오브젝트가 그 위치를 뒤늦게 따라가는 방식이기 때문에 서버와 클라이언트 간의 오브젝트의 실제 위치가 차이가 발생할 수 있다.





불필요한 데이터를 전송하고 있지는 않은가?


전송 빈도 다음으로 살펴볼 것은 정말로 필요한 데이터만 전송하고 있는가다. 가장 많이 동기화 되는게 위치 동기화기 때문에 이번에도 예시는 위치 동기화를 위주로 하게 될 것이다.


위치 동기화의 경우에 Vector3를 기본으로 사용한다는 것은 앞의 파트에서도 이야기했었다. Vector3라면 x, y, z 값 float 3개를 가지기 때문에 최소 12바이트를 전송하게 되는 것도 이야기를 했다. 그렇다면 만약 게임이 높낮이 없이 평면 상에서만 움직이는 게임이라면 과연 Vecter3를 이용해서 x, y, z의 모든 좌표를 동기화해야만 하는 것일까? 아니다. 높이 값이 필요없다면 그것을 Vector3를 이용하지 않고 Vector2를 이용해서 전송하는 것만으로도 단순 계산으로 데이터 전송량의 33%를 감소시킬 수 있다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    [SyncVar(hook = "ChangePosVect3")]
    Vector3 posV3;
    void ChangePosVect3(Vector3 pos)
    {
        posV3 = pos;
        transform.position = posV3;
    }

    [SyncVar(hook = "ChangePosVect2")]
    Vector2 posV2;
    void ChangePosVect2(Vector2 pos)
    {
        posV2 = pos;
        transform.position = new Vector3(pos.x, 0f, pos.y);
    }

    public void SyncPos()
    {
        posV3 = transform.position;
        posV2 = new Vector2(transform.position.x, transform.position.z);
    }
}


위의 코드는 Vector3를 이용하여 위치 동기화를 할 때와 Vector2를 이용하여 위치 동기화를 할 때의 차이를 보여준다. 분명 Vector3를 이용하여 동기화를 할 때에 비해서 무언가 처리해야할 것이 늘어나는 것은 사실이지만, 네트워크 최적화라는 것이 원래 네트워크의 부담을 줄이기 위해 그 부담을 서버나 클라이언트로 옮기는 것이다.


33% 감소의 효율을 보여주는 Vector2를 이용하는 위치 동기화만으로는 아직 만족스럽지 못할 수도 있다. 그렇다면 보다 좀 더 극단적인 효율을 보여주는 부분이 있는데, 바로 Rotation 동기화다.


만약 탑뷰 시점의 캐릭터가 마우스 방향을 바라보는 게임을 만든다고 가정해보자. 일반적으로 로테이션 동기화에는 유니티에서는 Quaternion 타입이 사용되는데 Quaternion 타입은 x, y, z, w로 무려 float 4개로 한 번 동기화 하는데 16byte가 사용된다. 탑뷰 시점에서 캐릭터가 마우스 방향으로 바라본다고 하면 y축의 각도만 전송하면 된다는 것을 생각해봤을 때, 무려 12byte가 낭비되고 있는 것이다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    [SyncVar(hook = "ChangeRotQuat")]
    Quaternion rotQuat;
    void ChangeRotQuat(Quaternion rot)
    {
        rotQuat = rot;
        transform.rotation = rotQuat;
    }

    [SyncVar(hook = "ChangeRotfloat")]
    float rotY;
    void ChangeRotfloat(float rot)
    {
        rotY = rot;
        transform.rotation = Quaternion.Euler(0f, rotY, 0f);
    }

    public void SyncRot()
    {
        rotQuat = transform.rotation;
        rotY = transform.rotation.eulerAngles.y;
    }
}


Quaternion을 사용하는 로테이션 동기화를 float 하나로 변경하는 것만으로도 데이터 사용량을 75%를 줄일 수 있게 된다.


이렇게 네트워크 통신에서 필요하지 않은 데이터를 배제하는 것만으로도 상당한 양의 네트워크 전송량을 감소시킬 수 있다.





그 외의 방법


위에서 언급한 방법 이 외에도 여러 가지의 아이디어나 테크닉이 있을 수 있다. 간단하게 예를 들자면 좌표(좌표를 하나의 변수에 압축해서 넣을 경우에는 일정 수준의 정밀도를 포기해야 한다)나 캐릭터의 여러 스탯을 하나의 변수에 압축하여 전송한 뒤 다시 분할해서 사용하는 방법을 사용하려 전송량을 감소시킬 수 있다.

반응형
  1. su 2018.09.03 16:26

    안녕하세요 유니티 networkmanager가 같은 네트워크 대역끼리만 접속가능한가요? 외부 ip로 접속하고싶은데 방법을 잘 몰라서요.

    • wergia 2018.09.04 10:00 신고

      Unet은 멀티플레이 게임 네트워크를 위해서 만들어진 기능이기 때문에 외부 ip에서도 충분히 접속 가능합니다.

  2. su 2018.09.05 09:47

    답변 감사합니다. 리눅스 서버에서 서버기능만하는 거를 돌리고싶은데 어떻게 하는지 아시나요? 리눅스로 빌드를 했더니 x86_64 파일이 나와서 서버에서 실행했는데 잘 안되서요.

    • wergia 2018.09.06 10:37 신고

      어떤 방식으로 구현하셔서 빌드했는지를 모르니 어떠한 이유로 실행이 잘 안되는지 자세히 설명드리기 어렵습니다.

      일단 서버 세션의 경우에는 실행하면 자동으로 세팅하고 서버를 시작하고 네트워크 처리를 할 수 있게 플래그 같은 걸로 나눠서 코드를 작성해두셨나요?

  3. hoho 2018.11.30 20:55

    안녕하세요, 글 보면서 대단히 도움 많이 받고있어요!

    한가지 궁금한것이 있는데요,
    작성자님께서는 NetworkTransform 컴포넌트를 사용하지 않고 동기화하고 계신것으로 이해해도 되는걸까요?

    만약 그렇다면, unet자체의 networktransform 컴포넌트를 사용하는것에 비하여 어떤 장단점이 있을까요?

    예상해보기로는,
    단점은 직접 위치정보를 쏴주고 받아서 처리하는과정을 직접해주어야되는것이고,
    장점은 보간등의 커스텀 처리가 가능할 것 같은데요...

    저는 현재 networktransform을 사용하여 위치를 동기화하고있는데, 뚝뚝 끊어짐을 해결하기위해 보간처리를 커스텀하기가 어려워서요 ㅠㅠ

    궁금한 부분을 정리하자면..
    1. 보간을 적용하려면, 직접 보간개념을 적용한 위치동기화 전송과정을 구현해야하는것인가?
    2. 그게 아니라면 네트워크 트랜스폼 컴포넌트를 사용하면서 보간을 추가적으로 처리할수 있는가?

    입니다.

    도움이 절실합니다~ ㅠ_ㅠ


    • wergia 2018.11.30 21:41 신고

      위치동기화는 모든 게임에 똑같은 방식으로 만들어지는게 아니고 게임의 특성에 따라달라질 수 있는 것이라, 저는 UNet에서 제공하는 NetworkTransform은 잘 사용하지 않는 편입니다.

      커스텀 위치동기화 클래스를 만드신다면 클라이언트 측에서 보간하는 과정을 직접 구현하셔야 합니다.

      UNet에서 제공하는 Network Transform의 경우 자세히는 모르지만 Interpolate Move Factor라는 옵션이 있습니다. 이부분이 내간법을 이용해서 보간처리를 해주는 옵션으로 추측됩니다.

반응형

네트워크 메시지(Network Message)


지난 섹션들 중에선 Rpc와 Command, SyncVar와 Hook등을 통해서 서버와 클라이언트 사이에에서 통신을 하는 방법과 이것을 이용하기 위해 Network Behaviour를 가진 오브젝트를 스폰하는 방법을 알아보았다. 하지만 이러한 기능들은 유니티에서 정의한 네트워크 API 중에서도 높은 수준의 API(HLAPI)이고 특히 Network Server와 Network Client를 기반으로 작동하기 때문에 거기에 구애받는 제약사항이 존재한다.


Rpc와 Command 같은 원격 액션은 NetworkBehaviour에 속한다.


그 제약사항은 전 섹션인 클라이언트의 준비에서 다루었다시피 ClientRpc나 Command 같은 원격 액션과 멤버 변수 동기화를 담당하는 SyncVar와 Hook은 클라이언트가 준비되기 이전에는 동작하지 않는다는 것이다. 그렇기 때문에 클라이언트가 서버에 접속했을 때, 서버에서 클라이언트에 게임 진행에 필요한 데이터를 전송한다던가 하는 초기화 작업에는 원격 액션이나 SyncVar를 사용하지 않아야 한다.


그렇다면 클라이언트가 아직 준비되지 않은 상태에서는 어떤 방식으로 서버와 클라이언트가 통신을 해야하는가? 그 해답은 바로 네트워크 메시지(Network Message)를 사용하는 것이다. 네트워크 메시지는 위의 이미지를 보면 Network Server와 Network Client보다 Low level인 Tranport 레이어에 훨씬 가까운 것을 확인할 수 있다. 때문에 네트워크 메시지는 클라이언트가 준비 되어있는가에 대한 제약을 전혀 받지 않고 원하는 메시지를 주고 받을 수 있다.





기본적인 네트워크 메시지 전송


커스텀 네트워크 메시지 타입

public class CustomMsgType
{
    public static short YourMsgType = MsgType.Highest + 1;
}


유니티 네트워크에서 메시지를 전송하기 위해서는 몇 가지 절차가 필요한데, 첫 번째로는 당신이 보내고자하는 메시지의 타입이 무엇인지 정의하는 것이다. 유니티 네트워크에서는 기본적인 메시지를 시스템 내에서 전송하기 위해서 기본적인 메시지 타입을 정의하고 있는데, 이것은 MsgType 클래스에 상수로 정의되어 있다. 그 중에서 Highest라는 메시지 타입이 있는데 이것은 유니티가 사용하는 기본 메시지 타입 중 가장 마지막 타입을 의미하며, 이 이후의 타입은 개발자가 원하는 타입으로 정의해서 사용하면 된다는 뜻이다.


새로운 메시지를 사용할 때마다, MsgType.Highest에 +n하여 사용하면 되는데, 그냥 사용할 때마다 MsgType.Highest + n 으로 사용해도 되지만, 새로 정의한 메시지 타입의 가독성을 확보하기 위해서는 위의 예시 코드와 같이 별도의 메시지 타입 클래스를 만들고 메시지 타입 상수를 정의해서 대입해두는 것이 좋다.





유니티에서 지원하는 기본 네트워크 메시지


바로 위 파트에서는 전송하고자 하는 네트워크 메시지의 종류를 정의했다면, 이번에는 메시지의 내용을 채우는 것이다. 보내고자 하는 메시지가 단지 무언가가 되었다는 신호를 보내는 것일 수도 있지만, 일반적으로는 보내고자 하는 메시지의 내용이 있을 것이다. 이것을 위해서 유니티에서는 아주 기본적인 메시지 클래스를 제공하는데 그것은 다음과 같다.


using UnityEngine.Networking.NetworkSystem;


EmptyMessage   // 빈 메시지 아무 내용도 전송하지 않는다.
IntegerMessage  // int 를 전송하는 메시지
StringMessage    // string 을 전송하는 메시지


유니티 네트워크 내장 메시지 클래스를 사용하기 위해서는 UnityEngine.Networking.NetworkSystem을 사용해야 한다.





네트워크 메시지를 처리할 핸들러 등록하기


다음으로 할 작업은 개발자가 정의한 타입의 메시지를 처리할 핸들러를 만들고 등록하는 것이다. 기본적인 네트워크 메시지 핸들러 함수는 다음과 같이 작성하면 된다.


private void OnCustomMessageHandler(NetworkMessage netMsg)
{
}


이렇게 만들어진 핸들러를 네트워크 매니저에 등록해야하는데, 이 과정은 서버와 클라이언트가 비슷하지만 약간 차이가 있다.


public override void OnStartServer()
{
    base.OnStartServer();

    NetworkServer.RegisterHandler(CustomMsgType.YourMsgType, OnCustomMessageHandler);
}


서버에서 핸들러를 등록할때는 NetworkServer 클래스의 정적 함수를 통해서 메시지의 타입과 핸들러 함수를 전달해주면 된다.


public override void OnStartClient(NetworkClient client)
{
    base.OnStartClient(client);

    client.RegisterHandler(CustomMsgType.YourMsgType, OnCustomMessageHandler);
}


클라이언트에서는 서버와는 다르게 클라이언트가 서버에 연결될 때, 네트워크 매니저에서 생성해서 전달해주는 NetworkClient 객체에 핸들러를 등록해야 한다.


서버에서만 받을 메시지라면 서버에만 핸들러를 등록하면 되고, 클라이언트에서만 받을 메시지라면 클라이언트에만 핸들러를 등록하면 된다.





네트워크 메시지 전송하기


앞에서 커스텀 메시지 타입을 만들고, 메시지 핸들러를 등록했다면 이번에는 기본적인 메시지를 전송해보자.


우선은 서버에서 클라이언트로 메시지를 전송하는 방법이다.


public override void OnServerConnect(NetworkConnection conn)
{
    base.OnServerConnect(conn);

    EmptyMessage msg = new EmptyMessage();
    conn.Send(CustomMsgType.YourMsgType, msg);
}


서버에서 클라이언트로 메시지를 전송하는 방법의 첫 번째는 서버 콜백의 매개변수로 전달되는 클라이언트의 네트워크 커넥션에 있는 Send() 함수로 메시지를 전달하는 방법이다. 이 방법은 콜백을 보낸 클라이언트에게 콜백에 대한 처리를 한 뒤에 메시지를 보낼 때 주로 사용된다.


Dictionary<string, NetworkIdentity> playerList = new Dictionary<string, NetworkIdentity>();

public void SomeSendMessageToClientProcess(string id)
{
    EmptyMessage msg = new EmptyMessage();
    playerList[id].connectionToClient.Send(CustomMsgType.YourMsgType, msg);
}


두 번째 방법은 플레이어 객체에 있는 NetworkConnect의 Send() 함수를 호출해서 메시지를 전송하는 방법이 있다. 두 번째 방법의 경우에는 원하는 클라이언트에 메시지를 보낼 수 있다는 장점이 있다. 단, connectionToClient는 서버 측에 있는 플레이어 객체에서만 유효하다. 즉, 클라이언트 측이나, 서버라도 플레이어 객체가 아닌 네트워크 객체에서는 connectionToClient가 null 값을 가지고 있기 때문에 그 connection에 메시지를 보낼 수 없다. 때문에 플레이어 객체가 만들어질 때, 네트워크 매니저에 만들어둔 하나의 컨테이너에 방금 만들어진 플레이어 객체의 Network Identity나, 플레이어 객체의 컴포넌트인 Network Behaviour를 등록해두고 빠르게 찾을 수 있도록 만들어 주는 것이 좋다.


다음은 반대로 클라이언트에서 서버로 메시지를 전송하는 방법이다. 이것 역시 서버로 메시지를 보내는 경로는 2가지 정도가 있다.


public override void OnClientConnect(NetworkConnection conn)
{
    base.OnClientConnect(conn);

    EmptyMessage msg = new EmptyMessage();
    conn.Send(CustomMsgType.YourMsgType, msg);
}


네트워크 매니저 콜백 섹션에서 이야기 했던 것과 같이 클라이언트 콜백의 매개변수로 넘어오는 Network Connection에는 연결된 서버에 대한 정보가 담겨있기 때문에 이 매개변수의 Send를 통해서 서버에 메시지를 보낼 수 있다.


public void SomeSendMessageToServerProcess()
{
    EmptyMessage msg = new EmptyMessage();

    client.Send(CustomMsgType.YourMsgType, msg);
}


서버로 메시지를 보내는 두 번째 방법은, 네트워크 매니저 클래스의 멤버 변수인 client를 통해서 Send() 함수를 호출하는 것이다.


서버에서 클라이언트로, 클라이언트에서 서버로 메시지를 보내는 방법을 알아보았는데, 클라이언트 간의 메시지 전송 방법에 대해서 궁금할 수도 있다. 하지만 서버를 구현할 때는, 보안과 안정성을 위해서 모든 메시지가 서버를 거쳐가도록 하는 것이 좋다. 만약 클라이언트에서 다른 클라이언트로 메시지를 보내고 싶다면, 메시지를 보내고자 하는 클라이언트에서 서버에 메시지를 보낸 뒤 서버가 그 메시지를 받아야 하는 클라이언트로 메시지를 보내도록 하는 중계 방식으로 구현하는 것이 좋다.





커스텀 메시지


기본적인 메시지 클래스를 이용해서 메시지를 주고 받는 방법을 확인했으니 이번에는 전송하고자 하는 메시지 내용에 맞는 커스텀 메시지를 만들고, 전송하는 법을 알아보자.


public class CustomMessage : MessageBase
{
    public int i;
    public float f;
    public string str;
}


커스텀 메시지 클래스를 구현하는 방법은 위의 예시 코드와 같이 MessageBase를 상속받아서 메시지 클래스를 구현하면 된다. 메시지 클래스에는 기본적인 값 타입의 변수와, 구조체, 배열과 Vector3와 같은 대부분의 일반 Unity Engine 타입인 멤버를 포함할 수 있고, 복잡한 클래스나 제네릭 컨테이너는 멤버로 포함할 수 없다.


public void SomeSendMessageProcess(NetworkConnection conn)
{
    CustomMessage msg = new CustomMessage()
    {
        i = 10,
        f = 3.14f,
        str = "Hello"
    };

    conn.Send(CustomMsgType.YourMsgType, msg);
}


커스텀 메시지를 전송하는 법은 매우 간단하다. 객체를 생성하고 앞 파트에서 배운 방식대로 그 메시지를 그대로 전송하면 된다.


private void OnCustomMessageHandler(NetworkMessage netMsg)
{
    var msg = netMsg.ReadMessage<CustomMessage>();


    Debug.Log(msg.i);
    Debug.Log(msg.f);
    Debug.Log(msg.str);
}


그리고 메시지를 받는 쪽에서는 메시지가 NetworkMessage 타입의 객체로 전달되는데 이것을 ReadMessage<>() 함수로 원래 타입의 메시지 객체로 변환해서 사용하면 된다.

반응형
반응형

클라이언트의 준비(Client Ready)


지난 섹션에서는 전반적인 네트워크 매니저 콜백에 대해서 알아보았다. 이번 섹션에서는 네트워크 과정 중에 하나인 클라이언트의 준비에 대해서 알아보자. 클라이언트의 준비란 서버에 접속한 클라이언트가 준비가 되었음을 알리는 과정인데, 유넷에서는 클라이언트 측에서 ClientScene.Ready() 함수를 호출함으로써 클라이언트가 서버에 동기화될 준비가 끝났음을 알린다.


이러한 클라이언트의 준비라는 과정을 네트워크 매니저 콜백 섹션에서 이야기하지 않고 별도의 섹션을 따로 만들어 이야기하는 것은 유넷에서의 클라이언트 준비라는 것이 상당히 중요한 역할을 하기 때문이다.





클라이언트 준비의 역할과 의미


지난 섹션 중 UNet Tutorial (7) - 오브젝트 스폰(Object Spawn) 의 내용 중에 다음과 같은 내용이 있다.


"스폰된 오브젝트는 유니티 네트워크의 스포닝 시스템이 관리하게 되며, 스포닝 시스템에 속하게 된 오브젝트가 서버에서 변화가 있으면 그것이 클라이언트에도 전송되고, 서버에서 오브젝트가 소멸하면 클라이언트에서도 소멸하게 된다. 그리고 스폰된 오브젝트는 서버가 관리하는 네트워크 오브젝트 집합에도 추가되기 때문에, 이후에 다른 클라이언트가 게임에 참여하더라도 프로그래머가 별도의 처리를 만들 필요없이 자동으로 오브젝트가 소환되고 동기화 되어야할 값들이 동기화된다."

스폰된 오브젝트는 유니티 네트워크의 스포닝 시스템이 관리하게 되며, 스포닝 시스템에 속하게 된 오브젝트가 서버에서 변화가 있으면 그것이 클라이언트에도 전송되고, 서버에서 오브젝트가 소멸하면 클라이언트에서도 소멸하게 된다. 그리고 스폰된 오브젝트는 서버가 관리하는 네트워크 오브젝트 집합에도 추가되기 때문에, 이후에 다른 클라이언트가 게임에 참여하더라도 프로그래머가 별도의 처리를 만들 필요없이 자동으로 오브젝트가 소환되고 동기화 되어야할 값들이 동기화된다.

출처: http://wergia.tistory.com/106?category=768883 [베르의 프로그래밍 노트]
스폰된 오브젝트는 유니티 네트워크의 스포닝 시스템이 관리하게 되며, 스포닝 시스템에 속하게 된 오브젝트가 서버에서 변화가 있으면 그것이 클라이언트에도 전송되고, 서버에서 오브젝트가 소멸하면 클라이언트에서도 소멸하게 된다. 그리고 스폰된 오브젝트는 서버가 관리하는 네트워크 오브젝트 집합에도 추가되기 때문에, 이후에 다른 클라이언트가 게임에 참여하더라도 프로그래머가 별도의 처리를 만들 필요없이 자동으로 오브젝트가 소환되고 동기화 되어야할 값들이 동기화된다.

출처: http://wergia.tistory.com/106?category=768883 [베르의 프로그래밍 노트]


위의 내용과 같이 네트워크를 통해서 스폰된 오브젝트는 유니티 네트워크의 스포닝 시스템이 관리하며, 게임 중에 다른 클라이언트가 참가하거나 게임 중에 접속이 끊어졌다가 재접속하는 유저에게 별도의 처리 없이 오브젝트나 값이 동기화 되는데, 바로 이 동기화 시작의 기준이 클라이언트의 준비 상태다.


즉, 클라이언트가 준비를 끝마친 후에야 네트워크 오브젝트들의 동기화가 시작된다. 거기에 지난 섹션들에서 언급한 SyncVar와 Hook, Command와 ClientRpc 역시 클라이언트 준비 이후에만 동기화 되고 원격 액션을 주고 받을 수 있게 된다. 이 말인 즉슨, SyncVar나 Command, ClientRpc로 게임을 준비하기 위한 초기화를 진행하려고 해서는 안된다는 것이다. 게임이 시작되기 전에 필요한 초기화를 SyncVar, Command, ClientRpc로 할 경우, 처음 접속은 올바르게 될 수도 있지만 재접속이나 게임 진행중에 접속하는 경우에 심각한 문제를 초래할 수 있다.


게임의 초기화를 위한 작업은 클라이언트의 네트워크가 준비되기 이전에 하는 것이 옳다. 원격 액션을 보내는 Command나 ClientRpc는 클라이언트가 준비된 이후에나 가능한데 그렇다면 클라이언트가 준비되기 이전에는 어떻게 통신해야 하는가는 이 다음 섹션에서 설명할 네트워크 메시지를 사용하면 된다. 네트워크 메시지의 경우에는 클라이언트가 준비되었느냐를 따지지 않고 서버와 클라이언트가 연결만 되어 있으면 주고 받을 수 있기 때문이다.





원하는 시점에서 클라이언트 준비하기


지난 네트워크 매니저 콜백 섹션에서 가볍게 이야기 했듯이, 유넷의 기본 네트워크 매니저에서는 클라이언트가 서버에 접속하면 별다른 처리 없이도 자동으로 ClientScene.Ready() 함수가 호출되어 클라이언트가 동기화될 준비가 끝났음을 서버에 알린다.


만약에 클라이언트가 서버에 접속하자마자 준비하는 것을 원하지 않고 일련의 다른 과정을 거친 후에 준비하기를 원한다면 다음과 별도의 처리가 필요하다.


public override void OnClientConnect(NetworkConnection conn)
{
    base.OnClientConnect(conn);
}


OnClientConnect 콜백을 커스텀 네트워크 매니저에서 오버라이드하면 위와 같이 코드가 작성되는데, 저기서 부모 클래스의 OnClientConnect를 호출하는 것을 볼 수 있다. 바로 이 부모 클래스의 OnClientConnect에서 ClientScene.Ready()와 ClientScene.AddPlayer()가 호출되기 때문에 클라이언트가 서버에 접속하자마자 자동으로 준비되는 것이다.


public override void OnClientConnect(NetworkConnection conn)
{
    //base.OnClientConnect(conn);
}


그렇기 때문에 서버에 접속하자 마자 클라이언트가 준비 신호를 보내고, 플레이어 객체를 생성하기를 원하지 않는다면 위의 코드 예시처럼 부모 클래스의 OnClientConnect()를 호출하는 라인을 주석처리하거나 삭제하고 아래의 예시 코드와 같이 개발자가 원하는 별도의 처리를 한 후에 ClientScene.Ready()와 ClientScene.AddPlayer()를 호출해주면 된다.


public override void OnClientConnect(NetworkConnection conn)
{
    //base.OnClientConnect(conn);

    /*
     * 개발자가 원하는 별도의 처리
     */

    ClientScene.Ready(client.connection);
    ClientScene.AddPlayer(0);
}



반응형
반응형

네트워크 매니저 콜백(Network Manager Callback)


지난 섹션에서는 유넷에서 제공하는 Network Manager HUD의 기능을 직접 구현해보았다. 그리고 그것을 테스트하는 중간에 네트워크 매니저가 보내는 콜백을 통해서 서버와 호스트, 클라이언트의 실행 여부와 접속 여부를 확인할 수 있었다. 네트워크 매니저 콜백은 그 외에도 유니티 네트워크가 지정한 특정한 상황을 알리고 그것에 대한 기본적인 처리를 하는 역할을 한다.


그 상황에 대한 기본적인 처리 외의 필요한 것은 콜백을 상속받아서 처리에 대한 코드를 직접 작성해주면 된다.


우선은 네트워크 매니저 콜백의 종류에 대해서 알아보자. 네트워크 매니저 콜백은 서버에서 호출되는 콜백, 클라이언트에서 호출되는 콜백, 호스트에서 호출되는 콜백, 이렇게 세 가지로 나눌 수 있다. 단, 호스트의 경우에는 서버와 클라이언트의 역할을 겸하기 때문에 호스트에서 호출되는 콜백 이외에도 서버 콜백과 클라이언트 콜백이 함께 호출된다.





서버 콜백(Server Callback)


public override void OnStartServer()


서버가 시작 되었을 때 호출되는 콜백이다.


public override void OnStopServer()


서버가 정지 되었을 때 호출되는 콜백이다.


public override void OnServerConnect(NetworkConnection conn)


서버에 새로운 클라이언트가 연결 되었을 때 호출되는 콜백이다. 매개변수로 새롭게 접속한 클라이언트의 Network Connection이 제공된다.


public override void OnServerReady(NetworkConnection conn)


서버에 접속한 클라이언트가 준비 되었을 때 호출되는 콜백이다. 매개변수로 준비된 클라이언트의 Network Connection이 제공된다.


public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)

public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader extraMessageReader)


서버에 접속한 클라이언트가 ClientScene.AddPlayer() 함수를 호출해서 새 플레이어를 추가할 때, 호출되는 콜백이다. 이 콜백의 기본 처리는 네트워크 매니저에 등록된 playerPrefab을 이용해서 새 플레이어 객체를 소환한다. 매개변수를 통해서 플레이어를 생성한 클라이언트의 Network Connection과 새로 생성되는 플레이어 컨트롤러의 id, 클라이언트에서 보내는 추가적인 메시지가 전달된다.


public override void OnServerRemovePlayer(NetworkConnection conn, PlayerController player)


클라이언트가 플레이어를 제거할 때, 서버에서 호출되는 콜백이다. 이 콜백의 기본 처리는 해당 플레이어 객체를 파괴한다. 매개변수를 통해서 플레이어를 제거한 클라이언트의 Network Connection과 제거되는 플레이어 객체가 전달된다.


public override void OnServerDisconnect(NetworkConnection conn)


서버에 접속해있던 클라이언트가 연결이 끊어졌을 때 호출되는 콜백이다. 매개변수를 통해서 연결이 끊어진 클라이언트의 Network Connection이 전달된다.


public override void OnServerError(NetworkConnection conn, int errorCode)


클라이언트 연결에 대한 네트워크 오류가 발생하면 서버에서 호출되는 콜백이다. 매개변수를 통해서 문제가 발생한 클라이언트의 Network Connection과 에러 코드가 전달된다.


public override void OnServerSceneChanged(string sceneName)


씬 로드가 완료되거나, 서버에서 ServerChangeScene()로 씬 로드가 시작될 때 호출되는 콜백이다. 매개변수를 통해서 새롭게 로드되는 씬의 이름이 전달된다.





클라이언트 콜백(Client Callback)


public override void OnStartClient(NetworkClient client)


클라이언트가 시작할 때 호출되는 콜백이다. 매개변수를 통해서 네트워크 시스템에서 사용되는 네트워크 클라이언트 객체가 전달된다. 이 네트워크 클라이언트에는 네트워크 서버에 연결하는데 사용되는 Network Connection이 포함되어 있다. 이 콜백의 기본 처리는 매개변수로 전달받은 네트워크 클라이언트 객체를 NetworkManager.client 멤버 변수에 할당하는 역할을 주로한다. 이 client 멤버 변수를 이용해서 서버에 메시지를 보내거나 서버의 상태를 확인할 수 있다.


public override void OnStopClient()


클라이언트가 정지 되었을 때 호출되는 콜백이다.


public override void OnClientConnect(NetworkConnection conn)


클라이언트가 서버에 연결되었을 때 호출되는 콜백이다. 매개변수를 통해서 서버의 Network Connection이 전달된다. 그리고 이 콜백은 자기 자신의 클라이언트가 서버에 연결 되었을 때만 호출된다. 이 콜백의 기본 처리는 클라이언트를 준비(Ready) 시키고 플레이어를 추가(Adds a player)하는 작업을 한다. 만약 클라이언트가 서버에 연결되자마자 준비하고 플레이어를 추가하는 것을 원하지 않고 별도의 작업을 한 이후에 준비 하고 플레이어를 추가하길 원한다면 base.OnClientConnect()의 호출을 주석처리하거나 코드를 제거하는게 좋다.


public override void OnClientDisconnect(NetworkConnection conn)


서버와의 연결이 해제 되었을 때 호출되는 콜백이다. 매개변수를 통해서 서버의 Network Connection이 전달된다.


public override void OnClientError(NetworkConnection conn, int errorCode)


네트워크 오류가 발생했을 때 호출되는 콜백이다. 매개변수를 통해서 서버의 Networ kConnection과 에러 코드가 전달된다.


public override void OnClientNotReady(NetworkConnection conn)


서버에서 더 이상 클라이언트가 준비 상태가 아님을 알려왔을 때 호출되는 콜백이다. 매개변수를 통해서 서버의 Network Connection이 전달된다.


이 콜백은 주로 씬 전환에 사용된다.





호스트 콜백(Host Callback)


public override void OnStartHost()


호스트가 시작 되었을 때 호출되는 콜백이다.


public override void OnStopHost()


호스트가 정지 되었을 때 호출되는 콜백이다.


호스트 콜백의 경우에는 종류가 시작과 정지 두 가지 뿐이다. 호스트는 서버와 클라이언트를 겸하기 때문에 호스트 콜백과 더불어 서버 콜백과 클라이언트 콜백을 함께 호출한다.


네트워크 매니저에는 위에서 설명한 콜백 이외에도 몇 가지 콜백들이 더 있으나, 나머지 콜백들은 유니티 매치메이커에서 사용되는 콜백이기 때문에 이번 세션에서는 소개하지 않고 추후에 매치메이커 세션에서 설명하도록 하겠다.





콜백의 호출 순서


서버에 새로운 클라이언트가 연결되거나 연결이 끊어졌을 때 등, 여러가지 상황을 처리하는데 콜백은 유용하게 사용된다. 이러한 상황을 처리할 때에 순서가 중요한 경우가 많기 때문에 콜백의 호출 순서를 확인 해두는 것이 좋다.


서버 콜백의 호출 순서


1단계 : 서버 시작


- StartServer() 함수 호출

- OnStartServer

- OnServerSceneChanged


2단계 : 클라이언트 접속


- OnServerConnect

- OnServerReady

- OnServerAddPlayer


3단계 : 클라이언트 연결 해제


- OnServerDisconnect


4단계 : 서버 중지


- OnStopServer





클라이언트 콜백의 호출 순서


1단계 : 클라이언트 시작


- StartClient() 함수 호출

- OnStartClient

- OnClientConnect

- OnClientSceneChanged


2단계 : 서버 중지 or 클라이언트 중지


- OnStopClient

- OnClientDisconnect





호스트 콜백의 호출 순서


1단계 : 호스트 시작


- StartHost() 함수 호출

- OnStartHost

- OnStartServer

- ServerConnect

- OnStartClient

- OnClientConnect

- OnServerSceneChanged

- OnServerReady

- OnServerAddPlayer

- OnClientSceneChanged


2단계 : 클라이언트 접속


- OnServerConnect

- OnServerReady

- OnServerAddPlayer


3단계 : 클라이언트 연결 해제


- OnServerDisconnect


4단계 : 호스트 중지


- OnStopHost

- OnStopServer

- OnStopClient


앞에서 이야기 한 것과 같이 호스트는 서버와 클라이언트의 역할을 겸하기 때문에 서버의 콜백과 클라이언트의 콜백이 함께 호출된다.

반응형
반응형

네트워크 매니저(Network Manager)


이전 섹션들에서는 유니티 네트워크에서 오브젝트를 스폰하는 법, 원격 액션을 주고 받는 법과 멤버 변수의 값을 동기화하는 SyncVar의 사용법에 대해서 알아보았고, 그 와중에 사용되는 네트워크 매니저는 기본적인 네트워크 매니저를 사용하고, 서버 열기와 클라이언트의 연결을 간단히 하기 위해서 유니티 네트워크에서 기본적으로 제공하는 Network Manager HUD를 사용해왔다.


유넷에서 제공하는 기본 네크워크 매니저와 네트워크 매니저 HUD


이렇게 기본적으로 제공되는 매니저와 HUD는 매우 기본적인 유넷 서버 열기와 클라이언트의 접속 기능을 제공하고 간단한 UI를 통해 서버를 열고 클라이언트로서 서버에 접속하게 할 수 있게 해준다.


Network Manager HUD를 사용하고 게임을 플레이하면 이러한 UI들을 출력해서 손쉽게 서버를 열고 접속할 수 있게 만들어준다.


기본적으로 제공되는 매니저와 HUD는 가볍고 손쉽게 유넷을 테스트할 수 있게 도와주지만, 게임 제작자의 경우, 간단한 테스트를 넘어서 자신이 원하는 기능을 구현하고자 할 것이다. 그렇기 때문에 이번 세션에서는 네트워크 매니저에서 서버와 호스트를 여는 기능과 열린 서버와 호스트에 클라이언트로서 접속하는 기능을 유니티 네트워크가 제공하는 HUD를 사용하지 않고 구현하는 방법을 알아볼 것이다.





네트워크 매니저에서 서버 & 호스트 열기와 클라이언트로 접속하는 기능의 구현


네트워크 매니저를 커스터마이즈하기 위해서는 우선 클래스를 하나 만들어서, Network Manager를 상속받아야 한다.


using UnityEngine.Networking;

public class CustomUNetManager : NetworkManager
{
}


우선은 간단하게 네트워크 매니저 HUD가 제공하는 기능을 직접 구현해보도록 하자.



유니티가 제공하는 기본 Ui를 이용해서 위의 이미지와 같이 버튼 세 개를 만들어 보자. 그리고 다음의 코드를 작성한 이후에 각 버튼과 매칭을 시켜주면 된다.


using UnityEngine.Networking;

public class CustomUNetManager : NetworkManager
{
    public void OpenServer()
    {
        StartServer();  
    }

    public void OpenHost()
    {
        StartHost();
    }

    public void ConnectClientToServer()
    {
        StartClient();
    }
}


위의 예시 코드에서 StartServer() 함수는 서버를 시작하는 것, StartHost() 함수는 호스트로서 서버와 클라이언트를 동시에 시작하는 것, StartClient() 함수는 클라이언트를 시작하고 서버에 연결하는 역할을 한다.


서버와 클라이언트, 호스트에 대한 설명은 유넷 튜토리얼 2번 섹션인 UNet Tutorial (2) - 간단한 개념에서 이야기했었다.


그 다음으로는 서버가 제대로 열리고 클라이언트가 제대로 접속되었는지 확인하기 위한 코드들을 작성해보자. 다음의 코드들은 CustomUNetManager 클래스 내에 작성되어야 한다.





public override void OnStartServer()
{
    base.OnStartServer();
    Debug.Log("[Server]Start Server");
}


StartServer() 함수를 호출해서 서버가 정상적으로 시작된 이후에 호출될 콜백 함수이다.


public override void OnStartHost()
{
    base.OnStartHost();
    Debug.Log("[Host]Start Host");
}


StartHost() 함수를 호출해서 호스트가 정상적으로 시작된 이후에 호출될 콜백 함수이다.


public override void OnStartClient(NetworkClient client)
{
    base.OnStartClient(client);
    Debug.Log("[Client]Start Client");
}


StartClient() 함수를 호출해서 클라이언트가 정상적으로 시작된 이후에 호출될 콜백 함수이다.





public override void OnServerConnect(NetworkConnection conn)
{
    base.OnServerConnect(conn);
    Debug.Log("[Client]Connect Server Sucess.");
}


서버에 클라이언트가 연결되었을 때, 서버에서 호출될 콜백 함수이다.


public override void OnClientConnect(NetworkConnection conn)
{
    base.OnClientConnect(conn);
    Debug.Log("[Server]Connected Client.");
}


서버에 클라이언트가 연결되었을 때, 클라이언트에서 호출될 콜백 함수이다.


네트워크 매니저에 포함된 콜백 함수들은 위의 5가지 이 외에도 여러 가지가 있지만, 이것에 대한 설명은 다른 섹션에서 진행할 것이다.


위의 코드를 모두 추가한 뒤에 빌드를 해서 서버나 호스트를 열고 클라이언트를 접속시켜보면 서버와 호스트, 클라이언트가 접속되는지 확인할 수 있을 것이다.


서버를 열고 클라이언트를 접속시켰을 때의 로그



호스트를 열고 클라이언트를 접속시켰을 때의 로그


클라이언트가 서버나 호스트에 연결되었을 때의 로그





서버 주소와 포트 설정하기


위의 예시에서는 같은 컴퓨터에 열린 로컬 서버에 클라이언트가 접속했다. 하지만 실제의 네트워크 게임에서는 서버와 클라이언트가 실행되는 컴퓨터가 다르기 때문에, 접속하고자 하는 서버의 IP와 Port를 지정해주어야 한다. 이번에는 사용자로부터 서버의 IP 주소와 Port를 입력받기 위한 Input Field를 만들어보자.



Input Field를 모두 추가한 이후에 아래와 같이 코드를 수정하고 Input Field들을 넣어주면 된다.


using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class CustomUNetManager : NetworkManager
{
    [SerializeField]
    InputField ipInputField;
    [SerializeField]
    InputField portInputField;

    public void OpenServer()
    {
        networkPort = int.Parse(portInputField.text);

        StartServer();  
    }

    public void OpenHost()
    {
        networkPort = int.Parse(portInputField.text);

        StartHost();
    }

    public void ConnectClientToServer()
    {
        networkAddress = ipInputField.text;
        networkPort = int.Parse(portInputField.text);

        StartClient();
    }

}



네트워크 매니저 테스트를 위한 예제는 아래의 첨부파일을 통해 받아볼 수 있다.

NetworkManagerTest.unitypackage


반응형
  1. unity초보 2018.07.17 22:58

    좋은 글 감사히 보고갑니다.
    제가 궁금한게 생겼는데 유니티상에서 지원하는 네트워크 기능을 사용해서 서버랑 클라이언트 네트워크 통신을 했는데 이경우 서버의 컴퓨터를 끄면 클라이언트가 닫히는 문제점이 있어서 서버를 웹에 올려놓고 쓰는 방법을 알고싶습니다

    • 2018.07.18 10:24

      비밀댓글입니다

+ Recent posts