コンテンツへスキップ

QualiArtsengineer blog

タスクキルを救う!安定動作を支える「中断再開」の仕組み

タスクキルを救う!安定動作を支える「中断再開」の仕組み

11 min read

はじめに

株式会社QualiArtsでUnityエンジニアをしている井上です。

多くのユーザーに楽しんでいただくモバイルゲーム開発では、安定した動作の保証が不可欠です。これは、イレギュラーなケースにおいても同様です。

ゲーム開発で発生する要件の中に、セッションの復元というものがあります。これは、ゲーム中にタスクキルをしてしまった場合に、同じ状態から復帰できるようにしたいケースのことです。中断状態から再開する挙動のため、私たちはこの機能を 「中断再開」 と呼んでいます。

しかしながら、中断再開の実装は容易ではありません。クライアント・サーバーを跨いだ状態管理が必要となったり、仕様レベルから考慮が必要なイレギュラーケースも存在します。そのうえゲーム開発の性質上、多くのデータや条件分岐が複雑に絡み合うため、その全てを検証でカバーするのも難しく、不具合を抑制するためには設計レベルでの担保が必要です。

そこで今回は、中断再開の実装において考慮すべき点と、実際に機能開発で用いている手法の事例を紹介します。なお、本記事では、クライアント・サーバー間で連携したゲームを前提とします。

中断再開の必要性と課題

ゲーム開発において、以下のようなシナリオを考えてみましょう。

1日1回しか挑戦できないミニゲームがあります。このとき、ミニゲームが終了したタイミングでその結果を記録し、「挑戦済み」のフラグを立てる挙動を取ったとするとどうでしょう。ミニゲームの結果が良くないと思ったら終了直前でタスクキルすることによって、本来1日に1回しか遊べないミニゲームが何度でも遊べてしまいます

逆に、ミニゲーム開始時に「挑戦済み」のフラグを立てる仕様だったとします。その場合、ミニゲーム開始後になんらかの事情でアプリが落ちてしまった場合、その日はもうミニゲームを遊ぶことができなくなります。もちろん、このような挙動は許容されないケースが多いです。

本来は、ミニゲーム開始後にタスクキルされた場合、タスクキルされた直前の状態から始まらないといけません。このように、特定のタイミングで現在の状態を保存し、再起動後に復元するような挙動を実装する必要があります。このような中断再開の挙動は、不可逆な(後戻りできない)状態遷移を行いたい場合に必要となります。

先ほどは簡単な例で説明しましたが、実際はゲームループ内に複数のステップがあり、その遷移が不可逆というケースがほとんどです。

ゲームの流れの例

ユーザーから見れば一見当たり前の機能と思えますが、実装の際には考慮すべき難しい点が多くあります。

  • ゲームの途中でタスクキルされた場合、どこから再開可能にするのか?
  • 中断後に、他の端末から再開された場合はどうするのか?
  • 中断したデータが、時間の経過により使えなくなった場合はどういう挙動を取るのか?

そこで私たちは、試行を重ねるにあたって見えた課題を洗い出し、安定して高い品質の実装を行う手法を確立しました。ここからは、仕様策定・設計実装・検証における中断再開実装のノウハウを解説していきます。

中断再開のための仕様策定

中断再開の実装において、エンジニアは仕様レベルから議論に参加していく必要があります。ではどんなところを起点に議論するのでしょうか?

状態遷移

まずは、ゲームループにおいて 「どこで中断したらどこからやり直しなのか」 を決めなければいけません。

ゲームには膨大な状態や条件分岐があるため、セッションの保存と復帰に関係のない要素を排除し、単純なモデルに落とし込む必要があります。そこで、ゲームループにおけるセッションとしての状態遷移図のようなものを作ります。

状態遷移図の例

状態遷移図を作ることは、仕様の単純化のほか、開発メンバー間の共通認識にも寄与します。わかりやすく「図」と言っていますが、具体的な図を書かずに簡易な箇条書きで済ませるケースもあります。

基本的にゲームループは一直線であることが多いため、どの情報が、どのタイミングで確定するのか?、という点に焦点を当て、区切りを決めていきます。

  • 編成が確定するのはどのタイミングか?
  • スコアが確定するのはどのタイミングか?
  • ユーザーの順位が確定するのはどのタイミングか?

また、必要に応じて「リタイア」や「強制破棄」のようなケースの考慮も必要です。

複数端末対応

1つのアカウントで複数の端末からプレイ可能なゲームの場合、中断状態で別の端末からアクセスした場合の挙動も考慮する必要があります。

複数端末でのアクセス

