This document is about: SERVER 4
SWITCH TO

Photon 플러그인 메뉴얼

Photon 플러그인은 Enterprise Cloud 또는 자체 호스트한 Photon Server v4에서만 사용할 수 있습니다.

Photon 4 플러그인으로 Photon 3에서 사용되었던 것을 상속을 통해 게임/룸 기능을 확장하는 새로운 방법이 도입 되었습니다.

모범 사례 및 자주 묻는 질문들에 대해서는 Photon Plugins FAQ를 참조하세요.

소개

Photon Loadbalancing 은 수년 동안 클라우드 내의 "현재" (서버 측 커스텀 코드가 없는)와 커스텀 행동으로 확장된 셀프-호스트 두 환경에서 수행되는 룸 기반 게임의 플랫폼으로써 진화 되어왔습니다.

또한 수년 동안 기능이 많이 변경 되었지만 클라이언트와 서버간 코어 흐름(게임 생성, 참여,나가기 등의 오퍼레이션)은 이전 버전과 호환 되며 안정적으로 유지 되었습니다.

Photon 3 까지 (Lite & Loadbalancing) 소스코드에 기초하며 커스터마이징되고 전형적으로 Game 클래스로 부터 상속됩니다.

유연하기는 하지만 개발시에는 약간 불안정하고 좀더 복잡합니다. 클라이언트, 게임서버와 마스터 간은 예상치 않게 연결이 끊어 질 수 있기 때문입니다. 예를 들어 클라이언트는 응답 또는 이벤트를 받기 위해서 아무것도 하지 못하는 상태로 될 수 있습니다.

플러그인 API는 코어 흐름과 유사하게 설계 되었습니다.(룸 생성, 참여, 나가기 등)

  1. 높은 유연도를 유지 시키십시오: 처리 전후에 코어 흐름안으로 들어가는 것을 허용 하세요.
  2. 흐름을 깨는 상황을 최소한으로 해주세요: 오류를 빠르게 클라이언트와 서버에게 제공 하세요.
  3. 락을 해제(Lock free)하는 코드 사용을 허용하세요: 플러그인 인스턴스는 특정시기에 하나 이상의 호출을 할 수 없고 프레임워크는 Photon 메시지 전송 아키텍쳐(fibers) 하위에 HTTP 클라이언트와 타이머를 통합하여 제공합니다.
  4. 복잡성을 최소화하고 사용 편의성을 증가시켜 주세요:"최소 플러그인"을 살펴 보세요.

개념

커스텀 서버 로직을 추가하기 위해서는 코드에 사전정의된 Photon Server 후크(Hooks)를 넣어 주어야 합니다. 현재 Photon 서버는 룸 이벤트에서 발생된 후크로써만 게임서버 플러그인을 지원 합니다.

의미상 Photon 플러그인은 유일한 이름을 가지고 있으며 이러한 이벤트의 콜백들을 구현 합니다. 커스텀 플러그인들은 플러그인 어셈블 이라고 불리는 DLL 파일에 컴파일되어 들어갑니다. 그리고나서 필요한 파일들은 Photon Server에 "디플로이"되거나 Enterprise Cloud에 업로드 됩니다.

설정된 플러그인 어셈블은 실행시에 각 룸 생성에서 동적으로 로드되어 집니다.

플러그인 인스턴스는 팩토리 패턴에 기반하여 생성됩니다.

플러그인 생성을 발생하는 룸은 "호스트"게임이라고 부릅니다.

후자는 직접 플러그인 액세스 할 수 있고, 둘 다 모두 동일한 라이프 사이클을 공유합니다.

웹훅은 Photon 플러그인의 좋은 예제 입니다. 웹훅 1.2의 소스코드는 플러그인 SDK 에 포함되어 있습니다. 소스를 한번 파헤쳐 보는 것이 좋습니다.

기본 흐름

Photon 후킹 메카니즘은 6단계의 흐름으로 구성되어 있습니다:

  1. 훅 호출 가로채기

콜백이 발생되면 호스트는 제어를 플러그인으로 넘겨 줍니다.

  1. [선택적] 호출 정보 변경 처리되기 전에 클라이언트/서버에 의해 전송된 요청 접근과 변경을 합니다.

  2. [선택적] 커스텀 코드 주입 호출을 처리하기 전에 호스트와 상호 작용(예, HTTP 요청 발행, 룸/액터 쿼리, 타이머 설정 등.)

  3. 훅 호출 처리 요청을 어떻게 처리 할지 결정("ICallInfo 처리 메소드"를 참고하세요)

  4. [선택적] 커스텀 코드 주입 처리가 되면 클라이언트/서버가 전송한 요청을 "읽기만 가능" 한 것으로 사용 됩니다. 플러그인은 여전히 처리된 이후에도 호스트와 상호작용을 할 수 있습니다.

  5. 리턴
    플러그인은 호스트로 제어를 되돌려 줍니다.

시작 하기

최소 플러그인

플러그인

플러그인을 작성하는데 쉽고 권장되는 방식은 모든 IGamePlugin 메소드를 직접 구현하는 것이 아닌 PluginBase 클래스를 상속 받는 것입니다. 그리고 필요한 것만 오버라이드(override) 할 수 있습니다.

PluginBase.Name 프로퍼티만 오버라이드 하여 구현 하면 가장 최소의 플러그인을 얻을 수 있습니다. 이 프로퍼티가 플러그인을 구분 해 주는 것 입니다.

"Default" 는 플러그인 이름으로 사용 할 수 없습니다.

C#

    namespace MyCompany.MyProject.HivePlugin
    {
      public class CustomPlugin : PluginBase
      {
        public override string Name
        {
            get
            {
                return "CustomPlugin"; // anything other than "Default"
            }
        }
      }
    } 

플러그인 팩토리

플러그인 팩토리 클래스는 플러그인 어셈블리의 일부로 구현되어야 합니다.

