グレンジでサーバー・インフラを担当しているスプラシューターベッチューがお気に入りの村田です 🦑 🐙
モノクロかっこいい
グレンジ Advent Calendar 2018 9日目の記事になります。
弊社で導入を検討しているOpen Matchをラクラク動かし、Open Matchを理解する足がかりになればといいな思っています。
※本記事は、Open Match v0.1.0-alphaタグ のバージョンに基づく内容になっています。
※ラクラク動かすことを目的としているのでコンポーネント等の詳細な説明を省いています。
※サミールさんが、先日0.2.0ベースの資料を展開してくれました。 コンポーネントなど深掘りするならこの資料もご確認ください。
Open Match
UnityとGoogle Cloudの共同プロジェクト「Open Match」
ゲームエンジンに依存しないマッチメイキングのオープンソースプロジェクトです。
ハロウィン2018 Doodle で使われていたのもこのOpen Match。
Kubernetesで動作してスケーラブルであり、開発者はマッチメイキングのロジックを作り込むだけで良いような構造になっています。
要は、スプラトゥーンみたいなイカした4vs4のマッチメイキングをガチで作れたりします。
プレイヤーがガチパワーをセットしてマッチングをリクエストすると、
ガチパワーの近いプレイヤーとマッチングを行い、チーム分けや接続するゲームサーバー名、ルーム番号を受け取れるような仕組みを作れます。
ラクラク動かしてOpen Matchを理解する足がかりに
今回はガチで作りません。
サンプルプログラムをラクラク変更して簡単な4対4のマッチメイキングを実現します。
プレイヤーのマッチングリクエスト送信、マッチング結果取得まで確認できれば、Open Matchの全体像を掴みやすく理解する足がかりになると思います。
マッチメイキング仕様
- 4vs4
- チーム参加無し、ソロ参加のみ
- ブキ判定無し
- 一定範囲のガチパワーのプレイヤーをマッチング
- Open Match側でゲームサーバー名とルーム番号を決定
開発者が主に作る部分について解説
- フロントクライアント
- バックエンドクライアント
- MMF(マッチメイキングファンクション)
この3つです。
フロントクライアントとは
Unityのプログラムなど、ゲームアプリに該当します。
Open MatchのFront APIを実行して、マッチングリクエストを送信します。
Front APIはgRPCで提供されています。
バックエンドクライアントとは
バックエンドのサーバーで動作させるプログラム。
Open MatchのBackend APIを実行して、プロファイル(MMFの設定値みたいなもの)をセットしてマッチメイキング開始の指示出しを行います。この指示出しにより、MMFが起動します。
Backend APIもgRPCで提供されます。
またバックエンドクライアントは、フロントエンドクライアントにマッチング結果を渡す役割も担います。
今回は、MMFが作成したチーム分けにゲームサーバー名とルーム番号を加え、それらをフロントクライアントに渡すことにします。
MMF(マッチメイキングファンクション)とは
プレイヤーのマッチングを行うプログラムです。
フロントクライアントから渡されたパラメータと、バックエンドクライアントから渡されたプロファイルを元にマッチングさせます。
フロントのパラメータとバックエンドのプロファイルは共にRedisにセットされています。
マッチング結果となるチーム分けもRedisにセットすればOKです。
MMFはdockerのコンテナで作成します。
コンテナで動作するなら、プログラムはどんな言語でも書いても構いません。
v0.1.0-alphaではC#のサンプルもあります。
サンプルプログラムをラクラク変更
フロントクライアント
examples/frontendclient にあり、ターミナルで実行します。
しかし、バックエンドクライアントとMMFのサンプルと連携できておらず、このままだとマッチング結果を受け取ることができません。
main.goを数ヶ所変更して連携させます。
ソロ参加に変更
4人1グループでマッチングリクエストを出していますが、1人1グループのソロにします。
func main() {
// determine number of players to generate per group
- numPlayers := 4 // default if nothing provided
+ numPlayers := 1 // default if nothing provided
var err error
プレイヤーデータ変更
マッチングリクエストで送信しているプレイヤーデータをまるっと変更します。
ガチエリアに参加、ガチパワーは1500〜2000のランダムとします。
// Generate players for the group and put them in
for i := 0; i < numPlayers; i++ {
playerID, playerData, debug := player.Generate()
+ playerData = make(map[string]int)
+ playerData["battletype"] = 1 // ガチエリア的な
+ playerData["gachipower"] = int(1500 + rand.Float32() * 500)
groupPlayer(g, playerID, playerData)
マッチング結果受信待ちのキーを変更
GetAssignmentはマッチング結果の受信待ちになるFront APIです。
このAPIは、グループID(ソロなのでプレイヤーIDと同意)のキーのconnstringフィールドにマッチング結果がセットされるのをポーリングしています。
サンプルプログラムでは、グループIDを仮の「bitters」とし、connstringフィールドをredis-cli等で手動で書きかえて受信できることを確認しているだけです。
受信待ちキーをリクエストで送信したグループID(プレイヤーID)に変更します。
// wait for value to be inserted that will be returned by Get ssignment
- test := "bitters"
- fmt.Println("Pausing: go put a value to return in Redis using HSET", test, "connstring <YOUR_TEST_STRING>")
- fmt.Println("Hit Enter to test GetAssignment...")
- reader := bufio.NewReader(os.Stdin)
- _, _ = reader.ReadString('\n')
+ test := g.Id
connstring, err := client.GetAssignment(ctx, &frontend.PlayerId{Id: test})
+ ppJSON(connstring.ConnectionString)
pretty.PrettyPrint(connstring.ConnectionString)
またマッチング結果として受け取ったJSONを標準出力させます。以下の様なデータを受け取ることがゴールです。
2018/12/05 23:21:17 {
"red": [
"937bec2f2a2f443bbbd2aaba96609107",
"99800ac7bc4343ad9f77d364b4943e85",
"d8daa32ea226405aa2628b58a73a98b4",
"d9b72c00007f4a2c99047b3fc0f57a1a"
],
"blue": [
"685c68acae1e4dc9affd14bae25cf87c",
"693ca010afba4db48bdcbe768d9f12da",
"7520c0c3517d4bc5bbdc8305ee7a8c64",
"8e9b20e35017427caa855a783beac394"
],
"gameServer": "example.com:12345",
"roomId": 3520882902
}
バックエンドクライアント
GameRoom構造体の定義を追加
MMFが作成したチーム分けRed、BlueをJSONデコードするための構造体です。
Red、BlueはグループID(プレイヤーID)の配列です。
また、フロントクライアントに渡すためにゲームサーバーとルームIDのメンバも追加しています。
+type GameRoom struct {
+ Red []string `json:"red"`
+ Blue []string `json:"blue"`
+ GameServer string `json:"gameServer"`
+ RoomId uint32 `json:"roomId"`
+}
MMFのマッチング結果が無しでも終了させない
マッチング開始指示・取得をループしています。
しかし、マッチング結果が無しだとfatalで終了して再起動させるのが面倒なので変更します。
match, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
- log.Fatalf("Error reading stream for ListMatches(_) = _, %v", err)
+ log.Printf("Error reading stream for ListMatches(_) = _, %v", err)
break
}
マッチング結果として渡す情報変更
サンプルプログラムは、マッチング結果として接続すべきゲームサーバー "example.com:12345" を渡すだけですが、
先ほど定義したGameRoom構造体を渡すように変更します。
まずMMFが作成した赤青チーム分けのJSONをデコード。
ゲームサーバーは仮で"example.com:12345" とします。
受け入れ可能なゲームサーバーはセットするイメージです。
ルーム番号もチーム分けJSONからハッシュ値を求めてセットします。
バトル中のルーム番号がユニークになればOK。
roster := &backend.Roster{PlayerIds: playerstr}
- ci := &backend.ConnectionInfo{ConnectionString: ""}
+ var gr GameRoom
+ // 赤青チーム分けJSONをデコード
+ json.Unmarshal([]byte(result.Raw), &gr)
+ gr.GameServer = "example.com:12345"
+
+ // 赤青チーム分けJSONからハッシュ値作成
+ h := crc32.NewIEEE()
+ h.Write([]byte(result.Raw))
+ gr.RoomId = h.Sum32()
+
+ bytes, _ := json.Marshal(&gr)
+ ci := &backend.ConnectionInfo{ConnectionString: string(bytes)}
MMFに渡すプロファイラを変更
サンプルプログラムのMMFは、properties.playerPoolのキーで指定したパラメータからプレイヤーを絞ってマッチングします。
battletypeは1、gachipowerは1500〜2000に該当するプレイヤーをマッチングさせるよう変更します。
{
"name":"testprofile",
"properties":{
"playerPool":{
- "region.europe-west1":"0-150",
- "mmr.rating":"950-1650",
- "mode.ctf":"0-9999999999"
+ "battletype":"1-1",
+ "gachipower":"1500-2000"
},
"roster":{
"blue":4,
"red":4
},
}
}
MMF
examples/functions/golang/simpleをそのまま使います。
変更しません。
デプロイと実行
Google Cloud Builderでビルド、Kubernetesデプロイ
frontapiは外部ネットワークから利用したいので、type LoadBalancerを指定してEXTERNAL IPを持たせます。
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "om-frontendapi"
},
"spec": {
"selector": {
"app": "openmatch",
"component": "frontend"
},
"ports": [
{
"protocol": "TCP",
"port": 50504,
"targetPort": "grpc"
}
- ]
+ ],
+ "type": "LoadBalancer"
}
}
あとは docs/development.md の下記4項目に従いビルドします。
- Example of building using Google Cloud Builder
- Example of starting a GKE cluster
- Configuration
- Running Open Match in a development environment
Open Matchのコアコンポーネントが実行され、サンプルプログラムのMMFコンテナイメージも登録されます。
※簡単に動作確認をするだけなので、GKE clusterはシングルゾーンにノード1つだけで構いません。
% gcloud container clusters create --machine-type n1-standard-4 open-match-dev-cluster --zone asia-east1-b --num-nodes=1
※optionalのOpenCensusやPrometheusのデプロイは不要です。
バックエンドクライアントのビルドと実行
バックエンドクライアントはGKEのクラスターで実行します。
% cd examples/backendclient
% gcloud builds submit --config cloudbuild.yaml
% kubectl run backendclient --image-pull-policy=Always --image=gcr.io/colorful-development/openmatch-backendclient:dev
フロントエンドクライアントの実行
フロントエンドクライアントはローカルのターミナルで実行します。om-frontendapiのEXTERNAL-IPを確認。
% kubectl get svc om-frontendapi
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
om-frontendapi LoadBalancer 10.55.253.150 35.194.249.159 50504:32071/TCP 2m
/etc/hostsに書き加えます。
sudo vi /etc/hosts
35.194.249.159 om-frontendapi
ターミナルを8つ開いて、フロントクライアントを8つ実行。
% cd examples/frontendclient
% go run main.go
画面上はフロントクライアント、画面下はバックエンドクライアントのログです。フロント側で、red、blueのチーム分けやgameServerやroomIdが確認できればマッチング成功です。
おわりに
OpenMatchをラクラク?動かすことができました。
redis-sentinel serviceにEXTERNAL IPを持たせてmonitorしたり、他のpodのログも眺めるとOpen Matchを理解しやすくなると思います。
サミールさんの資料も必見です。