반응형

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 제공 계획은 다음과 같습니다.




반응형
반응형

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)


이전 섹션들에서는 유니티 네트워크에서 오브젝트를 스폰하는 법, 원격 액션을 주고 받는 법과 멤버 변수의 값을 동기화하는 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

      비밀댓글입니다

반응형

오브젝트 스폰(Object Spawn)


유니티에서 게임 오브젝트를 생성할 때는, Instantiate() 함수를 사용해서 생성한다. 이것을 스폰(Spawn)이라고 한다. 하지만 이러한 스폰은 당연하게도 단순히 Instantiate() 함수만 호출해서 소환한다고 해서 자동으로 네트워크에서 스폰되고 동기화되는 것은 아니다. 평범하게 스폰된 게임 오브젝트를 네트워크에서 스폰하고 동기화하기 위해서는 유니티에서 정한 일련의 제약과 과정을 거쳐야 한다.


먼저 오브젝트가 네트워크로 스폰되기 위해서는 네트워크 매니저의 spawnPrefabs라는 리스트에 스폰 가능한 프리팹으로 등록되어 있어야 하는데 여기에 등록될 프리팹은 반드시 루트 게임 오브젝트에 NetworkIdentity 컴포넌트를 가지고 있어야 하며, Network Behaviour 스크립트는 반드시 이 Network Identity 컴포넌트와 동일한 게임 오브젝트에 있어야 한다.



정상적으로 Network Identity 컴포넌트를 가진 프리팹의 경우 Inspector 뷰에서 확인해보면 프리팹에 Asset ID가 고유한 값으로 할당되어 있는 것을 확인할 수 있다. 네트워크 매니저는 이 Asset ID를 통해 지금 네트워크 스폰하려는 오브젝트가 유효하게 spawnPrefabs에 등록되어 있는 프리팹인지 확인하고 스폰하게 된다.





이렇게 제대로 만들어진 프리팹을 spawnPrefabs 리스트에 등록하는 것은 스크립트에서도 가능하고 유니티 에디터에서도 가능하다.




using UnityEngine;
using UnityEngine.Networking;

public class SpawnObjTestNetManager : NetworkManager
{
    void Awake ()
    {
        // Resources 폴더에 있는 Character라는 이름을 가진 프리팹 파일을 가져온다.
        GameObject prefab = Resources.Load<GameObject>("Character");
        // spawnPrefabs 리스트에 스폰할 오브젝트를 추가
        spawnPrefabs.Add(prefab);
    }
}


위의 예시와 같이 spawnPrefabs에 등록된 프리팹들은 네트워크를 통해 스폰 가능해진다. 네트워크를 통해 스폰하는 예시 코드는 다음과 같다.


using UnityEngine;
using UnityEngine.Networking;

public class SpawnObjTestNetManager : NetworkManager
{

    // 서버측에서 클라이언트가 접속했을 때의 콜백
    public override void OnServerConnect(NetworkConnection conn)
    {
        base.OnServerConnect(conn);
        // spawnPrefabs에 등록된 프리팹을 스폰한다.
        GameObject charObj = Instantiate(spawnPrefabs[0]);

        // 네트워크를 통해서 이 오브젝트가 생성되었음을 클라이언트에 알린다.
        NetworkServer.Spawn(charObj);
    }
}


위의 예시들을 따라 작성하고 빌드하여 서버에 클라이언트를 접속시키면 Character 프리팹이 서버와 클라이언트 양측에 모두 소환되는 것을 확인할 수 있다.





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



또한 스폰된 오브젝트는 각각 서버와 클라이언트에서 동일한 "netId"라고 하는 고유한 네트워크 인스턴스 ID를 가지게 되는데, 이 ID를 통해서 각각의 오브젝트에 메시지를 보내고 식별할 수 있다.




네트워크를 통해 오브젝트가 생성되는 자세한 과정은 유니티 네트워크 문서를 통해 확인할 수 있다.


https://docs.unity3d.com/kr/current/Manual/UNetSpawning.html

반응형
반응형

UNet Tutorial (5) - Command와 Client Rpc


유니티 네트워크에서는 클라이언트와 서버 간에 액션을 수행하는 방법이 있다. 이를 원격 프로시저 호출(RPC)라고 하는데 유니티 네트워크 시스템에는 두 가지 타입의 RPC가 있다. 클라이언트에서 호출되어서 서버에서 수행되는 커맨드(Command)와 서버에서 호출되어서 클라이언트에서 수행되는 클라이언트 RPC(Client Rpc)가 그것이다.


