38. 클라이언트 지원 Part 1: 플러그인

클라이언트 플러그인을 사용하면 네트워크 통신, 공지사항 확인, 리소스 다운로드 등의 기능을 편리하게 사용할 수 있습니다. 현재 Unity, Unreal Engine 4, Cocos2d-x에서 사용할 수 있는 클라이언트 플러그인을 지원하고 있습니다. 클라이언트 플러그인은 GitHub 에서 받으실 수 있습니다.

이 문서에서는 클라이언트 플러그인의 사용방법, 주의할 사항, 사용팁 등을 설명합니다. 아래의 설명은 Unity 플러그인을 기준으로 작성되었습니다.

38.1. 개요

38.1.1. Session이란?

서버와 클라이언트의 네트워크 연결은 Session으로 관리합니다. Session은 직접 Close하거나, 설정한 시간 이상 네트워크 통신이 이루어지지 않아 Timeout이 발생하지 않는한 유지됩니다. 즉, 네트워크 연결이 끊기더라도 서버에서는 Session을 유지하며, 재연결시 동일한 Id의 Session이 있다면 연결이 끊어지지 않은 것처럼 계속 메시지를 주고 받을 수 있습니다.

하나의 Session은 TCP, UDP, HTTP 하나씩 최대 3개의 Transport 연결을 가질 수 있습니다. 같은 프로토콜 연결을 여러 개 만들고 싶다면 Session도 여러 개 만들어야 합니다.

Tip

Session의 내부 구현 방식과 관련된 자세한 내용은 (고급) 아이펀 엔진의 네트워크 스택 에서 설명하고 있습니다.

38.1.1.1. Session Id

서버에 접속하면 제일 먼저 서버로부터 Session Id 를 발급 받게 되고 이 후 서버와 주고 받는 모든 메시지에는 이 Session Id 가 포함됩니다.

JSON을 사용하는 경우, Session Id는 36 bytes 문자열입니다. Protobuf를 사용하는 경우에는 16 bytes Array, 혹은 JSON과 동일한 36 bytes 문자열로 사용할 수 있습니다. 서버와 주고 받는 모든 메세지에 Session Id가 포함되므로, 16 bytes Array를 사용하면 네트워크 사용량을 줄일 수 있습니다.

Protobuf에서 Session Id를 Byte Array로 사용하려면, 서버 MANIFEST 파일의 send_session_id_as_string 값을 false 로 설정하면 됩니다. 이 옵션의 기본 값은 true 입니다. JSON을 사용하는 경우, 이 옵션은 아무 동작도 하지 않고, 계속 36 bytes 문자열을 사용합니다.

Tip

개발 편의를 위하여 플러그인의 초기 설정은 연결할 때마다 새로운 Session Id 를 요청하는 것입니다. 클라이언트에서도 동일한 Session Id 를 계속 사용하고 싶다면 Session Reliability 옵션을 사용해 주세요.

38.1.1.2. FunapiSession 클래스

하나의 Session 을 관리하는 클래스입니다. Tranpsort 를 관리하고 메시지를 송수신하는 등 하나의 Session 으로 할 수 모든 기능을 담고 있습니다.

하나의 세션으로 연결할 수 있는 서버는 하나입니다. 처음 세션을 생성할 때 서버 주소가 정해지면 이 후 서버 주소는 변경할 수 없습니다. 그 외 Tranpsort 옵션이나 포트 번호 등은 변경이 가능합니다.

사용이 끝난 FunapiSession 객체는 FunapiSession.Destroy() 함수를 호출해 명시적으로 객체를 해제해 주어야 더 이상 업데이트가 이루어지지 않습니다. 앱 전체에서 시작부터 끝날 때까지 FunapiSession 객체를 하나만 사용할 경우에는 굳이 호출하지 않아도 괜찮습니다. OnApplicationQuit 이 호출되면 자동으로 해제됩니다.

Note

버전 247 이전의 플러그인 에서는 Session 마다 MonoBehaviour 객체를 생성/삭제했었는데 플러그인 종료처리가 완료되기 전에 MonoBehaviour 객체가 삭제되어 이후 처리가 안되는 경우가 있어 플러그인용 MonoBehaviour 객체를 하나만 두고 공용으로 사용하는 방식으로 변경되었습니다. 이로 인해 사용자가 사용이 끝난 FunapiSession 객체에 대해 FunapiSession.Destroy() 함수를 호출해 명시적으로 객체를 해제해 주어야 더 이상 업데이트가 이루어지지 않습니다.

Tip

플러그인 버전 158 이후 플러그인을 더 쉽고 편하게 사용할 수 있도록, 기존 FunapiNetwork 클래스를 대체하는 FunapiSession 클래스가 추가되었습니다.

FunapiNetwork는 더 이상 업데이트되지 않습니다. (버전 247에서 파일도 삭제됨) 기존의 FunapiNetwork에 대한 도움말은 이전 클라이언트 플러그인 설명 문서를 참고해주세요.

38.1.1.3. Session Reliability 옵션

Session reliability 는 FunapiSession 클래스를 생성할 때 전달하는 옵션 중의 하나입니다.

이 옵션을 true로 설정하면 서버와 연결이 끊겨도 재연결시 동일한 세션으로 계속해서 통신할 수 있습니다. 서버에 재접속 후 거쳐야 하는 인증 과정을 생략할 수 있고, 서버에서도 유저의 데이터 값을 그대로 유지할 수 있어 모바일처럼 연결이 자주 끊길 수 있는 환경에서는 이 옵션을 사용하는 것을 추천합니다.

Important

Session reliability 기능은 TCP 프로토콜에서만 사용 할 수 있습니다.

Session reliability는 서버와 클라이언트가 항상 동일한 설정을 사용해야 합니다. 서버 MANIFEST 파일의 use_session_reliability 옵션을 true로 설정했다면, 클라이언트에서도 FunapiSession 클래스를 생성할 때, session_reliability 파라미터에 true를 넘겨줘야 합니다. 이 값이 서로 다른 경우, 서버에 아래와 같은 로그가 출력됩니다.

// 서버는 Session reliability 옵션이 꺼져 있고 클라이언트는 켜져 있을 경우...
message with seq number. but seq number validation disabled: sid=..., transport_protocol=Tcp

// 서버는 Session reliability 옵션이 켜져 있고 클라이언트는 꺼져 있을 경우...
message without seq number: session_id=...

38.2. 서버에 연결하기

플러그인을 활용하여 구현하는 방법을 코드 예제와 함께 간략히 소개합니다. 전체 코드는 플러그인에 포함된 예제 프로젝트에서 확인하실 수 있습니다. (Assets/Sample/TestSession.cs 파일 참고)

Test.unity Scene을 실행하면 서버 주소, 포트, 프로토콜, 인코딩 타입을 입력할 수 있습니다. 이 값들을 info 객체에 저장해서 사용합니다.

38.2.1. FunapiSession 객체 생성

서버와 통신하기 위해서 FunapiSession 클래스를 생성하고, 설정하는 방법입니다.

// Session 옵션
SessionOption option = new SessionOption();
option.sessionReliability = false;            // 기본값이 false 입니다.
option.sendSessionIdOnlyOnce = false;         // 기본값이 false 입니다.

// FunapiSession 객체를 생성합니다.
// 전달하는 파라미터는 서버 주소와 SessionOption 입니다.
// option 값을 전달하지 않으면 기본 값을 사용합니다.
session = FunapiSession.Create(info.address, option);

// 이벤트 콜백을 등록합니다.
session.SessionEventCallback += onSessionEvent;
session.TransportEventCallback += onTransportEvent;
session.TransportErrorCallback += onTransportError;

// 받은 메시지를 처리하기 위한 콜백을 등록합니다.
session.ReceivedMessageCallback += onReceivedMessage;

플러그인에서 사용하는 콜백은 유니티의 event 객체를 사용하고 있습니다. 이벤트로 콜백을 등록하는 방법은 예제와 같이 ‘+=’ 연산자를 사용합니다. 하나의 이벤트에 여러 개의 콜백을 등록할 수도 있습니다.

위에서 만든 Session 객체로 TCP 연결을 시작합니다.

// AutoReconnect 옵션을 사용하기 위해 옵션 객체를 생성합니다.
// 특정 옵션을 사용하려는 것이 아니라면 Connect의 옵션 파라미터는 생략할 수 있습니다.
TcpTransportOption tcp = new TcpTransportOption();
tcp.AutoReconnect = false;    // 기본값이 false 입니다.

// 서버에 연결하기 위해서는 프로토콜 타입, 인코딩 타입, 포트 번호가 필요합니다.
// 옵션 값은 필요할 경우 사용하면 되고 지정하지 않으면 기본 값을 사용합니다.
session.Connect(info.protocol, info.encoding, info.port, option);

Connect 함수는 지정한 파라미터를 사용하여 Transport를 생성하고 연결을 시도합니다.

같은 프로토콜의 Transport가 연결된 적이 있고 옵션 값이 같다면 Transport를 다시 생성하지 않고 해당 Trnasport를 재연결합니다. 옵션 값이 다를 경우에는 Transport를 새로 생성하게 됩니다.

기존 연결로 재연결하는 경우에는 파라미터로 프로토콜만 받는 Connect 함수를 사용하는 것이 좋습니다.

아래는 Connect 함수에서 사용할 수 있는 protocolencoding 의 종류를 선언한 코드입니다.

// 프로토콜은 Tcp, Udp, Http, Websocket 4가지 타입을 지원합니다.
public enum TransportProtocol
{
  kDefault = 0,
  kTcp,
  kUdp,
  kHttp,
  kWebsocket
};

// 메시지 인코딩 방식은 JSON과 Protobuf 2가지 방법이 있습니다.
public enum FunEncoding
{
  kNone,
  kJson,
  kProtobuf
}

38.2.2. 이벤트 콜백 함수

FunapiSession에는 Session의 상태 변화를 알려주는 콜백과 Transport의 상태 변화를 알려주는 콜백 함수, Transport의 에러에 대해서 알려주는 콜백 함수가 있습니다. Transport에서 오류가 발생했을 경우, 에러 콜백과 함께 Transport의 이벤트 콜백이 함께 호출될 수도 있습니다.

38.2.2.1. Session Event

SessionEventCallback 에 콜백 함수를 등록하면 세션의 상태가 변경될 때마다 아래와 같은 세션 관련 이벤트 알림을 받을 수 있습니다.

public enum SessionEventType
{
  kOpened,             // 세션이 처음 연결되면 호출됩니다. 같은 세션으로 재연결시에는 호출되지 않습니다.
  kConnected,          // 모든 Transport의 연결이 완료되면 호출됩니다.
  kStopped,            // 세션에 연결된 모든 Transport의 연결이 끊기면 호출됩니다.
  kClosed,             // 세션이 닫히면 호출됩니다. Transport의 연결이 끊겼다고 세션이 닫히는 것은 아닙니다.
                       // 세션은 서버에서 명시적으로 Close가 호출되거나 세션 타임아웃이 되었을 때 종료됩니다.

  kRedirectStarted,    // Redirect 관련 이벤트는 아래 '서버간 이동' 항목을 참고해주세요.
  kRedirectSucceeded,
  kRedirectFailed
};

FunapiSession 에 있는 SessionEventCallback 은 다음과 같이 선언되어 있습니다.

void SessionEventHandler (SessionEventType type, string session_id);

38.2.2.2. Transport Event

Transport와 관련된 이벤트는 아래와 같습니다. kStarted 를 제외하고는 모두 연결에 실패하거나 연결이 끊겼을 때 발생하는 이벤트들입니다. kStopped 는 정상적으로 연결이 종료되었거나 오류로 인해 연결이 끊겼거나 Transport의 연결이 끊기면 항상 발생하는 이벤트입니다.

public enum TransportEventType
{
  kStarted,              // 서버와 연결이 완료되면 호출됩니다.
  kStopped,              // 서버와 연결이 종료되거나 연결에 실패하면 호출됩니다.
  kReconnecting          // 재연결을 시작할 때 호출됩니다.
};

이전에는 Disconnect 등 연결 종료에 대한 여러가지 타입의 이벤트가 있었으나 플러그인 버전 246 이후부터는 kStopped 이벤트 하나로 통일되었습니다. 어떤 이유로 연결이 끊겼던지 Transport 이벤트는 kStopped 하나만 호출됩니다. 자세한 종료 사유에 대해서는 로그로 확인하거나 FunapiSession.GetLastError 로 확인할 수 있습니다.

FunapiSession 에 있는 TransportEventCallback 은 다음과 같이 선언되어 있습니다.

void TransportEventHandler (TransportProtocol protocol, TransportEventType type);

38.2.2.3. Transport Error

Tranpsort의 에러 콜백의 경우에는 에러 타입과 함께 에러 메시지도 함께 전달됩니다.

public class TransportError
{
  public enum Type
  {
    kNone,
    kStartingFailed,      // Transport 초기화에 실패했을 때 호출됩니다.
    kConnectionTimeout,   // 연결 제한시간을 초과했을 경우에 호출됩니다.
    kEncryptionFailed,    // 메시지 암호화에 실패했을 때 호출됩니다.
    kSendingFailed,       // 메시지를 보내는 과정에서 오류가 발생했을 때 호출됩니다.
    kReceivingFailed,     // 메시지를 받는 과정에서 오류가 발생했을 때 호출됩니다.
    kRequestFailed,       // HTTP 요청에 실패했을 때 호출됩니다.
    kWebsocketError,      // Websocket에서 오류가 발생했을 때 호출됩니다.
    kDisconnected         // 예기치 않게 서버와의 연결이 끊겼을 때 호출됩니다.
  }

