Warcraft Rumble

「ウォークラフト ランブル」の舞台裏:ミニのXP計算

Blizzard Entertainment

ランブラーの皆さん、こんにちは!

「ウォークラフト ランブル」のサーバー機能を担当するリード・エンジニアのアンディ・リムです。サーバーチームはネットワーキングやクラウドコンピューティング、ストレージなど、サーバーに関するあらゆる物事を受け持っていますが、同時にキャンペーンの進行やクエストといったゲーム機能の構築にも携わっています。今回はそんな開発の舞台裏で、XPをストレージに格納して、それをミニのレベル算出に利用した手法について詳しくお話しします。


Cassandraってなに

アテンション・プリーズ!この先は少し難解な技術的な話となりますが、どうか辛抱強くお付き合いください。

まず最初に、大量に同時接続しているユーザーのプレイヤーデータについて、私たちは変更内容を始終トラッキングしていますが、その際に使っているストレージソリューション、「Cassandra」について簡単にお話ししておきます。Cassandraは拡張性が高く、広範に利用されているオープンソースの分散型データベースで、データの一貫性と汎用性のバランスが取れています。データ自体に関しては、Cassandraはハードなスキーマを強制することなく、幅広いデータセットを扱えます。そして私たちは各機能について必要となるデータベーステーブルと、テーブルスキーマを定義するツールを構築しているため、必要に応じて自由に構造化や組織化を行うことができます。つまり、スキーマの記述や認証、クエリを簡単に実行できるようになっています。

スキーマとはデータベーススキーマは、リレーショナルデータベース内でデータがどのように構造化されるかを定義するものです。例えば各テーブル(※表)の中で何が見つかるかといったことに係わります


XPをレッジャーとして格納

Cassandraはデータの書き込みは非常に高速ですが、読み込みが遅いことで知られています。インプレースデータの更新は基本的に読み込んでから書き込む操作になることから、これも遅くなります。これを回避するために、プレイヤーデータをレッジャー(ledger、台帳)として格納するようにデザインしました。レッジャーでは、各ラインを変化として書き込み、読み込む際には、全エントリーを読み込みんだ上でそこで計算を行います。

レッジャーの好例はクレジットカードです。各トランザクションはプラスかマイナスの値の1つのエントリーとして書き込まれます。買い物する度にマイナスの値のトランザクションが発生し、支払い終える度にプラスの値のトランザクションが発生します。では借債がどれくらいであるかは、どうすればわかるのでしょうか?それを知るには、レッジャー全体をチェックして、それぞれの値を加算/減算する必要があります。

インプレースデータは、内部にひとつのものしか格納できないとイメージしてみてください。その値を変更したいときは、それを読み込んで変更を行い、新たな値を書き込む必要があります。これがサッカーの合計得点なら、チームが点を決める度に、読み込んでこれまでの得点を調べ、新たなゴールを追加し、それを合計点に書き戻す必要があります。

その具体例な例が、「アークライト」で言うとミニ達のXPレッジャーです。ミッションが終了する度に、それぞれのミニのレッジャーに行が1つ追加され、そのミニの獲得したXP量が示されます。ミッションを5つこなすと、エントリーは以下のようになります。

テーブル1

プレイヤー

ミニ

時間

アンディ

ノールの暴れん坊

3

月曜日 14:00

アンディ

グリフォンライダー

3

月曜日 14:05

アンディ

ノールの暴れん坊

3

月曜日 14:10

アンディ

S.A.F.E.パイロット

3

月曜日 14:15

アンディ

ノールの暴れん坊

3

月曜日 14:20

最終的にミニの合計XPを取得するには、単純にレッジャーの全エントリーを取得して各ミニごとのグループに分け、加算してXPの値を算出します。結果はこのようになります。

テーブル2

プレイヤー

ミニ

合計

アンディ

ノールの暴れん坊

9

アンディ

グリフォンライダー

3

アンディ

S.A.F.E.パイロット

3


ロールアッププロセスをワインドアップ

XPの格納方法が決まったあと、各ミニごとに行う必要がある計算をどのように合理化したか、その方法に注目してみましょう。各ミニの獲得XPについて個々の行を格納すると、読み戻し作業が重くなってしまいます。数多くの行があり、このテーブルは際限なく…永遠に拡大してしまいます。「ランブル」の平均的なプレイで、1日にクエストやPVPを15回、1週間にゴールドのためにサージを20回、部隊のアップグレードのために1週間に1回ダンジョンをプレイしたとしましょう。この場合、1週間あたりのエントリーの数は100になります。3か月間プレイすると、レッジャーのエントリー数は1,260ほどになります。合計を計算する際には、Cassandraの読み込みが遅いシステムを通して、これら全ての値を取得することになります。これではいけませんね。

