「このデータはゲームをリリース後に調整したくなりそう」と言われたときに、ゲームプログラマが考えること

2024-03-03

リリースで終わらない、昨今のゲーム開発

近年のビデオゲームは、不具合修正やバランス調整、コンテンツ追加などの目的で、リリース後もしばしばアップデートが行われます。 この際、すでに遊んでいるプレイヤーのデータに影響が出ないように、ゲームデータの修正・追加を行う必要があります。

ゲームのソフトウェアを作る開発者は当然のように考えることですが、 こうした実務的なデータの扱い方や設計方法について言及している技術記事というのは、あまり見かけないように思います。 ゲームの作り方を教えてくれる本や資料は世の中にたくさんありますが、 「ゲームをリリースした後」 まで見据えて論じてくれる文献は、少ないと思うのです。

そこで本記事では、そういった話題に向き合ってみます。

モチベーション : アップデートを考慮しなければならないデータとは

ひとつ、具体例を挙げて話しましょう。

あなたはゲーム開発をしているプログラマーで、ゲームプランナー (ゲームの仕様を考えたり、データを作る人) から、ちょっとした クエスト機能 を作りたいと言われました。 プレイヤーに対して 1 つずつ課題が提示され、達成していれば報酬が受け取れる、というシンプルな機能です。 (ゲームによってはミッションやタスク、などと言い換えてもよいです)

プレイヤーは目先の課題を順にクリアしていけばよいので何をしたらいいのかがわかりやすくなり、 自然とゲームのプレイサイクルを理解することができるようになります。 体感的にも、小刻みに報酬がもらえて嬉しい… といった狙いの機能です。 このような機能は実際に、放置型ゲームと呼ばれるようなカジュアルなプレイ感のゲームに実装されていたりします。

「クエスト機能」のイメージ

「クエスト機能」のイメージ

さて、仕様の要件を聞いたプログラマーのあなたは、さっそくデータの設計をしました。 ソフトウェア開発に詳しくない方のために基礎的なことを言うと、 ソフトウェアというものは (語弊を恐れずにざっくり言えば) 「データとロジック」でできています。 もうちょっと言うと、データには静的なものと動的なものがあるので、以下の 3 つということになります:

「動的なデータ」「静的なデータ」「ロジック」

「動的なデータ」「静的なデータ」「ロジック」

「クエストが、どんな順番でどんな内容が提示されて、各報酬は何か」 というのは、静的なデータ に当たります。 ゲームが作られた時点で決まっていて、変わることのないデータ (設定、パラメータ) だからです。 こうしたデータは、ロジック (プログラム) から分離して、ゲームプランナーが自由に設定できるようにしておくのがプログラマーの仕事です。

データの表現方法・フォーマットには色々な形がありますが、こうしたデータは テーブル で持つことが多いです。 Excel をイメージしてもらえばわかりやすいでしょう。 実際、筆者は 10 数年ほどゲーム開発の仕事をしてきましたが、 ゲームのデータが Excel や Google スプレッドシートで管理・作成されている現場は数多くありました。 また、開発の現場 (とりわけスマホゲーム業界や Web サービス業界) では、 こうしたテーブルのデータを マスタデータ と呼称することが多いので、 本記事でも 「ゲームの内容を決める静的なデータ」 のことを マスタデータ と呼ぶことにします。

クエストの内容を定義するマスタデータは、以下のようなものになりました:

クエストのマスタ(疑似表現)

クエストのマスタ(疑似表現)

クエストは step = 1 から始まって順に提示されていくものとして、各 step の内容と報酬が設定できるようになっています。明確ですね。 これでゲームプランナーは自由にクエストを作ることができます。 (なお、クエストはたくさんあって、全部で 500 行程度 のボリュームのデータになるものとします)

さて、実際にゲームで動かすときには、 「プレイヤーがどこのクエストまで進んだか」 という 状態 を保存する必要があります。 ゲームを遊んだことがある人には、セーブデータ と言うと馴染みが深いでしょう。 このような 遊び始めた後に変わる状態 が、先で言った 「動的なデータ」 になります。 こちらは、(とりわけプレイヤーの持つデータを) ユーザデータ と呼ぶことが多いので、本記事でもそう呼びます。 ゲームのプレイヤーも、ユーザ と呼ぶことにしましょう。

ここでは、単純に 「ユーザが今いる step」 をユーザごとに保存することにします。以下のようなイメージです:

ユーザデータに step を保存