  public Type type = Type.kNone;
  public string message = null;
}

FunapiSession 에 있는 TransportErrorCallback 은 다음과 같이 선언되어 있습니다.

void TransportErrorHandler (TransportProtocol protocol, TransportError type);

Transport의 오류가 발생했을 때 연결을 시도하는 중이었다면 연결 시도가 중지되고 kStopped 이벤트가 호출됩니다.

서버와 연결이 된 상태에서 오류가 발생했을 때에는 UDP의 경우에는 에러만 전달 하고 그 외의 프로토콜은 연결을 종료한 후 kStopped 이벤트를 발생시킵니다.

Tip

Tranpsort에서 오류가 발생해서 연결이 종료되었을 경우 TransportErrorCallback 에서 재연결을 시도 해도 되지만 이벤트의 순서상 에러 콜백이 호출된 후 TransportEventCallback 으로 kStopped 이벤트가 호출됩니다. 그래서 재연결을 시도하는 시점은 Tranpsort의 연결이 완전히 종료되는 kStopped 이벤트가 호출된 후로 하는 것이 좋습니다.

38.2.3. Transport 옵션

FunapiSession의 Connect 함수를 호출할 때 파라미터로 Transport의 옵션을 전달할 수 있습니다. 이 값이 null인 경우에는 기본 값을 사용하게 됩니다. 사용할 프로토콜 (TCP, UDP,HTTP)에 따라 그에 맞는 TransportOption 클래스를 생성하여 지정해야 합니다. 예를 들어 TCP의 경우, TcpTransportOption 입니다. UDP의 경우에만 기본 클래스인 TransportOption 을 사용하면 됩니다.

아래는 TransportOption 클래스가 정의된 코드입니다.

38.2.3.1. Base 클래스, UDP 옵션

Transport 옵션의 기본 클래스입니다. UDP 연결에만 이 기본 클래스를 사용할 수 있습니다.

public class TransportOption
{
    // 암호화 타입을 지정합니다. 암호화를 사용하지 않을 경우 이 값을 변경하지 마세요.
    // 서버에서 암호화를 사용할 경우 이 값을 서버와 같은 타입 값으로 입력해야 합니다.
    // TCP의 경우 값을 입력하지 않아도 서버에서 사용중이라면 클라이언트도 암호화를 사용하게 됩니다.
    public EncryptionType Encryption = EncryptionType.kDefaultEncryption;

    // 서버와 주고 받는 메시지를 압축 할 수 있습니다.
    // 값을 지정하지 않을 경우 압축 기능을 사용하지 않습니다.
    // 압축 타입은 서버와 같은 값을 입력해야 합니다.
    public FunCompressionType CompressionType = FunCompressionType.kNone;

    // 메시지에 sequence number를 붙여서 메시지의 유효성을 보장해주는 옵션입니다.
    // Session reliability 옵션을 사용하지 않고 메시지의 유효성만 보장하고 싶을 때 사용할 수 있습니다.
    // Session reliability 옵션을 사용하면 이 옵션은 무시됩니다.
    // TCP, HTTP 프로토콜에서만 사용 가능합니다.
    public bool SequenceValidation = false;

    // 서버와 연결할 때 Timeout 시간을 정하고 싶다면 이 값을 입력하면 됩니다.
    // 지정한 시간 내에 연결이 되지 않으면 연결이 종료되고 kStopped 이벤트가 호출됩니다.
    public float ConnectionTimeout = 0f;
}

38.2.3.2. TCP 옵션

TCP Transport 옵션 클래스입니다. TransportOption 값도 포함하면서 TCP에만 적용되는 옵션이 추가되어 있습니다.

public class TcpTransportOption : TransportOption
{
    // 이 값이 true일 경우 연결에 실패하거나 연결이 끊겼을 경우 재연결을 시도합니다.
    // 재시도 Timeout을 정하고 싶다면 ConnectionTimeout 값을 입력하면 됩니다.
    // ConnectionTimeout 시간을 초과하면 재시도를 중단하고 kStopped 이벤트가 호출됩니다.
    public bool AutoReconnect = false;

    // 네이글 알고리즘은 효율적인 네트워크 사용을 위해 작은 크기의 패킷을 모아서 전송하는 기능입니다.
    // 이 값을 true로 하면 네이글 알고리즘을 사용하지 않습니다.
    // 기본 값은 false로 네이글 알고리즘을 사용합니다.
    public bool DisableNagle = false;

    // 클라이언트 Ping 사용 옵션입니다.
    public bool EnablePing = false;
    // Ping 값을 로그로 표시할지 여부를 결정합니다.
    public bool EnablePingLog = false;
    // Ping 메시지를 보내는 간격에 대한 시간 값입니다.
    public int PingIntervalSeconds = 0;
    // 서버로부터 Ping에 대한 응답을 기다리는 최대 시간 값입니다.
    // 이 시간 내에 Ping 응답이 하나도 오지 않는다면 서버와의 연결이 끊긴 것으로 보고 Disconnect 처리됩니다.
    // 보통 PingIntervalSeconds 값의 3-4배 이상의 시간을 권장합니다.
    public float PingTimeoutSeconds = 0f;

    // 위의 Ping 관련 옵션 값을 한 번에 설정할 수 있는 함수입니다.
    public void SetPing (int interval, float timeout, bool enable_log = false);
}

38.2.3.3. HTTP 옵션

HTTP Transport의 옵션 클래스입니다.

public class HttpTransportOption : TransportOption
{
    // HTTPS 사용 여부에 대한 옵션입니다.
    // 서버가 HTTPS일 경우 이 값을 true로 입력해야 합니다.
    public bool HTTPS = false;
    // UnityEngine.WWW 클래스 사용 여부에 대한 옵션입니다.
    // 기본 값은 HttpWebRequest 클래스를 사용합니다.
    public bool UseWWW = false;
}

플러그인에서는 HTTP 요청을 처리할 때 HttpWebRequest 클래스를 사용하고 있습니다. Windows에서 Unity Editor로 이 기능을 사용하는 경우, 간혹 에디터가 응답하지 않는 경우가 있습니다. (소켓을 명시적으로 닫아주지 않아서 발생하는 문제) 이 문제를 피하기 위해 UnityEngine 의 WWW 클래스를 사용할 수 있도록 UseWWW 옵션이 추가되었습니다. 에디터에서만 발생하는 현상이므로 기본적으로는 이 옵션을 false 로 두고 에디터에서 실행할 때 문제가 있을 경우에만 WWW 클래스를 사용하는 것이 좋습니다.

38.2.3.4. Websocket 옵션

Websocket Transport의 옵션 클래스입니다.

public class WebsocketTransportOption : TransportOption
{
    // WSS 사용 여부에 대한 옵션입니다.
    public bool WSS = false;
}

서버가 SSL, TLS 등의 보안 레이어를 사용할 경우 WSS 값을 true로 입력해야 합니다.

Note

아이펀 엔진 서버에서 아직 WSS 기능을 지원하지 않고 있습니다. 플러그인에 해당 옵션이 있으나 서버 쪽 설정이 필요하므로 WSS를 사용하고 싶을 경우 슬랙이나 메일로 지원 요청을 해주시기 바랍니다.

38.3. 메시지 전송 및 수신

38.3.1. 메시지 전송

메시지를 보내고 싶다면 FunapiSession 클래스의 SendMessage 함수를 호출하면 됩니다.

public void SendMessage (string msg_type, object message,
                         TransportProtocol protocol = TransportProtocol.kDefault,
                         EncryptionType enc_type = EncryptionType.kDefaultEncryption);

msg_type 은 메시지 타입을 나타내는 string 타입의 파라미터입니다. Protobuf로 메시지를 보내는 경우에는 string 타입 대신에 enum MessageType 타입을 사용할 수도 있습니다.

message 는 실제 데이터가 포함된 메시지로 object 타입입니다. JSON, Protobuf 메시지를 모두 보낼 수 있습니다.

protocol 은 지정하지 않을 경우 FunapiSession에 처음 등록(Connect) 된 프로토콜로 메시지가 전송됩니다. Transport가 여러 개 등록되어 있고 특정 프로토콜로 메시지를 보내고 싶다면 protocol 값을 지정하면 됩니다.

FunapiSession에 여러 개의 프로토콜이 연결되어 있고 메시지를 보낼 때 기본으로 사용되는 프로토콜을 별도로 지정하고 싶다면 FunapiSession의 DefaultProtocol 프로퍼티 값을 입력하면 됩니다.

session.DefaultProtocol = TransportProtocol.kHttp;

enc_type 은 암호화 타입 파라미터입니다. 암호화를 사용하는데 타입을 지정하지 않을 경우 기본 타입으로 암호화를 하게 되고 암호화를 사용하지 않을 경우에 이 값을 입력하면 오류가 발생합니다.

서버에서 하나의 프로토콜에 여러 종류의 암호화를 허용할 경우 메시지를 보낼 때 암호화 타입을 선택해서 보낼 수 있습니다. 암호화에 대한 좀 더 자세한 설명은 메시지 암호화 를 참고해 주세요.

38.3.1.1. JSON 메시지 보내기

Dictionary<string, object> message = new Dictionary<string, object>();
message["message"] = "hello world";
session.SendMessage("echo", message);

플러그인에서는 JSON 을 사용하기 위해 MiniJSON 라이브러리를 사용합니다. MiniJSON 의 데이터 타입은 Dictionary<string, object> 입니다. 필요한 경우 다른 JSON 라이브러리를 사용할 수도 있습니다. 자세한 설명은 JSON Helper 클래스 를 참고해 주세요.

38.3.1.2. Protobuf 메시지 보내기

PbufEchoMessage echo = new PbufEchoMessage();
echo.msg = "hello proto";
FunMessage message = FunapiMessage.CreateFunMessage(echo, MessageType.pbuf_echo);
session.SendMessage(MessageType.pbuf_echo, message);

Protobuf 메시지의 기본형은 FunMessage 입니다. 사용자 메시지는 extend 형태로 되어있습니다. extend 메시지 중 0 ~ 15번까지는 예약된 메시지입니다. 사용자 메시지는 16번 필드부터 사용이 가능합니다.

MessageType 은 서버에서 .proto 파일들을 빌드해서 메시지 DLL을 만들 때 함께 생성되는 enum 리스트입니다. 이를 통해 extend 메시지들의 숫자 대신 메시지 이름으로 사용할 수 있습니다.

MessageType 은 기본적으로 int 타입 으로 처리됩니다. 다만 기존 인터페이스와의 호환을 위해 받은 메시지의 메시지 타입은 다시 string 으로 변환해서 전달하고 있습니다. 서버가 구버전이라 int 타입의 메시지를 처리하지 못한다면 PROTOBUF_ENUM_STRING_LEGACY define을 심볼에 추가하면 MessageType을 string 타입으로 처리할 수 있습니다.

Important

Unity에서 Protobuf-net 기반의 메시지를 사용하려면 추가적인 빌드 과정을 따라야 합니다. Unity에서 protobuf 사용하기 설명을 참고해주세요.

38.3.1.3. Delayed ack, Piggy back 기능

Session Reliability 기능을 사용할 경우 메시지 동기화를 위해 서버로부터 메시지를 받을 때마다 ack (서버가 보낸 메시지를 받았다는 확인 메시지)를 보내는데 메시지를 받을 때마다 ack 메시지를 보내는 것이 부담스러울 수 있습니다. 이런 부담을 줄이기 위해 이후에 보내는 메시지에 ack를 실어 보내거나 일정 간격을 두고 한 번 씩만 보내도록 할 수 있습니다.

Session 옵션의 delayedAckInterval 값을 설정하면 Delayed ack, Piggy back 기능이 활성화됩니다.

public class SessionOption
{
    public float delayedAckInterval = 0f;    // seconds
    ...
}

delayedAckInterval 값이 설정되면 가능하면 보내는 메시지에 ack를 담아 보내고 그렇지 못 할 경우에는 정해진 간격마다 보내야 할 ack가 있는지 확인하고 ack 메시지를 보냅니다.

서버의 설정과는 별개로 동작하며 서버에서는 MANIFEST 파일에서 delayed_ack_interval_in_ms 값을 0보다 큰 값으로 설정해주면 됩니다.

38.3.2. 메시지 수신

서버로부터 메시지를 받기 위해서는 메시지 콜백 함수를 등록해야 합니다. ReceivedMessageCallback 은 다음과 같이 선언되어 있습니다.

public delegate void (string msg_type, object message);

데이터형이 object인 이유는 받는 메시지가 JSON일 수도 있고 Protobuf 메시지일 수도 있기 때문입니다. 등록한 핸들러에서 메시지 타입에 따라 message 객체를 JSON이나 Protobuf로 캐스팅해서 사용하면 됩니다.

아래는 메시지 핸들링에 대한 예제입니다.

// 받은 메시지를 처리할 콜백을 등록합니다.
session.ReceivedMessageCallback += onReceivedMessage;

// 기다리는 메시지에 timeout을 정해서 알림을 받고 싶다면 이 콜백을 등록합니다.
// 메시지가 지정한 시간내에 오지 않으면 이 콜백 함수가 호출됩니다.
session.ResponseTimeoutCallback += onResponseTimedOut;

// 메시지를 처리하는 방식은 정해진 형식이 없으며 사용하기 편한 형태로 쓰시면 됩니다.
// 여기에서는 받은 메시지를 처리하기 위해 각각의 메시지를 Key-Value 방식으로 저장하겠습니다.
message_handler_["echo"] = onEcho;
message_handler_["pbuf_echo"] = onEchoWithProtobuf;

