This document is about: SERVER 5
SWITCH TO

수정중인 페이지 입니다.

Photon 플러그인의 자주 묻는 질문들

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

환경 구성

Photon은 여러 개의 플러그인을 지원하나요?

애플리케이션 당 한 번에 하나의 플러그인 어셈블리(DLL) 또는 플러그인 팩토리만 구성할 수 있습니다. 이 DLL에서는 원하는 만큼 플러그인을 사용할 수 있습니다. 룸을 만들 때 하나의 플러그인만 로드되며 인스턴스화됩니다. 룸과 플러그인 인스턴스 사이에는 1:1의 관계가 있으므로 각 룸에는 자체 플러그인 인스턴스가 있습니다.

다음 구성은 허용되지 않습니다:

<PluginSettings Enabled="true">
    <Plugins>
        <Plugin …/>
        <Plugin …/>
    </Plugins>
</PluginSettings>

룸을 만들 때 사용할 플러그인을 선택하는 방법은 무엇입니까?

당사의 플러그인 모델은 팩토리 패턴을 사용합니다. 플러그인은 요청 시 이름으로 인스턴스화됩니다.

클라이언트가 roomOptions.Plugins를 사용하여 플러그인 설정을 요청하여 룸을 만듭니다. roomOptions.Pluginsstring[] 타입으로 첫 번째 문자열 (roomOptions.Plugins[0])은 플러그인 이름이 되어야 하며, 이 이름은 팩토리에 전달됩니다.

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

클라이언트가 아무것도 전송하지 않으면 서버는 기본값(아무것도 구성되지 않은 경우)을 사용하거나 플러그인 팩토리가 구성된 경우 생성 시 반환되는 것을 사용합니다.

팩토리에서 이름을 사용하여 다음과 같이 해당 플러그인을 로드합니다:

C#

public class PluginFactory : IPluginFactory 
{
    public IGamePlugin Create(IPluginHost gameHost, string pluginName, Dictionary<string, string> config, out string errorMsg) 
    {
        var 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;
    }
}

PluginFactory.Create에서 반환된 플러그인의 이름이 클라이언트에서 요청한 것과 일치하지 않는다면 플러그인이 언로드되고 클라이언트의 생성 또는 가입 작업이 실패하고 PluginMismatch (32757) 에러가 발생합니다.

런타임에 디스크에서 파일을 읽고 싶습니다. 플러그인이 파일 시스템에 접근할 수 있나요? 외부 서버에서 파일을 다운로드해야 하나요?

플러그인에 필수적인 파일과 함께 추가 파일을 업로드할 수 있습니다. 파일들의 경로는 typeof(yourplugin).Assembly.Location을 사용하여 얻을 수 있습니다.

종속 모듈(DLL)들이 주 플러그인 DLL와 같이 자동적으로 로딩되나요? System.TypeLoadException이 발생하는 이유는 무엇인가요?

외부 모듈 사용은 플러그인 솔루션의 DLL 또는 프로젝트를 참조하여 작동해야 합니다. 참조된 모든 종속성 모듈을 로드하고 연결하려면 해당 모듈을 플러그인 DLL과 동일한 디렉토리에 배포해야 합니다.

콜백

플레이어가 룸에 들어가려고 할 때 어떤 방법을 사용합니까?

이 질문에 답하기 위해서, 우리는 플레이어가 룸을 만들거나, 참여하거나, 다시 참여하는 것과 관련된 네 가지 다른 시나리오가 있다는 것을 이해해야 합니다.

일반적으로 JoinRoom 그리고 CreateRoom 오퍼레이션의 구조체는 매우 유사합니다. 서로 다른 JoinMode 값을 갖는 하나의 논리적 조인 작업으로 보는 것이 도움이 될 수 있습니다:

  1. Create: OpCreateRoom: 룸이 이미 존재하고 있는 경우 에러를 리턴합니다
  2. Join: OpJoinRoom (JoinMode.Default 또는 설정되지 않음), OpJoinRandomRoom: 룸이 존재하지 않으면 에러를 리턴합니다
  3. CreateIfNotExists: OpJoinRoom (JoinMode.CreateIfNotExist): 룸이 존재하지 않으면 룸을 생성합니다
  4. RejoinOnly: OpJoinRoom (JoinMode.RejoinOnly): 액터가 이미 룸에 들어가 있는 경우 에러가 발생합니다

룸이 메모리 2)에서 4)번에 있는 경우에 BeforeJoin을 트리거하고 {ICallInfo}.Continue(), OnJoin이 호출될것이라고 가정합니다.

