GS2-Chat
GS2-Chat 을 이용하면 게임 내에 텍스트 채팅을 손쉽게 구현할 수 있습니다. 텍스트 채팅은 새로운 메시지를 감지하기 위해 상시 접속 세션이 필요하며, 서버에 요구되는 요건도 복잡해집니다.
Game Server Services 를 이용하면 그러한 번거로운 상시 접속 세션 관리로부터 해방될 수 있습니다.
룸
GS2-Chat 을 이용하려면 먼저 룸을 생성해야 합니다. 룸은 플레이어가 자유롭게 생성할 수 있도록 할 수도 있고, 개발 측에서 준비한 룸을 플레이어가 사용하게 할 수도 있습니다. 또한 게시할 수 있는 사용자ID의 화이트리스트를 설정하여 특정 멤버 전용 룸을 구성하는 것도 가능합니다.
한 가지 주의해야 할 점은, GS2-Chat 은 룸당 1초에 3건까지만 메시지 게시를 보장한다는 것입니다. 이를 초과하는 빈도로 메시지를 게시하면 오류가 발생할 수 있습니다. 따라서 소셜 네트워크의 타임라인처럼 대량의 플레이어를 하나의 룸에 담는 용도로는 적합하지 않습니다.
비밀번호
룸에는 비밀번호를 설정할 수 있습니다. 비밀번호가 설정된 룸의 메시지를 가져오거나 메시지를 게시할 때는 비밀번호 지정이 필요합니다.
룸 설정 갱신
룸을 생성한 후에도 메타데이터, 비밀번호, 화이트리스트(게시 가능한 사용자ID 목록)를 갱신할 수 있습니다. 이를 통해 룸의 접근 권한을 동적으로 변경하거나 룸의 속성 정보를 변경할 수 있습니다.
구독
플레이어는 룸을 구독함으로써 룸에 게시되는 새로운 메시지에 대한 알림을 받을 수 있게 됩니다. 메시지에는 카테고리를 설정할 수 있으며, 룸 내 카테고리별로 구독이 가능합니다.
길드 채팅을 구현할 때 길드 마스터의 메시지에만 특별한 카테고리를 설정하여, 길드 멤버가 길드 마스터의 메시지만은 반드시 구독하도록 만드는 것도 가능합니다.
구독 설정은 이후에도 갱신할 수 있습니다. 수신할 카테고리의 추가·삭제나 오프라인 시 모바일 푸시 알림 전송 설정을 동적으로 변경할 수 있습니다.
구독에 따른 알림 처리는 GS2-Gateway 의 알림 기능이 사용되는데, GS2-Gateway 에는 알림 대상 플레이어가 오프라인인 경우 모바일 푸시 알림 등 게임 외부의 방법으로 알림을 보내는 기능이 마련되어 있습니다.
하나의 룸을 구독할 수 있는 인원수에 명확한 제한은 없지만, 알림을 전송하기 위해 GS2-Gateway 의 API 요청이 발생하므로 GS2 API 이용 요금이 알림 대상 1건마다 발생합니다. 또한 알림 대상이 많아지면 알림이 도착하기까지 걸리는 시간이 길어집니다.
룸 이름을 지정하여 해당 룸을 현재 누가 구독하고 있는지 목록을 가져올 수도 있습니다.
actor Player1
actor Player2
participant "GS2-Chat#Room"
participant "GS2-Gateway#Namespace"
participant "Firebase Cloud Messaging"
Player1 -> "GS2-Chat#Room" : Subscribe
Player2 -> "GS2-Chat#Room" : Post Message
"GS2-Chat#Room" -> "GS2-Gateway#Namespace" : Send Notification to Player1
"GS2-Gateway#Namespace" -> "Firebase Cloud Messaging" : Notification(if player is offline)
"GS2-Gateway#Namespace" -> Player1 : Notification
Player1 -> "GS2-Chat#Room" : Load New Message메시지
룸에 메시지를 전송할 수 있습니다.
메시지 보관 기간은 네임스페이스 설정 messageLifeTimeDays 로 1~30일 범위에서 지정할 수 있으며, 보관 기간을 초과한 메시지는 삭제됩니다.
또한 최신 메시지를 일정 건수만큼 가져오는 기능도 마련되어 있습니다. 이를 통해 룸에 입장한 직후의 과거 로그 표시 등을 효율적으로 처리할 수 있습니다.
금칙어
메시지 페이로드에 부적절한 단어가 포함되어 있을 수 있습니다. GS2-Script 와 연계하면 부적절한 단어가 포함된 메시지의 게시를 거부하거나, 메시지 페이로드를 재작성할 수 있습니다.
GS2-Script 는 HTTP 통신을 발행할 수 있으므로, 자체적으로 보유한 금칙어 체크 서버로 포워딩하여 처리하는 것도 가능합니다.
actor Player
participant "GS2-Chat#Room"
participant "GS2-Script#Script"
Player -> "GS2-Chat#Room" : Post Message
"GS2-Chat#Room" -> "GS2-Script#Script" : Trigger post message
"GS2-Script#Script" -> "GS2-Chat#Room" : Continue? / Replaced Message카테고리 모델과 알림 설정
메시지 카테고리는 마스터 데이터로 정의할 수 있으며, 카테고리별로 액세스 토큰을 사용한 게시를 거부할지 여부나 오프라인 시 모바일 푸시 알림으로 전송할지 여부를 설정할 수 있습니다.
트랜잭션 액션
GS2-Chat 에서는 트랜잭션 액션을 제공하지 않습니다.
메시지 게시의 스크립트 트리거에서 다른 마이크로서비스의 API를 호출함으로써, 특정 메시지 게시에 따라 사용자 데이터를 재작성하는 등의 연계는 가능합니다.
스크립트 트리거
룸 생성·삭제, 메시지 게시, 구독 및 구독 해제 각 처리의 전후에 GS2-Script 를 호출할 수 있습니다. 동기 실행으로 검증이나 변조를 수행하거나, 비동기 실행으로 Amazon EventBridge 를 경유한 외부 연계를 구성할 수도 있습니다.
설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.
createRoomScript(완료 알림:createRoomDone): 룸 생성의 전후deleteRoomScript(완료 알림:deleteRoomDone): 룸 삭제의 전후postMessageScript(완료 알림:postMessageDone): 메시지 게시의 전후subscribeRoomScript(완료 알림:subscribeRoomDone): 룸 구독의 전후unsubscribeRoomScript(완료 알림:unsubscribeRoomDone): 구독 해제의 전후
푸시 알림
설정할 수 있는 주요 푸시 알림과 설정 이름은 다음과 같습니다.
postNotification: 구독 중인 룸에 새로운 게시물이 있을 때 알림
알림 카테고리는 notificationTypes 로 제어할 수 있으며, 오프라인 단말에 대한 모바일 푸시 전송 설정도 가능합니다.
구현 예제
룸 생성
var result = await gs2.Chat.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).CreateRoomAsync(); const auto Domain = Gs2->Chat->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
);
const auto Future = Domain->CreateRoom(
"room-0001", // name
nullptr, // metadata
nullptr, // password
nullptr // whiteListUserIds
);
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.chat.namespace_(
"namespace-0001"
).me(game_session)
var async_result = await domain.create_room(
"room-0001", # name
null, # metadata
null, # password
null # white_list_user_ids
)
if async_result.error != null:
if async_result.error.type == "NoAccessPrivilegesException":
# 룸에 설정된 화이트리스트에 로그인 중인 사용자가 포함되어 있지 않습니다
pass
push_error(str(async_result.error))
return
var result = async_result.result룸 구독
var result = await gs2.Chat.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Subscribe(
roomName: "room-0001"
).SubscribeAsync(
notificationTypes: new [] {
new Gs2.Unity.Gs2Chat.Model.EzNotificationType {
category = 0,
enableTransferMobilePushNotification = false,
},
}
); const auto Domain = Gs2->Chat->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Subscribe(
"room-0001" // roomName
);
const auto Future = Domain->Subscribe(
[]
{
const auto v = MakeShared<TArray<TSharedPtr<Gs2::Chat::Model::FNotificationType>>>();
v->Add(MakeShared<Gs2::Chat::Model::FNotificationType>());
return v;
}() // notificationTypes
);
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.chat.namespace_(
"namespace-0001"
).me(game_session).subscribe(
"room-0001"
)
var async_result = await domain.subscribe(
[
(Gs2ChatEzNotificationType.new()),
] # notification_types
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result룸 구독 해제
var result = await gs2.Chat.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Subscribe(
roomName: "room-0001"
).UnsubscribeAsync(
); const auto Domain = Gs2->Chat->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Subscribe(
"room-0001" // roomName
);
const auto Future = Domain->Unsubscribe(
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.chat.namespace_(
"namespace-0001"
).me(game_session).subscribe(
"room-0001"
)
var async_result = await domain.unsubscribe(
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.result메시지 게시
var result = await gs2.Chat.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Room(
roomName: "room-0001",
password: null
).PostAsync(
metadata: "MESSAGE_0001",
category: 0
); const auto Domain = Gs2->Chat->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Room(
"room-0001", // roomName
nullptr // password
);
const auto Future = Domain->Post(
"MESSAGE_0001", // metadata
0 // category
);
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.chat.namespace_(
"namespace-0001"
).me(game_session).room(
"room-0001",
null
)
var async_result = await domain.post(
"MESSAGE_0001", # metadata
null # category
)
if async_result.error != null:
if async_result.error.type == "NoAccessPrivilegesException":
# 룸에 설정된 화이트리스트에 로그인 중인 사용자가 포함되어 있지 않습니다
pass
if async_result.error.type == "PasswordRequiredException":
# 룸에 접근하려면 비밀번호 지정이 필요합니다
pass
if async_result.error.type == "PasswordIncorrectException":
# 룸에 설정된 비밀번호와 지정한 비밀번호가 일치하지 않습니다
pass
push_error(str(async_result.error))
return
var result = async_result.result메시지 목록 가져오기
var items = await gs2.Chat.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Room(
roomName: "room-0001",
password: null
).MessagesAsync(
).ToListAsync(); const auto Domain = Gs2->Chat->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Room(
"room-0001", // roomName
nullptr // password
);
const auto It = Domain->Messages(
);
TArray<Gs2::UE5::Chat::Model::FEzMessagePtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}var iterator = ez.chat.namespace_(
"namespace-0001"
).me(
game_session
).room(
"room-0001",
null
).messages(
)
var async_result = await iterator.load()
if async_result.error != null:
# 에러를 처리
push_error(str(async_result.error))
return
var items = async_result.result메시지 수신 알림 핸들링
MessagesAsync 는 캐시가 있는 경우 캐시를 응답하며, 메시지 목록에서 가져올 수 있는 데이터의 캐시는 메시지 수신 알림을 받았을 때 SDK 에 의해 자동으로 갱신됩니다. 따라서 일반적으로는 이 이벤트를 핸들링할 필요가 없으며, 필요한 시점에 MessagesAsync 를 호출하면 보통 캐시를 이용하여 최신 메시지 목록에 접근할 수 있습니다.
gs2.Chat.OnPostNotification += PostNotification =>
{
var namespaceName = PostNotification.NamespaceName;
var roomName = PostNotification.RoomName;
}; Gs2->Chat->OnPostNotification().AddLambda([](const auto Notification)
{
const auto NamespaceName = Notification->NamespaceNameValue;
const auto RoomName = Notification->RoomNameValue;
});ez.gs2.notification_received.connect(func(message):
if message.subject != "Gs2Chat:PostNotification":
return
var notification = message.parse()
if notification == null:
return
var namespace_name = notification.namespace_name
var room_name = notification.room_name
var user_id = notification.user_id
var category = notification.category
var created_at = notification.created_at
)기타 기능
클라이언트에서 게시할 수 없는 카테고리
마스터 데이터의 CategoryModel 에서 rejectAccessTokenPost 를 true 로 설정하면 해당 카테고리에는 액세스 토큰을 사용한 게시가 불가능해집니다. 이 기능을 활용하면 스크립트 등을 경유해서만 게시할 수 있는 카테고리를 준비하여, 시스템 메시지를 저장하는 용도로 사용할 수 있습니다.
플레이어에 의한 룸 생성 가능 여부
네임스페이스 설정의 allowCreateRoom 을 이용하면 플레이어가 자유롭게 룸을 생성할 수 있는지 여부를 관리 측에서 제어할 수 있습니다.
allowCreateRoom=true : 플레이어가 직접 룸을 생성 가능 allowCreateRoom=false : 운영 측이 준비한 룸만 이용 가능
대규모 룸 난립을 방지하고 싶거나 특정 룸만 허용하고 싶은 경우, 이 설정을 적절히 조정하시기 바랍니다.
메시지 카테고리 활용 예
메시지에는 카테고리를 설정할 수 있습니다. 카테고리를 사용하면 클라이언트 측에서 페이로드 파싱 방법을 전환할 수 있습니다.
예를 들어 다음과 같은 사용 방법이 가능합니다.
카테고리 0: 텍스트가 페이로드에 그대로 포함되는 일반 텍스트 메시지카테고리 1: 페이로드에 스탬프 ID가 포함되는 스탬프 메시지
이처럼 카테고리를 메시지의 종류나 중요도별로 구분함으로써 게임 플레이어가 받는 알림의 종류를 스스로 설정할 수 있게 되어 사용자 경험을 향상시킬 수 있습니다.
마스터 데이터 관리
마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터와 동작을 설정할 수 있습니다.
마스터 데이터의 종류에는 다음이 있습니다.
CategoryModel: 카테고리 번호별 게시 가능 여부(rejectAccessTokenPost) 및 오프라인 시 모바일 푸시 알림 전송 설정
마스터 데이터 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub 에서 데이터를 반영하거나 GS2-Deploy 를 사용하여 CI 에서 등록하는 워크플로우를 구성하는 것도 가능합니다.