플러그인 내부에서 이미 Deserialize 된 메시지를 ReceivedMessageCallback 함수로 전달하기 때문에 메시지를 다시 Deserialize할 필요는 없습니다. 받은 object 메시지를 캐스팅만 해서 사용하면 됩니다.

아래는 message_handler_ 에 등록한 메시지 콜백 함수에 대한 구현입니다.

// JSON 메시지에 대한 처리 함수입니다.
void onEcho (object message)
{
    // 예제에서는 MiniJSON을 사용합니다.
    // 다른 JSON 라이브러리를 사용하고 싶다면 아래 JSON Helper 관련 설명을 참고해주세요.
    FunDebug.Assert(message is Dictionary<string, object>);
    Dictionary<string, object> json = message as Dictionary<string, object>;
    FunDebug.Log("Received an echo message: {0}", json["message"]);
}

// Protobuf 메시지에 대한 처리 함수입니다.
void onEchoWithProtobuf (object message)
{
    // iFun Engine에서 사용하는 protobuf 메시지는 FunMessage를 기본으로 하고
    // 사용자 메시지는 FunMessage 안에 extend 형태로 추가해서 사용합니다.
    FunDebug.Assert(message is FunMessage);
    FunMessage msg = message as FunMessage;
    // extend 된 echo 메시지를 가져옵니다.
    PbufEchoMessage echo = FunapiMessage.GetMessage<PbufEchoMessage>(msg, MessageType.pbuf_echo);
    if (echo == null)
        return;

    FunDebug.Log("Received an echo message: {0}", echo.msg);
}

38.3.3. Response Timeout

서버로부터 기다리는 메시지에 대해 타임아웃을 정하고 싶다면 FunapiSession 클래스에 있는 SetResponseTimeout 함수를 사용해 원하는 메시지 타입에 타임아웃 시간을 지정하면 됩니다.

아래는 Response Timeout 관련 함수들입니다. Json의 경우 string 타입을 사용해야 하고 Protobuf는 MessageType, int, string 타입 중 SendMessage 호출시에 전달했던 메시지 타입과 동일한 타입으로 등록해야 합니다.

// MessageType 타입으로 Response Timeout 메시지를 등록합니다.
// MessageType은 int 타입으로 변환되어 처리됩니다.
// 이미 등록된 메시지 타입일 경우 false가 반환됩니다.
public bool SetResponseTimeout (MessageType msg_type, float waiting_time)

// int 타입으로 Response Timeout 메시지를 등록합니다.
public bool SetResponseTimeout (int msg_type, float waiting_time)

// string 타입으로 Response Timeout 메시지를 등록합니다.
public bool SetResponseTimeout (string msg_type, float waiting_time)

// 등록된 MessageType 타입의 Response Timeout 메시지를 삭제합니다.
// 해당 타입의 메시지를 찾을 수 없을 경우 false가 반환됩니다.
public bool RemoveResponseTimeout (MessageType msg_type)

// 등록된 int 타입의 Response Timeout 메시지를 삭제합니다.
public bool RemoveResponseTimeout (int msg_type)

// 등록된 string 타입의 Response Timeout 메시지를 삭제합니다.
public bool RemoveResponseTimeout (string msg_type)

Tip

PROTOBUF_ENUM_STRING_LEGACY define을 선언하게 되면 MessageType의 메시지는 string 타입으로 처리되며 string 타입의 콜백 함수가 호출됩니다.

아래 코드는 string 타입의 sc_login 메시지를 10초 동안 기다리는 예제입니다. sc_login 메시지를 10초 내에 받지 못할 경우 ResponseTimeoutCallback 함수가 호출됩니다.

// 기다리는 메시지 타입과 시간을 지정합니다.
// 등록되는 순간부터 시간이 흘러갑니다.
session.SetResponseTimeout("sc_login", 10f);

아래는 ResponseTimeoutCallback 함수의 원형입니다. SetResponseTimeout 함수로 전달했던 메시지 타입을 파라미터로 받습니다. 타임아웃 시간내에 서버로부터 해당 메시지를 받는다면 타임아웃 설정이 해제되고 콜백 함수도 호출되지 않습니다.

// int 타입과 MessageType 타입은 이 콜백 함수가 호출됩니다.
public delegate void ResponseTimeoutIntCallback (int msg_type);

// string 타입으로 등록했을 경우 이 콜백 함수가 호출됩니다.
public delegate void ResponseTimeoutCallback (string msg_type);

Response Timeout 콜백 함수가 전달되는 파라미터의 타입에 따라 두 가지로 나뉘어 있으며 이름이 약간 다릅니다. 사용시 콜백 함수의 이름에 주의해주세요.

Tip

한 번 등록한 Response Timeout 은 서버로부터 응답을 받거나 타임아웃 이벤트가 발생하기 전까지 유지되며 타임아웃 이벤트가 만료된 후에는 다시 설정해야 합니다.

Tip

인자로 전달한 메시지 타입에 대한 타임아웃이 이미 설정되어 있는 경우 나중에 호출되는 타임아웃 값은 무시됩니다.

38.3.4. JSON Helper 클래스

기본적으로 플러그인은 MiniJSON을 사용합니다. 다른 JSON 라이브러리를 사용하고 싶다면 JsonAccessor 인터페이스 클래스를 상속받아 JSON 라이브러리를 핸들링하는 클래스를 만들어 FunapiMessage의 JsonHelper를 변경하면 됩니다.

JsonAccessor 클래스에는 없지만 필요한 함수가 있다면 JsonAccessor 파생 클래스에 추가해서 사용하시면 됩니다.

새로 만들어진 JsonAccessor 클래스는 아래와 같은 방법으로 등록하면 됩니다. 이후에 보내는 메시지와 받는 메시지는 모두 새로 등록된 JSON 클래스를 통해 Serialize나 Deserialize를 하게 됩니다.

NewJsonAccessor json_helper = NewJsonAccessor();
FunapiMessage.JsonHelper = json_helper;

Note

플러그인 버전 164에서 JsonAccessor 클래스의 인터페이스에 변경이 있었습니다. (2016년 8월 업데이트) 164 이전 버전의 JsonAccessor 클래스를 상속받아 JsonHelper를 구현하셨다면 플러그인 업데이트시 추가된 인터페이스에 대해 추가 구현을 해주셔야 합니다.

아래는 JsonAccessor 의 인터페이스입니다.

public abstract object Clone (object json);

public abstract string Serialize (object json);
public abstract object Deserialize (string json_str);

public abstract bool HasField (object json, string field_name);
public abstract void RemoveField (object json, string field_name);

public abstract int GetArrayCount (object json);
public abstract object GetArrayObject (object json, int index);

public abstract object GetObject (object json, string field_name);
public abstract void SetObject (object json, string field_name, object value);

public abstract string GetStringField (object json, string field_name);
public abstract void SetStringField (object json, string field_name, string value);

public abstract Int64 GetIntegerField (object json, string field_name);
public abstract void SetIntegerField (object json, string field_name, Int64 value);

public abstract bool GetBooleanField (object json, string field_name);
public abstract void SetBooleanField (object json, string field_name, bool value);

38.4. 메시지 암호화

서버와 메시지를 주고 받을 때 메시지를 암호화할 수 있습니다.

38.4.1. 암호화 타입

암호화 타입의 종류는 아래와 같습니다.

public enum EncryptionType
{
  // 암호화를 사용하지만 특정 메시지를 암호화하지 않은 상태로 보내고 싶을 때
  // SendMessage의 파라미터로 이 값을 전달하면 됩니다.
  kDummyEncryption,

  // iFun Engine에서 제공하는 암호화 타입입니다.
  // 메시지를 주고 받을 때마다 키 값이 변경되어 안정적인 암호화 방식입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kIFunEngine1Encryption,

  // iFun Engine에서 제공하는 암호화 타입입니다.
  // 고정된 암호화 키를 사용합니다. 프로토콜에 상관없이 사용 가능합니다.
  kIFunEngine2Encryption,

  // ChaCha20 암호화 타입입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kChaCha20Encryption,

  // Aes 128 암호화 타입입니다.
  // Tcp 프로토콜에서만 사용 가능합니다.
  kAes128Encryption
}

kIFunEngine1Encryption 타입은 메시지를 주고 받을 때마다 암호화 키가 변경됩니다. 동일한 메시지를 보내도 암호화 키 값이 매번 달라지므로 고정된 암호화 키를 사용하는 kIFunEngine2Encryption 보다 상대적으로 보안에 안정적인 암호화 타입입니다.

ChaCha20Aes128 은 외부 라이브러리(sodium)를 사용하고 있습니다. 해당 라이브러리 파일은 Plugins 폴더에 있습니다. 이 두 암호화 타입을 사용하려면 공개 키 를 설정해야 합니다.

FunapiEncryptor.public_key = "0b8504a9c1108584f4f0a631ead8dd548c0101287b91736566e13ead3f...";

public_key 는 FunapiEncryptor의 public static string 타입이며 Connect 함수가 호출되기 전에 먼저 이 값이 세팅되어 있어야 합니다. 공개 암호화 키는 메시지 암호화 를 참고해주세요.

Tip

IFunEngine1, ChaCha20, Aes128 암호화 방식은 TCP 프로토콜에서만 사용이 가능합니다.

암호화 설정을 사용하는 상태에서 특정 메시지에 대해서 암호화를 적용하지 않고 그대로 보내고 싶다면 SendMessage 의 암호화 타입으로 kDummyEncryption 을 전달하면 됩니다. 이 기능을 사용하려면 서버 쪽 암호화 설정에 dummy 가 포함되어 있어야 합니다.

38.4.2. 암호화 사용

TCP 의 경우 아무런 설정을 하지 않아도 서버에서 암호화 설정을 하면 클라이언트에서는 이를 따르도록 되어 있습니다. 처음 서버에 접속하면 암호화에 대한 동기화가 이루어지는데 서버에서 해당 프로토콜의 암호화 타입 목록을 전달하면 클라이언트는 서버에서 정한 암호화 방식을 사용하게 됩니다.

만약 암호화 타입이 여러 개라면 첫 번째 암호화 타입을 기본 암호화 타입으로 사용하게 됩니다. 메시지를 보낼 때 특정 암호화 타입을 지정하지 않는다면 이 때 지정된 기본 타입으로 암호화를 하게 됩니다. 특정 암호화 타입을 별도로 지정하고 싶다면 FunapiSession.Connect 함수를 호출할 때 TransportOption 의 Encryption 값을 정해주면 됩니다.

TcpTransportOption option = new TcpTransportOption();
option.Encryption = EncryptionType.kIFunEngine1Encryption;

만약 클라이언트의 암호화 타입이 서버에서 사용하지 않는 타입일 경우 오류가 발생하고 메시지를 주고 받을 수 없게 됩니다.

UDP 와 HTTP 의 경우에는 접속 후 서버와 암호화 타입을 동기화하는 과정이 없으므로 클라이언트에서 사용할 암호화 타입을 지정해주어야 합니다. 아이펀 엔진에서 제공하는 암호화 타입 중 UDP 와 HTTP 가 사용할 수 있는 암호화 타입은 IFunEngine2Encryption 밖에 없습니다. UDP 와 HTTP 에서 암호화를 사용하고 싶다면 이 값을 TransportOption의 Encryption 에 입력하면 됩니다.

TransportOption option = new TransportOption();
option.Encryption = EncryptionType.kIFunEngine2Encryption;

Note

GitHub 에 배포된 클라이언트 플러그인은 IFunEngine1Encryption과 IFunEngine2Encryption 타입의 암호화 기능이 포함되지 않은 무료 버전입니다. ChaCha20과 Aes128 타입은 사용 가능합니다. 유료 고객 의 경우 슬랙이나 Funapi support 로 요청 메일을 보내주시면 암호화 타입이 모두 포함된 플러그인 소스를 보내드립니다.

38.5. 메시지 압축

서버와 주고 받는 메시지를 압축할 수 있습니다.

현재 다음과 같은 압축 알고리즘을 지원합니다.

public enum FunCompressionType
{
  kNone,      // 기본 값. 압축을 사용하지 않습니다.
  kZstd,      // Zstdandard. 실시간 전송에 적합한 압축 알고리즘입니다.
  kDeflate    // Deflate. 지연 시간이 크고, 더 큰 메시지에 적당한 압축 알고리즘입니다.
}

FunCompressionType 은 TransportOption 에 있습니다. 기본 값은 kNone 입니다. 압축 관련 옵션은 서버와 클라이언트에서 같은 값을 사용해야 합니다. 같은 Session 이라 해도 프로토콜 별로 압축 타입을 다르게 사용할 수 있습니다.

38.5.1. 압축 옵션

압축 옵션은 필요할 경우 지정할 수 있고 지정하지 않을 경우 기본 값을 사용합니다. 현재 사용할 수 있는 옵션은 압축 최소 크기 지정, 딕셔너리 등록 (Zstdandard) 이 있습니다.

압축 클래스의 Base Class 는 FunapiCompressor 이며 이 클래스를 상속받아 만들어진 Zstdandard 압축을 수행하는 FunapiZstdCompressor 클래스가 있고 Deflate 압축을 수행하는 FunapiDeflateCompressor 가 있습니다.

압축 옵션을 지정하기 위해서는 타입에 맞는 FunapiCompressor 객체를 직접 생성해서 세션으로 넘겨줘야 하는데 이를 위한 콜백 함수가 CreateCompressorCallback 입니다. 이 콜백 함수 안에서 타입에 맞는 FunapiCompressor 객체를 생성하고 옵션을 지정해서 반환하면 됩니다.