こうした問題を回避するために、私たちは「ロールアップ」という解決策を考えました。ロールアップとは特定の時間までの値を合算することを意味します。この場合は、全ての値を合算して、1つの行にまとめます。そしてこれを2つ目のテーブルに格納します。ここでタイムスタンプが役立つことになります。XPにおいては、プレイヤーとミニが鍵となり、使われる計算は加算です。火曜日0:00までのレッジャーの全エントリーをロールアップして、得られた結果をCassandraの2つ目のテーブルに格納するとしましょう。エントリーは以下のようになります。

テーブル3

プレイヤー

ミニ

合計

終了日

アンディ

ノールの暴れん坊

9

火曜日 0:00

アンディ

グリフォンライダー

3

火曜日 0:00

アンディ

S.A.F.E.パイロット

3

火曜日 0:00

水曜日にさらに対戦して、追加のXPエントリーが生成されたとします。オリジナルのXPテーブルは以下のように拡大します。

テーブル4

プレイヤー

ミニ

時間

アンディ

ノールの暴れん坊

3

月曜日 14:00

アンディ

グリフォンライダー

3

月曜日 14:05

アンディ

ノールの暴れん坊

3

月曜日 14:10

アンディ

S.A.F.E.パイロット

3

月曜日 14:15

アンディ

ノールの暴れん坊

3

月曜日 14:20

アンディ

S.A.F.E.パイロット

3

水曜日 12:00

アンディ

雷電連鎖

3

水曜日 12:05

アンディ

グリフォンライダー

3

水曜日 12:10

ここから、これら全てのチェックを行って、対戦プレイ後の実際のミニのレベルを再計算します。両方のテーブル(エントリーが1つだけのロールアップデータのテーブルと、前回にロールアップを行った時間以降のエントリーが記載されたレッジャー)にクエリを送り、両方の値を合計して、最終的な合計XPのテーブルを作成します。つまり、テーブル3を読み込み、その後、テーブル4にテーブル3の各行の時間帯以降に存在するエントリーのみを求めるクエリを送ります。各ステップをリストアップすると以下のようになります。

  1. テーブル3から行を1つ読み込む
     

グリフォンライダー

3

火曜日 0:00

  1. テーブル4から火曜日0:00以降に発生したグリフォンライダーの行を読み込む
     

グリフォンライダー

3

水曜日 12:10

  1. 両方のデータセットを合算して合計値を生成する:

グリフォンライダー

6

このアプローチを使えば、テーブル4からの大量の読み込みをスキップできるのがおわかりでしょう。

新たなレッジャーのエントリーを格納した後に、新たに計算したデータだけを格納すればいいと思うかもしれません。理論上はそれで良さそうに見えますが、これではデータの不一致とパフォーマンスの劣化につながりかねません。パフォーマンス劣化の一例は、読み込み後に書き込みが必要になることが多いために、再計算とデータ格納でシステムがビジーになることです。この場合は、全てのプレイヤーのために安定したパフォーマンスでゲームを実行することと、計算との間でバランスを取る必要があります。

データの不一致の一例は、対戦終了時の計算です。私たちの用いているサーバーインフラとプラットフォームでは、遅れて届くメッセージにも対応しています。例えば、AとBの2つのデータセンター間で一時的なズレが生じ、プレイヤーにミニのXPを付与するメッセージが、Aからは送信されているものの、Bにはまだ届いていない場合があります。例えば、深夜0時直前に対戦していたとしましょう。あなたが勝利したタイミングは水曜日の23:59だったとします。ロールアップ・プロセッサーが1日1回、現在の日付までの全データをロールアップするように設定されていたとしましょう。これは深夜0時に実行されて、木曜日の0:00までの合計XPの計算を行います。このデータは2つ目のテーブルに格納され、新たにXPを読み込む際には、前回格納された値と、木曜日の0:00以降の全エントリーを合算します。遅れて到着したメッセージに何が起こるかというと、取得はしますが、対戦は水曜日の23:59に完了しており、XPのエントリーはその時間で書き込まれることになります。ロールアップテーブルにはこのエントリーは含まれず、以降のデータへのクエリにもこのエントリーは表示されません。こうした不完全なデータが問題になるわけです。数日後に到着したメッセージはどうなるのでしょうか?これに関しては、私たちはモニタリングやアラートのシステムを用意しており、この新たな情報はほとんどの場合、次のロールアップの時間までには処理されます。


ミニのレベルを算出

合計XPのテーブルを見たときに、ここではレベルが計算されていないことに気付いたでしょうか?テーブル内には、これまでに獲得したXPの合計しかありません。実はこれとは別に、ゲームデザイナーが設定した、以下のような固定のテーブルが存在します。ミニはレベル1から始まり、XPを1ポイント獲得するとレベル2になりますが、レベル3になるにはXPを3ポイント獲得する必要があります。

 レベル

次のレベル到達に必要なXP

1

1

2

3

3

6

