네트워크 메시지(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<>() 함수로 원래 타입의 메시지 객체로 변환해서 사용하면 된다.

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

클라이언트의 준비(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);
}

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

 

반응형

네트워크 매니저 콜백(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

 

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

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

네트워크 매니저(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
다운로드

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

오브젝트 스폰(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   

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

Inspector 뷰에서 공개된 enum의 값을 여러 개 선택 가능하게 하기

 

게임이나 프로그램을 제작할 때, 유닛이나 캐릭터, 객체의 상태를 표현하기 위한 방법 중 하나로 enum을 많이 사용한다. 정수나 문자열을 이용해서도 객체의 상태를 나타낼 수 있지만, 정수의 경우는 이 상태가 어떤 상태인지 직관적으로 알아보기 어려운 문제가 있고, 문자열의 경우에는 어떤 상태인지 알아보기 쉽지만, 정수형에 비해서 비교 연산 속도가 느리고, 비교하는 부분에서 오타가 발생할 경우 찾기 어려운 버그를 발생시킬 수도 있다.

 

public class Character : MonoBehaviour
{
    int state;
    // 주석이 없으면 캐릭터가 어떤 상태인지 알기 어렵다.
    public void CheckState()
    {
        switch (state)
        {
            case 0: // Jump
                break;

            case 1: // Attack
                break;

            case 2: // Move
                break;

            default:
                break;
        }
    }
}

비직관적인 정수형 상태 표현

 

public class Character : MonoBehaviour
{
    string state;

    // 정수형보다 비교가 느릴 수 밖에 없다.
    public void CheckState()
    {
        switch (state)
        {
            case "Jump": // Jump
                break;

            case "Attcak": // Attack 오타가 발생하면 버그가 발생하고 찾기 어려울 수 있다.
                break;

            case "Move": // Move
                break;

            default:
                break;
        }
    }
}

보기는 편하지만 느리고 오타에 의한 버그 발생 가능성이 높은 문자열 상태 표현

 

 

 

 

 

그에 비해서 enum 타입의 변수를 이용하면 정수를 사용한 것과 같은 비교 연산 속도와 문자열을 사용한 것과 같은 직관성을 가질 수 있다. 오타가 발생하면 그 즉시 신텍스 에러가 뜨는 것은 덤이다.

 

public enum ECharacterState
{
    Jump,
    Attack,
    Move,
}

public class Character : MonoBehaviour
{
    public ECharacterState state;
    // 알아보기 쉬우며 오타가 발생할 가능성을 차단해준다.
    public void CheckState()
    {
        switch (state)
        {
            case ECharacterState.Jump: // Jump
                break;

            case ECharacterState.Attack: // Attack
                break;

            case ECharacterState.Move: // Move
                break;

            default:
                break;
        }
    }
}

 

객체의 상태를 표시하는데 enum 타입을 사용하면 이렇게 여러가지 이점이 있다. 그리고 이런 enum 타입의 변수는 public이나 [SerializeField]로 설정하면 유니티 에디터의 Inspector 뷰에서 볼 수 있게 된다.

 

 

일반적인 경우라면 이것으로 충분하겠지만, 캐릭터의 상태의 경우에는 기획에 따라 다르겠지만, 여러 가지의 상태가 중첩되는 경우도 있을 수 있다. 예를 들자면, 점프하면서 공격한다는 상태도 있을 수 있고 점프하면서 이동한다는 상태도 있을 수 있다. 그 외의 경우 역시 가능하다. 중첩된 상태를 표현하기 위해서는 각 비트에 상태를 매핑해서 비트 연산을 하는 방법을 주로 사용한다. 즉, 각 상태의 값을 2의 n승으로 하여 해당 비트의 값이 1이면 그 캐릭터는 해당 상태인 것이다.

 

public enum ECharacterState
{
    Jump    = 0x00000001,   // 0001
    Attack  = 0x00000002,   // 0010
    Move    = 0x00000004,   // 0100
}

public class Character : MonoBehaviour
{
    public ECharacterState state;

    private void Start()
    {

        // or 연산을 통해 캐릭터의 state는 이동 중이면서 점프 중인 상태가 된다.

        state = ECharacterState.Move | ECharacterState.Jump;
    }

    public void CheckState()
    {

        // State에 Attack 상태를 and 연산하면 Attack 상태에 속하는 비트의 값만 검출할 수 있다.

        if ((state & ECharacterState.Attack) == ECharacterState.Attack)
        {
            // Attack 상태 처리
        }

        if ((state & ECharacterState.Jump) == ECharacterState.Jump)
        {
            // Jump 상태 처리
        }

        if ((state & ECharacterState.Move) == ECharacterState.Move)
        {
            // Move 상태 처리
        }
    }
}

 

 

 

 

이렇게 중첩이 가능한 상태의 경우에는 여러 개의 상태를 동시에 선택할 수 있어야 하는데 public이나 [SerializeField]로 공개된 enum 타입의 변수의 경우에 유니티 에디터에서 기본적으로는 하나의 값만을 선택할 수 있게 되어있다. 이런 기본적인 상태에서 중첩된 상태를 한 번에 선택하기 위해서는 단순한 방법으로는 중첩상태의 열거형을 일일이 만들어 주는 것이 있다.

 

public enum ECharacterState
{
    Jump                  = 0x00000001,   // 0001
    JumpAndAttack   = 0x00000003,   // 0011
    Attack                = 0x00000002,   // 0010
    AttackAndJump   = 0x00000006,   // 0110
    Move                  = 0x00000004,   // 0100
    MoveAndJump     = 0x00000005,   // 0101
}

 

 

이렇게 중첩된 상태를 일일이 열거형에 추가해서 선택가능하게 만드는 방법이 있지만, 이것은 상태가 지금처럼 3가지 밖에 없는 간단한 상황에서만 가능한 방법일 것이다. 상태가 한 가지 한 가지 추가될 때마다 추가해야되는 중간 값이 엄청나게 많아져서 결국에는 드롭다운의 길이가 끝없이 늘어나는 것이 뻔히 예측될 것이다. 그리고 그렇게 중첩된 상태에 대한 상태가 늘어나면 어떤 상태를 넣었는지 확인하기 힘들어져서 이상하게 입력하게 될 수도 있다. 바로 위의 예시에서 보듯이 Jump And Attack과 Attack And Jump를 넣어버리게 되는 것처럼 말이다.(그리고 Attack And Jump의 값은 또 Attack And Move의 값이 입력되었다.)

 

이런 상황을 막기 위해서는 Inspector에서 enum 타입의 드롭다운에서 여러 개의 값을 선택 가능하게 만들어줘야 한다. 이런 방식의 예시로는 Inspector 뷰에 공개된 Light 컴포넌트의 Culling Mask 변수가 있다.

 

Light 컴포넌트의 Culling Mask는 빛을 받을 레이어를 골라서 선택할 수 있게 해준다.

 

 

 

 

위의 Culling Mask처럼 Inspector 뷰에서 원하는 상태를 여러 가지를 동시에 선택할 수 있다면 열거형에 끝없이 중첩된 상태를 추가할 필요는 없어질 것이다. 이러한 기능을 넣기 위해서는 에디터 기능을 약간 수정할 필요가 있다. 다음과 같이 EnumFlagsAttributeDrawer.cs와 EnumFlagsAttribute.cs 코드를 추가해보자. EnumFlagsAttributeDrawer의 경우, 에디터의 기능을 수정하는 코드기 때문에 Editor 폴더에 넣어주어야 한다.

 

 

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(EnumFlagsAttribute))]
public class EnumFlagsAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect _position, SerializedProperty _property, GUIContent _label)
    {
        _property.intValue = EditorGUI.MaskField(_position, _label, _property.intValue, _property.enumNames);
    }
}