ユーザデータに step を保存

  • 普段の筆者なら、データが 0 スタートになるように「クリアした step」を保存することを選びますが、 今回は図をわかりやすくするために「次にクリアするべき step」を覚えることにしています

これでユーザは、ゲームを中断して再開した場合でも、前にやったクエストの続きから遊べるようになりました。 完璧です!

かくしてゲームは完成してリリースされ、あなたはゲームプランナーとともにリリースを祝いました。 たくさんのユーザがゲームを遊んで楽しんでくれるといいな… そんな希望を抱きながら、美味しいお酒を呑みます。

さて、そんなお酒の席で、ゲームプランナーがこんなことを言いました。

「あのクエスト機能だけど、リリース後に実際のユーザの動向を見て、バランス調整しようと思うんだよね」

データは調整したくなるもの

ゲームのユーザというのは多種多様ですから、リリース前にどんなに気を使ってバランス調整をしたとしても、 実際のユーザのプレイ感が開発者の意図しているものとはズレてしまった… ということはよく起こります。 パッケージ型のオフラインゲームでも、ユーザの声を受けてゲームバランスを調整するアップデートが行われる、 ということは珍しくありません。

また、調整や不具合修正以外にも、追加コンテンツの導入によって、既存機能のデータを修正したくなるようなケースもあります。 何にしても、「データは後から変えたくなる可能性がある」 と初めから思っておいた方が得策なのです。

リリース後に素朴にデータを変更すると何が起きるのか

完璧に作ったと思えたクエスト機能でしたが、現状ではリリース後の「素朴なデータの変更」には耐えられません。 今の状態で、具体的に「素朴なデータの変更」を行うと、何が起こるのでしょうか。

例えば、リリース後にクエストのマスタデータにデータを追加する というケースを考えてみます。

リリース後のクエストの追加

リリース後のクエストの追加

新しいクエスト (右図:黄色い行) を間に差し込んで、step を振り直しました。 step は振り直されたけどユーザデータの step はそのままですから、 ユーザから見ると 「アップデートしたらクエストの内容が変わった」 ように見えます。

ユーザ B 視点では、すでにクリアしたはずの 「1 回アイテム購入」 というクエストが再度現れました。 すでに達成している条件なので、再び報酬を受け取ることができます。 (※ ゲームを始めてから累計 1 回以上アイテムを購入していれば達成、という定義)

もしこの報酬が、1 回だけ受け取ることを想定した大きい報酬 だった場合、 2 回受け取ることでゲームバランスが想定範囲から崩れてしまうことになります。

では、 入れ替え を行った場合はどうでしょうか。

リリース後のクエストの入れ替え

リリース後のクエストの入れ替え

この場合でも、すでにクリアしたクエストが再び現れて報酬を余分に受け取ることができたり、 逆に 本来出会えるはずだったクエストに出会えなく なって、報酬を受け取りそこねるユーザが出てきたりします。 もしその報酬が、ゲームを進めるために必須のアイテムだった場合は……

このように、リリース後のデータの更新は、 すでに遊び始めている人のユーザデータ のことを考慮しないと、 ユーザや開発者にとって望ましくないことが起こるのです。

アイディア:クエストにユニークな ID を振る

どうも、この設計ではアップデートに対して不十分だったようです。 あなたはタイムマシンに乗って、ゲームがリリースされる前の時空に戻り、データの設計段階からやり直すことにしました。

step が振り直されることで、ユーザがそれまで見ていた step と内容が変わってしまった… というのが問題のひとつでした。 では元のクエストが識別できるように、step とは別の識別番号を持たせたらどうでしょう?

クエストにユニーク ID を振る

クエストにユニーク ID を振る

uid という列で、ユニークな ID を振りました。uid は、一度リリースしたら変えない というルールでデータを作ることにします。 ユーザデータには、step の代わりに uid を保存します。

こうすると、後からデータを間に差し込んでも、ユーザの見ているクエストの位置がずれないようにできます:

uid 形式でデータの追加

uid 形式でデータの追加

ところで、データの変更というのは、追加や入れ替えだけではありません。 データを削除したくなった 場合はどうでしょうか。 「このクエストが難しくてみんなここでゲームをやめているので、このクエストを削除したい」 といったシナリオは、ゲームの運営ではよくある話です。

このケースでは、 削除されたクエストの位置にいたユーザの処遇 を考える必要があります。 一例として、

  • uid と一緒に step も保存しておいて、見ている uid が削除された場合は新しいデータ上で step が同じ位置に移動する

といった処理が考えられます。

  • このような 「データのバージョン移行に伴ってユーザデータを変更する処理」 を、データの マイグレーション などと呼びます
