엔터티 그룹

PlayFab 길드 솔루션

길드, 클랜, 모임, 동료, 부족 또는 게임에서 뭐라고 부르든, 이런 것이 필요하다면 PlayFab을 이용하면 됩니다.

PlayFab은 새로운 엔터티 프로그래밍 모델 또는 더 구체적으로 그룹의 엔터티 형식을 사용하여 길드를 빌드합니다. 엔터티 그룹은 길드보다 훨씬 더 넓은 개념이지만, 기본적으로 엔터티 그룹은 길드용 솔루션으로 만들어졌습니다.

엔터티 그룹

엔터티 그룹은 클랜/길드의 필요성에 의해 영감을 받아서 생긴 근본 개념입니다.

기본적으로 엔터티 그룹은 논리적인 엔터티 그룹으로서 어떤 목적으로든 사용할 수 있습니다. 엔터티 그룹은 게임 내에서 동시에 여러 가지 목적을 수행할 수 있습니다.

  • 클랜/길드 - 이것으로부터 시작했으며 애초의 목적이었습니다. 엔터티 그룹을 사용하면 장기적인 결속 요인이 무엇이든 간에, 정기적으로 같이 게임을 플레이하는 플레이어 집합을 설명할 수 있습니다.

  • 파티 - 개별 플레이어들이 즉각적인 목표를 달성한 후에 쉽게 해산할 수 있도록 만들어진 단기 그룹용으로 엔터티 그룹을 사용할 수 있습니다.

  • 채팅 채널 - 단기 또는 장기 채팅 채널을 엔터티 그룹으로 정의할 수 있습니다.

  • 게임 내 정보 구독 - 게임 내에 단 하나의 전설 아이템이 있나요? 플레이어들이 그러한 아이템의 행방에 대한 지속적인 업데이트를 원하나요? 그렇다면 이 아이템에 집중된 엔터티 그룹을 만들어서 아이템에 관심이 있는 모든 플레이어 엔터티를 멤버로 포함할 수 있습니다.

간단히 말해서 엔터티 그룹은 NPC 또는 플레이어로 제어되든, 실제든 관념적이든 모든 유형의 엔터티 컬렉션으로 될 수 있으며, 여기에는 해당 그룹에 대한 지속적인 상태의 결합이 필요합니다.

친구 목록은 하나의 그룹입니다. 세 명이 참가한 비공개 채팅도 하나의 그룹입니다. 불가능한 게 있을까요?

참고 항목

우리가 예상치 못한 것을 하려고 할 때 포럼에서 이를 경고하는 시스템이 있으면 좋겠습니다.

또한 엔터티 그룹은 그 자체로도 엔터티이기 때문에, 엔터티의 모든 초기 특징을 포함합니다.

  • 개체 데이터
  • 파일 데이터
  • 프로필

이러한 특징이 그룹에 적절하다면 향후 새로운 엔터티 특징에도 이것들이 적용될 것입니다.

참고 항목

그룹에는 그룹당 1,000명의 멤버로 기본 제한이 있습니다.

엔터티 그룹 사용

현재 엔터티 그룹은 플레이어 및/또는 캐릭터를 포함할 수 있습니다. 그룹을 만들 때, 그룹에 추가되는 첫 번째 엔터티는 관리자 역할에 배치됩니다. 이 가이드에서는 간단히 하기 위해 이러한 엔터티를 소유자로 지칭합니다. 그런 후 소유자는 새 멤버를 초대하고, 다양한 종류의 사용자 지정 가능한 권한을 지닌 새로운 역할을 만들고, 멤버 역할을 수정하고, 멤버를 내보낼 수도 있습니다.

또한 엔터티에 대해 존재하는 동일한 엔터티 기능이 그룹에서도 작동하므로, JSON 개체 및 파일을 그룹에 직접 저장하여 임의의 게임별 데이터를 저장할 수 있게 됩니다.

아래 제공된 코드 예제는 기본적인 길드의 상호 작용을 바로 이해할 수 있게 해줍니다.

이 예에서는 그룹을 만들고, 멤버를 추가 및 제거하고, 그룹을 삭제할 수 있습니다. 이 예는 시작 지점으로 사용되며, 역할 또는 권한을 보여주지 않습니다.