OnCreateGame은 룸 생성 직후, 플러그인이 설정되고 액터가 룸에 추가되기 직전에 호출됩니다. 1), 3) 및 4)에 의해 트리거 될 수 있습니다. 후자는 상태 저장 및 로드가 정상적으로 처리되고 있는 경우에만 발생할 수 있습니다. 플레이어가 서버 메모리에서 제거된 룸에 다시 가입하도록 요청할 때 이 문제가 발생합니다. 어쨌든 Photon이 생성하고 플러그인을 설정할 것입니다. 그런 다음 플러그인은 데이터베이스 또는 외부 서비스에서 직렬화된 상태를 검색하고 SetSerializedGameState를 호출합니다. 상태는 액터의 목록, 모든 비활성 상태가 포함됩니다. 재참여는 다시 참여하는 액터를 다시 활성화됩니다.

플러그인 콜백에서 액터 번호는 어떻게 얻나요?

다음은 ActorNr을 플러그인 후크로 가져오는 방법입니다:

1. OnCreateGame, info.IsJoin == false:

ActorNr = 1: 게임의 첫 번째 액터, 게임을 만드는 사람은 항상 액터 번호를 1로 설정됩니다.

2. OnCreateGame, info.IsJoin == true:
a. info.Continue(); 이전

비활성 액터 목록에서 UserId 별로 ActorNr를 가져옵니다: 게임 상태를 로드하여 룸을 "재생성" 하는 경우입니다. 그런 다음 로드된 룸 상태에서 ActorList를 반복하고 UserId와 비교하여 로드된 게임 상태에서 Actor 번호를 찾아야 합니다. (UserId를 사용할 수 없어 재가입이 실패할 수 있으므로 CheckUserOnJoin 및 고유한 Actor 별 UserId로 룸을 생성해야 합니다.)

C#

// load State from webservice or database or re-construct it
if (this.PluginHost.SetGameState(state)) 
{
    int actorNr = 0;
    foreach (var actor in PluginHost.GameActorsInactive)
    {
        if (actor.UserId == info.UserId)
        {
            actorNr = actor.ActorNr;
            break;
        }
     }
     if (actorNr == 0) 
     {
         if (!asyncJoin) 
         {
             // error, join will fail with 
             // ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
         } 
         else 
         {
             actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
         }
   }
}

그렇지 않으면 JoinRoom 오퍼레이션에서 클라이언트부터 "올바른" ActorNr을 보내면 info.Request.ActorNr에서 가져올 수 있습니다.

b. info.Continue(); 이후

활성 액터 목록에서 UserId를 통해서 ActorNr 얻기

C#

int actorNr;
foreach (var actor in PluginHost.GameActorsActive)
{
    if (actor.UserId == info.UserId)
    {
        actorNr = actor.ActorNr;
        break;
    }
}
3. BeforeJoin
a. info.Continue(); 이전

C#

int actorNr = 0;
switch (info.Request.JoinMode)
{
    case JoinModeConstants.JoinOnly:
    case JoinModeConstants.CreateIfNotExists:
        actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
        break;
    case JoinModeConstants.RejoinOnly:
        foreach (var actor in PluginHost.GameActorsInactive)
        {
            if (actor.UserId == info.UserId)
            {
                actorNr = actor.ActorNr;
                break;
             }
         }
         if (actorNr == 0)
         {
             // error, join will fail with 
             // ErrorCode.JoinFailedWithRejoinerNotFound = 32748, // 0x7FFF - 19,
          }
          break;
      case JoinModeConstants.RejoinOrJoin:
          foreach (var actor in PluginHost.GameActorsInactive)
          {
              if (actor.UserId == info.UserId)
              {
                  actorNr = actor.ActorNr;
                  break;
              }
           }
           if (actorNr == 0)
           {
               actorNr = PluginHost.GetSerializableGameState().ActorCounter + 1;
           }
           break;
}

그렇지 않으면 JoinRoom 오퍼레이션에서 클라이언트부터 "올바른" ActorNr을 보내면 info.Request.ActorNr에서 가져올 수 있습니다.

b. after info.Continue();

활성 액터 목록에서 UserId를 통해서 ActorNr 얻기

C#

int actorNr;
foreach (var actor in this.PluginHost.GameActorsActive)
{
    if (actor.UserId == info.UserId)
    {
         actorNr = actor.ActorNr;
         break;
    }
}
4. OnJoin, OnRaiseEvent, BeforeSetProperties, OnSetProperties, OnLeave:

사용할 수 있는 info.ActorNr를 이용하세요.

5. BeforeCloseGame, OnCloseGame:

후크는 클라이언트 오퍼레이션이 아닌 서버에 의해 트리거 되므로 ActorNr를 가져올 방법은 없습니다.

서버에서 룸을 생성하는 것이 가능한가요?

아니오. 그렇게 할 수는 없습니다.

서버에서 룸을 제거하는 것이 가능한가요?

아니오. 그렇게 할 수는 없습니다.

플러그인 이벤트에서 사용할 수 있는 데이터를 알 수 있는 가장 좋은 방법은 무엇인가요?