データを削除するケース

データを削除するケース

このルールで運用すれば、データの追加も削除も、破綻が起きることはなさそうに見えます。

…本当にそうでしょうか?

データ追加と削除が同時に行われたケース を見てみます:

追加と削除が同時に起こったケース

追加と削除が同時に起こったケース

この例では、ユーザがいた位置の手前に 2 件データが追加された関係で、 「同じ step に移動する」というロジックでは 2 つ前のクエストに戻ることになりました。 ユーザ A 視点では、すでにクリアしたクエストを再び見ることになり、結局、前と同じような事象が発生します。

また、ユニーク ID を振ったとしても、データを入れ替えには弱いです:

入れ替えのケース

入れ替えのケース

この例では、ユーザ A と B の前後関係が入れ替わっており、むしろ前よりも破綻が大きくなっているように見えます。 UID 以外のデータを入れ替えるような操作をしたとしても、前と同レベルの問題が発生します。

結局、どうしたら良いのか?

方法論は色々あるのですが、初めにことわっておくと、銀の弾丸はありません。
全ては時と場合と、開発者の方針によるのです。

明確な回答を求めてやってきた読者には、ちょっと面白くない言い回しかもしれません。 当たり障りのないことを言いなすって… と思われるかもしれませんが、これこそがソフトウェア開発の真実です。

  • 全ての戦略には、メリットとデメリットがあります
  • 機能的に最善でも、処理コストがかかるデメリットや、管理しにくくなるデメリットが生じ得ます
  • 最適な戦略は、ゲームにおけるその機能の重要度や、ゲームの運用の方針によって変わります

コンピュータサイエンスを学んだことがある人なら、時間と空間のトレードオフ という言葉を聞いたことがあるかもしれません。 一般に、メモリ消費をいとわなければ高速な処理を作ることができ、逆に計算コストを許すならメモリ消費は抑えられる… そうしたトレードオフが発生しやすいことを言います。

実務的な話で言うと、ここにもうひとつ、作りやすさや管理のしやすさ、ユーザから見たわかりやすさなどの評価軸も入ってきます。 言わば 認知コスト です。 筆者はこうした認知コストをひっくるめて、「人間の都合」 と呼んでいます。 (※ 筆者がそう呼んでいるだけです)

ソフトウェアの設計を行う際には、この 時間と空間と人間の都合のトレードオフ を加味してやり方を選ぶ必要があります:

時間と空間と人間の都合のトレードオフ

時間と空間と人間の都合のトレードオフ

それでは、そうした前提を踏まえた上で、筆者の思いつくデータ設計の戦略と、メリット / デメリットをご紹介しましょう。

アップデートを踏まえたデータ設計の戦略とメリデメ

(a) 最もシンプルなパターン

最初に見た、最も素朴なパターンです:

step だけで管理

step だけで管理

  • 最もシンプルでわかりやすい
  • データ量が最小で済む
  • 追加や削除、入れ替えを行うと既存ユーザに破綻が起きる

もしこのクエスト機能がそこまで重要でなく、報酬量もゲームプレイ上でおまけ程度のもので、 ゲームのアップデートでユーザからの見え方が変わっても気にしない… という運営方針なのであれば、 実はこれが最善な戦略とも言えます。

(b) クエストにユニーク ID を振るパターン

2 つ目に見たパターンです:

ユニーク ID を振るパターン

ユニーク ID を振るパターン

  • 間にデータを差し込んでも位置がずれない
  • マイグレーション処理をすれば、削除も可能
  • 追加と削除を同時に行うと破綻が起きる場合がある
  • 入れ替えを行うと破綻が起きる
  • ユニーク ID は振ったら変えない、新規追加はまだ使ってない ID を振る、といったデータ作成上の制約が生まれる (手間と、ヒューマンリスクが増える)

(c)ユニーク ID を振り、クリアしたものを全て覚えておく

最も、後からどうとでもしやすい のがこのパターンです。 マスタデータは(b)と同じですが、ユーザデータに クリアしたクエストの uid を全て 覚えておくようにします。

新しいデータに対して、ユーザは 「クリアしたことがあるクエストかどうか」 を判断できるので、 明確なマイグレーション処理が可能になります。

  • 手前に追加された未クリアのクエストを既存ユーザにもやらせたい場合は、過去のクエストをチェックして未クリアのものに移動させる
  • そうでない場合、データ削除時は先にある直近の未クリアクエストに移動させればよい
  • 新たなクエストを見る際はクリア履歴をチェックして、クリア済みだったらスキップする