EnumFlagsAttributeDrawer.cs

 

using UnityEngine;

public class EnumFlagsAttribute : PropertyAttribute
{
    public EnumFlagsAttribute() { }
}

EnumFlagsAttribute.cs

 

public class Character : MonoBehaviour
{
    [EnumFlags]
    public ECharacterState state;
}

 

 

 

 

 

그리고 난 이후에 Inspector 뷰에서 여러 개의 값을 선택하고자 하는 enum 타입의 변수의 경우에 새롭게 추가해준 [EnumFlags] 어트리뷰트를 추가해주면아래의 이미지와 같이 우리가 추가한 enum 타입의 변수 역시 여러 개의 값을 선택할 수 있게 된다.

 

 

이렇게 여러 개의 값을 선택할 수 있게 사용하는 경우 Nothing은 0이고 Everything은 -1이다. 열거형에 값을 추가할 때는 각 열거형이 2의 n승이 되게 값을 정해줘야만 한다. 중간에 그 외의 값이 있으면 Inspector 뷰에서 선택한 값과 스크립트 상에서의 값이 일치하지 않는 버그가 발생할 수 있다.

 

public enum ECharacterState
{
    Jump    = 0x00000001,   // 0001
    Attack  = 0x00000002,   // 0010
    Move    = 0x00000004,   // 0100
}