일반적으로, 이벤트의 ICallInfo 콜백 파라미터는 필요한 정보를 노출해야 합니다. 대부분의 경우에, 실제 오퍼레이션 요청 파라미터들은 {ICallInfo}.OperationRequest (또는 Request) 속성을 통해 가져올 수 있습니다.

UseStrictMode는 무엇을 하나요?

플러그인의 아이디어는 수신되는 요청을 처리하기 전 또는 후에 "정상적인" Photon 흐름에 연결하는 것입니다.

초기에는 어떤 것을 호출해야 한다고 강요하지 않았습니다. 이는 본질적으로 수신된 요청의 기본 처리를 취소하는 것과 같습니다. 이것은 개발자가 기본 콜백 로직을 사용하지 않고 필요한 모든 것을 처음부터 구현해야 한다는 것입니다. 이로 인해 예상치 못한 문제가 발생했습니다. 취소하는 것이 개발자가 항상 원했던 것은 아닙니다. 그래서 우리는 개발자의 결정을 기대하는 엄격한 모드를 도입했습니다. 이제 Continue, Fail 또는 Cancel중 한 번만 호출해야 합니다.

PluginBase의 콜백 메소드를 오버라이드 할 때, base.XXX()를 호출해야 하나요?

PluginBase 내의 모든 콜백 메소드는 마지막에 Continue()에 대한 호출을 포함합니다. 즉, PluginBase로부터 상속하여 원하는 메소드만 오버라이드 하면 된다는 것입니다. 때때로 base.XXX() 뒤에 또는 그전에 코드를 추가해야 하고 기본 동작을 완전히 변경해야 하는 경우도 있습니다. 첫 번째 경우에는 ICallInfo 처리 메소드에 호출을 추가해서는 안 됩니다. 두 번째 경우에는 base.XXX()를 호출하면 안 되며, 처음부터 메소드를 직접 구현한 다음 사용 가능한 ICallInfo 처리 메소드 중 하나에 호출을 추가합니다. 하지만 한 가지 예외가 있는데, 바로 PluginBase.OnLeave입니다. 이 메소드는 현재 MasterClient가 나가는 경우에 MasterClient의 변경사항을 처리합니다.

OnLeave에서 왜 ILeaveGameCallInfo.ActorNr == -1인가요?

클라이언트가 룸 입장(생성 또는 참여)에 실패할 때 발생합니다 기본적으로 ILeaveGameCallInfo.ActorNr == -1의 의미는 우선 클라이언트가 룸에 추가되지 않았고 액터 번호를 받지 못했다는 것입니다. OnLeave(ILeaveGameCallInfo.ActorNr == -1)OnCreateGame 또는 BeforeJoin내의 info.Continue() 호출의 결과입니다. OnLeave가 트리거 될 때, 서버는 각각의 클라이언트에 대해 룸 안에 있는 액터를 찾지 못할 것이며, 이유는 ActorNr의 기본값이 -1("액터를 찾지 못함"으로 읽을수 있음)이기 때문입니다. 여기에서 ILeaveGameCallInfo.Reason는 항상 3이어야 합니다:LeaveReason.ServerDisconnect

다음은 감지하고 ILeaveGameCallInfo.ActorNr == -1을 기록하는 플러그인 예제입니다:

C#

using System.Collections.Generic;
using System.Linq;

namespace Photon.Plugins.Samples
{
    public class JoinFailureDetection : PluginBase
    {
        private static readonly Dictionary<string, ICallInfo> pendingJoin = new Dictionary<string, ICallInfo>();

        public override void OnCreateGame(ICreateGameCallInfo info)
        {
            this.PluginHost.LogInfo(string.Format("OnCreateGame UserId={0} GameId={1}", info.UserId, info.Request.GameId));
            this.AddPendingJoin(info.UserId, info);
            info.Continue(); // in case of join failure this will call OnLeave inside w/ ActorNr == -1
            this.CheckJoinSuccess(info.UserId, info);
        }

        public override void BeforeJoin(IBeforeJoinGameCallInfo info)
        {
            this.PluginHost.LogInfo(string.Format("BeforeJoin UserId={0} GameId={1}", info.UserId, info.Request.GameId));
            this.AddPendingJoin(info.UserId, info);
            info.Continue(); // in case of join failure this will call OnLeave inside w/ ActorNr == -1
            this.CheckJoinSuccess(info.UserId, info);
        }

        public override void OnLeave(ILeaveGameCallInfo info)
        {
            this.PluginHost.LogInfo(string.Format("OnLeave UserId={0} GameId={1}", info.UserId, this.PluginHost.GameId));
            this.CheckJoinFailure(info);
            info.Continue();
        }

