Amazon GameLift를 통한 맞춤형 서버리스 매치메이킹 서비스 만들기
세션 기반의 멀티플레이어 게임에서 가장 중요한 요소의 하나는 사용자의 숙련도, 접속 속도, 위치 등의 제약에서 벗어나면서 효율적이고 지능적으로 사용자들에게 재미있고 도전할만한 게임 매치를 제공할 수 있는지 여부일 것입니다. 시스템은 이전의 모든 경기 이력을 바탕으로 안정적이고 유연하게 성공적인 멀티플레이어 경험을 제공하는 것이 목표입니다.
2017년 GDC(Game Developers Conference)에서 Amazon GameLift의 Chris Byskal과 Geoff Pare GameLift를 통하여 내구성 있는 온라인 게임을 만드는 것에 대한 세션을 진행 했었습니다. 여기에서 Chris와 Geoff는 아마존 GameLift롤 통하여 클라우드 환경에서 다양한 형태의 게임을 구성하는 과정을 단순화 할 수 있는지를 이야기했습니다. GameLift를 통하여 어떤 방법으로 수천 시간의 개발 시간을 줄이고, 유휴서버를 줄이고, DDoS공격에서 보호하고, 그리고 매치메이킹과 자동화된 스케일링을 지원할 수 있는지를 설명했습니다.
이 블로그 글은 Chris와 Geoff의 발표에서 거론되었던 플레이어 매치 메이킹의 패턴에 대하여 좀 더 자세히 살펴보고, 그리고 게임에 따라 독자적으로 사용할 수 있는 사용자 사이의 매치메이킹 알고리즘, 그리고 사용자들을 서버에 연결할 수 있는 아키텍쳐를 살펴볼 것입니다. 덤으로, 실제 여러분의 게임에서 사용할 수 있는 사용자 정의 맞춤형 매치메이킹 코드 예제를 살펴보도록 하겠습니다.
이러한 형태의 서버리스 접근 방법은 많은 장점을 가지고 있습니다. 전통적인 환경에서 접할 수 있는 (다른 회사와 비교해서 차별화되지 않는) 통상업무 부담을 줄여줄 수 있습니다. 그리고 복잡한 백앤드의 구축을 단순화하여 게임 로직 자체에 집중 할 시간을 늘려 준다는 점이 가장 중요하다고 할 수 있습니다.
플레이어 매칭 패턴에 대하여
최근의 멀티 플레이어 게임은 크게 두가지 경향을 보이고 있습니다. 첫번째는 사용자가 직접 접속할 서버를 찾아서 선택하는 방법, 두번째는 매치 메이킹 서버를 통하여 자동으로 여러 사용자들을 연결해주는 형태입니다.
사용자가 직접 서버를 선택하는 방법은 구현하기가 상대적으로 간단합니다. 사용자에게 게임을 할 수 있는 서버의 목록을 전달하는 것이 전부입니다.
그림-1 : 서버 목록 찾기의 예제
만일 개발자가 이러한 형태로 구현할 경우, GameLift는 다음의 3가지 API콜을 이용하여 클라이언트에서 이러한 방법을 쉽게 구현 할 수 있는 몇 가지 방법을 제공하고 있습니다.
- 게임 서버의 목록 취득 – GameLift는 사용자가 지정하는 검색 항목을 사용한 검색을 지원합니다. 서버 브라우징을 사용 하는 경우, 모든 게임 세션을 검색 결과로 보여주거나 현재 사용 가능한 세션만 결과로 보여줄 수가 있습니다. 더욱 자세한 사항은 해당 API문서를 참고하시면 됩니다. (Amazon GameLift SearchGameSessions() API documentation)
- 지정한 게임에 참가 – 사용자는 자신이 속한 그룹이나 길드와 함께 특정한 게임에 참가하는 것이 가능합니다. 일단 사용자가 게임 세션을 지정하면 시스템은 그 사용자를 해당 게임에 참가 시킵니다. 만일 게임 세션에 추가로 참가 할 수 있는 여유가 있으면 GameLift가 해당 슬롯들을 선점하여 해당 정보를 전달하게 됩니다. 이러한 방법으로 사용자가 게임 세션을 선택하면, 참가 신청을 하는 순간 모든 세션이 가득 찰 수도 있게 됩니다. 좀 더 자세한 사항은 관련 문서를 참고하시기 바랍니다. (Amazon GameLift CreatePlayerSession() API Reference.)
- 새로운 게임 시작 – GameLift는 사용자 요청에 의하여 새로운 게임 세션을 만들 수도 있습니다. 생성이 되면 해당 게임 세션은 서버 목록에서 공개적으로 검색이 될 수 있습니다. 사용자는 또한 공개되지 않는 비공개 게임 세션을 만들 수도 있습니다. 자세한 사항은 관련 문서를 참고 하시기 바랍니다. (Amazon GameLift CreateGameSession() API Reference)
서버 검색 방식은 간단하고 사용자들이 직접 목록에서 원하는 게임을 선택할 수 있는 기회를 제공합니다. 또한 개발자들에게도 구현이 간단하다는 장점이 있습니다. 하지만 이러한 간단하고 직관적인 형태로는 사용자들에게 최적의 게임 경험을 제공 할 수 없을 수도 있습니다. 사용자의 팀/ 숙련도, 또는 기타 게임의 중요한 요소들을 고려하지 않는 매치들은 균형 잡히지 않은 일방적인 게임진행으로 참가한 모든 이들에게 즐겁지 않은 경험만 남길 수도 있습니다.
서버 검색 방식은 또한 여러분의 게임 인프라를 전체적으로 균형 잡힌 활용을 어렵게 할 수 있습니다. 왜냐하면 사용자들은 인프라의 상태를 고려할 수 없고 이로 인하여 특정 지역의 서버에 몰리는 현상을 가져올 수 있기 때문입니다. 이는 결과적으로 인프라 전반적으로 사용자 분배를 어렵게 하고, 또한 불필요하게 높은 비용을 가져다 줄 수 있습니다.
매치메이킹은 다른 접근법을 활용합니다. 사용자가 게임에 참가 요청을 하면 매치메이킹 알고리즘을 통하여 다양한 변수 – 사용자의 숙련도, 서버 레이턴시, 친구목록, 팀/ 그룹등 – 를 고려하여 매치를 선택해줍니다. 사용자들은 따라서 조금 더 자신과 비슷한 실력의 상대와 게임을 즐길 수 있습니다.
또한 이 방법은 사용자들을 게임 서버 사이에 더더욱 효율적으로 분배 할 수 있고 효율적인 세션 활용을 통하여 운영비용을 줄일 수가 있습니다. 매치메이킹의 단점은 구현이 더욱 복잡하다는 점입니다.
서버리스로 매치메이킹 시스템 만들기
매치매이킹을 사용하기로 결정하면 가장 먼저, 매치메이킹 시스템을 만들어야 합니다. 여기에서는 간단하게 서버리스로 맞춤형 메치메이킹을 어떻게 구현할 수 있는지를 살펴보도록 하겠습니다. 아래의 그림 2의 간단한 아키텍쳐를 그리고 있습니다. 이 아키텍쳐 안에는
- 사용자 정의 변수, 또는 알고리즘을 사용한 매치메이킹 시스템
- 게임서버 관리
- 게임세션 관리
- 서버 인스턴스의 오토스케일링
- 게임 연결 흐름도
들을 보여주고 있습니다.
이 서버레스 매치메이킹 시스템은 크게 3단계로 구현되고 있습니다.
그림 2 – 서버레스로 구현한 맞춤형 매치메이킹 아키텍쳐
- 게임 참가 요청
첫번째 단계에서 사용자는 게임 클라이언트를 통하여 게임 참가 요청을 합니다. 게임 클라이언트는 Amazon API Gateway의 엔드포인트를 호출합니다. 이 엔드포인트는 우리의 매치메이킹 로직을 실행하는 람다함수를 호출하고, 이 람다 함수는 게임리프트와 연동하여 적합한 게임 세션을 찾아줍니다. API게이트웨이는 개발자들이 API를 만들고 안전하게 관리할 수 있도록 도와주는 관리형 서비스입니다. API게이트웨이를 사용하는 것은 게임 클라이언트와 람다함수/ 게임리프트사이를 추상화 할 수 있기 때문입니다. 이는 아래와 같은 상황에 대한 유연성을 가져다 줍니다.- 버전관리 – 게임 클라이언트는 백앤드의 변화에 대하여 종속될 필요가 없습니다. 이는 몇가지 장점을 가지는데, 새로운 기술을 적용하기 쉬워집니다. 또한 새로운 게임리프트의 적용을 부드럽게 진행할 수 있습니다, 그리고 A-B테스팅을 쉽게 적용할 수 있습니다.
- 운영 메트릭 확보 – API 게이트웨이와 Amazon CloudWatch를 함께 활용하여 제작하는 API의 퍼포먼스의 확인으로 매치메이킹 서비스에서 발생할 수 있는 다양한 문제들을 빨리 파악할 수 있도록 도와줍니다.
- 보안 – API 게이트웨이와 함께 Amazon Cognito와 같은 AWS의 다양한 보안 도구를 활용하여 API의 활용을 인증을 거친 활용의 제어를 손쉽게 활용할 수 있습니다. 이를 통하여 손쉽게 구현하고 관리 할 수 있는 인증 시스템을 제공 할 수 있습니다.
- 게임 찾기
두번째 단계에서 우리는 AWS람다(AWS Lambda) 함수를 활용하여 매치메이킹을 구현할 것입니다. 람다를 통하여 서버를 할당하거나 관리할 필요없이 여러분의 코드를 클라우드상에서 실행할 수 있습니다. 람다에 코드를 배포하는 것은 여러분의 코드를 단순히 업로드 하는 것으로 구현됩니다. 람다는 이후, 코드의 실행과 스케일링에 필요로 하는 여러사항들을 자동으로 관리하게 됩니다. 여러분의 코드는 또한 다양한 AWS서비스에서 호출되거나 다른 웹서비스, 앱에서 호출될 수 있습니다. 매치메이킹은 실행시간이 짧고 비교적 자주 호출 되기 때문에 이러한 환경에서 상당히 효율적인 방법일 수가 있습니다. 서버 관리는 최소한으로 유지하면서 반대로 가용성은 최대로 유지됩니다. 람다의 사용료는 호출횟 수, 그리고 각 호출에서 실행 시간 100ms당 책정됩니다. 여러분은 또한 여러 버전의 프로세스를 함께 실행할 수도 있습니다. 이를 통하여 폭 넓은 게임클라이언트와 많은 사용자들을 지원할 수도 있습니다. 이러한 점을 염두에 두고 3가지 람다 함수를 만들어보겠습니다.- 매치 메이킹에 들어가기 – 이 함수는 게임 클라이언트로부터의 게임 참가 요청을 처리합니다. 이 함수는 요청을 받고, 동시에 클라이언트로부터 전달되는 필요 정보를 파악합니다. 그리고 이 정보들을 아마존DynamoDB(Amazon DynamoDB)의 게임 참가 대기자 테이블에 추가합니다. 이 함수는 그리고 클라이언트에게 매치메이킹이 진행중이라고 알려줍니다. (그리고 클라이언트는 사용자에게 그 사실을 전달하게 됩니다.)
- 메치메이커 – 이 함수는 주기적으로 실행되어 매칭할 수 있는 플레이어 그룹을 생성하고 그 그룹들이 새로운 게임 세션에 참가할 수 있도록 해줍니다. 여기에서 게임의 매치메이킹로직이 적용됩니다. 이 함수가 호출될 때마다 아까 만들었던 테이블의 대기자 사이에서 로직을 적용하여 가까운 그룹으로 분류합니다. 온전한 그룹이 만들어지면 해당 그룹은 게임 세션에 참가 가능하다고 분류됩니다. 함수는 이 정보를 아까의 DynamoDB테이블에 기록하고 종료됩니다. 아래의 그림 3에 이러한 로직에 대한 예제 코드가 있습니다.
- 서버 접속기 – 이 함수는 정기적으로 게임 서버에 할당할 수 있는 그룹을 확인하고, 게임 세션 생성 요청을 합니다. 이는 게임리프트의 CreateGameSession API를활용합니다.
def get_unmatched_players():
table = dynamodb.Table(table_name)
response = table.scan(
FilterExpression=Attr('MatchStatus').eq('Open')
)
players = response['Items']
print("Number of players watching for matches: " + str(len(players)))
return players
def create_groups(players):
print('Creating Groups')
groups = list()
# Sort players by skill
players = sorted(players, key=lambda player: player['Skill'])
# Group players into match sized groups
while (len(players) >= match_size):
new_group = {'Players': list()}
for i in range(0, match_size):
new_group['Players'].append(players.pop(0))
groups.append(new_group)
print("Number of groups created: " + str(len(groups)))
return groups
그림 3 – 매치메이킹 로직의 파이썬 예제
게임 서버 할당을 통하여, Amazon GameLift는 알고 있는 리전의 여러 서버 Fleet들 중에서 가장 적당한 호스팅 리소스를 할당합니다. 또한 새로운 게임 세션을 생성하여 앞의 그룹을 할당할 수 있습니다.
게임리프트에서 새로운 세션 할당 요청은 (game session queue)에 할당됩니다. 따라서 우리는 하나, 또는 그 이상의 서버 Fleet를(어떤 지역에 있어도 상관없습니다) 가지고 있는 큐를 만들고, 더불어 요청에 대하여 어느정도의 시간을 쓸지를 결정하는 타임아웃 수치를 지정합니다.
할당 요청을 처리할 때, 게임리프트는 큐에 할당 되어있는 모든 Fleet 대상으로 요청에 적합한지 살펴보고 응답하게 됩니다. 만일 적합한 Fleet이 없어서 판단하지 못할 경우 타임아웃이 발생하게 됩니다. 기본적으로 게임 리프트는 큐 구성에 나열 되어있는 순서대로 Fleet들을 살펴봅니다. 이를 통하여 새로운 게임 서버 구성 요청에 대하여 각 Fleet에 대한 우선도를 지정할 수도 있습니다.
아주 낮은 지연속도가 필요로 하는 게임의 경우, 게임리프트는 지연속도 정보를 바탕으로 그룹의 모든 사용자에 대하여 가장 낮은 평균 지연을 경험할 수 있는 지역으로 할당 할 수도 있습니다.
이러한 기능을 활용하기 위해서는 우리는 클라이언트로부터 모든 지역에 대한 지연 속도 정보를 모아야 합니다. 여러 사용자로부터 지연 속도 정보를 받으면, 게임 리프트는 큐의 서버 Fleet정보를 평균 Lag이 낮은 순서로 우선 순위를 변경하게 됩니다.
이 부분에 대한 자세한 내용은 이전의 블로그 글을 참고하세요
그림 4의 코드는 게임 세션 할당 요청에 대한 예제 코드입니다. 코드의 StartGameSessionPlacement() 함수는 큐 이름을 전달 받습니다. (이 큐 이름은 큐 구성에서 지정한 것입니다.)
게임리프트는 또한 고유한 할당 ID를 필요로 합니다. 우리는 이 ID를 활용하여 요청의 상태를 추적하게 됩니다. 개발자가 할당 ID를 정의하면 되고, 단지 상호간의 고유성만 보장되면 됩니다. 아래의 예제에서는 UUID를 사용하며, 호출자에게 전달되어 이후 처리됩니다.
부가적으로, 우리의 할당 요청은 세션의 게임과 플레이어 ID를 요청할 수도 있습니다. 만일 해당 정보가 전달되면, AWS콘솔을 통하여 게임 세션들을 사용자가 보는 것처럼 확인할 수 있습니다. 이를 통하여 언제 얼마나 사용자들의 활동이 있는 지를 확인할 수가 있습니다.
새로운 게임 세션 할당은 PENDING 상태로 만들어집니다. 아래의 예제 코드는 할당 요청에 대한 응답을 표시하게 되어있습니다. 할당 요청의 결과 상태에 따라서 추후 작업도 진행할 수 있습니다. 예를 들어 만일 PENDING 응답이 전달되면, 우리는 게임 클라이언트에게 곧 세션이 시작됨을 알리고 이후의 상태 확인 상태로 들어갈 수 있습니다. 만일 할당 요청이 타임 아웃이 발생하면 다른 큐를 통하여 할당 요청을 다시 보낼 수가 있을 것입니다.
def start_game_placement(queue_name, group):
print("Starting Game Session Placement")
placement_id = str(uuid.uuid4())
desiredPlayerSessions = list()
for player in group['Players']:
desiredPlayerSessions.append({
'PlayerId': player['PlayerId'],
'PlayerData': player['Skill']
})
response = gamelift.start_game_session_placement(
PlacementId=placement_id,
GameSessionQueueName=queue_name,
GameProperties=[
{
'Key': 'Skill_Level',
'Value': 'Highest'
},
],
MaximumPlayerSessionCount=match_size,
GameSessionName='My Matched MP Game',
DesiredPlayerSessions= desiredPlayerSessions
)
print("Game Session Status: " + response['GameSessionPlacement']['Status'])
return placement_id
def update_players_in_group_for_match(group):
for player in group['Players']:
update_player(player, group['PlacementId'], 'Matched')
def update_player(player, placement_id, status):
print('Updating Player with Placement Id and setting status to Matched')
table = dynamodb.Table(table_name)
response = table.update_item(
Key={
'PlayerId': player['PlayerId'],
'StartDate': player['StartDate']
},
UpdateExpression="set PlacementId = :g, MatchStatus= :t",
ExpressionAttributeValues={
':g': placement_id,
':t': status
},
ReturnValues="UPDATED_NEW"
)
그림 4 –게임리프트의 세션 할당의 예제 파이썬 코드
- 게임에 접속하기
세번째 단계에서 게임 클라이언트는 람다 함수와 게임리프트를 통하여 게임 세션에 관련한 상세한 정보를 수령합니다. 게임 클라이언트들은 이제 게임 서버에 직접 접속하여 게임을 시작할 수가 있습니다. 게임 클라이언트가 게임 서버에 직접 연결한다는 점은, 게임리프트가 게임 진행 자체에 어떠한 레이턴시도 더하지 않는다는 것을 의미합니다.
결론
멀티플레이어 게임은 계속해서 인기를 누리고 있습니다. 이러한 환경에서 성공하기 위해서는 빠르고, 부드러운 확장성과 함께 수백만 사용자들이 선호할 수 있는 높은 수준의 안정성을 지원 해야할 것입니다.
많은 멀티플레이어 게임은 개성적인 매치메이킹을 통하여 비슷한 수준의 사용자들과 함께 게임을 진행하는 최적의 경험을 제공할 수 있습니다. 많은 장르의 게임 사용자들은 자신과 유사한 수준의 상대와 수준 높은 매체메이킹을 기대하게 됩니다. 따라서 여러분의 게임의 성공을 위해서 매치메이킹은 아주 중요한 요소일 것입니다. 여기에서 거론된 서버리스 매치메이커 패턴은 여러분이 원하는 알고리즘을 활용하면서, 동시에 아마존의 게임리프트를 활용하여 여러분의 서버 리소소를 적극적으로 활용할 수 있는 가능성을 제시하고 있습니다.
게임리프트 세션 할당 기능은 위 예제에서 보여준 매치메이킹 방식을 지원하는 적합한 방법이며, 사용자에게 최고의 경험을 전달할 수 있습니다. 그리고 더불어 관리형 서비스의 하나로 게임리프트는 서버 Fleet의 유지관리를 전담하게 됩니다.
사용자의 수요에 따라서 자동적으로 용량을 조절하고 사용한 만큼 비용을 청구하게 됩니다. 이를 통하여 사용자에게 직접 영향을 주지 않으면서 비용 관리를 할 수 있도록 도와줍니다. 게임리프트는 또한 동시에 여러 버전의 Fleet들 을 동시에 구동하고 이들 사이를 Alias를 통하여 전환할 수 있도록 해줍니다. 이를 활용하기 위해서 필요한 SDK는 게임리프트 SDK(Amazon GameLift Server SDK)입니다. 관리는 관리 콘솔(AWS Management Console, AWS CLI), 또는 게임리프트 API(Amazon GameLift APIs)를 통하여 진행할 수 있습니다. (C++, C#, 그리고 몇가지 다른 언어로 제공됩니다.)
게임리프트는 미지의 수요에 대하여 걱정 없이 대응할 수 있는 기반을 제공하고 있으며, 이는 여러 개발자들이 독특하고 차별화된 게임 경험 그 자체에 집중할 수 있는 환경을 제공합니다. 이 서버리스 매치케이킹 패턴은 많은 사용자가 몰리더라도 유연하고 확장 가능한 백-엔드를 제공하며 인프라에 대한 많은 관리 부담을 줄여줄 수 있습니다. 결과적으로 게임 개발자, 운영자의 부담을 줄여주고 여러분의 게임을 즐기는 고객들에게 좋은 게임 경험을 제공할 수 있습니다.
피터 챕맨(Peter Chapman)은 Amazon Gamelift, Lumberyard 팀의 솔루션스 아키텍트입니다. 그는 12년 이상의 소프트웨어 개발 및 아키텍쳐경험을 가지고 있습니다. 게이밍 뿐만 아니라 건강/ 소매 다양한 분야에서 솔루션을 디자인한 경험을 가지고 있습니다. 이 글은 아마존웹서비스 코리아의 솔루션즈 아키텍트가 국내 고객을 위해 전해 드리는 AWS 활용 기술 팁을 보내드리는 코너로서, 김성수 솔루션즈 아키텍트께서 작성해주셨습니다.