4

10

5

20

10

250

2つを合わせると、現在までの累計を計算することで、ランタイムでミニの実際のレベルを算出することが可能になります。そして最終的に以下のデータセットが完成します。

ミニ

レベル

XP

次のレベル到達に必要なXP

ノールの暴れん坊

3

5

1

グリフォンライダー

3

2

4

S.A.F.E.パイロット

3

2

4

雷電連鎖

2

2

1

こうしておけば、ゲームデザイナーはレベルを1つ上げるのに必要なXP量を自由に変更することができるようになります。低レベル帯でレベルアップするのが難しく感じられたら、次のレベルに到達するために必要なXP量を減らしたりといったことが可能です。そうすれば、データベースに保存されているプレイヤーデータには一切変更を加えることなく、全てのミニが現在のレベルから少しブーストされることとなり、次のレベルに到達するために必要なXP量が減少して、早くレベルアップできるようになります。これは逆の場合でも機能します。ゲームデザイナーがやっぱりレベルの上昇が早すぎると感じて、レベルアップに必要なXP量を増やそうと考えた場合でも、ミニを変更導入前のレベルに戻すためにXPを操作する必要はありません。とはいえ、レベルアップを容易にするために、ミニにXPを付与する選択肢もないわけではありませんが。


採用されなかったアプローチ

ミニの獲得XPの格納と最終的なレベルの計算に関しては、現在の解決策を採る前に様々なアプローチを考慮しました。プレイヤーのXPに変更を加えるための特別なダウンタイムを設ける方法や、進行状況こそ維持しながらも、プレイヤーに良い体験を届けることだけに的を絞った案も考慮しました。

「ミニのレベル、XP、次のレベルに到達するために必要なXP量を常に格納する方法」

単純にスタンダードなSQLパターンを使って、トランザクションアップデートを利用すればどうなるのでしょうか?トランザクションアップデートはデータセット全体をアップデートするため、「ゼロか全てか」です。一か所だけ書き込みできなかった場合は、全てが元の値にロールバックされます。これらはACID特性と呼ばれる、原子性(atomicity)、一貫性(consistency)、独立性(isolation)、永続性(durability)を持っています。

これは1つの行で各ミニのXPと合計レベルを管理するため、読み込みが非常に軽くなります。

ミニがXPを獲得すると、XPが2ポイント、次のレベル到達までに必要なXP量が8ポイント、現在はレベル3としてこのミニがアップデートされます。このタイプの変更はデータを読み込み、再計算し、その後データベースに書き込むことを強制します。このようなパターンではCassandraの長所を活かすことができません。さらにもうひとつの問題は、レベルアップに必要なXP量を変更したい場合、ゲームをオフラインにして全プレイヤーの全ミニのデータを書き換える必要がある点です。

「パーセンテージで格納する方法」

また、次のレベルに到達するまでに必要なXP量をパーセンテージで格納する手法も考慮しました。これは以下のようになります。

ミニ

レベル

次のレベル到達に必要なXP量

ノールの暴れん坊

3

20%

グリフォンライダー

2

20%

S.A.F.E.パイロット

2

20%

そして対戦後に簡単な計算を行います。例えば、ノールの暴れん坊が3ポイントXPを獲得し、パーセンテージは3/10となるので、ではノールの暴れん坊にレベル到達までの30%を付与しようということになって、結果は以下のようになります。

ミニ

レベル

次のレベル到達に必要なXP量

ノールの暴れん坊

3

50%

グリフォンライダー

2

20%

S.A.F.E.パイロット

2

20%

これなら、ミニのレベル上昇曲線に変更を行ってもミニのレベルは変化しないため、一定の自由度が得られます。また、単純にメーターを30%満たすだけで、追加の計算が必要ないことから、ゲームUIのビジュアル表示も容易になります。しかし、レベルが上がってくると、パーセンテージは微々たるものになってきます。例えば対戦ごとに10 XPを獲得しているミニが、次のレベルに上がるのに100,000 XPが必要だった場合、10 XPは0.01%となります。CPUは浮動小数は扱えますが、浮動小数点演算の性質を考慮しないと、正確度と精度の点でバグが発生する可能性があります。


それではまた次回…

援用可能なツールから、データの格納方法と計算方法まで、データベーステクノロジー全般について様々な選択肢が考慮されたこと、おわかりいただけましたでしょうか。全てはまだ開発中ですので、もっといい方法が見つかった場合は、こうした全てが変更される可能性もありますが、このゲームでは、これが良い出発点になると言えそうです。

お読みいただきありがとうございました!

~ アンディ・リム

ところで明言しておきますが、私はギョロ目の盗賊ではありませんよ。チームに加入した初日に、誰かがいたずらして私の最新のモニターにギョロ目の盗賊を貼り付けてくれましたが。一日中見られているようで困ったものです…一日中ですよ…