        private void AddPendingJoin(string userId, ICallInfo info)
        {
            if (string.IsNullOrEmpty(userId))
            {
                this.PluginHost.LogError("UserId is null or empty");
            }
            else
            {
                this.PluginHost.LogInfo(string.Format("User with ID={0} is trying to enter room {1}, ({2})", userId, this.PluginHost.GameId, info.GetType()));
                pendingJoin[userId] = info;
            }
        }

        private void CheckJoinFailure(ILeaveGameCallInfo info)
        {
            if (info.ActorNr == -1)
            {
                this.PluginHost.LogWarning(string.Format("ILeaveGameCallInfo.ActorNr == -1, UserId = {0} Reason = {1} ({2}) GameId = {3} Details = {4}", info.UserId, info.Reason, LeaveReason.ToString(info.Reason), this.PluginHost.GameId, info.Details));
            }
            if (string.IsNullOrEmpty(info.UserId))
            {
                this.PluginHost.LogError("ILeaveGameCallInfo.UserId is null or empty");
                return;
            }
            if (pendingJoin.ContainsKey(info.UserId))
            {
                this.PluginHost.LogError(string.Format("User with ID={0} failed to enter room {1}, removing pending request {2}", info.UserId, this.PluginHost.GameId, pendingJoin[info.UserId]));
                pendingJoin.Remove(info.UserId);
            }
            else
            {
                this.PluginHost.LogError(string.Format("no previous pending join for UserID = {0} GameId = {1}", info.UserId, this.PluginHost.GameId));
            }
        }

        private void CheckJoinSuccess(string userId, ICallInfo info)
        {
            if (info.IsSucceeded)
            {
                if (string.IsNullOrEmpty(userId))
                {
                    this.PluginHost.LogError("userId is null or empty");
                    return;
                }
                if (this.PluginHost.GameActorsActive.Any(u => userId.Equals(u.UserId)))
                {
                    if (pendingJoin.ContainsKey(userId))
                    {
                        this.PluginHost.LogInfo(string.Format("User with ID={0} succeeded to enter room {1}, removing pending request {2}", userId, this.PluginHost.GameId, pendingJoin[userId]));
                        pendingJoin.Remove(userId);
                    }
                    else
                    {
                        this.PluginHost.LogDebug(string.Format("no previous pending join for UserID = {0} GameId = {1}", userId, this.PluginHost.GameId));
                    }
                }
            }
        }

        public override void OnCloseGame(ICloseGameCallInfo info)
        {
            info.Continue();
            foreach (var pair in pendingJoin)
            {
                this.PluginHost.LogError(string.Format("Room {0} is being removed, unexpected leftover pending join UserID = {1} ICallInfo = {2}", this.PluginHost.GameId, pair.Key, pair.Value));
            }
        }
    }
}

참여 실패에 대한 몇 가지 예제 (룸 생성 또는 참가 작업에 대한 클라이언트 오류 코드를 살펴볼 수 있습니다):

  • 동일한 UserID를 가진 비활성 액터가 없는 동안 사용자가 룸에 다시 참여하려고 합니다. 클라이언트는 에러코드 JoinFailedWithRejoinerNotFound = 32748을 받습니다.
  • 동일한 UserID를 가진 활성 액터가 있는 동안 사용자가 룸에 참여하려고 합니다. 클라이언트는 에러코드 JoinFailedFoundActiveJoiner = 32746을 받습니다.
  • 동일한 UserID를 가진 비활성 액터가 있는 동안 사용자가 룸에 참여하려고 합니다. 클라이언트는 에러코드 JoinFailedFoundInactiveJoiner = 32749를 받습니다.

ILeaveGameCallInfo.ActorNr == -1가 참여 실패의 결과가 아닌 경우 비정상이므로 사용자 지정 플러그인 코드나 내부 서버 코드에 예외가 있습니다. 어떤 경우든 플러그인 측에서 이를 조기에 발견하여 스택 트레이스에 기록할 수 있습니다. 또한 ILeaveGameCallInfo.ActorNrILeaveGameCallInfo.Continue 전과 후에 변경되어야 하며, 여전히 이것을 감지하고 리포트할 수 있습니다. ReportError 메소드를 구현하거나 PluginBase에서 확장한 경우 오버라이드 하여 코드에서 예외를 탐지할 수 있습니다.

이벤트

HttpForward 속성은 무엇이고 WebFlags 클래스가 하는 일은 무엇인가요?

WebHooks에 관련된 기능입니다. WebFlags는 WebHooks v1.2 플러그인에서 도입되었습니다. 이 페이지로 이동하여 WebFlags에 대해서 더 읽어보세요. HttpForward는 속성으로 webflag에 상응하는 값을 나타냅니다.

처음에는 WebHooks 및 WebRPC 용으로 만들어졌지만 플러그인에도 사용할 수 있습니다.

플러그인에서 이벤트를 어떻게 전송하나요?