CreateCompressorCallback 함수의 원형은 아래와 같습니다.

public delegate FunapiCompressor (TransportProtocol protocol);

38.5.1.1. 압축 최소 크기 지정

compression_threshold 는 압축 최소 크기를 지정하는 옵션입니다. 기본 값은 128 이며 메시지 길이가 128 이상일 경우 메시지를 압축하게 됩니다.

아래는 압축할 메시지의 최소 크기를 256으로 지정하는 예제입니다.

session.CreateCompressorCallback += delegate (TransportProtocol protocol)
{
    var compressor = new FunapiDeflateCompressor();
    compressor.compression_threshold = 256;
    return compressor;
};

38.5.1.2. Zstd 압축 사전 지정

Zstdandard 의 경우 자주 사용되는 메시지를 모아서 사전을 만들 수 있습니다. 이 데이터를 사전으로 등록해 두면 압축에 소요되는 시간을 단축시킬 수 있습니다. 사전은 서버와 클라이언트가 동일한 데이터를 사용해야 합니다.

아래는 base64 인코딩으로 되어 있는 파일을 사전 데이터로 설정하는 예제입니다. (사전 데이터를 파일로 읽어들일 경우 파일의 확장자는 bytes 여야 유니티에서 바르게 읽어들일 수 있습니다)

session.CreateCompressorCallback += delegate (TransportProtocol protocol)
{
    var compressor = new FunapiZstdCompressor();

    // 파일로부터 사전 데이터를 읽어옵니다.
    TextAsset text = Resources.Load<TextAsset>("zstd_dict");
    if (text != null)
    {
        byte[] zstd_dic = Convert.FromBase64String(text.text);
        compressor.Create(zstd_dic);
    }

    compressor_ = compressor;
    return compressor;
};

...

// 리소스를 해제하는 시점에 사전도 삭제해야 합니다.
if (compressor_ != null)
  compressor_.Destroy();

38.6. 연결 종료 및 재연결

38.6.1. 연결 종료

서버와의 연결을 종료하기 위해서는 FunapiSession 의 Stop 함수를 호출하면 됩니다.

session.Stop();

파라미터가 없는 Stop 함수는 연결된 모든 Transport 의 연결을 끊습니다. 특정 Transport 의 연결을 끊고 싶다면 파라미터로 프로토콜 타입을 전달하면 됩니다.

session.Stop(TransportProtocol.kTcp);

Stop 함수는 비동기 방식으로 Transport 의 연결을 끊습니다. Stop 함수가 호출됐을 때 연결을 바로 종료하지 않는 이유는 Transport 가 서버에 연결을 시도하는 중일 때 이를 기다리거나 보내지 못한 메시지가 있을 때 버퍼에 남아 있는 메시지를 보내고 연결을 끊기 위해서입니다.

Tip

연결 종료전 미전송 메시지를 보내고 연결을 종료하는 기능은 TCP 만 지원합니다.

TCP 연결의 경우 SendMessage는 호출되었으나 아직 전송하지 못하고 메시지가 남아 있는 경우, 남은 메시지를 모두 보낸 후 연결을 끊습니다. 이를 통해 Stop 을 호출하기 직전에 로그아웃 등의 메시지를 보내도 메시지의 전송을 보장합니다.

일부 기다림 없이 종료되는 경우도 있는데 서버에서 세션을 닫았을 경우, 서버간 이동을 위해 연결을 종료해야 할 경우, 앱이 종료되는 경우에는 대기 없이 바로 연결을 끊습니다.

Note

Connect 도중에 소켓이 닫힐 경우 IL2CPP 관련 버그로 인해 iOS 에서 크래쉬가 발생하기 때문에 서버에 연결을 시도하는 중간에 Stop이 호출됐을 경우에는 연결이 완료되기를 기다린 후 종료합니다.

38.6.2. 연결 종료 알림

38.6.2.1. Transport Event

Transport 의 연결이 종료되면 Transport Event 로 kStopped 가 전달됩니다. 이 이벤트는 오류로 인해 연결이 끊겼거나 정상적으로 연결이 종료되었거나 상관없이 Transport의 연결이 끊기면 항상 발생합니다.

Transport 의 이벤트는 FunapiSession 의 TransportEventCallback 을 등록해서 받을 수 있습니다.

38.6.2.2. Session Event

Session 에 연결된 모든 Transport 의 연결이 끊기면 Session Event 로 kStopped 이벤트가 전달됩니다.

Session kClosed 이벤트는 Transport 의 연결이 끊겼다고 발생하는 것이 아니라 Session Timeout 이 되거나 서버에서 명시적으로 Session 을 닫았을 경우에만 발생합니다. 서버로부터 Session 이 닫혔다는 메시지를 받으면 모든 Transport 의 연결을 끊고 kClosed 이벤트를 전달하게 됩니다.

Session 이벤트는 FunapiSession 의 SessionEventCallback 을 등록해서 받을 수 있습니다.

38.6.3. 재연결

서버와의 연결을 종료한 후 재연결을 할 때에는 FunapiSession.Connect 함수를 사용합니다. 연결할 Transport 의 프로토콜 타입만 파라미터로 전달하면 됩니다.

public void Connect (TransportProtocol protocol)

TransportOption 파라미터가 포함 된 Connect 함수를 사용해서 재연결을 할 수도 있지만 한 번 등록된 Transport 의 옵션은 변경할 수 없으며 새로운 TransportOption 을 전달할 경우에는 기존의 Transport 를 삭제하고 새로운 Transport 를 생성해서 연결하게 됩니다. 동일한 프로토콜에 같은 옵션으로 재연결시에는 프로토콜만 전달하는 Connect 함수를 호출하는 것이 좋습니다.

void onTransportEvent (TransportProtocol protocol, TransportEventType type)
{
    if (type == TransportEventType.kStopped)
    {
        // 연결이 끊겼을 경우
        if (hadDisconnect)
        {
            // 동일한 프로토콜에 같은 옵션으로 재연결 시도
            session.Connect(protocol);
        }
    }
}

void onTransportError (TransportProtocol protocol, TransportError error)
{
    if (error.type == TransportError.Type.kDisconnected)
    {
        hadDisconnect = true;
    }
    else
    {
        // 연결 끊김 이외의 오류가 발생했을 경우 연결을 종료함
        session.Stop();
    }
}

38.6.4. AutoReconnect 옵션

TCP 의 경우 연결이 끊겼을 때 자동으로 지속해서 재연결을 시도하게 할 수 있는데 TcpTransportOption 의 AutoReconnect 옵션 값을 true로 입력하면 됩니다. AutoReconnect 옵션이 true일 경우 연결이 종료된 것을 감지했을 때 항상 재연결을 시도합니다.

재연결을 시작할 때 Transport 의 이벤트인 kReconnecting 이벤트가 호출되며 연결에 실패할 경우 kStopped 이벤트가 호출됩니다.

void onTransportEvent (TransportProtocol protocol, TransportEventType type)
{
    if (type == TransportEventType.kReconnecting)
    {
        // 재연결 시작시 호출됩니다.
        // 연결이 끊겨서 직접 Connect를 호출할 때에는 발생하지 않습니다.
        // AutoReconnect 옵션으로 인해 재연결을 시도할 때에만 호출됩니다.
    }
}

재연결 시도에 타임아웃을 설정하고 싶다면 Transport 옵션의 ConnectionTimeout 값 (초단위) 을 지정하면 됩니다. 재연결 시도 도중 ConnectionTimeout 으로 지정한 시간을 초과하면 재연결 시도가 중단되고 kStopped 이벤트가 호출됩니다.

재연결 시도 도중 연결을 종료하고 싶다면 아무때나 원하는 시점에 Stop() 함수를 호출하면 됩니다.

Note

버전 247 이전의 플러그인 에서 AutoReconnect 옵션은 첫 연결시에만 연결 실패시 20초 동안 4-5회 가량 재연결을 시도하는 기능이었으나 효용성이 없어 해당 옵션이 true일 경우 연결이 종료된 것을 감지하면 항상 재연결을 시도 하는 것으로 변경되었습니다.

재연결을 시작할 때 Transport 이벤트인 kReconnecting 이벤트가 발생 하며 기존에 연결 종료시 발생하던 다양한 타입의 Transport 이벤트는 kStopped 하나로 통일 되었습니다.

38.7. Ping 사용하기

네트워크의 연결 상태를 확인하기 위해 터미널에서 사용하는 ping 이라는 명령어가 있습니다. 플러그인에도 이와 비슷하게 동작하는 Ping 기능이 있습니다. 서버와의 연결이 유지되고 있는지 확인하는 용도로 사용할 수 있습니다.

Ping 기능을 사용할 경우 주기적으로 서버와의 연결을 체크해서 일정 시간 이상 서버에서 응답이 없을 경우 연결이 끊기고 kStopped 이벤트가 발생합니다.

예측하기 힘든 모바일 네트워크 환경에서 Ping을 사용하여 지속적으로 연결 상태를 검사할 수 있습니다. 이를 통하여 연결이 끊겼는지를 좀 더 빠르게 판단하고 대응할 수 있습니다.

Tip

이 기능은 TCP 프로토콜에서만 사용 가능합니다.

38.7.1. Ping 설정

Ping은 FunapiSession.Connect 함수를 호출하기 전에 Transport 옵션으로 설정할 수 있습니다. Ping 기능을 사용하려면 아래와 같이 SetPing 함수를 호출하거나 각각의 값을 입력해주면 됩니다.

TcpTransportOption tcp_option = new TcpTransportOption();

// 이 함수만 호출하면 Ping 설정이 완료됩니다.
// 파라미터는 Ping 간격, Timeout, 로그 출력 여부 입니다.
tcp_option.SetPing(3, 20, true);

...

// 각각의 값을 따로 입력하고 싶다면 아래와 같이 하면 됩니다.

// 이 값을 true로 주면 Ping 기능을 사용할 수 있습니다.
// EnablePing의 기본 값은 false 입니다.
tcp_option.EnablePing = true;
// Ping 값을 로그로 보고 싶다면 이 값을 true로 주면 됩니다.
tcp_option.EnablePingLog = true;
// Ping 메시지를 보내는 간격을 지정합니다. (초단위)
tcp_option.PingIntervalSeconds = 3;
// 서버로부터 Ping 응답을 기다리는 최대 시간을 지정합니다. (초단위)
// 이 시간 이내에 응답이 하나도 없을 경우 연결을 끊고 Disconnect 처리를 하게 됩니다.
tcp_option.PingTimeoutSeconds = 20;

Ping 타임아웃 시간 내에 서버로부터 응답이 하나도 없을 경우 연결을 끊고 Disconnect 처리를 하게 됩니다. Disconnect 되면 FunapiSession의 TransportEventCallback으로 kStopped가 전달됩니다.

서버에서 Ping 기능을 활성화 하는 법은 세션 Ping(RTT) 에서 확인할 수 있습니다.

38.8. 서버간 이동

서버의 요청에따라 접속 중인 서버의 연결을 끊고 새로운 서버로 접속해야 하는 경우가 있을 수 있습니다. 이 과정은 플러그인 내부에서 이루어지며 서버 쪽의 요청으로 이루어지는 동작이므로 클라이언트에서 서버에 이동을 요청할 수는 없습니다.

38.8.1. 서버간 이동 상태 확인

서버간 이동이 시작되면 SessionEventCallback 을 통해서 서버간 이동에 관한 진행 상태가 전달됩니다. 아래는 콜백을 통해서 받게 되는 이벤트 타입의 종류입니다.

public enum SessionEventType
{
    ...
    kRedirectStarted,       // 서버간 이동을 시작합니다.
    kRedirectSucceeded,     // 서버간 이동을 완료했습니다.
    kRedirectFailed         // 서버간 이동에 실패했습니다.
};

서버간 이동이 시작되면 kRedirectStarted 이벤트를 전달한 후 현재 연결된 서버와의 접속을 종료하고 새로운 서버로 연결을 시작합니다. 서버간 이동이 완료될 때까지는 Redirect와 관련된 이벤트를 제외한 Session이나 Transport 관련 이벤트는 발생되지 않습니다. 연결 도중 오류가 발생해도 디버그 로그만 출력할 뿐 어떠한 이벤트도 발생되지 않습니다. 서버간 이동이 완료되면 kRedirectSucceededkRedirectFailed 이벤트가 전달됩니다.

38.8.2. 서버 이동 후 사용할 Transport 의 옵션

서버를 이동할 때 접속 중인 서버와 이동하는 서버 간의 프로토콜 종류나 개수가 다를 수도 있습니다. 이럴 경우 Transport를 새로 만들게 되는데 이 때 Transport의 옵션은 클라이언트에서 직접 설정해줘야 합니다. 옵션 값을 정해주지 않으면 기본 값을 사용하게 됩니다. 옵션 값을 전달하는 콜백 함수의 원형은 아래와 같습니다.

public delegate TransportOption TransportOptionHandler (string flavor, TransportProtocol protocol);
public event TransportOptionHandler TransportOptionCallback;

flavor 는 서버의 종류를 나타냅니다. 이 타입은 서버에서 정해주는 문자열 값입니다. 서버로부터 이동 메시지를 받으면 이동할 서버의 Transport를 새로 만들기 전에 이 콜백 함수를 호출합니다. 해당 서버의 종류와 프로토콜 타입에 따라 옵션을 구성해서 반환하면 됩니다.

이미 사용중이던 Transport와 동일한 옵션을 사용하거나 기본 옵션으로 연결해도 되는 경우에는 굳이 이 이벤트 콜백을 등록할 필요는 없습니다.