플러그인 팩토리는 각 룸에 대해서 플러그인 인스턴스의 생성에 대한 책임이 있습니다.

간결함을 위해 다음 스니팻(snippet)에서 팩토리는 기본적으로 클라이언트에서 요청된 플러그인 이름을 검토 없이 CustomPlugin 의 인스턴스를 리턴 합니다.

플러그인 인스턴스 생성을 트리거한 룸은 IPluginFactory.Create 메소드의 IPluginHost 파라미터로 전달 됩니다.

게임 내의 플러그인 자체로 참조를 유지 하기 위해서 동일한 파라미터는 IGamePlugin.SetupInstance 로 전달 되어야 합니다.

IPluginHost 는 룸 데이터와 오퍼레이션에 접근을 제공 합니다.

C#

    namespace MyCompany.MyProject.HivePlugin
    {
      public class PluginFactory : IPluginFactory
      {
        public IGamePlugin Create(
              IPluginHost host,
              string pluginName, // name of plugin requested by client
              Dictionary<string, string> config, // plugin settings
              out string errorMsg)
        {
            var plugin = new CustomPlugin();
            if (plugin.SetupInstance(host, config, out errorMsg))
            {
                return plugin;
            }
            return null;
        }
      }
    } 

설정

Photon Enterprise Cloud:

새로운 플러그인을 추가하려면:

  1. 지원되고 있는 제품 유형 중 하나의 관리화면으로 이동합니다.
  2. 나열되어 있는 Photon 어플리케이션 하나의 관리 페이지로 이동 합니다.
  3. 페이지 하단의 "Create a new Plugin(신규 플러그인 생성)" 버튼을 클릭 합니다.
  4. 문자열 형태의 키/값 엔트리를 추가하여 플러그인을 설정 합니다. 문자열의 키/값을 정의 하면 설정이 완료 됩니다. 각 문자열의 최대 길이는 256 문자 입니다. 필요한 설정들은 다음과 같습니다:
  • AssemblyName: 플러그인을 포함하고 있는 업로드된 DLL 파일의 전체 이름.
  • Version: 플러그인의 버전. Photon이 제공하는 PowerShell 스크립트를 이용한 플러그인 파일들을 업로드 할 때 사용되거나 리턴 되는 동일한 문자열
  • Path: 어셈블리 파일의 경로. 다음의 형식으로 되어야 합니다: "{customerName}\{pluginName}".
  • Type: 사용되는 PluginFactory 클래스의 전체이름. 다음의 형식이어야 합니다: "{plugins namespace}.{pluginfactory class name}".

Photon Server:

GameServer 어플리케이션의 "Photon.LoadBalancing.dll.config" 파일안에 있는 XML 노드에 다음의 코드를 추가 합니다. <Plugin.../> 엘리먼트에 필요한 속성(attribute)들이 예제에 표시되어 있습니다. 이외에 다른 속성들은 선택 사항 입니다.

<PluginSettings> 요소(element) 내의 Enabled 속성을 변경하여 플러그인의 활성 여부를 쉽게 변경 할 수 있습니다.

XML

    <PluginSettings Enabled="true">
        <Plugins>
          <Plugin
              Name="{pluginName}"
              AssemblyName="{filename}.dll" 
              Type="{namespace}.{pluginfactory class name}"
          />
        </Plugins>
     </PluginSettings>

Factory(팩토리)

우리의 플러그인 모델은 팩토리 디자인 패턴을 사용합니다. 플러그인은 필요에 따라 이름으로 인스턴스가 생성됩니다. 단일 Photon 플러그인 어셈블리에는 다수 플러그인 클래스들과 하나의 액티브 "PluginFactory"가 포함되어 있을 수 있습니다.

가능은 할지라도 하나 이상의 "PluginFactory" 를 만들어 사용하는 특별한 경우는 없습니다. 반대로 여러개의 플러그인 클래스를 작성하는 것은 매우 유용 합니다.

예를들어, 게임 유형(또는 모드 또는 난이도 등)별로 플러그인을 가질 수 있습니다.

클라이언트는 roomOptions.Plugins 를 사용하여 플러그인 설정을 요청하여 룸을 생성 합니다. roomOptions.Pluginsstring[] 형 으로써 첫번째 문자열(roomOptions.Plugins[0])은 팩토리에 전달되는 플러그인 이름 이어야 합니다.

예제:
roomOptions.Plugins = new string[] { "NameOfYourPlugin" };
또는
roomOptions.Plugins = new string[] { "NameOfOtherPlugin" };

만약 클라이언트가 아무것도 전송하지 않는 다면, 서버는 디폴트(아무 것도 설정되지 않았을 때)를 사용하거나 설정 되어있는 경우에는 생성시 리턴되는 플러그인 팩토리 아무 것이나 사용하게 됩니다.

PluginFactory.Create에서 리턴된 플러그인 이름이 클라이언트가 요청한 것과 일치 하지 않으면 플러그인은 언로드(unload)되고 클라이언트는 PluginMismatch (32757) 오류를 내며 생성 또는 참여에 실패합니다.

또한 현재roomOptions.Plugins는 최소한 한개의 엘리먼트(플러그인 이름 문자열)가 있어야 합니다. 하나 이상의 엘리먼트가 전송(roomOptions.Plugins.Length > 1)되면 PluginMismatch 오류가 수신되며 룸 참여 또는 생성이 실패하게 됩니다.

팩토리 내에서 다음과 같이 상응하는 플러그인을 로드 하기 위하여 이름을 사용 합니다.

C#