PluginHost.BroadcastEvent는 이 목적으로 사용되어야 합니다. 이것은 중요 차이점이 있는 클라이언트로부터 이벤트를 보내는 것과 같은 방식으로 동작합니다. 서버를 표시하기 위해 전송자 액터 번호를 0으로 설정할 수 있습니다.

더 많은 정보는, read the 플러그인 매뉴얼의 "플러그인으로 부터 이벤트 전송하기를 읽어보세요.

PluginHost.BroadcastEventOnRaiseEvent에서 수행된 것과는 다르게 OnJoin 콜백 내부에서 호출될 때 이벤트 전송에 절대로 성공하지 못하는 이유는 무엇인가요?

OnRaiseEvent에서는 클라이언트가 이미 참여를 하고 있으므로, 이벤트를 수신할 수 있습니다. OnJoin에서는 클라이언트가 요청이 {ICallInfo}.Continue()를 이용하여 처리되지 않는 한 완전히 참여하지 않은 상태입니다.

따라서 {ICallInfo}.Continue() 이후에 PluginHost.BroadcastEvent가 호출되면, 이벤트는 목표 클라이언트에 의해 수신되어야 합니다.

플러그인 후크는 이 방식으로 동작합니다: {ICallInfo}.Continue()에 대한 호출로 Photon의 일반 처리를 트리거합니다. 이 경우 참여가 완전히 완료된 후 이벤트를 전송하는 것이 더 적합합니다.

클라이언트들이 플러그인으로부터 전송된 이벤트 데이터를 왜 수신하지 못할까요?

알려진 이슈입니다. 클라이언트들은 이벤트가 잘 알려진 키 코드를 가진 미리 정의된 특정 구조체를 갖기를 기대합니다. 이벤트 데이터의 경우 245이고 배우 번호는 254입니다. 이 문제를 해결하려면 플러그인의 이벤트 데이터 전송 방식을 약간만 변경하면 됩니다: (Dictionary<byte,object>)eventData 대신에 new Dictionary<byte,object>(){{245,eventData},{254,senderActorNr}}을 전송합니다.

플러그인은 사용자 지정 오퍼레이션을 지원하나요?

아니요. 플러그인은 사용자 지정 오퍼레이션을 지원하지 않습니다. Photon 플러그인은 오퍼레이션 집합(Create, Join, SetProperties, RaiseEvent 및 Leave)에 대해서만 콜백을 제공합니다. 자체 호스팅 된 Photon Server에 추가된 사용자 지정 작업은 Plugins SDK를 사용하여 인터셉트하거나 새 작업을 확장할 수 없습니다.

그러나 양방향 이벤트를 교환하여 동일한 결과를 얻을 수 있습니다:

  • LoadBalancingClient.OpRaiseEvent를 호출하여 클라이언트에서 플로그인으로.
  • PluginHost.BroadcastEvent를 호출하여 플러그인에서 클라이언트로.

게임 상태

"활성" 사용자와 "비활성" 사용자를 구분하는 것은 무엇인가요?

플레이어가 Photon의 연결을 끊으면 일반적으로 정리되고 액터가 제거되지만 룸을 생성할 때 클라이언트에서 CreateOptions내의 PlayerTTL을 정의할 수 있습니다. 엄격하게 긍정적인 경우 Photon은 정리하기 전에 해당 시간(밀리초 단위로 정의됨)까지 대기합니다. 그러는 동안, 그 액터는 활동적이지 않은 것으로 간주되어 게임에 다시 참여할 수 있습니다. 이 작업이 성공적으로 수행되면 플레이어가 되고 다시 활성화됩니다.

이 기능은 게임 상태를 저장하고 플레이어가 다시 할 수 있도록 해줍니다. 예를 들어 연결 불량으로 인하여 짧은 시간 동안 연결이 끊길 수 있는 RTS 게임에서 연결이 끊겨도 다시 게임에 돌아갈 수 있어 유용합니다.

PluginHost.GameActorsActive는 룸(참여한) 안의 모든 액터를 포함하고 있으며, PluginHost.GameActorsInActive에는 룸을 떠난 모든 액터(방출되지 않은)들이 들어가 있습니다.

플러그인으로 액터를 방을 나가게 할 수 있나요? 할 수 있다면 어떻게 하나요?

네. 가능합니다. 플러그인 클래스에서 PluginHost.RemoveActor(int actorNr, string reasonDetails)를 호출해야 합니다. 이 3개의 파라미터를 받는 이 메소드를 오버로드하고 호출하여 이유를 설정할 수 있습니다: PluginHost.RemoveActor(int actorNr, byte reason, string reasonDetails).

룸 상태를 어떻게 보관하나요?

룸 상태를 저장하기 위해서:

  1. PluginHost.GetGameState를 호출하고 SerializableGamestate를 받습니다.
  2. 상태를 직렬화합니다 (예를 들면 JSON).
  3. 데이터 저장소에 상태 저장.