38.8.3. 서버 이동 중 보내는 메시지의 전송 보장

서버 이동 중에 보내는 메시지는 기본적으로 무시됩니다. 이동 중 보내는 메시지의 전송을 보장받고 싶다면 SessionOption 의 useRedirectQueue 값을 true 로 설정하면 됩니다.

public class SessionOption
{
    public bool useRedirectQueue = false;
}

useRedirectQueue 값을 true 로 설정하면 서버 이동 중 보내는 메시지를 큐에 넣어두었다가 이동이 완료된 후 순차적으로 메시지를 전송합니다.

저장된 메시지를 확인하고 싶다면 FunapiSession 의 RedirectQueueCallback 에 콜백 함수를 등록하면 됩니다. 이 콜백을 등록해두면 서버 이동이 완료된 후 저장된 메시지를 전송하기 전에 이 콜백이 호출됩니다.

public delegate void RedirectQueueCallback (TransportProtocol protocol,
                                            List<string> current_tags,
                                            List<string> target_tags,
                                            Queue<UnsentMessage> queue);

저장된 메시지의 프로토콜 별로 콜백 함수가 호출됩니다. current_tags 는 이전에 접속중이던 서버의 태그 목록이며 target_tags 는 이동한 서버의 태그 목록입니다. 이 목록에 들어 있는 값으로 서버의 종류를 구분할 수 있습니다.

콜백이 호출되었을 때 큐잉된 메시지 중 보내고 싶지 않은 메시지를 선별할 수 있습니다. discard 값을 true 로 설정해두면 해당 메시지는 전송하지 않고 버려집니다.

foreach (UnsentMessage msg in queue)
{
    if (조건)
    {
        msg.discard = true;
    }
}

서버 이동 중에 전송을 보장할 메시지를 구분해야할 경우 이 콜백을 등록해주세요.

38.9. 멀티캐스팅과 채팅

플러그인의 멀티캐스팅 기능을 사용하면 원하는 채널에 접속해서 채널의 모든 유저들과 메시지를 주고 받을 수 있습니다. FunapiSession 객체를 이용해 멀티캐스팅 역할을 하는 서버와 연결을 하고 메시지를 전송합니다. FunapiMulticastClient 객체를 만들면서 FunapiSession 객체를 전달하여 멀티캐스팅 기능을 사용할 수 있습니다.

FunapiMulticastClient 는 같은 공간(채널)에 있는 모든 유저에게 메시지를 보내고자 할 때 사용할 수 있습니다. _bounce 옵션을 true 로 주면 보내는 주체인 ‘나’도 그 메시지를 받을 수 있습니다.

채팅에 특화된 클래스인 FunapiChatClient 는 FunapiMulticastClient 를 상속받아 만들어진 파생 클래스입니다. FunapiChatClient 는 텍스트만 주고 받는 클래스로 플레이어간 채팅에 사용하기 적합합니다.

멀티캐스팅은 TCP 를 사용 하며, Encoding 은 연결된 FunapiSession에서 사용하는 방식을 따릅니다. JSON과 Protobuf 를 사용할 수 있습니다.

Important

멀티캐스팅은 하나의 Session마다 하나의 객체만 연결할 수 있습니다. 두 개 이상의 멀티캐스팅 객체를 연결하게 되면 Session에서 멀티캐스트 메시지를 받았을 때 어느 객체에게 메시지를 전달해야 하는지 알 수 없습니다. FunapiMulticastClient 클래스와 FunapiChatClient 클래스도 역시 동시에 두 개를 같은 Session으로 연결할 수 없습니다.

38.9.1. 멀티캐스팅 인터페이스

FunapiMulticastClient 에는 다음과 같은 인터페이스가 존재합니다.

// FunapiMulticastClient 생성자입니다.
// FunapiSession와 주고 받을 메시지의 Encoding을 입력받습니다.
public FunapiMulticastClient (FunapiSession session, FunEncoding encoding);

// FunapiMulticastClient에서 사용하던 리소스를 반환합니다.
public void Clear ();

// 내 id(name)입니다.
// sender를 지정하면 채널 입/퇴장이나 메시지를 보낼 때 sender를 함께 전송합니다.
// 나를 포함한 채널의 모든 유저가 해당 메시지를 보낸 이를 확인할 수 있습니다.
public string sender { set; }

// 사용 중인 인코딩 타입입니다.
public FunEncoding encoding;

// 서버와 연결되어 있는지 확인하기 위한 property 입니다.
// 서버와 연결이 되어 있다면 true를 반환합니다.
public bool Connected;

// 채널 목록을 요청하는 함수입니다.
// 채널 목록과 함께 채널에 있는 유저 수도 함께 전달됩니다.
// 요청에 성공하면 ChannelListCallback에 등록한 함수가 호출됩니다.
public void RequestChannelList ();

// channel_id 가 입장한 채널의 id 인지 확인하는 property 입니다.
// 입장한 채널이면 true를 반환합니다.
public bool InChannel (string channel_id);

// 채널에 입장할 때 사용하는 함수입니다.
// channel_id 는 입장할 channel id 입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
// handler는 메시지를 전달받으면 호출되는 함수입니다.
// 이미 channel_id에 해당하는 채널에 입장했거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
public bool JoinChannel (string channel_id, ChannelMessage handler);

// 기능은 위의 JoinChannel 함수와 동일합니다. 토큰을 포함해서 보낼 때 사용합니다.
public bool JoinChannel (string channel_id, string token, ChannelMessage handler);

// 채널을 나갈 때 사용하는 함수입니다.
// channel_id는 나갈 channel id 입니다.
// 입장한 채널이 없거나 서버와 연결되어 있지 않으면 false 를 반환합니다.
public bool LeaveChannel (string channel_id);

// 입장해 있는 모든 채널에서 나갈 때 사용하는 함수입니다.
public void LeaveAllChannels ();

// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// 이 함수는 protobuf로 메시지를 전송할 때 사용합니다.
public bool SendToChannel (FunMulticastMessage mcast_msg);

// 이 함수는 json으로 메시지를 전송할 때 사용합니다.
public bool SendToChannel (object json_msg);


// 채널 목록을 받으면 호출되는 Handler의 delegate 입니다.
public delegate void ChannelList(object channel_list);

// 채널에 입/퇴장을 알려주는 Handler의 delegate 입니다.
// 채널의 입/퇴장 알림을 받고 싶다면 ChannelNotify의 event 객체인
// JoinedCallback이나 LeftCallback을 등록하면 됩니다.
public delegate void ChannelNotify(string channel_id, string sender);

// 채널로부터 메시지를 받으면 호출되는 Handler의 delegate 입니다.
// 이 delegate와 동일한 타입으로 함수를 만들고 JoinChannel() 함수의 두 번째 인자로 전달합니다.
// channel_id 는 메시지를 전달받은 채널 id 입니다.
// body는 전달받은 메시지이며 Encoding에 따라서 JSON이나 Protobuf인 FunMulticastMessage가 전달됩니다.
public delegate void ChannelMessage(string channel_id, string sender, object body);

38.9.2. 멀티캐스팅 예제

FunapiMulticastClient 로 멀티캐스팅을 처리하기 위한 사전 준비로서, 먼저 FunapiSession 을 만들고 Tcp로 Connect 합니다.

string kServerIp = "127.0.0.1";

// 우선 FunapiSession 을 만듭니다.
// 멀티캐스팅은 session reliability 와 상관없이 동작합니다.
// 예제의 단순함을 위해 여기서는 이 기능을 사용하지 않겠습니다.
session = FunapiSession.Create(kServerIp, false);

// Transport의 이벤트를 받기 위한 콜백함수를 등록합니다.
session.TransportEventCallback += onTransportEvent;

// 서버에 접속합니다.
// Transport의 옵션 설정은 생략하겠습니다.
session.Connect(TransportProtocol.kTcp, FunEncoding.kJson, 8012);

이제 메시지를 전송하기 위해 FunapiMulticastClient 를 만들겠습니다.

채널 입/퇴장을 알려주는 콜백 함수를 등록할 수 있습니다. 나를 포함해서 채널에 들어오고 나가는 모든 유저들의 입/퇴장을 알려줍니다.

JoinedCallback 유저가 채널에 입장하면 호출되는 함수
LeftCallback 유저가 채널에서 나가면 호출되는 함수
ErrorCallback 오류가 발생하면 에러코드를 전달해주는 함수
// FunapiMulticastClient를 만듭니다.
// 위에서 만든 session과 session과 같은 encoding 타입을 전달합니다.
FunapiMulticastClient multicast = new FunapiMulticastClient(session, FunEncoding.kJson);

// 내 아이디를 입력합니다.
multicast_.sender = "my name";

// 채널 목록을 받았을 때 호출되는 콜백입니다.
multicast_.ChannelListCallback += delegate (object channel_list) {
    onMulticastChannelList(encoding, channel_list);
};

// Player가 채널에 입장하면 호출되는 콜백입니다.
multicast_.JoinedCallback += delegate(string channel_id, string sender) {
    DebugUtils.Log("JoinedCallback called. player:{0}", sender);
};

// Player가 채널에서 퇴장하면 호출되는 콜백입니다.
multicast_.LeftCallback += delegate(string channel_id, string sender) {
    DebugUtils.Log("LeftCallback called. player:{0}", sender);
};

// 에러가 발생했을 때 알림을 받는 콜백입니다.
// 에러 종류는 enum FunMulticastMessage.ErrorCode 타입을 참고해주세요.
multicast_.ErrorCallback += delegate(FunMulticastMessage.ErrorCode code) {
    // error
};

채널에 입장하기 위해서 JoinChannel() 함수를 호출합니다. 해당 채널로부터 메시지가 전송되면 onMulticastChannelReceived 함수가 호출됩니다.

동시에 여러 개의 채널에 입장하려면 입장을 원하는 채널마다 JoinChannel() 함수를 호출하면 됩니다.

// 입장할 채널 id 를 입력합니다.
// 채널이 존재하지 않으면 JoinChannel() 함수를 호출할 때 채널이 생성됩니다.
string channel_id = "test_channel";

// 채널에 입장하기 위해 JoinChannel() 함수를 호출합니다.
// 해당 채널로부터 메시지가 전송되면 onMulticastChannelReceived 함수가 호출됩니다.
multicast.JoinChannel(channel_id, onMulticastChannelReceived);

// 채널에 메시지 전송
PbufHelloMessage hello_msg = new PbufHelloMessage();
hello_msg.message = "multicasting message";

PbufHelloMessage 는 서버의 {project}_messages.proto 파일에 존재합니다. funapi_initiator 로 처음 프로젝트를 생성하면 기본적으로 추가되어 있습니다. PbufHelloMessage는 예제를 위해 기본 생성된 것이며 MulticastServer 에서는 proto message 의 이름이나 field 를 제한하지 않습니다. 따라서 필요한 이름으로 proto message 를 생성하고 사용할 field 들을 자유롭게 정의하시면 아이펀 엔진에서는 이를 알아서 클라이언트에게 전송합니다. 이것은 JSON 도 마찬가지입니다.

아래는 서버의 {project}_messages.proto 파일에 있는 기본 내용입니다.

message PbufEchoMessage {
  required string msg = 1;
}

message PbufAnotherMessage {
  optional string msg = 1;
}

extend FunMessage {
  optional PbufEchoMessage pbuf_echo = 16;
  optional PbufAnotherMessage pbuf_another = 17;
}


import "funapi/service/multicast_message.proto";

message PbufHelloMessage
{
  optional string message = 1;
}
extend FunMulticastMessage
{
  optional PbufHelloMessage pbuf_hello = 16;
}

아래 onMulticastChannelReceived 의 protobuf 예제 코드에서 MulticastMessageType.pbuf_hello 라는 메세지는, 플러그인에 기본적으로 추가되어 있지는 않습니다. 서버를 빌드할 때 proto 파일도 함께 빌드할 수 있는데 이때 extend FunMulticastMessage 에 입력한 PBufHelloMessage 의 field 이름과 extend 번호를 자동으로 인식하여 enum 을 만들게 됩니다. 이에 대한 자세한 설명은 Unity에서 protobuf 사용하기 를 참고해주세요.

private void onMulticastChannelReceived (string channel_id, string sender, object body)
{
    if (multicast_.encoding == FunEncoding.kJson)
    {
        // 채널이 맞는지 확인합니다.
        string channel = FunapiMessage.JsonHelper.GetStringField(body, "_channel");
        FunDebug.Assert(channel != null && channel == channel_id);

        // 메시지를 전송할 때 "message" 필드에 전송할 메시지를 담았습니다.
        // 따라서 수신한 메시지는 mcast_msg["message"] 에 있습니다.
        string message = FunapiMessage.JsonHelper.GetStringField(body, "_message");

        // 여기서는 간단하게 로그를 출력하겠습니다.
        DebugUtils.Log("Received a multicast message from the '{0}' channel.\nMessage: {1}",
                       channel_id, message);
    }
    else
    {
        // 수신한 메시지를 Protobuf인 FunMulticastMessage로 처리합니다.
        FunMulticastMessage mcast_msg = body as FunMulticastMessage;

        // MulticastMessageType 은 서버를 빌드할 때 proto 파일도 함께 빌드하면 자동 생성됩니다.
        // 이 예제에서는 PbufHelloMessage를 사용하므로 이것의 field 이름인 pbuf_hello를 사용합니다.
        PbufHelloMessage hello_msg = FunapiMessage.GetMulticastMessage<PbufHelloMessage>(mcast_msg, MulticastMessageType.pbuf_hello);
        if (hello_msg == null)
            return;

        // 메시지를 전송할 때 PbufHelloMessage의 message field에 메시지를 저장합니다.
        // 따라서 hello_msg.message 에 전달된 메시지가 담겨 있습니다.
        DebugUtils.Log("Received a multicast message from the '{0}' channel.\nMessage: {1}",
                       channel_id, hello_msg.message);
    }
}