EnumFlags를 사용해서 여러 값을 선택할 수 있게 하려면 중첩 상태를 제외하고 기본 상태만 2의 n승 값을 가지게 정의해줘야 한다.

 

 


참고 :

 

Default Editor : Enum as flags ? - Unity Answers

 

answers.unity.com

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

UNet Tutorial (6) - SyncVar와 Hook

 

지난 Unet Tutorial 섹션에서는 Command와 ClientRpc를 이용한 클라이언트와 서버 간의 액션 통신 방법에 대해서 알아보았다. 이것은 함수를 통한 동작 수행을 처리하는 것이었다. 하지만 이번 섹션에서 알아볼 SyncVar와 Hook은 NetworkBehaviour를 상속받는 클래스의 멤버 변수를 동기화하기 위한 것이다.

 

지난 섹션에서 배운 Command 와 ClientRpc를 이용해서도 멤버변수에 대한 동기화를 구현할 수는 있다. Command와 ClientRpc를 통한 멤버 변수 동기화를 구현한 코드는 다음과 같다.

 

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    // 플레이어의 체력
    public int hp;

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만들고 Rpc를 통해 클라이언트에 알려준다.
        if (isServer)
        {
            hp = 100;
            RpcSyncHp(hp);
        }
    }
   
    [ClientRpc]
    // 클라이언트는 Rpc를 통해 현재 hp를 받으면 자신의 객체에 적용한다.
    public void RpcSyncHp(int hp)
    {
        this.hp = hp;
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 빼고 남은 hp를 Rpc로 클라이언트에 알려준다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
        RpcSyncHp(hp);
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}

위의 예시에서는 CmdDamaged를 호출할 때는 클라이언트에서 데미지 값을 넣도록 했지만 실제로 구현할 때는 저렇게 하지 않도록 하자. 클라이언트에서 중요한 값을 넣을 수 있도록하면 해킹에 취약해진다.

 

Command와 ClientRpc를 이용하여 멤버 변수 동기화 구현은 가능하지만 약간은 복잡하다.

 

 

 

 

SyncVar를 통한 멤버 변수 동기화

 

SyncVar를 통해 멤버 변수 동기화를 이용하면 아래의 예시 코드처럼 조금 더 간단한 코드가 작성 가능해진다.

 

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{
    [SyncVar]
    // 플레이어의 체력
    public int hp;

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만든다.
        if (isServer)
        {
            hp = 100;
        }
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 뺀다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}

 

위의 예시 코드처럼 동기화하고자 하는 멤버 변수에 SyncVar 어트리뷰트를 붙여주면 서버에서 hp의 값이 변경되면 클라이언트의 해당 객체에 변경된 값을 알려주고 동기화하게 된다. 이러한 SyncVar의 동기화에서 알고있어야 할 점은 이 멤버 변수 동기화는 서버에서 클라이언트의 방향으로만 이루어진다는 점이다. 즉, 서버에서 변수가 수정되면 모든 클라이언트로 동기화되고, 클라이언트에서는 이 변수의 값을 아무리 수정해도 다른 클라이언트나 서버에서는 변경이 되지 않는다는 점이다.

 

 

 

 

Hook

 

멤버 변수가 변경된 것을 클라이언트에서 동기화 받고 난 뒤에 클라이언트에서 처리해야할 작업이 더 있는 경우가 더러 있다. 위의 코드를 예를 들자면 hp가 동기화된 이후에 클라이언트 측에서는 변경된 hp의 양에 맞춰서 hp ui를 변경해 주어야할 것이다. 그런 것을 처리하는 역할을 할 수 있는 것이 바로 hook이다. hook의 구현은 다음 예시 코드와 같이할 수 있다.

 

using UnityEngine;
using UnityEngine.Networking;

public class Player : NetworkBehaviour
{

    public delegate void OnChangeHp(int hp);

    public OnChangeHp onChangeHp;

 

    [SyncVar(hook = "ChangeHookHp")]
    // 플레이어의 체력
    public int hp;
    void ChangeHookHp(int hp)
    {

        // hook을 이용하는 경우에는 동기화해야할 값이 매개 변수로 넘어오고 그것을 직접 대입해주어야 한다.
        this.hp = hp;

        // 만약 delegate event에 hp ui의 함수를 등록해두었다면 hp ui가 갱신될 것이다.

        onChangeHp(this.hp);
    }