룸 상태를 불러오기 위해서:

  1. 데이터 스토어에서 상태를 가져오기.
  2. 상태를 역직렬화하기.
  3. PluginHost.SetGameState 호출.

노트: {ICallInfo}.Continue() 호출하기 전에 OnCreateGame에서는 PluginHost.SetGameState 호출만 허용됩니다.

SerializableGameState내에서 사용자 지정 룸 속성을 찾을 수 없고 로비 속성만 찾을 수 있습니다. 왜 이런가요?

직렬화 가능한 게임 상태에서는 설계상 모든 사용자 지정 속성에 대한 액세스를 제공하지 않습니다. 로비와 공유하는 사용자만 노출되며 이는 "보기" 목적일 뿐입니다. 결합된 모든 속성은 바이너리 배열에 포함됩니다. 이를 통해 JSON에 직렬화할 수 있으며 역직렬화 시 유형 정보가 손실되지 않습니다. 이 기능은 주로 저장/불러오기 시나리오를 위해 설계되었습니다. 우리는 앞으로 이 동작을 바꿀지도 모릅니다.

플러그인에서 룸 속성은 어떻게 접근하나요 (MaxPlayers, IsVisible, IsOpen, 등.)?

모든 룸 속성들은 Hashtable 타입인 PluginHost.GameProperties에서 접근할 수 있습니다. 이러한 속성들은 "원시" 또는 "잘 알려진" 속성들이 포함되어 있습니다. Photon.Hive.Plugin.GameParameters에 속성 목록이 나열되어 있습니다.

PluginHost.GameProperties은 또한 사용자 지정 룸 속성들이 들어가 있습니다. 이 속성들은 문자열 키를 가지고 있고 개발자에 의해 조작됩니다.

다른 한편, 사용자 지정 속성들만 로비에서 보이고 Dictionary<string, object>PluginHost.CustomGameProperties내에 저장됩니다. 이 속성은 읽기 전용으로 취급되어야 합니다.

다음과 같이 룸과 액터의 속성을 플러그인으로부터 접근(읽고 쓰기) 할 수 있습니다.:

속성을 가져오기 위한 일부 헬퍼 메소드들:

C#

private bool TryGetRoomProperty<T>(byte key, out T property)
{
    property = default;
    if (this.PluginHost.GameProperties.ContainsKey(key))
    {
        property = (T)this.PluginHost.GameProperties[key];
    }
    return false;
}

private bool TryGetCustomRoomProperty<T>(string key, out T property)
{
    property = default;
    if (this.PluginHost.GameProperties.ContainsKey(key))
    {
        property = (T)this.PluginHost.GameProperties[key];
    }
    return false;
}

private bool TryGetActorByNumber(int actorNr, out IActor actor)
{
    actor = this.PluginHost.GameActors.FirstOrDefault(a => a.ActorNr == actorNr);
    return actor == default;
}

private bool TryGetActorProperty<T>(int actorNr, byte key, out T property)
{
    property = default;
    if (this.TryGetActorByNumber(actorNr, out IActor actor) && actor.Properties.TryGetValue(key, out object temp))
    {
        property = (T)temp;
    }
    return false;
}

private bool TryGetCustomActorProperty<T>(int actorNr, string key, out T property)
{
    property = default;
    if (this.TryGetActorByNumber(actorNr, out IActor actor) && actor.Properties.TryGetValue(key, out object temp))
    {
        property = (T)temp;
    }
    return false;
}

쓰기 예제:

C#

PluginHost.SetProperties(actorNr: 0, properties: new Hashtable { { "map", "america" } }, expected: null, broadcast: false); // actor=0 for Room properties
PluginHost.SetProperties(actorNr: 1, properties: new Hashtable { { "health", 100 } }, expected: null, broadcast: true);

쓰레딩

.NET 플러그인 컴포넌트의 쓰레딩 요건에 대해서 더 읽어 보세요

단일 Photon server에서 쓰레드는 몇 개나 수행되나요?

쓰레드의 사용은 분리됩니다:

  1. 네이티브 - 9 개 쓰레드
  2. 매니지드 - .NET 기본 설정을 사용하는 .Net ThreadPool 에 의존함

이 설정은 상당히 집중적으로 테스트되었으며 다양한 부하 프로파일에 매우 적합합니다.

매니지드 쓰레드의 사용은 (.NET Windows Performance counters에서 보고된 것처럼) 다양합니다:

a) 전형적인 Photon cloud RealTime 부하에서 ~12 b) 클라이언트 클라우드가 플러그인 간 통신(잠금)을 통해 플러그인을 실행함으로써 (우리의 코드와 달리) 일부 더 높은 경합이 발생하는 곳에서 35 (및 그 이상)

