Photon 플러그인 메뉴얼
Photon 4 플러그인으로 Photon 3에서 사용되었던 것을 상속을 통해 게임/룸 기능을 확장하는 새로운 방법이 도입 되었습니다.
모범 사례 및 자주 묻는 질문들에 대해서는 Photon Plugins FAQ를 참조하세요.
소개
Photon Loadbalancing
은 수년 동안 클라우드 내의 "현재" (서버 측 커스텀 코드가 없는)와 커스텀 행동으로 확장된 셀프-호스트 두 환경에서 수행되는 룸 기반 게임의 플랫폼으로써 진화 되어왔습니다.
또한 수년 동안 기능이 많이 변경 되었지만 클라이언트와 서버간 코어 흐름(게임 생성, 참여,나가기 등의 오퍼레이션)은 이전 버전과 호환 되며 안정적으로 유지 되었습니다.
Photon 3 까지 (Lite
& Loadbalancing
) 소스코드에 기초하며 커스터마이징되고 전형적으로 Game 클래스로 부터 상속됩니다.
유연하기는 하지만 개발시에는 약간 불안정하고 좀더 복잡합니다. 클라이언트, 게임서버와 마스터 간은 예상치 않게 연결이 끊어 질 수 있기 때문입니다. 예를 들어 클라이언트는 응답 또는 이벤트를 받기 위해서 아무것도 하지 못하는 상태로 될 수 있습니다.
플러그인 API는 코어 흐름과 유사하게 설계 되었습니다.(룸 생성, 참여, 나가기 등)
- 높은 유연도를 유지 시키십시오: 처리 전후에 코어 흐름안으로 들어가는 것을 허용 하세요.
- 흐름을 깨는 상황을 최소한으로 해주세요: 오류를 빠르게 클라이언트와 서버에게 제공 하세요.
- 락을 해제(Lock free)하는 코드 사용을 허용하세요: 플러그인 인스턴스는 특정시기에 하나 이상의 호출을 할 수 없고 프레임워크는 Photon 메시지 전송 아키텍쳐(fibers) 하위에 HTTP 클라이언트와 타이머를 통합하여 제공합니다.
- 복잡성을 최소화하고 사용 편의성을 증가시켜 주세요:"최소 플러그인"을 살펴 보세요.
개념
커스텀 서버 로직을 추가하기 위해서는 코드에 사전정의된 Photon Server 후크(Hooks)를 넣어 주어야 합니다.
현재 Photon 서버는 룸 이벤트에서 발생된 후크로써만 게임서버 플러그인을 지원 합니다.
의미상 Photon 플러그인은 유일한 이름을 가지고 있으며 이러한 이벤트의 콜백들을 구현 합니다.
커스텀 플러그인들은 플러그인 어셈블 이라고 불리는 DLL 파일에 컴파일되어 들어갑니다.
그리고나서 필요한 파일들은 Photon Server에 "디플로이"되거나 Enterprise Cloud에 업로드 됩니다.
설정된 플러그인 어셈블은 실행시에 각 룸 생성에서 동적으로 로드되어 집니다.
플러그인 인스턴스는 팩토리 패턴에 기반하여 생성됩니다.
플러그인 생성을 발생하는 룸은 "호스트"게임이라고 부릅니다.
후자는 직접 플러그인 액세스 할 수 있고, 둘 다 모두 동일한 라이프 사이클을 공유합니다.
웹훅은 Photon 플러그인의 좋은 예제 입니다.
웹훅 1.2의 소스코드는 플러그인 SDK 에 포함되어 있습니다.
소스를 한번 파헤쳐 보는 것이 좋습니다.
기본 흐름
Photon 후킹 메카니즘은 6단계의 흐름으로 구성되어 있습니다:
- 훅 호출 가로채기
콜백이 발생되면 호스트는 제어를 플러그인으로 넘겨 줍니다.
[선택적] 호출 정보 변경
처리되기 전에 클라이언트/서버에 의해 전송된 요청 접근과 변경을 합니다.[선택적] 커스텀 코드 주입
호출을 처리하기 전에 호스트와 상호 작용(예, HTTP 요청 발행, 룸/액터 쿼리, 타이머 설정 등.)훅 호출 처리
요청을 어떻게 처리 할지 결정("ICallInfo 처리 메소드"를 참고하세요)[선택적] 커스텀 코드 주입
처리가 되면 클라이언트/서버가 전송한 요청을 "읽기만 가능" 한 것으로 사용 됩니다.
플러그인은 여전히 처리된 이후에도 호스트와 상호작용을 할 수 있습니다.리턴
플러그인은 호스트로 제어를 되돌려 줍니다.
시작 하기
최소 플러그인
플러그인
플러그인을 작성하는데 쉽고 권장되는 방식은 모든 IGamePlugin
메소드를 직접 구현하는 것이 아닌 PluginBase
클래스를 상속 받는 것입니다.
그리고 필요한 것만 오버라이드(override) 할 수 있습니다.
PluginBase.Name
프로퍼티만 오버라이드 하여 구현 하면 가장 최소의 플러그인을 얻을 수 있습니다. 이 프로퍼티가 플러그인을 구분 해 주는 것 입니다.
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:
새로운 플러그인을 추가하려면:
- 지원되고 있는 제품 유형 중 하나의 관리화면으로 이동합니다.
- 나열되어 있는 Photon 어플리케이션 하나의 관리 페이지로 이동 합니다.
- 페이지 하단의 "Create a new Plugin(신규 플러그인 생성)" 버튼을 클릭 합니다.
- 문자열 형태의 키/값 엔트리를 추가하여 플러그인을 설정 합니다.
문자열의 키/값을 정의 하면 설정이 완료 됩니다. 각 문자열의 최대 길이는 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.Plugins
은 string[]
형 으로써 첫번째 문자열(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가지 가능한 방법 중에서 어떤 방법을 사용할지 결정 해야 합니다:
Continue()
: 디폴트 Photon 처리를 재개하는데 사용됩니다.Cancel()
: 조용하게 프로세싱 취소에 사용됩니다. 즉 클라이언트에게 오류 및 알림 표시 없이 진행하는 것입니다. 다음들의 처리를 건너 띄는 것과 동일 합니다:OnRaiseEvent
내부 호출은 수신 이벤트를 무시합니다.BeforeSetProperties
내부 호출은 속성 변경을 취소 합니다.
Fail(string msg, Dictionary<byte,object> errorData)
: 클라이언트에게 오류 응답을 리턴해주는 이후 프로세싱을 취소하는데 사용 됩니다.
클라이언트로 부터OperationResponse.DebugMessage
내의msg
파라미터와OperationResponse.Parameters
에서errorData
를 얻을 수 있습니다.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 |
|
Fail |
CreateGame 오퍼레이션 응답은 ReturnCode == ErrorCode.PluginReportedError 오류코드와 같이 클라이언트에게 전송 됩니다. |
Cancel |
N/A |
Defer |
N/A |
- 노트:
- 처리 요청전의 룸 상태는 초기화 되어 있지 않으며 내용은 기본값으로 채워져 있습니다.
이 때가IPluginHost.SetGameState
를 호출하여 룸 상태를 외부 소스에서 읽혀져 해석되고 룸에 할당될 수 있는 유일한 시기 입니다. - 요청 전에
PluginHost.SetProperties
또는PluginHost.BroadcastEvent
로 호출은 모두 무시됩니다. ICreateGameCallInfo.IsJoin
과ICreateGameCallInfo.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 |
|
Fail |
|
Cancel |
N/A |
Defer |
N/A |
OnLeave(ILeaveGameCallInfo info)
사전조건: 클라이언트가 OpLeave
를 호출하고 , 피어 연결해제 또는 PlayerTTL
경과시간. (액터 라이프 사이클확인 해 보세요).
Processing Method | Processing Result |
---|---|
Continue |
|
Fail |
OpLeave 오퍼레이션에 의해서 트리거 되었으면, ReturnCode == ErrorCode.PluginReportedError 를 포함한 응답이 클라이언트에게 전송됩니다. |
Cancel |
N/A |
Defer |
N/A |
- 노트:
PlayerTTL
은OpCreateRoom
또는OpJoinOrCreate
의RoomOptions
을 통하여 클라이언트에 의해서 설정될 수 있습니다.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 |
|
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 |
- 노트:
EmptyRoomTTL
은OpCreateRoom
또는OpJoinOrCreate
의RoomOptions
에서 클라이언트가 설정할 수 있습니다.- 룸 이벤트 캐시가 변경되지 않는 한
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
로 첫번째로 정의된 후 UserId
와 NickName
을 사용 합니다.
또한 액터는 커스텀 프로퍼티를 가질 수 있습니다.
만약 플레이어가 최초로 룸에 입장하게 되면 플레이어는 룸 내에서 플레이어만의 고유한 액터 번호를 부여 받습니다.
또한 새로 입장한 플레이어인 액터는 룸의 ActorsList
에 추가 됩니다.
만약 플레이어가 영원히 룸을 나가면 해당 액터는 ActorsList
에서 제거 됩니다.
플레이어가 룸에서 퇴장 후에 다시 입장 하도록 허용 하려면 룸 옵션에서 조정할 수 있습니다.
이 경우에 플레이어가 방을 퇴장할 때 액터가 비활성으로 표기됩니다.
이 이벤트의 경과시간은 액터 프로퍼티 DeactivationTime
에 저장됩니다.
룸 안에서 액터들이 비활성 시간을 제한 할 수 도 있습니다.
이 기간은 룸 생성시에 PlayerTTL
옵션을 밀리세컨트 단위의 값으로 설정하여 정의 합니다.
만약 값이 음수 또는 최대 정수값과 같다면 액터들은 영원히 비활성 상태로 있을 수 있습니다.
그렇지 않으면 비활성 되어 있는 액터들은 PlayerTTL
이 DeactivationTime
시간에 도달 하면 룸에서 제거 됩니다.
플레이어는 그때까지 룸에 다시 참여 할 수 있습니다.
그리고 플레이어가 임시적으로 룸을 다시 떠나게 되면 새로운 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 요청들을 구성하기 위한 헬퍼 클래스 입니다.
이 클래스를 사용하면 필요한 Accept
와 ContentType
헤더들을 URL 과 HTTP 메소드에 설정할 수 있습니다.
이러한 프로퍼티들의 값은 HttpWebRequest 에서 지원 해야 합니다.
추가적으로 IDictionary<string, string>
을 HttpRequest.CustomHeaders
에 할당하여 커스텀 HTTP 헤더를 지정할 수 있습니다.
MemoryStream
객체로 변환하고 HttpRequest.DataStream
에 할당하여 요청 데이터를 추가 할 수 도 있습니다.
Post JSON 예제를 참고하여 어떻게 하는지 상세한 정보를 살펴 보시기 바랍니다.
-
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();
}
}
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.Status
이HttpRequestQueueResult.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;
}
직렬화 메소드는 커스텀 타입의 객체를 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>