아래의 다이어그램을 통해서 유니티 네트워크에서 원격 액션이 동작하는 방향을 확인할 수 있다.






커맨드(Command)


커맨드는 클라이언트의 플레이어 오브젝트에서 서버의 플레이어 오브젝트로 보내진다. 커맨드는 클라이언트 자신의 플레이어 오브젝트에서만 보낼 수 있으며 타 클라이언트가 소유한 플레이어 오브젝트를 통해서는 보낼 수 없다. 또한 일반적으로 플레이어 오브젝트가 아닌 네트워크 오브젝트에서도 커맨드를 보낼 수 없지만 5.2 버전부터 플레이어가 없는 오브젝트(클라이언트 권한 있음)에서도 커맨드를 보낼 수 있다. 이러한 오브젝트는 NetworkServer.SpawnWithClientAuthority를 통해 스폰되거나 NetworkIdentity.AssignClientAuthority를 통해 권한을 설정한 상태여야 한다. 이러한 오브젝트에서 전송한 커맨드는 오브젝트의 서버 인스턴스에서 실행되고, 클라이언트에서의 해당 플레이어 오브젝트에서 실행되지 않는다.


함수를 커맨드로 바꾸려면 [Command] 커스텀 속성을 사용하고 함수의 이름에 "Cmd" 접두사를 붙여주어야 한다.


using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    int count = 0;

    [Command]
    public void CmdTestFunction(int i)
    {
        Debug.Log("Command Function Call. " + i);
    }

    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            CmdTestFunction(count++);

            Debug.Log("Input A. " + count);
        }
    }
}



서버와 클라이언트를 실행하고 클라이언트 측에서 'A'를 입력하면 커맨드가 호출되서 서버에서 수행되는 것을 확인할 수 있다.





클라이언트 Rpc(Client Rpc)


클라이언트 Rpc는 커맨드와 반대로 서버의 오브젝트에서 호출되어서 클라이언트의 오브젝트에서 수행된다. 서버의 경우에는 모든 권한을 가지고 있기 때문에 Network Identity를 포함해서 모든 서버의 오브젝트에서 이 호출을 보낼 수 있고 이것은 보안 상의 문제가 되지 않는다. 함수를 ClientRpc 호출로 만들기 위해서는 커맨드와 같이 [ClientRpc] 커스텀 속성을 함수 앞에 추가하고 함수 이름에 "Rpc"라는 접두사를 추가해야 한다.


using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    int count = 0;

    [ClientRpc]
    public void RpcPlayerConnected()
    {
        Debug.Log("Client Rpc Call.");
    }

    [Command]
    public void CmdTestFunction(int i)
    {
        Debug.Log("Command Function Call. " + i);
    }

    void Update ()
    {
        if(Input.GetKeyDown(KeyCode.A))
        {
            CmdTestFunction(count++);
            Debug.Log("Input A. " + count);
        }
    }
}


using UnityEngine;
using UnityEngine.Networking;

public class CustomNetworkManager : NetworkManager
{
    public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId)
    {
        base.OnServerAddPlayer(conn, playerControllerId);

        Debug.Log("Add Player.");

        var players = FindObjectsOfType<Player>();
        foreach(var player in players)
        {
            player.RpcPlayerConnected();
        }
    }
}


위의 예제 코드는 서버에 새로운 클라이언트가 접속해서 새 플레이어 오브젝트가 생성되었을 때, 서버에 접속된 모든 클라이언트의 플레이어 오브젝트에 알려주는 역할을 하는 코드이다.







원격 액션에서의 매개변수


커맨드와 클라이언트 Rpc의 호출로 전달되는 매개변수는 직렬화되어 네트워크로 전송된다. 매개변수로 전달가능한 타입은 다음과 같다.

  • 기본 타입(byte, int, float, string, UInt64 등)
  • 기본 타입 배열
  • 허용 가능 타입을 포함하는 구조체
  • 빌트인 Unity 수학 타입(Vector3, Quaternion 등)
  • NetworkIdentity
  • NetworkInstanceId
  • NetworkHash128
  • NetworkIdentity 컴포넌트가 부착된 게임 오브젝트

원격 액션으로 전달되는 매개변수는 스크립트 인스턴스나 트랜스폼과 같이 게임 오브젝트의 하위 요소여서는 안된다. 직렬화되어 네트워크로 보내질 수 없는 타입도 매개변수가 될 수 없다.

반응형

+ Recent posts