using PlayFab;
using PlayFab.GroupsModels;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace TestGuildController
{
    /// <summary>
    /// Assumptions for this controller:
    /// + Entities can be in multiple groups
    ///   - This is game specific, many games would only allow 1 group, meaning you'd have to perform some additional checks to validate this.
    /// </summary>
    [Serializable]
    public class GuildTestController
    {
        // A local cache of some bits of PlayFab data
        // This cache pretty much only serves this example , and assumes that entities are uniquely identifiable by EntityId alone, which isn't technically true. Your data cache will have to be better.
        public readonly HashSet<KeyValuePair<string, string>> EntityGroupPairs = new HashSet<KeyValuePair<string, string>>();
        public readonly Dictionary<string, string> GroupNameById = new Dictionary<string, string>();

        public static EntityKey EntityKeyMaker(string entityId)
        {
            return new EntityKey { Id = entityId };
        }

        private void OnSharedError(PlayFab.PlayFabError error)
        {
            Debug.LogError(error.GenerateErrorReport());
        }

        public void ListGroups(EntityKey entityKey)
        {
            var request = new ListMembershipRequest { Entity = entityKey };
            PlayFabGroupsAPI.ListMembership(request, OnListGroups, OnSharedError);
        }
        private void OnListGroups(ListMembershipResponse response)
        {
            var prevRequest = (ListMembershipRequest)response.Request;
            foreach (var pair in response.Groups)
            {
                GroupNameById[pair.Group.Id] = pair.GroupName;
                EntityGroupPairs.Add(new KeyValuePair<string, string>(prevRequest.Entity.Id, pair.Group.Id));
            }
        }

        public void CreateGroup(string groupName, EntityKey entityKey)
        {
            // A player-controlled entity creates a new group
            var request = new CreateGroupRequest { GroupName = groupName, Entity = entityKey };
            PlayFabGroupsAPI.CreateGroup(request, OnCreateGroup, OnSharedError);
        }
        private void OnCreateGroup(CreateGroupResponse response)
        {
            Debug.Log("Group Created: " + response.GroupName + " - " + response.Group.Id);

            var prevRequest = (CreateGroupRequest)response.Request;
            EntityGroupPairs.Add(new KeyValuePair<string, string>(prevRequest.Entity.Id, response.Group.Id));
            GroupNameById[response.Group.Id] = response.GroupName;
        }
        public void DeleteGroup(string groupId)
        {
            // A title, or player-controlled entity with authority to do so, decides to destroy an existing group
            var request = new DeleteGroupRequest { Group = EntityKeyMaker(groupId) };
            PlayFabGroupsAPI.DeleteGroup(request, OnDeleteGroup, OnSharedError);
        }
        private void OnDeleteGroup(EmptyResponse response)
        {
            var prevRequest = (DeleteGroupRequest)response.Request;
            Debug.Log("Group Deleted: " + prevRequest.Group.Id);

            var temp = new HashSet<KeyValuePair<string, string>>();
            foreach (var each in EntityGroupPairs)
                if (each.Value != prevRequest.Group.Id)
                    temp.Add(each);
            EntityGroupPairs.IntersectWith(temp);
            GroupNameById.Remove(prevRequest.Group.Id);
        }

        public void InviteToGroup(string groupId, EntityKey entityKey)
        {
            // A player-controlled entity invites another player-controlled entity to an existing group
            var request = new InviteToGroupRequest { Group = EntityKeyMaker(groupId), Entity = entityKey };
            PlayFabGroupsAPI.InviteToGroup(request, OnInvite, OnSharedError);
        }
        public void OnInvite(InviteToGroupResponse response)
        {
            var prevRequest = (InviteToGroupRequest)response.Request;

            // Presumably, this would be part of a separate process where the recipient reviews and accepts the request
            var request = new AcceptGroupInvitationRequest { Group = EntityKeyMaker(prevRequest.Group.Id), Entity = prevRequest.Entity };
            PlayFabGroupsAPI.AcceptGroupInvitation(request, OnAcceptInvite, OnSharedError);
        }
        public void OnAcceptInvite(EmptyResponse response)
        {
            var prevRequest = (AcceptGroupInvitationRequest)response.Request;
            Debug.Log("Entity Added to Group: " + prevRequest.Entity.Id + " to " + prevRequest.Group.Id);
            EntityGroupPairs.Add(new KeyValuePair<string, string>(prevRequest.Entity.Id, prevRequest.Group.Id));
        }

        public void ApplyToGroup(string groupId, EntityKey entityKey)
        {
            // A player-controlled entity applies to join an existing group (of which they are not already a member)
            var request = new ApplyToGroupRequest { Group = EntityKeyMaker(groupId), Entity = entityKey };
            PlayFabGroupsAPI.ApplyToGroup(request, OnApply, OnSharedError);
        }
        public void OnApply(ApplyToGroupResponse response)
        {
            var prevRequest = (ApplyToGroupRequest)response.Request;

            // Presumably, this would be part of a separate process where the recipient reviews and accepts the request
            var request = new AcceptGroupApplicationRequest { Group = prevRequest.Group, Entity = prevRequest.Entity };
            PlayFabGroupsAPI.AcceptGroupApplication(request, OnAcceptApplication, OnSharedError);
        }
        public void OnAcceptApplication(EmptyResponse response)
        {
            var prevRequest = (AcceptGroupApplicationRequest)response.Request;
            Debug.Log("Entity Added to Group: " + prevRequest.Entity.Id + " to " + prevRequest.Group.Id);
        }
        public void KickMember(string groupId, EntityKey entityKey)
        {
            var request = new RemoveMembersRequest { Group = EntityKeyMaker(groupId), Members = new List<EntityKey> { entityKey } };
            PlayFabGroupsAPI.RemoveMembers(request, OnKickMembers, OnSharedError);
        }
        private void OnKickMembers(EmptyResponse response)
        {
            var prevRequest= (RemoveMembersRequest)response.Request;
            
            Debug.Log("Entity kicked from Group: " + prevRequest.Members[0].Id + " to " + prevRequest.Group.Id);
            EntityGroupPairs.Remove(new KeyValuePair<string, string>(prevRequest.Members[0].Id, prevRequest.Group.Id));
        }
    }
}

