GS2-Friend
게임 전용 소셜 그래프를 형성하기 위한 기능을 제공합니다. 소셜 그래프는 플랫폼 측에서도 제공되는 경우가 있지만, 게임 내에서 독자적으로 형성하는 것에도 가치가 있습니다.
플랫폼 전체에서 사용되는 친구 관계는 등록을 신중하게 하는 경향이 있습니다. 왜냐하면 “지금 함께 플레이하고 있는 이 게임에서는 함께 하고 싶지만, 다른 게임까지 함께 플레이하고 싶지는 않다"는 케이스가 존재하기 때문입니다. 플랫포머가 제공하는 소셜 그래프는 현실에 가까운 관계성을 반영한 것이고, 게임 고유의 소셜 그래프는 그 게임 내에서의 관계성을 반영한 것으로 취급하면, 플레이어는 더 부담 없이 친구 기능을 이용하게 됩니다.
친구
친구가 되기 위해서는 쌍방향의 동의 관계가 필요합니다.
친구 관계를 구축하고자 하는 플레이어 중 한쪽이 상대방에게 친구 요청을 보내고, 친구 요청을 받은 플레이어가 이를 승낙함으로써 친구 관계가 성립합니다.
팔로우
팔로우는 상대방의 동의 없이 관계를 구축할 수 있습니다.
팔로우당하고 있는 사람은 자신을 누가 팔로우하고 있는지를 목록으로 알 수 있는 방법이 없습니다. 이 사양은 소셜 네트워크의 팔로우 사양을 떠올리면 위화감이 들 수도 있습니다. 그러나 게임 내에서 팔로우 기능을 구현하고 있는 많은 게임의 사양을 살펴본 결과, 그 필요성이 없다고 판단했습니다.
자신을 팔로우하고 있는 플레이어가 자신의 고스트 캐릭터와 함께 모험을 떠난 경우, 모험에서 얻은 보상을 나누어 주기만 하면 충분하며, 누가 함께 모험해 주었는지에는 그다지 관심이 없었기 때문입니다.
목록으로 조회할 수는 없지만, 모험에서 얻은 보상을 나누어 주는 처리로서 GS2-Inbox에 보상이 포함된 메시지를 전달할 때 메시지의 페이로드에 함께 모험을 떠난 플레이어의 사용자ID를 실음으로써, 프로필을 전달하는 것 자체는 가능합니다.
프로필
GS2-Friend는 플레이어의 프로필을 보관하는 영역을 제공합니다. 프로필에는 임의의 값을 보관할 수 있으며, 공간이 3곳 존재합니다.
- 다른 플레이어가 자유롭게 참조할 수 있는 “퍼블릭 프로필”
- 해당 플레이어를 팔로우하고 있는 플레이어가 참조할 수 있는 “팔로워 프로필”
- 친구가 참조할 수 있는 “친구 프로필”
각각 용도에 맞게 구분하여 사용할 수 있습니다.
블랙리스트
게임을 오래 플레이하다 보면 불쾌하게 느껴지는 플레이어가 있을 수 있습니다. 그러한 플레이어를 목록화하여 영속화하는 기능입니다.
어디까지나 영속화하는 기능이 존재할 뿐이며, 여기에 추가하는 것만으로는 아무런 기능도 하지 않습니다. GS2-Matchmaking의 매치메이킹 조건에 목록을 전달하는 등, 별도로 여기에 기록한 목록을 필요에 따라 사용해야 합니다.
스크립트 트리거
네임스페이스에 followScript·unfollowScript·sendRequestScript·cancelRequestScript·acceptRequestScript·rejectRequestScript·deleteFriendScript·updateProfileScript를 설정하면, 팔로우나 친구 요청, 프로필 갱신 등 각 조작의 전후로 커스텀 스크립트를 실행할 수 있습니다. 스크립트는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기에서는 GS2-Script나 Amazon EventBridge를 통한 외부 연계에도 대응합니다. 이러한 설정은 GS2-Deploy나 각 언어용 CDK로 템플릿화하여 관리할 수 있습니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.
followScript(완료 알림:followDone): 팔로우 전후unfollowScript(완료 알림:unfollowDone): 팔로우 해제 전후sendRequestScript(완료 알림:sendRequestDone): 친구 요청 전송 전후cancelRequestScript(완료 알림:cancelRequestDone): 친구 요청 취소 전후acceptRequestScript(완료 알림:acceptRequestDone): 친구 요청 승낙 전후rejectRequestScript(완료 알림:rejectRequestDone): 친구 요청 거부 전후deleteFriendScript(완료 알림:deleteFriendDone): 친구 삭제 전후updateProfileScript(완료 알림:updateProfileDone): 프로필 갱신 전후
푸시 알림
설정할 수 있는 주요 푸시 알림과 설정명은 다음과 같습니다.
followNotification: 팔로우되었을 때 알림receiveRequestNotification: 친구 요청 수신 시 알림cancelRequestNotification: 친구 요청이 취소되었을 때 알림acceptRequestNotification: 친구 요청 승낙 시 알림rejectRequestNotification: 친구 요청 거부 시 알림deleteFriendNotification: 친구 삭제 시 알림
모두 GS2-Gateway를 경유하여 리얼타임으로 알림을 보낼 수 있으며, 오프라인 단말로의 모바일 푸시 알림 전송도 활성화할 수 있습니다.
트랜잭션 액션
GS2-Friend에서는 다음과 같은 트랜잭션 액션을 제공하고 있습니다.
- 획득 액션: 프로필 갱신
“프로필 갱신"을 획득 액션으로 이용함으로써, 게임 내 화폐를 소비하여 플레이어 이름을 변경하는 등, 일련의 트랜잭션 안에 프로필 갱신을 포함시킬 수 있습니다.
구현 예제
프로필 갱신
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Profile(
).UpdateProfileAsync(
publicProfile: "public",
followerProfile: "follower",
friendProfile: "friend"
);
var item = await result.ModelAsync(); const auto Domain = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Profile(
);
const auto Future = Domain->UpdateProfile(
"public", // publicProfile
"follower", // followerProfile
"friend" // friendProfile
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
// obtain changed values / result values
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).profile(
)
var async_result = await domain.update_profile(
"public", # public_profile
"follower", # follower_profile
"friend" # friend_profile
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result자신의 프로필 조회
var item = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Profile(
).ModelAsync(); const auto Domain = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Profile(
);
const auto item = Domain.Model();var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).profile(
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result다른 사람의 공개 프로필 조회
var item = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).User(
userId: "user-0001"
).PublicProfile(
).ModelAsync(); const auto Domain = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->User(
"user-0001" // userId
)->PublicProfile(
);
const auto item = Domain.Model();var domain = ez.friend.namespace_(
"namespace-0001"
).user(
"user-0001"
).public_profile(
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result친구 요청 전송
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SendRequestAsync(
targetUserId: "user-0002"
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->SendRequest(
"user-0002"
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session)
var async_result = await domain.send_request(
"user-0002" # target_user_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result전송한 친구 요청 목록 조회
var items = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SendRequestsAsync(
).ToListAsync(); const auto It = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->SendRequests(
);
TArray<Gs2::UE5::Friend::Model::FEzFriendRequestPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.friend.namespace_(
"namespace-0001"
).me(
game_session
).send_requests(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result수신한 친구 요청 목록 조회
var items = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).ReceiveRequestsAsync(
).ToListAsync(); const auto It = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->ReceiveRequests(
);
TArray<Gs2::UE5::Friend::Model::FEzFriendRequestPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.friend.namespace_(
"namespace-0001"
).me(
game_session
).receive_requests(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result친구 요청 승낙
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).ReceiveFriendRequest(
fromUserId: "user-0002"
).AcceptAsync(
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->ReceiveFriendRequest(
nullptr, // targetUserId
"user-0002" // fromUserId
)->Accept(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).receive_friend_request(
"user-0002"
)
var async_result = await domain.accept(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result친구 요청 거부
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).ReceiveFriendRequest(
fromUserId: "user-0002"
).RejectAsync(
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->ReceiveFriendRequest(
nullptr, // targetUserId
"user-0002" // fromUserId
)->Reject(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).receive_friend_request(
"user-0002"
)
var async_result = await domain.reject(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result친구 목록 조회
var items = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).FriendsAsync(
).ToListAsync(); const auto It = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Friends( // withProfile
);
TArray<Gs2::UE5::Friend::Model::FEzFriendUserPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.friend.namespace_(
"namespace-0001"
).me(
game_session
).friends(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result친구 삭제
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Friend(
withProfile: null
).FriendUser(
targetUserId: "user-0002"
).DeleteFriendAsync(
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Friend(
nullptr // withProfile
)->FriendUser(
"user-0002" // targetUserId
)->DeleteFriend(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).friend(
null
)
var async_result = await domain.delete_friend(
"user-0002" # target_user_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result팔로우
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).FollowUser(
targetUserId: "user-0002",
withProfile: true
).FollowAsync(
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->FollowUser(
"user-0002", // targetUserId
true // withProfile
)->Follow(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).follow(
true
).follow_user(
"user-0002"
)
var async_result = await domain.model()
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result팔로우 중인 플레이어 목록 조회
var items = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).FollowsAsync(
).ToListAsync(); const auto It = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Follows(
);
TArray<Gs2::UE5::Friend::Model::FEzFollowUserPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.friend.namespace_(
"namespace-0001"
).me(
game_session
).follows(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result언팔로우
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).FollowUser(
targetUserId: "user-0002",
withProfile: true
).UnfollowAsync(
); const auto Domain = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->FollowUser(
"user-0002", // targetUserId
true // withProfile
);
const auto Future = Domain->Unfollow(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError())
{
return false;
}var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).follow(
null
).follow_user(
"user-0002"
)
var async_result = await domain.unfollow(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result블랙리스트에 등록
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).BlackList(
).RegisterBlackListAsync(
targetUserId: "user-0002"
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->BlackList(
)->RegisterBlackList(
"user-0002" // targetUserId
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).black_list(
)
var async_result = await domain.register_black_list(
"user-0002" # target_user_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result블랙리스트에서 해제
var result = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).BlackList(
).UnregisterBlackListAsync(
targetUserId: "user-0002"
); const auto Future = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->BlackList(
)->UnregisterBlackList(
"user-0002" // targetUserId
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.friend.namespace_(
"namespace-0001"
).me(game_session).black_list(
)
var async_result = await domain.unregister_black_list(
"user-0002" # target_user_id
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result블랙리스트 목록 조회
var items = await gs2.Friend.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).BlackListUsersAsync(
).ToListAsync(); const auto It = Gs2->Friend->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->BlackListUsers(
);
TArray<FString> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.friend.namespace_(
"namespace-0001"
).me(
game_session
).black_list_users(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result상한 인원수
친구 수 및 팔로우 수의 상한은 각각 1000명입니다. 단, 연동하는 서비스에 따라서는 각 서비스의 정책에 따라 추가 제한이 붙을 수 있습니다.