노트: 필요에 따라, .Net ThreadPool 설정은 조정될 수 있습니다. 지금까지 기본값은 각 버전마다 다를 수 있지만 좋은 결과를 얻었습니다.

Photon 호스트가 쓰레드에서 자유로운가요? 어떤 쓰레드라도 아무 때나 룸에 들어갈 수 있나요?

메시지를 전달하는 아키텍처가 있습니다: 플러그인은 한 번에 하나의 쓰레드에 의해서만 호출됩니다. 그러나 쓰레드 풀을 사용하기 때문에 호출마다 쓰레드가 다를 수 있습니다.

플러그인 작성 시 쓰레드 안전성에 문제가 있나요?

일반적으로 플러그인에 대한 모든 호출이 직렬화된다고 가정해도 무방합니다(실제로 한 쓰레드 / 반드시 동일한 물리적 쓰레드일 필요는 없음).

Enterprise Cloud

엔터프라이즈 클라우드의 런타임 환경과 관련된 질문

플러그인은 어떻게 구성하나요?

Photon Enterprise Cloud의 경우: 애플리케이션의 관리 페이지인 Photon 관리 화면으로 이동하여 새 플러그인을 추가해야 합니다. 그런 다음 페이지 하단의 "새로운 플러그인 생성" 버튼을 클릭해야 합니다. 이제 키/값 항목을 추가하여 플러그인을 구성할 수 있습니다. AssemblyName, Version, Path 그리고 Type은 필수 항목입니다.

Photon 플러그인을 만드는 파이프라인 프로세스는 무엇인가요?

Photon 플러그인을 작성하는 파이프라인 프로세스는 간단합니다:

  1. 필요한 SDK와 서버 바이너리를 다운로드합니다.
  2. 코딩하고 플러그인 어셈블리를 빌드 합니다.
  3. 배포하고 테스트합니다.
  4. 업로드합니다.
  5. 환경 구성을 합니다.

Photon 플러그인의 환경 설정은 다음과 같을 수 있습니다:

  • 개발: 로컬 머신.
  • 테스트: 로컬 네트워크.
  • 스테이징: 당사 클라우드에 AppId 분리.
  • 출시: 당사 클라우드에 라이브 AppId.

플러그인 업로드는 자동화되어 있나요?

네. 기업 고객에게 사설 클라우드 관리를 돕기 위해 PowerShell 스크립트를 제공합니다. 상세 내용에 대해서는 플러그인 업로드 온라인 지침서를 확인해 주세요.

플러그인의 성능은 어떻게 모니터링하나요?

관리 화면에서 사용할 수 있는 여러 카운터를 추적합니다.
또한 맞춤형 카운터를 추가하거나 카운터에 대한 외부 툴도 통합할 수 있습니다.(예 New Relic) 이러한 서비스들이 필요한 경우, 컨설팅 계약이 필요합니다.

플러그인의 로그를 가질 수 있는 방법이 있나요?

서버의 로그 파일에 대한 접근은 허용되지 않습니다. 따라서 로그나 알림에는 외부 서비스를 사용해야 합니다. Logentries 또는 Papertrail을 사용하는 것을 권장합니다. 따라서 기업 고객님들께서는 당사와 연락하여 원하는 로깅 서비스로 프라이빗 클라우드를 구성할 것을 요청드립니다. Logentries를 사용하고 싶으신 경우 구성된 로그 토큰이 제공됩니다. Papertrail을 사용하고 싶으신 경우 포트 번호를 포함한 귀하의 사용자 지정 URL이 제공됩니다.

Logentries 토큰은 어떻게 받나요?

Logentries 계정을 생성하고 다음 단계를 따라 합니다:

  1. "Logs"/"Add New Log"를 선택합니다.
  2. "Libraries"/".NET"을 선택합니다.
  3. 로그 집합에 대한 이름을 입력합니다.
  4. "Create Log Token"을 클릭합니다.
  5. "Finish & View Log"를 클릭합니다.
  6. 새로운 로그 세트를 선택하고 "Setting" 탭을 선택합니다. 토큰을 지금 보실 수 있습니다.
  7. 이메일로토큰을 당사로 보내주세요.

Papertrail URL을 어떻게 받나요?

Papertrail 계정을 생성하고 다음 단계를 따라 합니다:

  1. "System"을 추가합니다
  2. 다음 페이지의 상단에서, "Your logs will go to logs6.papertrailapp.com:12345 and appear in Events."와 같은 메시지가 보이게 됩니다.
  3. 그 URL을 이메일로 당사로 보내주세요.

새로운 플러그인 버전을 롤아웃할 때 권장되는 방법은 무엇입니까?

Currently Photon Plugins support only side-by-side assembly versioning: one plugin DLL version per AppId.

Here are two methods we recommend for rolling out new plugins versions:

A. "Compatible" plugins deploy: does not require new client version

  1. Upload new version of plugins assembly.
  2. On a staging AppId: test to verify that new version works as expected. (recommended)
  3. Update production AppId configuration to use new plugins assembly version.