    void Start()
    {
        // 서버에서 플레이어가 생성되면 hp를 100으로 만들고 Rpc를 통해 클라이언트에 알려준다.
        if (isServer)
        {
            hp = 100;
        }
    }

    [Command]
    // 서버는 Cmd로 플레이어가 받아야될 데미지를 받으면 현재 hp에서 데미지를 빼고 남은 hp를 Rpc로 클라이언트에 알려준다.
    public void CmdDamaged(int dmg)
    {
        hp -= dmg;
    }

    void Update()
    {
        // 클라이언트에서 스페이스 버튼을 누르면 Cmd를 호출해서 서버에 플레이어가 입어야될 데미지를 알려준다.
        if (isClient && isLocalPlayer)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                CmdDamaged(10);
            }
        }
    }
}

 

위의 코드처럼 SyncVar의 어트리뷰트에 hook을 등록해두면 서버에서 hp값이 수정되고 그것이 클라이언트에 통지되었을 때, 자동으로 hook으로 등록해둔 함수가 호출된다. 즉, hp 값이 변경되었을 때, 클라이언트에서 추가적으로 처리해야 하는 일들을 처리할 수 있게 되는 것이다.

 

단, 이 hook을 사용할 때 알아두어야 할 것이 있다. hook은 서버에서 변경된 값이 클라이언트에 통지되었을때 동작하는 것임으로 클라이언트에서만 동작한다는 것과, hook을 사용한 경우에는 클라이언트의 멤버 변수에 동기화되어야하는 값이 바로 적용되지 않는다는 점이다. 멤버 변수에 바로 동기화되지 않고, hook으로 등록한 함수의 매개 변수를 통해서 전달되기 때문에, 클라이언트 측의 멤버 변수에 매개 변수의 값을 직접 대입해주어야 한다.

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

BoxCast, SphereCast, CapsuleCast 제대로 사용하기

 

지난 Physics 섹션에서는 매 프레임 콜라이더 충돌을 검출하지 않고 원하는 순간에만 콜라이더를 검출해낼 수 있는 Cast 계열의 함수들에 대해서 알아보았다. 이번 섹션에서는 그 중에서도 BoxCast, SphereCast, CapsuleCast를 제대로 사용하는 방법에 대해서 알아보자.

 

지난 섹션에서 설명했듯이 Cast 계열의 함수는 오브젝트에서 콜라이더 컴포넌트를 가질 필요도 없고, OnCollision이나 OnTrigger같은 이벤트를 통해 매 프레임 동작하는 낭비를 줄일 수 있다. 하지만 이러한 Cast 계열의 함수에는 작은 단점이 하나 존재한다.

 

게임 오브젝트가 콜라이더 컴포넌트를 가지고, 이벤트를 통해 작동하는 경우에는 Scene 뷰에서 콜라이더의 위치나 크기, 방향 등을 확인할 수 있지만, 이 Cast 계열의 함수의 경우에는 위치, 방향, 크기 등을 직관적으로 확인할 수 없다는 것이다. 즉, 이 Cast 계열 함수에 익숙하지 않은 사람이라면 이 Cast의 위치, 방향, 크기가 자신이 의도한대로 작동되고 있는지 확인하기가 쉽지가 않다는 의미이다. 당연하게도 이것은 확인하기 어려운 문제를 발생시킬 것이다.

 

그리고 이 Cast의 범위를 확인하기 어려운 문제는 함수의 매개변수의 이름이 불분명한 것과 함수의 매개변수 중 지정하지 않은 값의 기본값이 얼마라고 명시되어 있지 않은 점과 맞물려 전혀 예측하지 못한 형태로 작동하게 만들어 버린다.

 

 

 

 

문제

 

캐릭터 주변의 반지름 내의 범위에 존재하는 모든 적들을 찾아서 데미지를 입히는 스킬을 구현한다고 가정해보자. 이 스킬을 구현하기 위해서는 우선 SphereCastAll 함수를 사용해야 할 것이다. 우선 SphereCastAll 함수의 제일 간단한 형태를 보자면 다음과 같다.

 

RayCastHit[] Physics.SphereCastAll(Vector3 origin, float radius, Vector3 direction);

 

그림 1 : 파란색 화살표는 forward를 의미한다.

 