메시지를 전송하려면 SendToChannel() 함수를 호출합니다.

// channel id를 입력합니다.
string channel_id = "test_channel";

if (multicast_.encoding == FunEncoding.kJson)
{
    // JSON으로 메시지를 전송합니다.
    Dictionary<string, object> mcast_msg = new Dictionary<string, object>();

    // _channel 필드에 channel id를 입력합니다.
    mcast_msg["_channel"] = channel_id;

    // 보낸 메시지를 나도 받고 싶다면 _bounce 필드값을 true로 설정합니다.
    mcast_msg["_bounce"] = true;

    // 메시지를 mcast_msg["message"] 에 담겠습니다.
    // 이는 자유롭게 변경하셔도 되며 필요하다면 다른 필드를 추가하셔도 됩니다.
    mcast_msg["message"] = "multicast test message";

    // 다른 필드 추가
    // mcast_msg["something"] = "...";

    // 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
    multicast.SendToChannel(mcast_msg);
}
else
{
    // protobuf로 메시지를 전송합니다.
    // PbufHelloMessage 파일은 서버의 {project}_messages.proto 파일에 정의 되어 있습니다.
    // 기본적으로 포함된 proto message인데 자유롭게 이름과 field 등을 정의하셔도 됩니다.
    PbufHelloMessage hello_msg = new PbufHelloMessage();

    // message field에 전송할 메시지를 담겠습니다.
    hello_msg.message = "multicast test message";

    // 멀티캐스팅 메시지를 전송하기 위해서 FunMulticastMessage 를 생성합니다.
    // 이 부분은 멀티캐스팅 역할을 하는 서버에서 필요한 proto message 이므로 반드시 필요한 부분입니다.
    FunMulticastMessage mcast_msg = FunapiMessage.CreateMulticastMessage(hello_msg, MulticastMessageType.pbuf_hello);

    // channel field 에 channel id 를 입력합니다.
    mcast_msg.channel = channel_id;

    // 보낸 메시지를 나도 받고 싶다면 bounce 필드 값을 true로 설정합니다.
    mcast_msg.bounce = true;

    // 멀티캐스팅 역할을 하는 서버에 메시지를 전송합니다.
    multicast_.SendToChannel(mcast_msg);
}

채널에서 나갈 때는 LeaveChannel() 함수를 호출합니다.

// 채널 나가기
string channel_id = "test_channel";
multicast.LeaveChannel(channel_id);

38.9.3. 채팅 인터페이스

FunapiChatClient 에는 다음과 같은 인터페이스가 존재하며, FunapiMulticastClient 와의 차이점은 메시지를 object 객체로 받지 않고 string 만 받는다는 것입니다.

// FunapiChatClient 생성자입니다.
// FunapiSession과 주고 받을 메시지의 Encoding을 입력받습니다.
public FunapiChatClient (FunapiSession session, FunEncoding encoding);

// 채널에 입장할 때 사용하는 함수입니다.
// 이 함수를 호출하기 전에 sender에 유저 이름을 입력해야 합니다.
// chat_channel은 입장할 channel id 입니다. 존재하지 않는 채널이면 서버에서 생성합니다.
// handler는 메시지를 전달받으면 호출되는 함수입니다.
// 이미 channel_id 에 해당하는 채널에 입장했거나 서버와 연결되어 있지 않으면 false를 반환합니다.
public bool JoinChannel (string chat_channel, OnChatMessage handler);

// 기능은 위의 JoinChannel 함수와 동일합니다. 토큰을 포함해서 보낼 때 사용합니다.
public bool JoinChannel (string chat_channel, string token, OnChatMessage handler);

// 채널을 나갈 때 사용하는 함수입니다.
// 채널에서 나갈 때는 FunapiMulticastClient의 LeaveChannel 함수를 호출하면 됩니다.
public bool LeaveChannel (string channel_id);

// 채널에 메시지를 전송할 때 사용하는 함수입니다.
// chat_channel은 메시지를 전송할 channel 의 id 입니다.
// text는 전송할 메시지입니다.
public bool SendText (string chat_channel, string text);

// 그 외 FunapiMulticastClient에 public으로 선언되어 있는 함수들을 사용할 수 있습니다.
// 해당 함수들에 대한 설명은 FunapiMulticastClient 인터페이스 설명을 참고해주세요.


// 채널로부터 메시지를 받으면 호출되는 Handler의 delegate 입니다.
// 이 delegate와 동일한 타입으로 함수를 만들고 JoinChannel() 함수의 인자로 전달합니다.
public delegate void OnChatMessage(string channel_id, string sender, string text);

38.9.4. 채팅 예제

FunapiChatClient 는 멀티캐스팅 기능을 활용하여 만든 클래스로써 사용법은 FunapiMulticastClient 와 매우 유사합니다. 따라서 이곳에서는 간단하게 FunapiChatClient 의 사용법을 익히도록 하겠습니다.

예제에서는 Encoding 을 Json 으로 하겠습니다.

// FunapiSession을 생성합니다.
FunapiSession session = FunapiSession.Create(kServerIp, false);
session.TransportEventCallback += onTransportEvent;

// 서버에 연결합니다.
session.Connect(TransportProtocol.kTcp, FunEncoding.kJson, 8012);

...

// FunapiChatClient 객체를 생성합니다.
FunapiChatClient chat = new FunapiChatClient(session, FunEncoding.kJson);

// 입장하는 플레이어의 이름을 입력합니다.
chat_.sender = "my name";

// 채널 입장
string channel_id = "test_channel";
chat.JoinChannel(channel_id, onMulticastChannelReceived);

메시지를 전달받으면 onMulticastChannelReceived() 함수가 호출됩니다.

// 메시지를 전달받으면 호출됩니다.
// FunapiMulticastClient 와는 달리 encoding에 따라 message를 처리하지 않아도 됩니다.
// 메시지 보낸 사람인 sender와 전송된 메시지인 text를 처리하시면 됩니다.
private void onMulticastChannelReceived (string chat_channel, string sender, string text)
{
    // 여기서는 간단하게 로그를 출력하겠습니다.
    DebugUtils.Log("Received a chat channel message.\nChannel={0}, sender={1}, text={2}",
                   chat_channel, sender, text);
}

SendText() 함수를 이용하여 채널에 메시지를 전송합니다.

// 채널에 메시지 전송
string channel_id = "test_channel";
string message = "Hello World";
chat.SendText(channel_id, message);

채널에서 나갈 때는 LeaveChannel() 함수를 사용합니다.

// 채널 나가기
string channel_id = "test_channel";
chat.LeaveChannel(channel_id);

38.10. 공지사항 확인하기

엔진에서 제공하는 공지서버를 사용하고 있다면 클라이언트 플러그인을 통해 공지서버로부터 공지사항 목록을 받을 수 있고 언제든지 원하는 시점에 공지사항을 업데이트할 수 있습니다.

FunapiAnnouncement 클래스로 서버에 공지사항 목록을 요청할 수 있습니다. 아래는 FunapiAnnouncement 클래스를 사용해서 서버에 공지사항을 요청하고 콜백 함수에서 공지사항 목록을 파싱하는 샘플 코드입니다. 메시지는 타입은 JSON 입니다.

FunapiAnnouncement announcement = new FunapiAnnouncement();

void Init ()
{
    // 공지사항은 웹 서비스입니다.
    // 서버에서 공지사항 용도로 열어 둔 ip와 port를 주소로 요청합니다.
    announcement.Init("http://127.0.0.1:8080");
    announcement.ResultCallback += OnAnnouncementResult;

    // 이 함수를 호출하면 목록을 가져오거나 이미 있을경우 갱신해줍니다.
    announcement.UpdateList();
}

void OnAnnouncementResult (AnnounceResult result)
{
    // 결과가 Success가 아닐경우
    if (result != AnnounceResult.kSuccess)
        return;

    // 메시지는 JSON 타입이며 MiniJSON을 사용합니다.
    if (announcement.ListCount > 0)
    {
        for (int i = 0; i < announcement.ListCount; ++i)
        {
            // 각각의 목록에 대해 정보를 가져올 수 있습니다.
            Dictionary<string, object> list = announcement.GetAnnouncement(i);
            string buffer = "";

            foreach (var item in list)
                buffer += item.Key + ": " + item.Value + "\n";

            Debug.Log("announcement >> " + buffer);
        }
    }
}

공지사항에 들어가는 항목들은 정해져 있지 않습니다. 서버 관리자가 필요한 항목들을 정의해서 사용하면 됩니다. 단, 기존에 사용중인 필드들을 변경할 경우 플러그인 코드도 함께 수정해야 할 수도 있으므로 기존 필드를 변경해야 할 경우엔 Funapi support 로 문의해주시기 바랍니다.

공지사항에 이미지가 포함되어 있을 경우 공지사항을 다 받은 후에 순차적으로 다운로드 하여 image_url 에 들어 있는 파일을 FunapiUtils.GetLocalDataPath/ + "announce/" 경로 아래에 저장합니다. 모든 다운로드가 끝나면 OnAnnouncementResult 콜백 함수가 호출됩니다. 다운로드 할 이미지가 많을 경우 콜백 함수 호출까지 시간이 오래걸릴 수도 있습니다.

38.11. 서버 점검 메시지

서버가 점검 중이라면 클라이언트에서 보낸 메시지는 무시되고 서버는 클라이언트로부터 메시지를 받을 때마다 점검 안내 메시지를 보냅니다. 서버 연결 후 클라이언트가 아무런 메시지도 보내지 않는다면 서버도 점검 안내 메시지를 보내지 않습니다.

점검 안내 메시지 처리는 메시지 수신 콜백이 아닌, FunapiSession 의 MaintenanceCallback 함수로 전달됩니다. 서버 점검 메시지를 처리하고 싶다면 MaintenanceCallback 함수를 등록하고 콜백 함수에서 필요한 작업을 수행하면 됩니다.

// 서버로부터 점검 안내 메시지를 받았을 때 이 콜백함수가 호출됩니다.
session.MaintenanceCallback += onMaintenanceMessage;

서버 점검 메시지를 받으면 아래와 같은 방식으로 파싱해서 사용하면 됩니다. 메시지 내용은 JSON 타입으로 점검 시작 일시, 종료 일시, 메시지 로 이루어져 있습니다.

date_start string 서버 점검 시작 일시
date_end string 서버 점검 종료 일시
messages string 메시지
if (encoding == FunEncoding.kJson)
{
  JsonAccessor json_helper = FunapiMessage.JsonHelper;
  FunDebug.Log("Maintenance message\nstart: {0}\nend: {1}\nmessage: {2}",
               json_helper.GetStringField(message, "date_start"),
               json_helper.GetStringField(message, "date_end"),
               json_helper.GetStringField(message, "messages"));
}
else if (encoding == FunEncoding.kProtobuf)
{
  FunMessage msg = message as FunMessage;
  MaintenanceMessage maintenance = FunapiMessage.GetMessage<MaintenanceMessage>(msg, MessageType.pbuf_maintenance);
  if (maintenance == null)
    return;

  FunDebug.Log("Maintenance message\nstart: {0}\nend: {1}\nmessage: {2}",
               maintenance.date_start, maintenance.date_end, maintenance.messages);
}

38.12. 리소스 파일 다운로드

서버에서 클라이언트 리소스 서비스를 사용한다면 클라이언트에서 별도의 바이너리 업데이트 없이도 클라이언트의 리소스를 업데이트할 수 있습니다.

38.12.1. FunapiHttpDownloader 클래스

이 기능을 사용하려면 FunapiHttpDownloader 클래스를 사용해야 합니다. FunapiHttpDownloader 클래스가 갖고 있는 함수와 프로퍼티는 아래와 같습니다.

// 다운받을 리소스 파일들의 목록을 요청합니다.
// 파라미터는 서버 주소와 포트, HTTPS 여부, 저장될 폴더의 경로를 지정합니다.
// file_path는 특정 리소스 목록을 받을 때 사용합니다.
public void GetDownloadList (string hostname_or_ip, UInt16 port, bool https,
                             string target_path, string file_path = "");

// 다운받을 리소스 파일들의 목록을 요청합니다.
// 파라미터는 서버 URL과 파일이 저장될 폴더의 경로를 지정합니다.
public void GetDownloadList (string url, string target_path, string file_path = "");

// 다운로드를 시작합니다.
public void StartDownload ();

// 중지된 다운로드를 다시 시작합니다.
public void ContinueDownload ();

// 파일을 다운로드 중일 경우 다운로드를 중지합니다.
// 아직 다운로드 전 단계일 경우 실패 콜백이 호출되며 다운로드가 종료됩니다.
public void Stop ();

// 다운로드가 중지되었는지 여부
public bool IsPaused;

// 다운로드 중인지 여부
public bool IsDownloading;

// 다운로드된 파일이 저장되는 경로
public string DownloadPath;

// 다운로드를 시작하기 전에 몇 가지 확인할 수 있는 정보들이 있습니다.
// 아래의 Property들은 ReadyCallback 함수가 호출된 이후에 사용해야 합니다.
// 다운로드 받을 전체 파일 개수
public int TotalDownloadFileCount;

// 다운로드 받을 전체 파일의 크기 (bytes)
public UInt64 TotalDownloadFileSize;

// 다운로드가 완료된 파일 개수
public int CurrentDownloadFileCount;