public class PluginFactory : IPluginFactory {
        public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg) {
            PluginBase plugin = new DefaultPlugin(); // default
            switch(pluginName){ 
                case "Default":
                    // name not allowed, throw error
                break;
                case "NameOfYourPlugin":
                    plugin = new NameOfYourPlugin();
                break;
                case "NameOfOtherPlugin":
                    plugin = new NameOfOtherPlugin();
                break;
                default:
                    //plugin = new DefaultPlugin();
                break;
            }
            if (plugin.SetupInstance(gameHost, config, out errorMsg)) {
                return plugin;
            }
            return null;
        }
    }

ICallInfo 프로세싱 메소드

플러그인의 사상은 서버가 수신된 요청을 처리하기 전 또는 후에 "일반적인" Photon 흐름을 낚아 채는 것 입니다. 요청 유형에 따라 4가지 가능한 방법 중에서 어떤 방법을 사용할지 결정 해야 합니다:

  1. Continue(): 디폴트 Photon 처리를 재개하는데 사용됩니다.
  2. Cancel(): 조용하게 프로세싱 취소에 사용됩니다. 즉 클라이언트에게 오류 및 알림 표시 없이 진행하는 것입니다. 다음들의 처리를 건너 띄는 것과 동일 합니다:
    • OnRaiseEvent 내부 호출은 수신 이벤트를 무시합니다.
    • BeforeSetProperties 내부 호출은 속성 변경을 취소 합니다.
  3. Fail(string msg, Dictionary<byte,object> errorData): 클라이언트에게 오류 응답을 리턴해주는 이후 프로세싱을 취소하는데 사용 됩니다. 클라이언트로 부터 OperationResponse.DebugMessage 내의 msg 파라미터와 OperationResponse.Parameters 에서 errorData 를 얻을 수 있습니다.
  4. Defer(): Photon 은 제어권이 되돌아 가기전에 하나의 처리 메소드가 호출되는 것을 예상합니다. 이 메소드는 처리를 연기하는 데 사용 됩니다. 나중에 다시 계속 할 수 도 있습니다. 즉 타이머의 콜백 또는 비동기 요청을 보낼 때 입니다. Outbound HTTP 섹션에서 이러한 유즈 케이스를 더 읽어 보시기 바랍니다.