このようにすれば、理論上はデータを入れ替えたりしても破綻は起きなくなります:

クリアした uid を全て覚えておくパターン

クリアした uid を全て覚えておくパターン

  • 追加、削除、入れ替えが可能になる
  • 運用方針に応じてマイグレーション方法が柔軟に選べる
  • データ量と計算コストが増える
    • (クエストがたくさんあった場合、それだけユーザデータのサイズが大きくなってしまう)
    • (また、データを走査して未クリアを探したり、毎回クリア済みかをチェックする処理コストが生じる)
  • ユニーク ID 運用の管理コストは(b)と同様

手堅いですが、重要度の低い機能だった場合は、コストをかけすぎとも言えます。
(折衷案で、完全性を欠くが直近 n 件だけ覚えておく、といった落とし所もあります)

(d)データ自体にバージョンを持たせて、新規ユーザにのみ適用する

マスタデータそのものを新旧で 2 つ作り、新しいデータはアップデート後のゲームで開始したユーザだけが見るようにする、 とユーザから見た世界を切り分ける方式です。

既存ユーザに一切影響を与えたくない場合にとる戦略で、アップデートによる不整合が起こるリスクは減らせますが、 逆に言うと既存ユーザに対するバランス調整目的では使えない方法です。

データにバージョンを持たせるパターン

データにバージョンを持たせるパターン

  • 既存ユーザを気にせず大胆なアップデートができる
  • 既存ユーザのデータ不整合が起こるリスクが最小になる
  • 既存ユーザのバランス調整が行えない (行う場合は(a)と同等の性能になる)
  • マスタをバージョンぶん持つことになるので、マスタデータの量が膨れる
  • ユーザの中に複数のデータのバージョンが共存することになり、他の部分のバランス調整やデータ分析の際のコストが上がる
  • この方法は、ゲーム序盤のチュートリアル のような、 他の機能との影響度が高く複雑だが短期間で終わるようなデータの管理には向いています

(e)マイグレーション処理を丁寧にやる

今回の例で言うと、「この step に居たユーザはこの step に移す」 といった詳細なルールを用意し、 丁寧にマイグレーション処理を行う、といったやり方です:

マイグレーションを頑張るパターン

マイグレーションを頑張るパターン

  • 単純なロジックで表現できない柔軟な移行ができる (この範囲にいるユーザはまとめてここからやり直し、のような)
  • 追加や削除、入れ替えが起こった時に何が起こるかを明確に定義できる
  • マイグレーションルールを用意するコストがかかる
  • 各種状態からの移行で問題が起きないか、というテストケースが増え、テストに時間がかかる

サーバサイドにユーザデータを持っているオンラインゲームなどでは、 ゲームをメンテナンス状態にしてマイグレーションでどうにかするケースが多いかもしれません。

(f)細かいことは気にすんな! の精神で運用する

例えば今回の例では、

  • クエストを大型改修したので、全員のクエスト進捗をリセットだ!
  • 既存ユーザはまた報酬を全部もらい直せてウハウハだが、許す! 先行者特権だ!

という フランクな運営方針 で良いということであれば、たくさんの面倒事を考えなくて済むようになり、 時間と空間のコストも最小にできます。

冗談のようですが、ゲームバランスにクリティカルな影響がないなら、これも最善の戦略になり得る方法です。


クエストとは違う例ですが、スキルツリー のようなデータ構造が複雑な機能では、 どうしても既存のユーザデータに影響を与えないように大きな改修を行うことが難しい、といったケースもあります。

そうした場合に、

  • 全てのユーザのスキルツリーをリセットして、スキル習得に使うポイントも返却する
  • 新しくなったスキルツリーで、またイチからスキルを取得し直してね!

といった運用方法もあり得るわけです。 (実際、そのようにしたオンラインゲームがあった、という話を聞いたことがあります)

ユーザに作業コストを強いる方法にはなりますが、ユーザにとっても楽しみがあるやり方になる可能性はあります。 時にはシステムの完璧さを求めるよりも、柔軟に運用方法でカバーするといった解決策も選択肢に入るでしょう。

おわりに

本記事では、

  • リリースした後にデータを変えたくなった時、既存のユーザデータに破綻を起こさない方法として、どのような戦略があるか

というテーマについて、実践的な例を挙げて説明しました。
この記事が、ゲーム開発をする誰かの時間を節約するものになれば幸いです。

他にもこんなやり方はどうか? とか、こういった観点もあるぞ、といったご意見などがあれば、 XDiscord でコメントください 😊