// 다운로드가 완료된 파일의 크기 (bytes)
public UInt64 CurDownloadFileSize;

다운로드의 시작은 파일 목록을 받는 것부터 시작합니다. GetDownloadList 함수를 호출해서 리소스 목록을 받고 로컬 파일들을 확인합니다. GetDownloadList 함수를 호출하면 서버에 다운로드할 파일들의 목록을 요청합니다. 목록을 받으면 이전에 받은 파일과의 유효성을 검증하고 새로 받아야 할 파일이 무엇인지 확인합니다.

파일 확인 작업이 끝나면 ReadyCallback 함수가 호출됩니다. ReadyCallback 함수가 호출되면 새로 받아야할 총 파일의 개수와 데이터 크기를 확인할 수 있고 이 후 StartDownload 함수를 호출해야 다운로드가 시작됩니다.

새로 다운 받을 파일이 없다면 ReadyCallback 함수는 호출되지 않고 FinishedCallback 함수만 호출됩니다.

다운로드 도중의 진행상황을 확인하고 싶다면 UpdateCallback 이벤트에 콜백 함수를 등록하면 현재 다운로드 중인 파일의 정보를 주기적으로 알려줍니다. 해당 콜백으로 전달되는 정보는 파일 이름, 전체 파일 크기, 현재까지 받은 파일의 크기, 받은 파일의 Percentage 입니다.

다운로드가 완료되거나 실패했을 경우 FinishedCallback 함수가 호출됩니다.

VerifyCallback 파일마다 유효성 검사가 끝났을 때 호출됩니다.
ReadyCallback 리소스 목록을 받고 파일 유효성 검사를 마친 후 다운로드 준비가 완료됐을 때 호출됩니다.
UpdateCallback 다운로드 중인 파일의 진행상황을 알려줍니다. UI 업데이트용으로 사용하시면 좋습니다.
FinishedCallback 리소스 다운로드가 완료되면 호출됩니다.

콜백 함수 중 ReadyCallback 과 FinishedCallback 함수는 꼭 필요한 콜백이고 나머지는 필요에 따라 등록해서 사용하면 됩니다.

38.12.2. FunapiHttpDownloader 예제

아래는 FunapiHttpDownloader 를 생성하고 콜백 함수들을 등록하는 예제 코드입니다.

// FunapiHttpDownloader 객체를 생성합니다.
FunapiHttpDownloader downloader = new FunapiHttpDownloader();

// 필요한 콜백을 등록합니다.
// ReadyCallback과 FinishedCallback은 필수로 등록해야 합니다.

// 이전에 다운받은 파일들의 유효성을 검사할 때 검사가 완료된 파일의 정보를 알려줍니다.
downloader.VerifyCallback += delegate (string path)
{
    FunDebug.DebugLog2("Check file - {0}", path);
};

downloader.ReadyCallback += delegate (int total_count, UInt64 total_size)
{
    // 다운로드 준비가 완료되면 다운로드를 시작합니다.
    downloader.StartDownload();
};

downloader.UpdateCallback += delegate (string path, long bytes_received, long total_bytes, int percentage)
{
    // 다운로드 중인 각각의 파일에 대한 진행 상황을 확인할 수 있습니다.
    FunDebug.DebugLog2("Downloading - path:{0} / received:{1} / total:{2} / {3}%",
                       path, bytes_received, total_bytes, percentage);
};

downloader.FinishedCallback += delegate (DownloadResult code)
{
    if (code == DownloadResult.SUCCESS)
        // 다운로드가 완료되었습니다.
    else if (code == DownloadResult.PAUSED)
        // 다운로드가 중단되었습니다.
    else if (code == DownloadResult.FAILED)
        // 다운로드에 실패했습니다.
};

// 다운로드할 파일의 목록을 요청합니다.
// 파라미터로 서버 주소(포트를 포함한 URL)와 파일이 저장될 경로를 넘겨줍니다.
downloader.GetDownloadList("http://127.0.0.1:8020", "target/path");

38.12.3. 리소스 폴더의 특정 폴더만 다운로드

FunapiHttpDownloader의 기본 기능은 서버의 client_data 폴더에 있는 파일을 모두 다운로드 하는 것입니다. client_data 폴더 안의 특정 폴더만 다운로드 하고 싶을 경우 아래와 같은 방법으로 사용할 수 있습니다.

먼저 폴더 별로 리소스 목록을 JSON 파일로 만들어야 합니다. 아이펀 엔진이 설치되어 있고 리소스가 있는 머신에서 funapi_client_resource_generator 를 사용해서 목록을 생성합니다. 폴더가 여러 개일 경우 개수만큼 목록을 만들면 됩니다.

~$ /usr/bin/funapi_client_resource_generator --include_dirname /path/client_data/android android.json
~$ /usr/bin/funapi_client_resource_generator --include_dirname /path/client_data/windows windows.json

이 후 클라이언트에서 GetDownloadList 함수를 호출할 때 3번째 파라미터로 파일명을 전달하면 됩니다.

GetDownloadList(url, path/to/save, "windows.json");
GetDownloadList(url, path/to/save, "android.json");

Tip

클라이언트에서 다운받을 수 있는 json 파일은 한 개이며 2개 이상 중복해서 다운로드할 경우 기존 파일이 지워질 수도 있습니다.

38.12.4. 에셋 번들 다운로드

유니티에서 사용하는 특정 에셋(모델, 텍스처, 프리팹, 오디오 클립, 씬 등)들을 묶어 하나의 파일로 만들고 실행시에 이 파일을 다운 받아 사용할 수 있습니다. 에셋 번들을 만들고 사용하는 방법은 유니티 공식 문서의 고급 개발 > 에셋 번들 항목에서 자세하게 설명하고 있습니다.

아래 링크는 Unity 2018.2 기준입니다. https://docs.unity3d.com/kr/2018.2/Manual/AssetBundlesIntro.html

에셋 번들 파일을 만든 후에는 다른 파일들처럼 다운받아 사용하시면 됩니다.

38.13. 소셜네트워크 플러그인

Funapi Social plugin을 사용하면 Facebook과 Twitter의 계정으로 로그인 및 포스팅 등을 손쉽게 할 수 있습니다. 설치 패키지 파일은 플러그인의 루트 폴더에 additional-plugins/Packages 폴더 안에 있습니다. Scripts 폴더는 패키지를 Import 했을 때 더해지는 파일들을 풀어 놓은 폴더입니다. 패키지로 설치했다면 Scripts 폴더 안의 파일은 복사하지 않아도 됩니다.

Tip

소셜네트워크 플러그인은 현재는 유니티 버전만 제공됩니다.

38.13.1. 인터페이스 클래스

Funapi Social Plugin 에는 공통으로 사용하는 인터페이스 클래스가 있습니다. SocialNetwork 라는 클래스인데 아래와 같은 인터페이스를 갖고 있습니다.

public abstract class SocialNetwork : MonoBehaviour
{
  // 친구 정보를 담고 있는 클래스입니다.
  public class UserInfo
  {
      public string id;         // 계정 아이디
      public string name;       // 계정 이름
      public string url;        // 프로필 사진 url
      public Texture2D picture; // 프로필 사진 이미지
  }

  // 초기화 함수입니다.
  // 페이스북의 경우 파라미터가 필요 없지만 트위터는 consumer key 와 consumer secret 값을 전달해야 합니다.
  public abstract void Init (params object[] param);

  // 메시지를 포스팅하는 함수입니다.
  public virtual void Post (string message)

  // 메시지와 이미지를 함께 포스팅하는 함수입니다.
  public virtual void PostWithImage (string message, byte[] image)

  // 메시지와 함수가 호출되는 시점의 화면을 함께 포스팅하는 함수입니다.
  public virtual void PostWithScreenshot (string message)

  // 친구 목록에서 해당하는 아이디의 친구 정보를 반환합니다.
  public UserInfo FindFriendInfo (string id)

  // 친구 목록에서 해당하는 인덱스의 친구 정보를 반환합니다. (인덱스는 0부터 시작)
  public UserInfo FindFriendInfo (int index)

  // 초대 목록에서 해당하는 아이디의 친구 정보를 반환합니다.
  public UserInfo FindInviteFriendInfo (string id)

  // 초대 목록에서 해당하는 인덱스의 친구 정보를 반환합니다. (인덱스는 0부터 시작)
  public UserInfo FindInviteFriendInfo (int index)

  // 내 계정의 아이디를 반환합니다.
  public string my_id;

  // 내 계정의 이름을 반환합니다.
  public string my_name;

  // 내 계정의 프로필 사진 Texture2D를 반환합니다.
  public Texture2D my_picture;

  // 친구 목록의 전체 개수를 반환합니다.
  public int friend_list_count;

  // 초대 목록의 전체 개수를 반환합니다.
  public int invite_list_count;

  // 이벤트 함수 원형
  public delegate void EventHandler (SNResultCode code);
  public delegate void PictureDownloaded (UserInfo user);

  // 초기화, 로그인, 친구 목록 가져오기, 포스팅 등의 응답에 대한 이벤트 함수입니다.
  // 콜백 호출시 파라미터로 enum SNResultCode 값이 전달됩니다.
  // 요청에 실패할 경우 SNResultCode.kError가 전달됩니다.
  public event EventHandler OnEventCallback;

  // 프로필 사진을 받으면 호출되는 이벤트 함수입니다.
  // 해당 프로필 사진의 UserInfo가 호출과 함께 전달됩니다.
  public event PictureDownloaded OnPictureDownloaded;
}

이 SocialNetwork 클래스를 상속받아 만들어진 FacebookConnector 클래스와 TwitterConnector 클래스를 사용하면 됩니다.

Note

플러그인 연동을 시작하기 전에 해당 소셜 앱을 만들어야 합니다. 페이스북은 페이스북 앱, 트위터는 트위터 앱 을 만들어 주세요. 페이스북의 경우에는 저희가 제공하는 facebook-plugin 을 설치하기 전에 Facebook SDK for Unity 도 설치해야 합니다.

38.13.2. 페이스북

플러그인 기능

Facebook SDK 의 기능을 Wrapping 해서 몇 가지 기능만을 제한적으로 제공하고 있습니다. 아래에 제공하는 기능 외에 사용하고 싶은 기능이 있다면 Funapi support 로 요청하실 수 있습니다.

  • 페이스북 계정으로 로그인
  • 내 정보와 프로필 사진 요청
  • 친구 목록 및 프로필 사진 요청

FacebookConnector 는 MonoBehaviour 객체이므로 Scene 에 미리 등록되어 있어야 합니다. 다음은 등록된 Object 에서 FacebookConnector 객체를 가져와서 초기화하는 코드입니다.

// FacebookConnector 컴포넌트를 가져옵니다.
FacebookConnector facebook = GameObject.Find("object name").GetComponent<FacebookConnector>();

// 초기화, 로그인 등의 요청에 대한 응답을 알려주는 이벤트 핸들러를 등록합니다.
facebook.OnEventCallback += OnEventHandler;

// 프로필 사진을 다운받으면 호출되는 이벤트 핸들러를 등록합니다.
facebook.OnPictureDownloaded += delegate(SocialNetwork.UserInfo user) {
    // do something with profile picture
};

// 초기화 함수를 호출합니다.
facebook.Init();

페이스북 로그인

로그인 함수는 두 종류가 있는데 읽기 권한만 필요할 때는 LogInWithRead 함수로 로그인하고 쓰기 권한까지 필요할 때는 LogInWithPublish 함수로 로그인해야 합니다.

로그인 함수의 파라미터로는 권한 목록을 전달해야 합니다. 이 권한은 페이스북 앱의 Status & Review 페이지에서 확인할 수 있는데 기본적으로 제공하는 기능이 몇 가지 있고 포스팅 등의 대부분의 기능은 권한 요청을 하고 페이스북에서 승인을 해준 뒤에 앱에서 사용이 가능합니다. 유니티 에디터에서 테스트할 때는 Graph API Explorer 를 통해 테스트용 Access Token 을 얻을 수 있습니다.

// 사용자 정보와 친구 목록을 읽어올 수 있도록 LogInWithRead 함수로 로그인합니다.
facebook.LogInWithRead(new List<string>() {
    "public_profile", "email", "user_friends"});

// 친구 목록을 요청합니다. 최대 100명의 친구 목록을 가져옵니다.
facebook.RequestFriendList(100);

친구 목록을 요청할 때 자동으로 사진도 함께 요청하는데 모든 친구의 사진이 당장 필요하지 않다면 선택적으로 사진을 다운받을 수도 있습니다.

// 이 옵션을 false로 주면 친구 목록을 요청할 때 프로필 사진을 같이 요청하지 않습니다.
// 이 값은 친구 목록을 요청하기 전에 설정해야 합니다. FacebookConnector 객체를 초기화 할 때 설정하는 것이 좋습니다.
facebook.auto_request_picture = false;

// 친구 목록에 있는 유저들의 프로필 사진을 요청합니다.
// 시작 인덱스 값과 최대 개수를 전달합니다.
facebook.RequestFriendPictures(0, 20);

사진을 요청하면 순차적 비동기 방식으로 사진을 다운로드합니다. 프로필 사진의 다운로드가 완료되면 PictureCallback 이벤트 함수가 호출됩니다. 사진의 다운로드가 완료됐을 때 알림을 받고 싶다면 FacebookConnector 객체를 초기화 할 때 PictureCallback 이벤트에 콜백 함수를 등록해야합니다.

38.13.3. 트위터

플러그인 기능