노트:

  • 플러그인은 기본값으로 strict 모드 이어야 합니다(UseStrictMode = true). Strict mode 의미는 각 플러그인 콜백내에서 ICallInfo 처리 메소드들 중 하나를 호출한다는 것 입니다. 사용할 수 없는 메소드를 호출하게 되면 예외가 발생합니다.

  • IGamePlugin를 구현 한 PluginBase 의 모든 콜백들은 {ICallInfo}.Continue()` 를 호출 합니다.

  • Continue(), Fail()Cancel() 한번만 호출 되게 되어 있습니다. 다시 호출하게 되면 예외가 발생 합니다.

  • Defer()Cancel()OnRaiseEvent 또는 BeforeSetProperties 내부에서만 호출 됩니다.

  • ICallInfo 를 구현한 모든 클래스 들은 사용할 수 있을 때 클라이언트의 오리지널 오퍼레이션 요청을 노출합니다. {ICallInfo}.OperationRequest (또는 Request) 에서 오퍼레이션 코드와 파라미터들을 얻을 수 있습니다.

  • ICallInfo를 구현 한 모든 클래스들은 헬퍼 프로퍼티를 포함하고 있으며 이 프로퍼티를 통해서 오퍼레이션 요청의 처리 CallStatus 를 알 수 있습니다:

    • IsNew: 요청이 수행되지 않았는지 연기 되었는지를 나타냅니다.
    • IsProcessed: 요청이 이미 처리 되었는지를 나타냅니다 (즉, Continue 또는 Cancel 또는 Fail 메소드가 호출 된 것 입니다).
    • IsSucceeded: 요청이 성공적으로 처리 되었는지를 나타냅니다(즉, Continue 메소드가 호출된 것 입니다).
    • IsCanceld: 요청이 취소 되었는지를 나타냅니다 (즉, Cancel 메소드가 호출 된 것 입니다).
    • IsDeferred: 요청이 연기 되었는지를 나타냅니다 (즉, Defer 메소드가 호출 된 것 입니다).
    • IsFailed: 요청이 "실패" 했는지를 나타냅니다 (즉, Fail 메소드가 호출 된 것 입니다).

플러그인 콜백

Photon 서버에는 사전에 정의된 9개의 후크가 있습니다. 이러한 후크는 코드에서 명시적으로 등록할 필요가 없습니다. 기본적으로 모든 플러그인 클래스는 9개의 이벤트를 가로 챕니다. 하지만 필요에 따라 코드를 구현해야 합니다. PluginBase 를 상속받아서 필요한 콜백을 오버라이딩 하는 방식을 권장 합니다.

모든 코어 이벤트 콜백들은 특별한 ICallInfo 계약이 있습니다. 대부분의 콜백들은 클라이언트 액션에 의해서 발생 됩니다. 클라이언트가 전송한 오퍼레이션 요청은 ICallInfo.Request 에서 제공됩니다.

OnCreateGame(ICreateGameCallInfo info)

사전조건: 클라이언트가 OpCreateRoom 또는 OpJoinOrCreateRoom 또는 OpJoinRoom 을 호출 하고 룸이 Photon 서버의 메모리에서 찾을 수 없음.

Processing Method Processing Result
Continue
  • CreateGame 오퍼레이션 응답은 ReturnCode == ErrorCode.Ok와 같이 클라이언트에게 전송됩니다.
  • Join 이벤트는 SuppressRoomEvents == false 이지 않는 한 클라이언트에게 다시 전송됩니다.
  • 만약 ICreateGameCallInfo.BroadcastActorProperties == true 이고 플레이어 커스텀 프로퍼티들이 있다면 이벤트 파라미터들에 추가 됩니다.
  • 최초로 초기화 되었다면 룸 옵션들과 초기 파라미터들은 룸 상태에 할당,ActorList 에는 첫번째 액터의 디폴트 프로퍼티(UserId and NickName)가 포함되어 있어야 합니다. 만약 요청에 커스텀 액터 프로퍼티들이 포함된다면 리스트 액터 엔트리에 추가 되어야 합니다.
  • 로드 되었으면 룸 상태는 변경되지 않는 한 Photon 서버 메모리에서 마지막으로 제거되기 전과 동일 해야 합니다.
Fail CreateGame 오퍼레이션 응답은 ReturnCode == ErrorCode.PluginReportedError 오류코드와 같이 클라이언트에게 전송 됩니다.
Cancel N/A
Defer N/A
  • 노트:
    • 처리 요청전의 룸 상태는 초기화 되어 있지 않으며 내용은 기본값으로 채워져 있습니다. 이 때가 IPluginHost.SetGameState 를 호출하여 룸 상태를 외부 소스에서 읽혀져 해석되고 룸에 할당될 수 있는 유일한 시기 입니다.
    • 요청 전에 PluginHost.SetProperties 또는 PluginHost.BroadcastEvent 로 호출은 모두 무시됩니다.
    • ICreateGameCallInfo.IsJoinICreateGameCallInfo.CreateIfNotExists 를 사용하여 어떤 유형의 오퍼레이션 요청인지 파악할 수 있습니다.
Operation 메소드 IsJoin CreateIfNotExist
OpCreateRoom false false
OpJoinRoom true false
OpJoinOrCreateRoom true true

BeforeJoin(IBeforeJoinGameCallInfo info)

사전조건: 클라이언트는 OpJoinRoom 또는 OpJoinOrCreateRoom 또는 OpJoinRandomRoom 를 호출 했고 룸은 Photon 서버 메모리 내에 있습니다.

Processing Method Processing Result
Continue OnJoin 콜백을 트리거.
Fail JoinGame 오퍼레이션 응답은 ReturnCode == ErrorCode.PluginReportedError과 같이 클라이언트에게 전송됩니다.
Cancel N/A
Defer N/A
  • 노트:
    • IBeforeJoinGameCallInfo 처리 전에 이벤트를 캐시하지 않는 한 PluginHost.BroadcastEvent 를 호출하여도 참여하는 액터가 이벤트 수신을 받지는 않습니다.

OnJoin(IJoinGameCallInfo info)

사전조건: BeforeJoin 에서 IBeforeJoinGameCallInfo.Continue() 가 호출 됩니다.

Processing Method Processing Result
Continue
  • 참여가 허용 되면 참여하고 있는 액터는 기본 값(UserIdNickName)을 가지고 ActorList에 추가됩니다.
  • 요청이 커스텀 액터 속성을 가지고 있다면 역시 설정되어야 합니다.
  • JoinGame 오퍼레이션 응답은 ReturnCode == ErrorCode.Ok와 같이 클라이언트에게 전송됩니다.
  • 다른 액터들의 IJoinGameCallInfo.PublishUserId == true, UserId 는 오퍼레이션 응답에 다시 클라이언트에게로 전송됩니다.
  • Join 이벤트는 SuppressRoomEvents == false가 아니면 브로드캐스트 됩니다.
  • IJoinGameCallInfo.BroadcastActorProperties == true 이고 플레이어 커스텀 프로퍼티가 존재하면 이벤트 파라미터에 포함됩니다.
Fail
  • JoinGame 오퍼레이션 응답은 ReturnCode == ErrorCode.PluginReportedError와 같이 클라이언트에게 전송됩니다.
  • 액터의 추가는 복귀 됩니다.
Cancel N/A
Defer N/A

OnLeave(ILeaveGameCallInfo info)

사전조건: 클라이언트가 OpLeave를 호출하고 , 피어 연결해제 또는 PlayerTTL 경과시간. (액터 라이프 사이클확인 해 보세요).

Processing Method Processing Result
Continue
  • OpLeave 오퍼레이션에 의해 트리거 되었으면 ReturnCode == ErrorCode.Ok과 같이 응답은 클라이언트에게 전송됩니다.
  • Leave 이벤트는 SuppressRoomEvents == false 가 아니면 모든 액터들에게 전송됩니다.
  • ILeaveGameCallInfo.IsInactive == true의 경우이면:
    • 액터는 비활성으로 표기 됩니다.
    • 액터의 프로퍼티에 DeactivationTime가 추가됩니다.
  • ILeaveGameCallInfo.IsInactive == false 인 경우:
    • 액터와 액터의 프로퍼티들은 ActorList 에서 제거 됩니다.
    • 관련된 캐시된 이벤트들도 DeleteCacheOnLeave == true 인 경우에 제거 됩니다.
    • ActorList에 아무것도 없다면 BeforeCloseGameEmptyRoomTTL 밀리세컨드 후에 호출 됩니다.
Fail OpLeave 오퍼레이션에 의해서 트리거 되었으면, ReturnCode == ErrorCode.PluginReportedError를 포함한 응답이 클라이언트에게 전송됩니다.
Cancel N/A
Defer N/A
  • 노트:
    • PlayerTTLOpCreateRoom 또는 OpJoinOrCreateRoomOptions 을 통하여 클라이언트에 의해서 설정될 수 있습니다.
    • PluginHost.BroadcastEvent 호출 하더라도 룸을 나가는 액터들은 이벤트를 수신 하지 못합니다.

OnRaiseEvent(IRaiseEventCallInfo info)

사전조건: 클라이언트가 OpRaiseEvent를 호출 합니다.

Processing Method Processing Result
Continue
  • 캐싱 옵션이 사용되면 룸 상태의 이벤트 캐시가 변경됩니다.
  • 파라미터에 의하여 커스텀 이벤트가 전송 됩니다.
Fail RaiseEvent 오퍼레이션은 ReturnCode == ErrorCode.PluginReportedError를 포함하여 클라이언트에게 전송됩니다.
Cancel 처리를 조용하게 스킵 합니다.
Defer 처리가 연기 됩니다.
  • 노트:
    • IRaiseEventCallInfo 의 처리가 성공 했으면 클라이언트에게 아무런 응답도 전송 되지 않습니다.

BeforeSetProperties(IBeforeSetPropertiesCallInfo info)

사전조건: 클라이언트가 OpSetProperties를 호출 합니다.

Processing Method Processing Result
Continue
  • 룸 또는 액터 프로퍼티가 갱신됩니다.
  • SetProperties 오퍼레이션 응답은 ReturnCode == ErrorCode.Ok와 같이 클라이언트에게 전송됩니다.
  • PropertiesChanged 이벤트는 브로드캐스트 됩니다.
Fail SetProperties 오퍼레이션 응답은 ReturnCode == ErrorCode.PluginReportedError 과 같이 클라이언트로 전송됩니다.
Cancel 처리를 조용하게 스킵합니다.
Defer 처리가 연기 됩니다.
  • 노트:
    • 룸 또는 액터에 속한 프로퍼티들이 변경 되었는지 인지 하려면 IBeforeSetPropertiesCallInfo.Request.ActorNumber 값을 체크 합니다. 만약 0이면 룸의 프로퍼티들이 변경되려고 하는 것 입니다. 이외에는 타겟 액터 번호의 프로퍼티들이 변경될 필요가 있는 것입니다.
    • IBeforeSetPropertiesCallInfo.ActorNr 와 이전에 언급된 ActorNumber를 혼동 하지 마시기 바랍니다. IBeforeSetPropertiesCallInfo.ActorNr는 오퍼레이션 요청을 만드는 액터를 말합니다.

#### `OnSetProperties(ISetPropertiesCallInfo info)`

사전조건: BeforeSetProperties 에서 IBeforeSetPropertiesCallInfo.Continue() 가 호출 됩니다.

Processing Method Processing Result
Continue nil.
Fail 실패시에만 로그함.
Cancel N/A
Defer N/A

BeforeCloseGame(IBeforeCloseGameCallInfo info)

사전조건: 모든 피어all peers disconnected and the EmptyRoomTTL elapses.

Processing Method Processing Result
Continue triggers OnCloseGame.
Fail 실패시에만 로그함.
Cancel N/A
Defer N/A
  • 노트:
    • EmptyRoomTTLOpCreateRoom 또는 OpJoinOrCreateRoomOptions 에서 클라이언트가 설정할 수 있습니다.
    • 룸 이벤트 캐시가 변경되지 않는 한 PluginHost.BroadcastEvent 에 대한 호출은 모두 무시됩니다.

#### `OnCloseGame(ICloseGameCallInfo info)`

사전조건: BeforeCloseGame 에서 IBeforeCloseGameCallInfo.Continue() 가 호출 됩니다.

Processing Method Processing Result
Continue Photon 서버 메모리에서 룸이 제거되고 플러그인 인스턴스가 언로드 됩니다.
Fail 실패시에만 로그함.
Cancel N/A
Defer N/A
  • 노트:
    • ICloseGameCallInfo 처리 전에 룸 상태를 저장할 것인지 영원히 없앨 것인지를 선택 할 수 있습니다. webhooks 에서는 룸안에 최소 한명 이상의 비활성 액터가 있을 때만 가능 합니다.

고급 개념

액터 라이프 사이클

Peer <-> Actor <-> Room

액터는 룸 안에 있는 플레이어 입니다. 플레이어가 룸을 생성 또는 참여를 통해서 룸에 입장하게 되면 플레이어는 액터로 표현됩니다. 액터는 ActorNr 로 첫번째로 정의된 후 UserIdNickName 을 사용 합니다. 또한 액터는 커스텀 프로퍼티를 가질 수 있습니다. 만약 플레이어가 최초로 룸에 입장하게 되면 플레이어는 룸 내에서 플레이어만의 고유한 액터 번호를 부여 받습니다. 또한 새로 입장한 플레이어인 액터는 룸의 ActorsList에 추가 됩니다. 만약 플레이어가 영원히 룸을 나가면 해당 액터는 ActorsList 에서 제거 됩니다. 플레이어가 룸에서 퇴장 후에 다시 입장 하도록 허용 하려면 룸 옵션에서 조정할 수 있습니다. 이 경우에 플레이어가 방을 퇴장할 때 액터가 비활성으로 표기됩니다. 이 이벤트의 경과시간은 액터 프로퍼티 DeactivationTime 에 저장됩니다. 룸 안에서 액터들이 비활성 시간을 제한 할 수 도 있습니다. 이 기간은 룸 생성시에 PlayerTTL 옵션을 밀리세컨트 단위의 값으로 설정하여 정의 합니다. 만약 값이 음수 또는 최대 정수값과 같다면 액터들은 영원히 비활성 상태로 있을 수 있습니다. 그렇지 않으면 비활성 되어 있는 액터들은 PlayerTTLDeactivationTime 시간에 도달 하면 룸에서 제거 됩니다. 플레이어는 그때까지 룸에 다시 참여 할 수 있습니다. 그리고 플레이어가 임시적으로 룸을 다시 떠나게 되면 새로운 DeactivationTime 으로 리셋 됩니다. 따라서 재참여 하는 횟수에는 제한이 없습니다.

Photon 플러그인 SDK 는 다음의 프로퍼티를 사용하여 아무때나 모든 액터들을 얻는 방법을 제공 합니다:

  • IPluginHost.GameActorsActive 는 룸 내의 모든 액터를 포함합니다(활성 또는 비활성).
  • IPluginHost.GameActorsActive 는 현재 방에 참여하고 있는 모든 액터를 포함합니다.
  • IPluginHost.GameActorsInActive 는 룸을 떠난 모든 액터를 포함 합니다(퇴출이 아닌 액터들입니다).

플러그인으로 부터 이벤트 전송하기

Photon 플러그인 SDK 를 사용하여 룸 안에서 커스텀 이벤트를 전송 할 수 있습니다. 커스텀 이벤트 타입과 내용은 코드에 의해서 정의 됩니다. 커스텀 이벤트 코드의 값은 200 보다 작아야 합니다.

이에 대한 두 개의 오버로드 메소드가 있습니다. 비록 BroadcastEvent 이름은 브로드캐스트 이벤트일 것 같으나 필터에 기초한 멀티 캐스트를 하거나 단일 액터에게도 전송 할 수 있습니다.

  • 액터들의 특정 그룹으로 전송:

C#

void IPluginHost.BroadcastEvent(byte target, int senderActor, byte targetGroup, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

"잘 알려진" 그룹의 하나로 target 아규먼트를 설정 할 수 있습니다.:

  • 0: ReceiverGroup.Others
  • 1: ReceiverGroup.All
  • 2: ReceiverGroup.MasterClient

targetGroup 아규먼트를 사용하여 "Interest Group" 으로 수신 액터들을 필터 할 수 있습니다. 이 이벤트는 "Interest Group" 에 등록된 액터들에게만 전송됩니다. 기본값으로 모든 액터들은 "Interest Group" 0 에 등록되어 있습니다.

  • 특정 액터 리스트에게 전송:

C#

void IPluginHost.BroadcastEvent(IList<int> recieverActors, int senderActor, byte evCode, Dictionary<byte, object> data, byte cacheOp, SendParameters sendParameters = null);

Photon 이벤트들은 이벤트의 원천(전송자)으로써 액터 번호가 필요하고 액터번호를 전달하는 두가지 방법이 있습니다.

  • 액터로 가장하기: 룸에 참여한 액터(활성 또는 비활성)의 액터번호로 senderActor 아규먼트를 설정합니다.
  • 권위있는 룸 이벤트 전송: senderActor 아규먼트를 0 으로 설정합니다. 액터 번호 0은 플레이어에게 절대 부여 되지 않습니다. 따라서 이벤트의 원천이 클라이언트가 아니라는 것을 알 수 있습니다.

또한 룸 이벤트 캐시를 업데이트 하기 위하여 메소드를 사용하는 것도 가능합니다. cacheOp 파라미터를 사용하여 캐싱 옵션을 정의 할 수 있습니다. "Photon Events Caching"에 대해서 자세히 알아 보세요.

저희가 권장하는 방식으로 PluginBase 에서 플러그인 클래스를 상속 받아 룸에 참여한 모든 액터들에게 이벤트를 브로드캐스트 하기 위해 다음의 헬퍼 메소드를 사용할 수 있습니다.

C#

protected void BroadcastEvent(byte code, Dictionary<byte, object> data)
클라이언트 코드 변경 없이 플러그인으로부터 전송된 이벤트 데이터와 클라이언트에서 보낸 전송자 액터번호를 가져올 수 있게 하려면 (Dictionary<byte, object>)eventData 대신에 다음의 형태로 이벤트 구조체new Dictionary<byte, object>(){{245,eventData },{254,senderActorNr}} 데이터를 전송 해 주세요.

Outbound HTTP 호출

HttpRequest는 HTTP 요청들을 구성하기 위한 헬퍼 클래스 입니다. 이 클래스를 사용하면 필요한 AcceptContentType 헤더들을 URL 과 HTTP 메소드에 설정할 수 있습니다. 이러한 프로퍼티들의 값은 HttpWebRequest 에서 지원 해야 합니다. 추가적으로 IDictionary<string, string>HttpRequest.CustomHeaders 에 할당하여 커스텀 HTTP 헤더를 지정할 수 있습니다. MemoryStream 객체로 변환하고 HttpRequest.DataStream 에 할당하여 요청 데이터를 추가 할 수 도 있습니다. Post JSON 예제를 참고하여 어떻게 하는지 상세한 정보를 살펴 보시기 바랍니다.

Photon 에서 배제 되지만 플러그인 로직에서는 중요한 두개의 프로퍼티가 있다는 것에 주목하세요.
  • Async: 응답이 수신 여부와 관계없이 룸 로직의 일반적인 처리가 인터럽트 될지 여부를 나타내는 플래그 입니다. 만약 룸 로직이 HTTP 응답에 의존하고 있다면 false 로 설정되어야 합니다.
  • UserState: Photon 서버는 각 요청과 응답의 콜백으로 전송된 객체를 저장합니다. 유즈 케이스의 한 예로 지연된 ICallInfo (OnRaiseEvent 내부 또는 BeforeSetProperties)은 나중에 (Continue() 호출)로 처리 될 수 있습니다.

HttpRequest 클래스는 다음의 서명정보가 있어야 하는 응답 콜백에 대한 레퍼런스가 있어야 합니다.

public delegate void HttpRequestCallback(IHttpResponse response, object userState).

요청 객체가 설정 되었으면 IPluginHost.HttpRequest(HttpRequest request) 를 호출하여 전송 할 수 있습니다.

유즈 케이스 예제:

**예: OnRaiseEvent 에서 Defer 와 Async 이용하기 **

이 기본예제에서는 RaiseEvent 오퍼레이션 프로세스를 HTTP 응답이 수신될 때 까지 어떻게 지연 시키는지를 보여주고 있습니다.

C#

public override void OnRaiseEvent(IRaiseEventCallInfo info) {
  HttpRequest request = new HttpRequest() {
    Callback = OnHttpResponse,
    Url = "https://requestb.in/<token>", // change URL
    Async = !WebFlags.ShouldSendSync(info.Request.WebFlags),
    UserState = info
  };
  // here you can modify the request to suit your needs
  PluginHost.HttpRequest(request);
  info.Defer();
}

응답이 수신될 때 RaiseEvent 오퍼레이션을 정상적으로 처리할 것인지 또는 중단 할 것인지를 결정 해주어야 합니다.

C#

private void OnHttpResponse(IHttpResponse response, object userState) {
  ICallInfo info = userState as ICallInfo;
  // here you can make an extra check to resume or cancel the RaiseEvent operation
  if (info.IsDeferred) {
    info.Continue();
  }
}

예제: JSON 전송<!--

C#

void PostJson(string url, HttpRequestCallback callback, string json) {
  var stream = new MemoryStream();
  var data = Encoding.UTF8.GetBytes(json);
  stream.Write(data, 0, data.Length);
  HttpRequest request = new HttpRequest() {
    Callback = callback,
    Url = url,
    DataStream = stream,
    Method = "POST",
    ContentType = "application/json"
  };
  // here you can modify the request to suit your needs
  PluginHost.HttpRequest(request);
}

예제: querystring 전송<!--

C#

void HttpGet(string url, HttpRequestCallback callback, Dictionary<string, object> getParams) {
  StringBuilder sb = new StringBuilder();
  sb.AppendFormat("{0}?", url);
  foreach(var p in getParams)
  {
    sb.AppendFormat("{0}={1}&", p.Key, p.Value);
  }
  HttpRequest request = new HttpRequest() {
    Callback = callback,
    Url = sb.ToString().TrimEnd('&'),
    Method = "GET"
  };
  // here you can modify the request to suit your needs
  PluginHost.HttpRequest(request);
} 

HTTP 응답 제어하기

응답 콜백에서 가장 먼저 IHttpResponse.Status 를 체크 해야 합니다. HttpRequestQueueResult 값은 다음중 하나 입니다:

  • Success (0): 엔드포인트가 성공한 HTTP 상태코드( 2xx 코드들)로 리턴됨.
  • RequestTimeout (1): 엔드포인트가 시간내에 응답을 리턴 하지 못함.
  • QueueTimeout (2): HttpRequestQueue 내부에서 요청 타임아웃. 타이머는 요청이 큐에 들어갈때 시작합니다. 이전 쿼리가 너무 오랜시간 동안 시간을 끌게 되면 타임아웃이 발생합니다.
  • Offline (3): 어플리케이션의 개별 HttpRequestQueue 이 오프라인 모드에 있습니다. HttpRequestQueue 가 재연결에 걸리는 10초 이내에는 HttpRequest 가 발생하며 안됩니다.
  • QueueFull (4): 각각의 어플리케이션의 특정 HttpRequestQueue 임계값에 도달 했습니다.
  • Error (5): 요청 URL이 해석될 수 없거나 호스트명을 해석 할 수 없거나 종말점에 도달 할 수 없는 경우 입니다. 또한 엔드포인트가 HTTP 의 오류 상태코드(예,400:BAD REQUEST)를 리턴 할 때 발생됩니다.

결과가 Success (0) 이 아닌 경우에는 다음의 프로퍼티를 사용하여 오류의 상세 내용을 볼 수 있습니다:

  • Reason: 읽을 수 있는 오류 형식. IHttpResponse.StatusHttpRequestQueueResult.Error 와 동일한 경우에 유용합니다.
  • WebStatus: WebExceptionStatus 의 코드를 포함하고 있으며 WebException을 나타냅니다.
  • HttpCode: 리턴된 HTTP 상태코드 입니다.

어떻게 코드에서 구현하고 있는지 아래 예제에서 보여줍니다:

C#

private void OnHttpResponse(IHttpResponse response, object userState) {
  switch(response.Status){
    case HttpRequestQueueResult.Success:
      // on success logic
      break;
    case HttpRequestQueueResult.Error:
      if (response.HttpCode <= 0) {
        PluginHost.BroadcastErrorInfoEvent(
          string.Format("Error on web service level: WebExceptionStatus={0} Reason={1}", 
          (WebExceptionStatus)response.WebStatus, response.Reason));
      } else {
        PluginHost.BroadcastErrorInfoEvent(
          string.Format("Error on endpoint level: HttpCode={0} Reason={1}", 
          response.HttpCode, response.Reason));
      }
      break;
    default:
      PluginHost.BroadcastErrorInfoEvent(
        string.Format("Error on HttpQueue level: {0}", response.Status));
      break;
  }
}

편의성과 쉬운 사용을 위해서 Photon 플러그인 SDK는 HTTP 응답에서 데이터를 얻는 두가지 방식을 제공 합니다. IHttpResponse를 구현하는 클래스에는 두개의 프로퍼티로 접근할 수 있습니다.:

  • ResponseData: 응답 body의 바이트 배열. 수신된 데이터가 텍스트가 아닌 경우 유용할 수 있습니다.
  • ResponseText: 응답 body의 UTF8 스트링 버전. 수신된 데이터가 텍스트인 경우 유용 합니다.

응답 클래스에는 HttpRequest 에 상응하는 참조값을 가지고 있어 필요에 따라 사용할 수 있습니다. IHttpResponse.Request 에서 사용할 수 있습니다.

타이머

타이머는 특정 시간 이후에 호출되는 목적으로 사용할 수 있는 오브젝트 입니다. 카운트 다운은 타이머가 생성되면 자동으로 시작 합니다. 플러그인에서 주기별로 또는 코드 실행을 지연 시키는 가장 좋은 방식 입니다. Photon 플러그인 SDK 에서는 유즈 케이스에 따라 두 종류의 타이머를 제공합니다:

1회성 타이머

1회성 타이머는 특정 시간이 지난 후 에 한번 트리거 되는 메소드를 의미 합니다.

이러한 타이머를 생성하기 위해서, 2개의 아규먼트를 가지는 다음의 메소드를 오버로드 해서 사용해야 합니다: object CreateOneTimeTimer(Action callback, int dueTimeMs);

이러한 종류의 타이머는 타이머가 발생하기 전에 예정된 액션을 취소할 필요가 없으면 중지시킬 필요가 없습니다. 만약 이러한 경우가 발생 한다면 void IPluginHost.StopTimer(object timer) 를 사용 해야 합니다.

Example: delaying SetProperties<!--

C#

public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info) {
  PluginHost.CreateOneTimeTimer(
                () => info.Continue(), 
                1000);
  info.Defer();
}

반복 타이머

반복되는 타이머는 주기적으로 메소드를 호출 합니다. 첫 번째 콜백의 실행시간과 이후 반복적으로 발생하는 간격을 지정할 수 있습니다. 이러한 타이머를 생성하기 위해서는 3개의 아규먼트를 가진 다음의 메소드를 오버라이드 해서 사용해야 합니다:
object CreateTimer(Action callback, int dueTimeMs, int intervalMs);
이러한 종류의 타이머는 로드된 플러그인이 계속 수행 중에는(룸이 폐쇄되지 않음) 상응하는 메소드를 계속 호출하게 됩니다. void IPluginHost.StopTimer(object timer) 를 사용하여 언제든지 타이머를 종료 시킬 수 있습니다.

Example: scheduled events<!--

C#

private object timer;
public override void OnCreateGame(ICreateGameCallInfo info){
  info.Continue();
  timer = PluginHost.CreateTimer(
        ScheduledEvent,
        1000,
        2000);
}
private void ScheduledEvent(){
  BroadcastEvent(1, new Dictionary<byte, string>() { { (byte)245, "Time is up" } });
}
public override void BeforeCloseGame(IBeforeCloseGameCallInfo info){
  PluginHost.StopTimer(timer);
  info.Continue();
}

커스텀 타입

Photon 이 커스텀 클래스를 직렬화(serialization) 할 수 있게 하려면 클래스의 타입을 등록 해 주어야 합니다. 개별 타입에 대해서 수동으로 코드(타입)를 지정해야 하며 클래스의 필드, 프로퍼티들의 직렬화와 비직렬화 메소드를 제공 해야 합니다. 새로운 타입의 등록에 사용되는 코드는 클라이언트에서도 동일하게 사용되어야 합니다. 그리고 등록을 완료하기 위해서 다음 메소드를 호출 해 주어야 합니다.

C#

bool IPluginHost.TryRegisterType(Type type, byte typeCode, Func<object, byte[]> serializeFunction, Func<byte[], object> deserializeFunction);

예제: CustomPluginType 등록

아래는 등록할 커스텀 타입의 예 입니다.

C#

class CustomPluginType
{
  public int intField;
  public byte byteField;
  public string stringField;
}
커스텀 타입의 등록은 양측에서 수행되어야 합니다. Photon 클라이언트도 동일한 코드와 직렬화 메소드를 가진 커스텀 타입을 등록 해야 합니다.

직렬화 메소드는 커스텀 타입의 객체를 byte 배열로 변환할 수 있어야 합니다. 먼저 예상되는 타입인 (CustomPluginType)으로 객체를 캐스트 해야 한다는 것을 주의 하세요.

C#

private byte[] SerializeCustomPluginType(object o)
{
  CustomPluginType customObject = customObject as CustomPluginType;
  using (var s = new MemoryStream()){
    using (var bw = new BinaryWriter(s))
    {
      bw.Write(customObject.intField);
      bw.Write(customObject.byteField);
      bw.Write(customObject.stringField);
      return s.ToArray();
    }
  }
}

비직렬화 메소드는 반대의 경우로 수행 되어야 합니다. 비직렬화 메소드는 바이트 배열로 부터 커스텀 타입 오브젝트로 구성해 줍니다.

C#

private object DeserializeCustomPluginType(byte[] bytes)
{
  CustomPluginType customObject = new CustomPluginType();
  using (var s = new MemoryStream(bytes)){
    using (var br = new BinaryReader(s))
    {
      customObject.intField = br.ReadInt32();
      customObject.byteField = br.ReadByte();
      customObject.stringField = br.ReadString();
    }
  }
  return customobject;
}

마지막으로,CustomPluginType 을 등록 해야 합니다. SetupInstance 내에서 플러그인이 초기화 되자 마자 할 수 있습니다:

C#

public override bool SetupInstance(IPluginHost host, Dictionary<string, string> config, out string errorMsg)
{
  host.TryRegisterType(typeof (CustomPluginType), 1, 
    SerializeCustomPluginType, 
    DeserializeCustomPluginType);
  return base.SetupInstance(host, config, out errorMsg);
}

Logging

플러그인으로 부터 로그를 기록 하려면 PluginHost.LogXXX 메소드 중의 하나를 사용하세요. 기본적으로 로그 출력은 "GSGame.log" 파일에서 GameServer log 엔트리로 찾을 수 있습니다. 로그 파일을 분리하여 사용하려면 다음의 스니펫을 게임서버의 log4net 설정 파일("log4net.config")에 추가하시면 됩니다.

XML

<!-- "plugin" log file appender -->
    <appender name="PluginLogFileAppender" type="log4net.Appender.RollingFileAppender">
        <file type="log4net.Util.PatternString" value="%property{Photon:ApplicationLogPath}\\Plugins.log" />
        <param name="AppendToFile" value="true" />
        <param name="MaxSizeRollBackups" value="20" />
        <param name="MaximumFileSize" value="10MB" />
        <param name="RollingStyle" value="Size" />
        <param name="LockingModel" type="log4net.Appender.FileAppender+MinimalLock" />
        <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
        </layout>
    </appender>
<!-- CUSTOM PLUGINS:  --> 
   <logger name="Photon.Hive.HiveGame.HiveHostGame.Plugin" additivity="false">
     <level value="DEBUG" />
    <appender-ref ref="PluginLogFileAppender" />
   </logger>
Back to top