別端末からアクセスした場合の挙動をすべての状態に対して決めるのは現実的でないため、基本的には 「中断状態が存在するか」のみを判定 することで動作を分岐させます。 もし別端末から再開可能にする場合は、必要な情報をサーバーから返す必要があり、逆に再開不可能にする場合は、既存の中断状態を破棄して上書きするなどの挙動を取る必要があります。

そして、これとは別に中断した端末側での挙動の考慮も必要です。 たとえば、中断データから再開しようとしたら、別端末ですでに再開されていたり、破棄されている可能性があります。しかし、こういったケースを考えると考慮事項が膨れ上がってしまうため、同一セッションが複数端末で並列進行しないことを保証するのが最も安全だと思います。

複数端末でのセッション破棄

1セッションごとに有効な端末は常に1つであるとして、他端末でセッションが復元された場合、元の端末でセッション外の画面に戻すような仕様にすると良いでしょう。

また、しばしば「中断状態かどうか」は外部から判定する必要があることを忘れてはいけません。例えば、「1つ手前の画面で、中断中であることを通知したい」といったケースがよく出てきます。中断中であることを必要な画面で判定できる設計にしておきましょう。

イレギュラーケースの考慮

中断時には有効だった状態が、外的要因により再開時には有効でなくなっているケースを考慮する必要があります。例えば、再開時にはイベント期間を過ぎていた、中断時と再開時でキャラクターの性能が変更されていた、などです。

イレギュラーケースの例

このようなケースは、検知して強制的に中断状態を破棄する仕組みを作るのが、安定動作を保証する上で最も良いです。

中断再開の設計・実装手法

ここまでは仕様レベルで考慮するべきことを説明しました。先ほど決めたセッション内の状態遷移は、基本的にサーバーのAPIをクライアントから実行することで進行します。

APIによる状態遷移

この上で、エンジニアによる設計実装段階での着眼点を解説していきます。

サーバー保存 vs クライアント保存

中断データはどこに保存すれば良いかという話です。

結論から書くと、基本的にすべてサーバーに保存するのが最も安全です。サーバーのみにデータが存在していれば、クライアントの状態に関係なく全く同じ挙動を保証できます。 しかし、保存するデータサイズやデータの更新頻度の制約により、クライアントに保存しなければいけないケースがあります。そういったデータのみクライアント保存にすると良いです。

ちなみに、クライアント保存のデータが原因でユーザーが進行不能になってしまった場合、こちらからすぐにサポートできないというリスクもあります。この問題は度々運用中の課題として挙がっているため、こういった観点でもデータは極力サーバーに保存すべきです。

複数端末対応をしている場合、他端末でセッションが終了していても、最後にクライアント保存したデータが残ったままといったケースがあるため、これを検知してクライアント保存データの初期化を実行する必要があります。

セッション判定

「新規開始」か「中断状態から再開」かを判定する部分です。複数端末対応をしている場合、「他端末に中断状態が存在するか」も判定する必要があります。 ここでは、複数端末の判定に対応した1つの事例を紹介します。

まず、サーバーとクライアントで最新のセッションIDの文字列を保持します。セッションIDには、重複しない任意の文字列(UUIDなど)を生成して使用します。保持したセッションIDは、互いにセッションを破棄したタイミングで空文字にします。

セッションの識別

このとき、以下の2つのフラグを用いることで、必要なすべてのパターンを判定することができます。

  • サーバーに中断データが存在するか(=セッションIDが空文字でないか)
  • クライアントとサーバーの持つセッションIDが一致しているか(双方が空文字の場合は一致とみなす)

それぞれが以下のような状態を表します。

【サーバーに中断データが存在しない】【サーバーに中断データが存在する】
【セッションIDが一致する】クライアントにもサーバーにも中断状態がない
→ 新規開始
クライアントに有効な中断状態がある
→ 中断状態から再開可能
【セッションIDが一致しない】クライアントに中断状態があるが、サーバーにはない
→ 別端末に中断状態があったが、破棄or完了済み
サーバーに中断状態があるが、このクライアントで有効でない
→ 別端末に中断状態がある

セッション更新

中断再開の対象となるゲームの本体部分です。

セッションの状態遷移や復元に関する処理は、ゲーム本体と完全に分離することが好ましいです。以下では、その例を説明します。

SessionGatewayセッションの状態遷移や復帰を行うクラス。ゲーム本体の実装から、必要なタイミングで呼び出す。
SessionContextセッション内の状態を保持するクラス。ゲーム本体の実装から参照・更新する。