B. "Incompatible" plugins deploy: requires new client version

  1. Upload new version of plugins assembly.
  2. Setup a new production AppId.
  3. Configure the new production AppId to use new plugins assembly version.

가능하지만 더 발전된 또 다른 기술은 다음과 같습니다.

실제 게임 로직은 명시적으로 로드되는 다른 DLL에서 가져온 것이지만 코어 서버 업데이트 루프가 있는 플러그인 DLL을 구축할 수 있습니다. 게임 로직에는 버전당 둘 이상의 DLL이 있어야 하지만 코어 플러그인 DLL은 업데이트되지 않아야 합니다. 코어 플러그인 DLL은 클라이언트 버전에 따라 적절한 게임 로직 DLL을 로드합니다. 완전한 호환성을 위해 서버 쪽 코드를 클라이언트 쪽 코드와 매핑하는 것과 같습니다. 이렇게 하면 다음과 같은 플러그인의 버전 호환 업데이트가 가능합니다. 이렇게 하면 새 플러그인 버전이 원격 설치될 때 클라이언트 업데이트를 강제할 필요가 없습니다. 게임 로직 DLL은 버전별로 별도의 폴더에 넣거나 같은 폴더 안에 넣되 버전별로 이름이 다를 수 있습니다.

플러그인에서 static 필드를 사용할 수 있나요?

Same plugins assembly will be shared across rooms and applications. Static fields of same plugin class will be shared as well. If you cannot avoid using static fields, here is what you could do to avoid using same plugins assembly in two applications:

  1. Upload same plugin files under two different plugin names:

    a- Upload plugin archive with name X
    b- Upload plugin archive with name Y

  2. Same configuration for two apps except "Path":

    a- Configure app A to use plugin X: "Path": "{customerName}\X"
    b- Configure app B to use plugin Y: "Path": "{customerName}\Y"

기타

플러그인이 RemoveActor를 실행할 때 reason을 어떻게 가져오나요?

현재 클라이언트로 사유(코드 byte 또는 메시지 string)를 보내지 않습니다. 사용자 지정 이벤트를 사용하여 클라이언트의 연결을 끊기 전에 이를 알릴 수 있습니다. 예를 들어, 이유가 있는 사용자 지정 이벤트를 보내고 타이머를 사용하여 200ms 후에 RemoveActor를 예약할 수 있습니다.

다음과 같은 헬퍼 메소드를 사용할 수 있습니다:

C#

private const int RemoveActorEventCode = 199;
private const int RemoveActorTimerDelay = 200;

private void RemoveActor(ICallInfo callInfo, int actorNr, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, reason }}, 0);
    this.PluginHost.CreateOneTimeTimer(callInfo, () => this.PluginHost.RemoveActor(actorNr, reason),
        RemoveActorTimerDelay);
}

private void RemoveActor(ICallInfo callInfo, int actorNr, byte reasonCode, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, new { reasonCode, reason } }}, 0);
    this.PluginHost.CreateOneTimeTimer(callInfo, () => this.PluginHost.RemoveActor(actorNr, reason),
        RemoveActorTimerDelay);
}

private void RemoveActor(int actorNr, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, reason }}, 0);
    var fiber = this.PluginHost.GetRoomFiber();
    fiber.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reason), RemoveActorTimerDelay);
}

private void RemoveActor(int actorNr, byte reasonCode, string reason)
{
    this.PluginHost.BroadcastEvent(new List<int> { actorNr }, 0, RemoveActorEventCode, 
        new Dictionary<byte, object> { { 254, 0 }, { 245, new { reasonCode, reason } }}, 0);
    var fiber = this.PluginHost.GetRoomFiber();
    fiber.CreateOneTimeTimer(() => this.PluginHost.RemoveActor(actorNr, reasonCode, reason), RemoveActorTimerDelay);
}

클라이언트 SDK는 사용자 지정 유형을 등록하여 직렬화를 확장하는 기능을 지원합니다. 서버에서 이러한 유형을 어떻게 역직렬화합니까?

다음과 같이 클라이언트 SDK에서처럼 사용자 정의 유형을 등록할 수 있습니다:

C#

PluginHost.TryRegisterType(type: typeof (CustomPluginType), typeCode: 1, serializeFunction: SerializeFunction, deserializeFunction: DeserializeFunction);

상세 내용에 대해서는 플러그인 매뉴얼의 "사용자 지정 유형"을 읽어 보세요.

플러그인 .DLL 파일 크기에는 제한이 있나요?

아니요. 하지만 너무 크지 않아야 한다고 판단합니다.

Photon 플러그인에서 PUN의 PhotonNetwork.ServerTimestamp을 어떻게 얻나요?

Environment.TickCount을 사용합니다.

Back to top