グレンジ Advent Calendar 2019 20日目担当の中陳です。
弊社ではゲームマッチングシステムとしてOpen Matchを導入しています。
Open MatchはGoogleが開発しているオープンソースのゲームマッチメイキングフレームワークです。
Open Matchのアーキテクチャの説明については、公式ドキュメントを参照してください。
https://open-match.dev/site/docs/guides/matchmaker/
今年の「Google Cloud INSIDE Games & Apps」で発表した資料もよければご覧ください。
https://storage.googleapis.com/gweb-cloudnext19.appspot.com/event-assets/tokyo/materials/D1-5-I08.pdf
「Google Cloud Japan Customer Engineer Advent Calendar 2019」でも、12/22に「Open Match 超入門」が予定されているようなので、興味がある人はチェックしてみてください。
Open Matchの理解を深めるために、デモを動かしそのデータの流れを追ってみたいと思います。
データのやり取りはRedisを介して行われています。
準備
下記ドキュメントを参考に Core Open Match と Demo をインストールします。
# Install the core Open Match services.
kubectl apply --namespace open-match \
-f https://open-match.dev/install/v0.8.0/yaml/01-open-match-core.yaml
kubectl apply --namespace open-match \
-f https://open-match.dev/install/v0.8.0/yaml/06-open-match-override-configmap.yaml \
-f https://open-match.dev/install/v0.8.0/yaml/07-open-match-default-evaluator.yaml
kubectl create namespace open-match-demo
kubectl apply --namespace open-match-demo \
-f https://open-match.dev/install/v0.8.0/yaml/02-open-match-demo.yaml
kubectl port-forward --namespace open-match-demo service/om-demo 51507:51507
http://localhost:51507/ にアクセスすると、次のように表示されます。
{
"clients": {
"fakeplayer_0": {
"Status": "Sleeping (pretend this is playing a match...)",
"Assignment": {
"connection": "85.98.86.16:2222"
}
},
"fakeplayer_1": {
"Status": "Main Menu",
"Assignment": null
},
"fakeplayer_2": {
"Status": "Waiting match with ticket Id bnrnjbh4tpl76e14h460",
"Assignment": null
},
"fakeplayer_3": {
"Status": "Sleeping (pretend this is playing a match...)",
"Assignment": {
"connection": "85.98.86.16:2222"
}
},
"fakeplayer_4": {
"Status": "Waiting match with ticket Id bnrnjf14tpl76e14h470",
"Assignment": null
}
},
"director": {
"Status": "Sleeping",
"LatestMatches": [
{
"match_id": "profile-1v1-time-2019-12-16T12:33:59.73-num-0",
"match_profile": "1v1",
"match_function": "a-simple-1v1-matchfunction",
"tickets": [
{
"id": "bnrnjbp4tpl76e14h46g"
},
{
"id": "bnrnjb14tpl76e14h45g"
}
]
}
]
},
"uptime": 1374
}
プレイヤ (Client) が Open Match (Frontend) に対してリクエストを送り、マッチが完了するのを待ちます。
Director は Open Match (Backend) マッチ実行をリクエストし、完了したマッチに対してゲームサーバーの割り当てを行います。
下記デモのソースコードをベースに追っていきます。
Redisのデータの流れの確認には、MONITOR コマンドを使います。
127.0.0.1:6379> MONITOR
Client → Open Match (Frontend)
チケット作成
CreateTicketでチケット作成を行います。
s.Status = "Creating Open Match Ticket"
update(s)
var ticketId string
{
req := &pb.CreateTicketRequest{
Ticket: &pb.Ticket{},
}
resp, err := fe.CreateTicket(ctx, req)
if err != nil {
panic(err)
}
ticketId = resp.Ticket.Id
}
1576500156.947800 [0 10.1.13.146:55264] "MULTI"
1576500156.947826 [0 10.1.13.146:55264] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0"
1576500156.947901 [0 10.1.13.146:55264] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500156.947906 [0 10.1.13.146:55264] "EXEC"
1576500156.948580 [0 10.1.13.146:55264] "MULTI"
1576500156.948589 [0 10.1.13.146:55264] "SADD" "ic$bnrnnf14tpl76e14h5a0" "allTickets"
1576500156.948737 [0 10.1.13.146:55264] "ZADD" "allTickets" "0" "bnrnnf14tpl76e14h5a0"
1576500156.948750 [0 10.1.13.146:55264] "EXEC"
1576500159.893873 [0 10.1.13.146:59696] "MULTI"
1576500159.893893 [0 10.1.13.146:59696] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0"
1576500159.893903 [0 10.1.13.146:59696] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500159.893909 [0 10.1.13.146:59696] "EXEC"
1576500159.894889 [0 10.1.13.146:59696] "MULTI"
1576500159.894901 [0 10.1.13.146:59696] "SADD" "ic$bnrnnfp4tpl76e14h5b0" "allTickets"
1576500159.895011 [0 10.1.13.146:59696] "ZADD" "allTickets" "0" "bnrnnfp4tpl76e14h5b0"
1576500159.895022 [0 10.1.13.146:59696] "EXEC"
以下のデータが書き込まれています。
- チケットIDのキーにチケット情報
- チケットIDに紐付くインデックスキー
- インデックスキー(ここではallTickets)にチケットIDと検索値
マッチ完了待ち
GetAssignmentsでマッチ結果を待ちます。
結果のConnection文字列が設定されいる場合にマッチ完了となります。
s.Status = fmt.Sprintf("Waiting match with ticket Id %s", ticketId)
update(s)
var assignment *pb.Assignment
{
req := &pb.GetAssignmentsRequest{
TicketId: ticketId,
}
stream, err := fe.GetAssignments(ctx, req)
for assignment.GetConnection() == "" {
resp, err := stream.Recv()
if err != nil {
// For now we don't expect to get EOF, so that's still an error worthy of panic.
panic(err)
}
assignment = resp.Assignment
}
err = stream.CloseSend()
if err != nil {
panic(err)
}
}
1576500156.951293 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.051971 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.153294 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500157.254225 [0 10.1.13.146:47834] "GET" "bnrnnf14tpl76e14h5a0"
1576500159.897335 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500159.998534 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.100184 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
1576500160.200872 [0 10.1.13.146:34212] "GET" "bnrnnfp4tpl76e14h5b0"
チケットIDキーがループで参照されています。
チケットのAssignment情報に変更があった場合に、クライアントに返されます。
Director → Open Match (Backend)
マッチ実行
FetchMatchesを使って、マッチ実行及び、マッチ結果の受け取りを行います。
このデモでは、1v1の条件なしのマッチ実行を行っています。
s.Status = "Match Match: Sending Request"
ds.Update(s)
var matches []*pb.Match
{
req := &pb.FetchMatchesRequest{
Config: &pb.FunctionConfig{
Host: "om-function.open-match-demo.svc.cluster.local",
Port: 50502,
Type: pb.FunctionConfig_GRPC,
},
Profiles: []*pb.MatchProfile{
{
Name: "1v1",
Pools: []*pb.Pool{
{
Name: "Everyone",
},
},
},
},
}
stream, err := be.FetchMatches(ds.Ctx, req)
if err != nil {
panic(err)
}
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
matches = append(matches, resp.GetMatch())
}
}
1576500160.702074 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "proposed_ticket_ids" "1576500100701965766" "1576500160701965766"
1576500160.702482 [0 10.1.13.148:52540] "ZRANGEBYSCORE" "allTickets" "-Inf" "+Inf"
1576500160.703530 [0 10.1.13.148:52540] "MGET" "bnrnnf14tpl76e14h5a0" "bnrnnf94tpl76e14h5ag" "bnrnnfp4tpl76e14h5b0"
Frontendで書き込まれたインデックスキーから値の範囲を指定してチケット情報を取得しています。
ここでは検索条件がないので、allTicketsの全てを取得しています。
このデータがマッチングロジックに渡されます。
1576500165.703594 [0 10.1.13.147:47004] "MULTI"
1576500165.703622 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnfp4tpl76e14h5b0"
1576500165.703640 [0 10.1.13.147:47004] "ZADD" "proposed_ticket_ids" "1576500165703326682" "bnrnnf14tpl76e14h5a0"
1576500165.703664 [0 10.1.13.147:47004] "EXEC"
マッチングロジックから提案されたチケットIDが検索から除外されるように追加されています。
割り当て
マッチが完了したら、AssignTicketsでゲームサーバーの割り当てを行います。
ここではランダムなアドレスを設定しています。
s.Status = "Assigning Players"
ds.Update(s)
for _, match := range matches {
ids := []string{}
for _, t := range match.Tickets {
ids = append(ids, t.Id)
}
req := &pb.AssignTicketsRequest{
TicketIds: ids,
Assignment: &pb.Assignment{
Connection: fmt.Sprintf("%d.%d.%d.%d:2222", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)),
},
}
resp, err := be.AssignTickets(ds.Ctx, req)
if err != nil {
panic(err)
}
_ = resp
}
1576500165.730838 [0 10.1.13.157:35400] "GET" "bnrnnfp4tpl76e14h5b0"
1576500165.731933 [0 10.1.13.157:35400] "GET" "bnrnnf14tpl76e14h5a0"
1576500165.732428 [0 10.1.13.157:35400] "MULTI"
1576500165.732438 [0 10.1.13.157:35400] "SET" "bnrnnfp4tpl76e14h5b0" "\n\x14bnrnnfp4tpl76e14h5b0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.732448 [0 10.1.13.157:35400] "EXPIRE" "bnrnnfp4tpl76e14h5b0" "43200"
1576500165.732453 [0 10.1.13.157:35400] "EXEC"
1576500165.733721 [0 10.1.13.157:35400] "MULTI"
1576500165.733735 [0 10.1.13.157:35400] "SET" "bnrnnf14tpl76e14h5a0" "\n\x14bnrnnf14tpl76e14h5a0\x1a\x16\n\x14156.106.164.233:2222"
1576500165.733769 [0 10.1.13.157:35400] "EXPIRE" "bnrnnf14tpl76e14h5a0" "43200"
1576500165.733779 [0 10.1.13.157:35400] "EXEC"
チケットIDキーに対して、割り当て情報を設定を行っています。
1576500165.735677 [0 10.1.13.157:35396] "MULTI"
1576500165.735686 [0 10.1.13.157:35396] "EXEC"
1576500165.736289 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736528 [0 10.1.13.157:35396] "MULTI"
1576500165.736541 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnfp4tpl76e14h5b0"
1576500165.736550 [0 10.1.13.157:35396] "DEL" "ic$bnrnnfp4tpl76e14h5b0"
1576500165.736554 [0 10.1.13.157:35396] "EXEC"
1576500165.737618 [0 10.1.13.157:35396] "SMEMBERS" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738435 [0 10.1.13.157:35396] "MULTI"
1576500165.738445 [0 10.1.13.157:35396] "ZREM" "allTickets" "bnrnnf14tpl76e14h5a0"
1576500165.738452 [0 10.1.13.157:35396] "DEL" "ic$bnrnnf14tpl76e14h5a0"
1576500165.738457 [0 10.1.13.157:35396] "EXEC"
1576500165.739455 [0 10.1.13.157:35396] "MULTI"
1576500165.739465 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnfp4tpl76e14h5b0"
1576500165.739472 [0 10.1.13.157:35396] "ZREM" "proposed_ticket_ids" "bnrnnf14tpl76e14h5a0"
1576500165.739477 [0 10.1.13.157:35396] "EXEC"
割り当てが完了したチケットが検索対象にならないように、インデックスから削除を行っています。
最後に
デモを実際に動かして、簡単にですがデータの流れを追ってみました。
Open Matchの理解に少しでも役立てればと思います。