SessionGateway を実装する際は正常系を一本道として書いておき、セッションの復帰処理時にまったく同じ順番で SessionContext を復元するメソッド設計をおすすめします。こうすることで、新規セッション時とセッション復帰時の双方で SessionContext が同じ状態であることを保証できます。

public static class SessionGateway
{
    public static async Task CreateSession(SessionContext context) { /* 状態遷移1の処理 */ }
    public static async Task StartGame(SessionContext context) { /* 状態遷移2の処理 */ }
    public static async Task EndGame(SessionContext context) { /* 状態遷移3の処理 */ }
    public static async Task Finish(SessionContext context) { /* 状態遷移4の処理 */ }

    // セッション復帰処理
    public async Task<SessionContext> ResumeSession(ServerData server, LocalData local) {
        var context = new SessionContext();

        /* 状態遷移1の処理 */
        if (!IsGameStarted()) return context;

        /* 状態遷移2の処理 */
        if (!IsGameEnd()) return context;

        /* 状態遷移3の処理 */
        return context;
    }
}

public class SessionContext
{
    // セッション開始時にあるデータ
    public SessionInitData SessionInitData { get; set; }

    // ゲーム開始時にあるデータ
    public GameStartData GameStartData { get; set; }

    // ゲーム終了時にあるデータ
    public GameEndData GameEndData { get; set; }
}

また、セッション復帰の際は、初期状態から再開したい状態に辿り着くまでの全ての情報をサーバーから返してもらう、もしくはクライアントに保存するようにしておくと安全です。そうでない場合、ゲーム本体で任意の処理を実行した際に、セッション復帰時だけ参照先がnullで進行不能といったケースが頻発します。 上記の例で言うと、中断再開かどうかに限らず SessionContext が同じ状態であることを SessionGateway で保証しておくことによって、ゲーム本体部分は中断再開かどうかを気にしなくて済みます

また、SessionContext のようなセッション間のデータ保持では、データの生存期間を明記すると事故が減ります。例えば、セッション開始時に必ず存在するデータだけを集めたクラスを作る、ミニゲーム完了時に確定するデータだけを集めたクラスを作る、いつ格納されるかわからないデータはその旨をコメントに書く、などが有効です。

クライアントとサーバーの同時更新

状態遷移の際に、クライアント保存とサーバー保存のデータを同時に更新したいケースがあります。 このとき、なんらかの理由により片方だけ失敗したケースを考慮しなければいけません。

基本的には、クライアントまたはサーバーどちらかの更新成功を待ってからもう片方を実行するような実装を行い、後の方だけ失敗したケースを検知してフォールバックを行う形式になります。

同時更新の例

上の画像の例だと、サーバーのAPI実行に失敗したため、クライアントがゲーム終了状態になっているのに対しサーバーがゲーム終了状態になっていません。この状況では、セッション復元時にサーバーから「ゲームが終了しているか」の状態を返してもらい、サーバーのAPIを再度実行するよう実装しています。

中断再開の検証手法

安定した動作を保証するために、リリース前には実機でデバッグ作業を行います。

中断再開を検証するためには、検証したい状態をサーバーおよびクライアントに作る必要がありますが、このフェーズではフラットな検証を行うために、デバッグ機能は極力使わずに人の手で不具合を洗い出しています。このとき、検証シナリオとして、セッションの判定部分・更新部分のすべての分岐をカバーするようケースを提示しています。 実際、実装面の工夫をしていても、このフェーズで思わぬ不具合が発覚することは少なくありません。設計を過信せず、想定されるシナリオを洗い出せるようにしておきましょう。

ここまで、実際に運用してきた中断再開の実装ノウハウを説明してきました。最後にまとめとして、その所感と課題点をお伝えしたいと思います。

運用所感と現存課題

もともと、中断再開は仕様でいえばイレギュラーケースに当たるため、必要となるごとに対処法を検討しており、考慮漏れや不具合報告が多発した箇所でした。この現状に課題を感じ、試行を重ねて上記手法を確立したところ、再利用するうちに中断再開の実装フローが固定化され、不具合の報告件数が明確に減少しました。また、他実装箇所においてセッション復帰に対する考慮が不要になったことで、ゲーム機能実装時の認知負荷が減った実感があります。

しかしながら、この手法は実装知識の属人化を招いており、リリース後の運用における品質保証には依然課題が残っているため、改善の余地があると感じています。

まとめ

本記事では、ゲーム開発におけるセッションの保存および復元に関する実装ノウハウを紹介しました。紹介した内容は実際の開発で発生した課題から得た一つの回答であり、セッション復帰機能の実装における再現性の高い手法を示せたと考えています。この知見が皆さんの助けになれば幸いです。