origin과 radius는 간단하게 이해할 수 있지만, 일반적으로 SphereCast가 위의 그림처럼 캐릭터 중심으로 구형으로 작동할 것이라고 받아들이기 쉽기 때문에 SphereCast에 익숙하지 않거나 처음 사용하는 사람의 경우에는 'Sphere가 방향이 바뀌면 뭐가 달라지지?'하는 생각에 direction에서 당황하게 될 것이다. 그리고 일반적으로 아래의 예시 코드와 같이 RayCast를 사용하듯이 direction에 character의 정면에 해당하는 transform.forward를 넣게 될 확률이 높다.

 

var hits = Physics.SphereCastAll(transform.position, radius, transform.forward);

 

 

이렇게 코드를 작성하면 잘 모르는 사용자는 위의 이미지처럼 캐릭터를 중심으로 구형의 범위 내에서만 캐스팅을 할 것이라고 생각하겠지만, 이것은 놀랍게도 전혀 다른 결과를 가져온다.

 

 

그림 2

 

첫번째 이미지처럼 동작할 것이라고 생각한 코드는 사실은 바로 위의 두번째 이미지처럼 구가 캐릭터의 전방으로 무한히 늘어선 형태로 캐스팅을 해버린다. 마치 드래곤볼의 손오공이 에너지파를 전방으로 쏘아낸 것처럼 말이다.

 

왜 이런 결과가 나왔는지에 대해서 알아보자면 다음의 좀 더 복잡한 매개변수를 가지는 SphereCastAll의 오버로드를 살펴보아야 한다.

 

RayCastHit[] Physics.SphereCastAll(Vector3 origin, float radius, Vector3 direction, float maxDistance, int layerMask);

 

위 코드를 살펴보면 제일 처음 보았던 매개변수에 maxDistance라는 매개변수가 추가된 것이 보인다. 이것은 원점인 origin으로부터 direction 방향으로 얼마나 되는 거리까지 캐스팅할 것인지를 의미한다. 그리고 이 값의 기본값은 Infinity, 즉 무한대이다. 그렇기 때문에 그림 2처럼 캐릭터의 정면을 향해서 무한히 확장되는 형태로 캐스팅이 되는 것이다.

 

이것들이 의미하는 바를 살펴보자면 BoxCast, SphereCast, CapsuleCast 등은 사실 가장 기본적인 RayCast와 똑같이 동작하며, 그 범위만 하나의 점에서 육면체, 구, 캡슐 모양으로 확장된 형태로 동작한다는 것이다.

 

그렇다면, Cast대신에 다시 콜라이더를 사용해야 하는가? 아니다. 몇가지 방법을 이용하면 충분히 캐릭터 주변의 반지름 내에서 적들을 찾아낼 수 있다.

 

 

 

 

1. direction을 Vector3.up으로

 

var hits = Physics.SphereCastAll(transform.position, radius, Vector3.up)

 

첫번째 방법은 매개변수 중에 direction 값을 Vector3.up으로 주는 것이다.

이렇게 하면 캐스팅이 위쪽으로 확장되면서 캐릭터의 주변에 있는 적들만 검출해낼 수 있게 된다. 다만, 이 방법을 썼을 때는 캐릭터의 위쪽에 있으면서 반지름 내의 범위에 있는 적도 검출되기 때문에, 캐릭터를 중심으로 완전히 구형 내에서만 적을 찾고자 한다면 다음의 방법을 쓰는게 좋다. 그리고 성능적인 면에서도 다음 방법이 훨씬 좋다.

 

 

 

 

2. maxDistance 값을 0으로

 

var hits = Physics.SphereCastAll(transform.position, radius, Vector3.up, 0f)

 

캐스팅의 범위를 캐릭터의 주변으로 제한하는 방법 중 가장 확실하고 좋은 방법은 maxDistance 값을 0으로 설정하는 것이다. 이렇게 하면 SphereCast나 BoxCast, CapsuleCast의 범위가 direction 방향으로 확장되지 않고 아래의 그림처럼 설정된 크기 내에서만 캐스팅이 이루어지게 된다.

 

 

이번 섹션에서 이야기한 문제는 예시를 보인 SphereCast뿐만 아니라 BoxCast, CapsuleCast에서도 똑같이 작용한다. 다만 SphereCast가 훨씬 간단하게 사용하고 예시를 보여줄 수 있기 때문에 SphereCast로만 예시를 보였다.

 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

+ Recent posts