예제 분해

이 예는 컨트롤러로 작성되었으며, 최소한의 데이터를 로컬 캐시에 저장하며(PlayFab이 관리 데이터 계층으로 사용됨), 그룹에서 CRUD 작업을 수행할 수 있는 방법을 제공합니다.

이제 제공된 예에서 일부 함수를 살펴보겠습니다.

  • OnSharedError - PlayFab 예제의 일반적인 패턴입니다. 오류를 처리하는 가장 쉬운 방법은 이를 보고하는 것입니다. 게임 클라이언트는 아마 훨씬 더 정교한 오류 처리 논리를 보유할 것입니다.

  • ListMembership - 지정된 엔터티가 속한 모든 그룹을 확인하기 위해 ListMembership을(를) 호출합니다. 플레이어는 자신이 참가한 그룹을 알고 싶을 것입니다.

  • CreateGroup/DeleteGroup - 대부분 자체 설명입니다. 이 예에서는 이러한 호출을 성공적으로 수행할 때 로컬 그룹 정보 캐시를 업데이트하는 방법을 보여줍니다.

  • InviteToGroup/ApplyToGroup - 그룹 가입은 2단계 프로세스이며 양방향으로 활성화할 수 있습니다.

    • 플레이어가 그룹 참가를 요청할 수 있습니다.
    • 그룹에서 플레이어를 초대할 수 있습니다.
  • AcceptGroupInvitation/AcceptGroupApplication - 조인 프로세스의 두 번째 단계입니다. 응답하는 엔티티가 초대를 수락하면 플레이어를 그룹에 포함시키는 프로세스가 완료됩니다.

  • RemoveMembers - 그렇게 할 권한이 있는 구성원(역할 권한으로 정의됨)은 그룹의 구성원을 시작할 수 있습니다.

서버 대 클라이언트 비교

모든 새로운 엔터티 API 메서드와 마찬가지로, 서버 API와 클라이언트 API는 명확하게 구분되지 않습니다.

프로세스의 인증 방법에 따라 호출자에 의해 작업이 수행됩니다. 클라이언트는 그렇게 식별되고, 이러한 메서드를 타이틀 플레이어 엔터티로서 호출하며, 모든 호출이 있을 때마다 그룹 내에서 그 역할 및 권한이 평가되어, 이 작업을 수행할 권한이 있는지 확인합니다.

서버는 동일한 developerSecretKey을(를) 사용하여 인증되며, 이 경우 그러한 프로세스를 타이틀 엔터티로 식별합니다. 타이틀은 역할 검사를 우회하고, 타이틀에서 실행된 API 호출은 멤버가 아닌데도 불구하고 엔터티를 제거할 수 없는 경우와 같이 작업 수행이 불가능할 때만 실패합니다.