트위터 플러그인에서 제공하는 기능은 아래와 같습니다. 아래 기능 외에 사용하고 싶은 기능이 있다면 Funapi support 로 요청하실 수 있습니다.

  • 트위터 계정으로 로그인
  • 내 정보와 프로필 사진 요청
  • 친구 목록 및 프로필 사진 요청
  • 내 트위터에 글 올리기

TwitterConnector 는 MonoBehaviour 객체이므로 Scene 에 미리 등록되어 있어야 합니다. 다음은 등록된 Object 에서 TwitterConnector 객체를 가져와서 초기화하는 코드입니다.

// TwitterConnector 컴포넌트를 가져옵니다.
TwitterConnector twitter = GameObject.Find("object name").GetComponent<TwitterConnector>();

// 초기화, 로그인, 포스팅 등의 요청에 대한 응답을 알려주는 이벤트 핸들러를 등록합니다.
twitter.OnEventCallback += OnEventHandler;

// 프로필 사진을 다운받으면 호출되는 이벤트 핸들러를 등록합니다.
twitter.OnPictureDownloaded += delegate(SocialNetwork.UserInfo user) {
    // do something with profile picture
};

// 초기화 함수를 호출합니다.
// 트위터 앱의 Consumer Key와 Consumer Secret 값을 전달합니다.
twitter.Init("4RnU4YDXmu8vmwKW5Lgpej3Xc",
             "voDDAoaTNXj8VjuWRDhfrnCpa9pnVgpRhBJuKwjJpkg62dtEhd");

트위터 초기화는 Consumer KeyConsumer Secret 값을 전달인자로 넣어서 Init() 함수를 호출해야 합니다. 이 값은 트위터 앱 의 [Keys and Access Tokens] 탭에서 확인할 수 있습니다.

트위터 로그인

트위터는 로그인하는 방법이 상대적으로 복잡하지만 한번 로그인하고 앱을 승인한 뒤에는 처음 로그인할 때 받은 Access Token 을 계속 사용하게 되므로 이와 같은 복잡한 과정은 한번만 거치면 됩니다.

// 로그인 함수를 호출해 앱을 승인하고 PIN Code를 얻습니다.
// 웹을 통해서 얻은 PIN Code를 입력받는 UI는 클라이언트에서 구현해야 합니다.
twitter.Login();

// Access Token을 요청합니다.
// 로그인 함수를 통해 얻은 PIN Code를 파라미터로 전달합니다.
twitter.RequestAccess("pin code");

RequestAccess() 함수를 호출해 얻은 Access Token 은 영구적으로 사용 가능한 인증 값으로 이 값만 있으면 언제든지 트위터에 포스팅을 할 수 있습니다. 한 번 로그인을 한 뒤에는 이 값을 플러그인에서 암호화해서 저장해두었다가 다음 Init() 함수 호출시에 확인하고 인증과정 없이 콜백함수를 통해 로그인이 완료되었음을 알려줍니다.

로그인이 되었다면 메시지를 포스팅 할 수 있습니다. 아래와 같이 메시지를 보내면 내 트위터에 글을 올려줍니다.

twitter.Post("Funapi plugin test message~");

38.14. Unity 플러그인

Unity client plugin은 GitHub 를 통해 받으실 수 있습니다. 전체 암호화가 포함된 버전이 필요하시면 Funapi support 로 요청 하셔야합니다.

GitHub 에서 받은 소스 코드에서 Assets 폴더의 파일들 중 Funapi, Plugins 폴더를 플러그인을 사용할 프로젝트의 Assets 폴더로 복사하면 됩니다. HTTPS나 WSS를 사용한다면 Editor와 Resources 폴더도 함께 복사해 주세요. Sample 폴더에는 플러그인의 예제 파일들이 있습니다.

38.14.1. Unity에서 protobuf 사용하기

iOS / Android의 제약사항 때문에 protobuf-net 으로 생성한 C# 코드를 바로 사용할 수는 없습니다. protobuf.NET 이 생성하는 C# 파일을 바로 사용하면 메시지를 읽는 some_protobuf_message.GetExtension(...) 형태의 코드에서, 아래와 유사한 오류 메시지를 남기면서 libmono 의 JIT 컴파일러 안에서 크래시 합니다.

F/mono  (1021): * Assertion at mini-arm.c:2595, condition `pdata.found == 1' not met
F/libc  (1021): Fatal signal 11 (SIGSEGV) at 0x0000600d (code=-6), thread 1033 (UnityMain)

따라서 메시지 serialize/deserialize 하는 부분을 C# 코드가 아닌 AOT 빌드한 .dll 을 사용하도록 변경해야 합니다.

Protobuf 용 .dll 빌드하게 설정하기

게임 서버의 가장 상위 디렉터리에 있는 CMakeLists.txt 를 보면 다음과 같이 GENERATE_UNITY_PROTOBUF_DLL 라는 설정이 있습니다. 이 값을 true 로 변경하면 빌드 디렉터리에 필요한 .dll 파일이 생성합니다.

# Source managed by git can append a build number from git commit id.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_GIT false)

# Source managed by svn can append a build number from svn info.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_SVN false)

# Generate .DLL for Unity Engine
set(GENERATE_UNITY_PROTOBUF_DLL true)

생성한 .dll 파일 복사하기

게임 서버 빌드 디렉터리, 예를 들어 hello 란 프로젝트의 디버그 설정을 빌드한 경우에는 hello-build/debug/unity_dll 디렉터리에 다음 .dll 파일이 생성됩니다.

  • protobuf-net.dll : Unity 엔진 용의 protobuf-net 파일
  • messages.dll: 엔진에서 생성한 protobuf 메시지 정의
  • FunMessageSerializer.dll: 엔진에서 생성한 protobuf 읽고/쓰는 루틴

이 파일을 복사해서 클라이언트의 Assets 디렉터리에 복사하고, Unity 에디터에서 확인하시면 됩니다. Windows 환경이라면 https://winscp.net/eng 에서 제공하는 프로그램을 사용하면 편리합니다.

Tip

개별 메시지와 메시지들의 enum 에 대한 소스 코드는 서버 빌드 디렉터리 밑의 unity_cs 디렉터리에서 messages.cs, messages_enum.cs 파일로 생성됩니다.

38.14.2. C# Runtime Test Code

플러그인 폴더 내에 플러그인 봇 테스트를 위한 C# Runtime 테스트 코드가 있습니다. 유니티에서는 동시에 열 수 있는 소켓의 최대 수에 제한이 있기 때문에, 테스트를 위한 봇 프로그램을 직접 작성하는 것을 추천합니다. 좀 더 자세한 설명은 방법2: 유니티 플러그인을 이용 문서에서 확인할 수 있습니다.

38.14.3. Unity Plugin 로그 보기

플러그인 관련 로그는 유니티 에디터나 C# Runtime 실행시에만 표시됩니다. 로그를 남기는 것 자체가 게임의 퍼포먼스에 영향을 줄 수도 있기 때문에 에디터로 실행할 때 이외에는 로그를 남기지 않도록 하고 있습니다.

// 유니티 에디터 or C# Runtime일 경우 로그 표시
#if UNITY_EDITOR || NO_UNITY

  // 디버그 로그 표시
  #define ENABLE_LOG

  // 로그 레벨, 레벨이 높아질 수록 자세한 로그를 남기지만 로그의 양은 더 많아집니다.
  // LOG_LEVEL_1 ~ LOG_LEVEL_4
  #define LOG_LEVEL_1

#endif

테스트를 위해 플러그인의 로그를 항상 보고 싶다면 Funapi/DebugUtils.cs 파일의 ENABLE_LOG 가 항상 정의되도록 수정하거나 빌드 세팅에서 define symbol 을 추가하면 됩니다. 빌드 세팅의 Other Settings 탭에서 Scripting Define Symbols 항목에 ENABLE_LOG 를 추가하면 됩니다. 추가로 LOG_LEVEL_1 ~ LOG_LEVEL_4 심볼을 정의해서 로그 수준을 설정 할 수 있습니다. LOG_LEVEL_1 이 기본값이며, 동시에 정의할 경우에는 높은 레벨이 기준이 됩니다.

38.14.4. 로그 저장하기

플러그인의 로그를 문자열이나 파일로 저장할 수 있습니다. 이 기능을 사용하려면 Funapi/DebugUtils.cs 파일에서 ENABLE_SAVE_LOG define 을 활성화하면 됩니다.

아래는 FunDebug 클래스에 있는 로그 저장과 관련된 함수와 프로퍼티들입니다.

// 이 옵션을 활성화하면 로그가 버퍼에 저장됩니다.
#define ENABLE_SAVE_LOG

public class FunDebug
{
  ...

  // 버퍼에 저장된 내용을 파일로 저장합니다. 버퍼의 크기는 1MBytes 입니다.
  // 버퍼가 꽉 차면 자동으로 파일로 저장되고 버퍼가 초기화됩니다.
  // 파일은 로컬 저장 경로의 'Data/Logs/' 폴더에 저장됩니다.
  public static void SaveLogs();

  // 저장된 로그의 크기를 반환합니다.
  public static int GetLogLength();

  // 저장된 전체 로그를 반환합니다.
  public static string GetLogString();

  // 버퍼를 초기화합니다.
  public static void ClearLogBuffer();

  // 저장된 모든 로그 파일을 삭제합니다.
  public static void RemoveAllLogFiles();
}

38.14.5. 디버그 로그 파일 만들기

유니티 플러그인을 사용해서 개발 도중 플러그인에 문제가 있다고 판단될 경우 아래와 같은 방법으로 로그를 저장해서 파일과 함께 재현 스텝을 알려주시면 감사하겠습니다.

Funapi/DebugUtils.cs 파일에서 아래 define 들을 활성화합니다.

#define ENABLE_LOG
#define LOG_LEVEL_4
#define ENABLE_SAVE_LOG

게임 종료 시점이나 재현 상황이 발생한 직후 아래 함수를 호출해서 로그를 저장합니다.

// 로그를 파일로 저장
FunDebug.SaveLogs();

에디터에서 실행할 경우 이렇게 저장된 로그는 프로젝트 폴더의 Assets 이 있는 폴더와 같은 경로에 Data/Logs 폴더에서 확인할 수 있습니다. 모바일 기기에서 테스트할 경우에는 디바이스에서 앱의 저장 폴더에 접근하는 것을 제한하고 있으므로 파일에 접근이 어려울 수 있습니다.

38.15. Unreal Engine 4 플러그인

Unreal Engine 4 plugin은 GitHub 를 통해 받으실 수 있습니다. 암호화가 포함된 버전이 필요하시면 Funapi support 로 요청 메일을 보내주십시오.

38.15.1. Editor에서 실행하기

플러그인에는 FunapiSession 테스트를 위한 간단한 샘플코드가 포함되어 있습니다. 이 액터 클래스의 객체가 언리얼 에디터에 표시가 되어야 tester 샘플맵이 오류없이 플레이가 됩니다. 처음 프로젝트를 실행하면 에디터에 funapi_tester 객체가 표시되지 않을 수 있습니다. 이 때 프로젝트를 한 번 컴파일하면 클래스 객체가 만들어집니다. 혹시 컴파일을 해도 에디터에 표시되지 않는다면, 에디터를 종료 후 프로젝트를 다시 로드해야합니다. funapi_tester 객체가 에디터의 콘텐츠 브라우저(C++ Class > funapi_plugin_ue4)에 보인다면 tester 맵을 로드한 후 플레이 버튼을 눌러 샘플 프로그램을 실행해 볼 수 있습니다.

38.15.2. 외부 라이브러리 컴파일하기

언리얼 플러그인에서는 다음과 같은 외부 라이브러리를 사용하고 있습니다.

libcurl HTTP 통신에 필요한 기능을 사용하고 있습니다.
libcrypto Openssl 라이브러리에 포함된 암호화 라이브러리. MD5를 사용하고 있습니다.
libprotobuf Google protocol buffer 라이브러리입니다.
libsodium ChaCha20 AES-128 암호화를 위한 라이브러리입니다.

위의 라이브러리를 위한 컴파일용 스크립트와 컴파일된 라이브러리 파일을 함께 배포하고 있습니다. 플러그인 폴더에 ThirdParty 폴더 안에 빌드 스크립트와 라이브러리 파일들이 플랫폼 별로 구분되어 있습니다.

ThirdParty
  ㄴ build     // 라이브러리 빌드 스크립트 파일들
  ㄴ include   // 헤더 파일들
  ㄴ lib       // 라이브러리 파일들
  ㄴ proto     // .proto 파일과 proto 파일용 컴파일 스크립트

배포된 컴파일용 스크립트로 라이브러리를 직접 빌드해서 사용해도 되지만 특별히 라이브러리를 새로 빌드해야 할 이유가 있는 것이 아니라면 배포된 라이브러리 파일을 그대로 사용할 것을 권장합니다.

38.15.3. Protobuf 파일 빌드하기

.proto 파일을 빌드하려면 ThirdParty/proto/make-win.bat 파일이나 make-mac.sh 파일을 실행하면 됩니다. .proto 파일을 빌드 목록에 추가하려면 배치 파일을 열어서 하단 파일 목록에 추가해주면 됩니다. 배치 파일을 실행하면 결과 파일이 Source 폴더 안에 funapi 폴더에 추가/수정됩니다.

38.16. Cocos2d-x 플러그인

Cocos2d-x client plugin은 GitHub 를 통해 받으실 수 있습니다. 암호화가 포함된 버전이 필요하시면 Funapi support 로 요청 메일을 보내주십시오.

38.17. 버그 신고

Client plugin 에 대한 건의사항이나 버그 신고는 Funapi support 로 메일을 보내주십시오.