『呪術廻戦 ファントムパレード』「バトルリザルト」の大規模リファクタ ~工数10営削減までの裏側~

alt_text

目次

はじめに

サイバーエージェントに2024年新卒入社した水田です。現在はサムザップへ配属され、『呪術廻戦 ファントムパレード(以下、ファンパレ)』のアウトゲーム開発チームにてクライアントエンジニアをしております。

ファンパレはリリースしてから1年以上が経過し、新バトルコンテンツの追加などに起因してバトルリザルトのコードが複雑化、保守や新規開発の工数肥大化が課題となっていました。

そこで、バトルリザルトの機能に関してリファクタリング(以下、リファクタ)を実施しました。

本記事では、実際にリファクタを実施した背景から、実施方法やバトルリザルト周りの設計・実際に得られたインパクトについて、「リファクタ方針」と「バトルリザルト実装編」へざっくりと分けて紹介していきます。

ゲームエンジニアが直面する運用課題やリファクタ, 設計などについての知見を皆様へ届けることができれば幸いです。 

また、説明を分かりやすくするために若干抽象化してお伝えしている内容や、本来の意味合いとは若干異なる説明を行なっている箇所もあります。

バトルリザルトが抱えていた課題について

ゲームにおいてバトルリザルト画面の実装は、一見すると運用に入れば大きな改修や追加開発を行う必要がない箇所のように思えます。

しかし、ソーシャルゲームのバトルリザルトに関しては、各種期間限定イベントや新規コンテンツの追加があるたびに、常に新しいページやダイアログが追加されます。

実際にファンパレの運用1年半で追加されたバトルリザルトの例として「全体討伐バトル」「呪霊掃討戦」「幻境戦SP」など、様々なバトルに対してのバトルリザルト画面が追加されていきました。

元々、こういったバトルリザルトが運用で追加されることを想定していない設計となっており、管理用の1クラスでまとめて実装されていたため、追加仕様があった際の実装柔軟性が少ない状況でした。

他に抱えていた課題として、バトルリザルトはインゲームで表示されるUIであるため、インゲームをメインの担当とするインゲームチームでしか実装の実態を把握できていない状態でした。

結果として、実装時のタスク分配などでインゲームチームのタスク負荷が高まっている際に、UI表現であるにもかかわらず、アウトゲームチームによるバトルリザルトの対応が難しかったのです。

本来であれば、インゲームといえどもUI表示物であるため、アウトゲームやインゲームといったチームに関係なく、クライアントチームであれば誰でも開発できるように改良することが理想でした。

このような課題によってバトルリザルト周りの作業タスクでは、開発工数が肥大化し、実装における負債もどんどんと大きくなっていました。

リファクタリングの実施方針

リファクタ作業を実施したのは2025年1月〜2月中旬にかけて作業を実施しました。

実際に行った手順としては以下になります。

  1. バトルリザルト演出のフローを洗い出す
  2. 課題を洗い出して、リファクタリングのゴールを決める
  3. QAチームとの期間と対応想定内容のすり合わせ
  4. プログラミング設計を考える
  5. 基盤の実装を進め、バトルリザルトへ反映させる

各手順でどのような事を意識して作業を進めたのかを詳細ににまとめたいと思います。

リファクタ時の注意した項目について

ファンパレにおいて、バトルリザルトはインゲーム終了時に必ず表示される、欠かすことの出来ない画面となっております。

だからこそ、今回のリファクタリングは今後の開発効率化へ寄与するために実施した施策であり、本番で運用済みとなっているバトルリザルトへの影響は最小とすることが求められていました。

例えば、バトルで得られた経験値の演出1つをとってもロジックの実装影響を極力出さないように実装した上で、QAチームに確認を詳細に行ってもらうことでリリースアプリでの不具合発生を防ぐ対策を取っていました。

この注意すべき点を念頭に置きつつ、手順について解説していきます。

バトルリザルトのリファクタフロー

1.バトルリザルト演出のフローを洗い出す

既存のバトルリザルト実装全てと、エンジニア観点で行いたいリファクタリングの内容を照らし合わせて、これからの方針を決めるフェーズとなります。

ファンパレのバトルリザルトは大まかに以下のように演出が実行されています。

[1] バトルリザルトの動画演出を実行

alt_text

[2] バトルリザルトのラベルを表示

alt_text

[3] バトルリザルトUIの表示とアニメーションを実行(× ページの数)

  • コンテンツ表示物
  • クエストラベル
  • 評価ランク
  • フッターのボタン

また、演出中に次へボタンを押下された場合には演出アニメーションのスキップ処理を行っております。

[4] 必要であれば演出ダイアログ等も表示する

alt_text

2. 課題を洗い出して、リファクタリングのゴールを決める

演出フローの洗い出しが完了したタイミングで、リファクタのゴールと目的の再確認を行いました。

前述の通り、バトルリザルトは運用して1年半ということでかなり複雑な実装がされており、対応期間とインパクトを天秤にかけてどこへ根本的な対応を入れるかを考えることが非常に重要でした。

実際にBIGTHINGSという本でも触れられていますが、「達成したいことを最後まで見失わない」ことがとても大切でした。というのも、こういった作業はやって行くうちにあれやこれやとやりたいことが増えていきがちです。

今回のリファクタ前の調査を進めて行く上で、「演出フロー」「表示物制御」「アニメーション制御」など様々な箇所に課題があることがわかりました。

一方で、これまでの開発で明確に工数が増大していた演出フローのリファクタリングのみに対応を絞り、View(見た目部分)の改修対応は行わないこととしました。

※ [3]と[4]の表示物に関しては、バトルの種別やイベントの開催状況によって表示する個数と順番が変化します。

また、敗北の演出では若干流れや表示物が変化しますが、概ね同じような挙動をしています。

3. QAチームとの期間と対応想定内容のすり合わせ

前述の「リファクタ時の注意事項について」でもお伝えしたように、バトルリザルトでの致命的な不具合は避ける必要があります。

QAチームにて作成したバトルリザルトでの不具合チェックを行ってもらうことで、本番での不具合発生を開発チーム内で未然に防ぐことが出来ます。

ですが、今回はバトルリザルトの全改修ということもありQAチームに確認してもらう物量が非常に多く、施策の作業実施が1月だったのですが、すり合わせ自体は昨年(2024年)の9月頃から行っていました。

実際にすり合わせた観点としては、以下の3点となりました。

「バトル終了後にバトルリザルトが表示されるかを全てチェック」
「全バトルリザルトで共通の実装は1箇所で確認できればOK」
「ロジックに不具合が入る可能性がある箇所は全てをチェック」

チェックが必要な項目と除外した項目を判断した基準としては、リファクタの実装で挙動が変化する可能性があるかどうかという点です。

例えば、今回はバトルリザルトにて表示物の表示順序の制御を行うことがリファクタリングの目的であり、UI演出周りへの対応を行うと本来の目的から逸れるため行っておりませんでした。

そういった、目的から逸れるために対応しなかった項目に対しては、リファクタ後であっても実装挙動について変化する可能性が低く、QAチェックは不要としていました。

このように、QAで必要な項目とそうでない項目を正しく選別し、QAチームにて信頼を担保してもらうことが重要です。

4. プログラミング設計を考える

前提条件の洗い出しを終えた段階で、今までの運用を踏まえて、効果的な設計リファクタ方針を決めます。

社内のエンジニアと設計について議論し、結論としてバトルリザルトの設計は以下の設計パターンを組み合わせたハイブリッドなものとなりました。

「Command(コマンド)パターン」をメインに「Composite(コンポジット)パターン」と「Decorator(デコレーター)パターン」を組み合わせ、Commandパターンの内部は「Template Method(テンプレートメソッド)パターン」で実装を制約しています。

まずは、それぞれの設計パターンをざっくり説明して、どの様に利用したかを解説します。

Commandパターン

実行したい振る舞いをメソッドとして実装するのではなく、1つのオブジェクトとして表現する方法です。

例えば、「朝起きたら実行するルーティーン」を実装したとします。その動作の中には、「カーテンを開ける」「歯を磨く」「服を着替える」という振る舞いを実行することが出来ると思います。

こういった1つ1つの動作を切り出して、実行するデザインパターンです。

今回のバトルリザルトでいうと、各演出をそれぞれのコマンドとして切り出しを行いました。

メインクエスト勝利リザルト:「勝利文字演出→経験値演出→ランクアップ演出→幻境の残穢/呪骸電池獲得の演出」
呪霊掃討戦勝利リザルト:「勝利文字演出→掃討ランク演出→獲得報酬演出」

Decoratorパターン

既存の実装に対して、新しい振る舞いや機能を追加する事が出来るデザインパターンです。

また、実装に於いては1つ1つのコマンドを再利用できるような実装意識をしております。

例えば

  • 勝利時の勝利テキストラベルなどであれば、1つのコマンドで「通常バトル:勝利」「掃討戦:掃討結果」
  • ページ演出やダイアログ演出では、それぞれ大本となる基盤のクラスを作成する。

など、再利用や改造する余地を残した設計としています。

Compositeパターン

ふるまいを実行する実装と、そのなかで定義されている実行内容を同一視するデザインパターンです。

今回のバトルリザルトでは、それぞれのコマンドをCompositeパターンとして実装しています。

例えば、勝利ラベル演出の直後に実行される演出(以降ページと表記)の場合「勝利ラベルの上部へ移動」「クリアランク」「Exp獲得数演出」「クエスト名」それぞれを別のコマンドとして実装しています。

ですが、別々のコマンドで実装すると、演出を同時に実行することが出来ません。とはいっても、「メインクエスト用、Mapイベント用」など、それぞれのバトルリザルト毎に1ページを作成すると、それこそ実装負荷が軽減されず本末転倒になってしまいます。

この時に複数のコマンドをまとめて同時実行できるコマンド(Compositeパターンで動作する実装)にすることで、保守性と利便性を担保しています。

alt_text
バトルリザルト内でコマンドの対象となっているUI

Template Methodパターン

基底クラスでアルゴリズムの骨格を定義し、派生クラスでは構造を変えることなく振る舞いの具体をそれぞれで作成する方式です。

これは、Unityを利用している方であれば無意識に利用されている、Start関数やUpdate関数といったものをイメージして頂ければわかりやすいかと思います。

今回のバトルリザルトについては、それぞれのコマンドで共通している振る舞いの順序制御を行うために利用しています。

/// <summary> 初期化処理 </summary>
public UniTask InitializeAsync(CancellationToken token);

/// <summary> 表示前の準備処理 </summary>
public UniTask PrepareAsync(TData cacheData, CancellationToken token);

/// <summary> コマンドの実行処理 </summary>
public UniTask ExecuteAsync(CancellationToken token);

/// <summary> コマンドの実行が全て完了まで待機 </summary>
public UniTask WaitForComplete(CancellationToken token);

/// <summary> コマンドの実行終了時処理 </summary>
public void Complete();

/// <summary> コマンド実行状態 </summary>
public PerformanceState State { get; set; }

/// <summary> コマンドの実行をスキップ可能かを管理 </summary>
public bool CanCommandSkip { get; }

/// <summary> コマンド実行後に待機するフラグ </summary>
public bool IsWaitingRequired { get; }

5. 基盤の実装を進め、バトルリザルトへ反映させる

ここまでバトルリザルト実行機能の具体を固めることができたら、実装を進めていきます。

ここで、今までの情報をまとめた大体の設計をUMLで表してみます。

これを見ると、今までの解説内容と設計がある程度イメージ付くのではないでしょうか?

では、今度は組み立てて来たコマンドの処理を制御するバトルリザルトの実行処理本体を作成していきます。

alt_text
バトルリザルト基盤のUML図

コードで実装すると、このようになります。

ExecuteCommandsAsyncメソッド(実行本体)で自動的にコマンドがforeachを用いて順番に実行されていることが分かるかと思います。

このようにして、演出処理の呼び出しタイミングを正確に制御できるようにしました。

/// <summary>
/// コマンド実行処理
/// </summary>
public async UniTask ExecuteCommandsAsync(
    List<IPerformanceCommand<TReuseData>> commands, 
    TData inputData, CancellationToken ct)
{
    Debug.Log($"コマンドの実行開始");

    foreach (var command in _performanceCommandList)
    {
        Debug.Log($"コマンド{command.GetType().Name}の処理開始");

        try
        {
            await command.PrepareAsync(inputData, ct);
            Debug.Log($"コマンド{command.GetType().Name}の実行開始");
            await _currentCommand.ExecuteAsync(ct);
            await _currentCommand.WaitForComplete(ct);
        }
        catch(OperationCanceledException)
        {
            // 握りつぶし
            Debug.Log($"コマンドがスキップされました");
        }
        finally
        {
            Debug.Log($"コマンド{command.GetType().Name}の実行終了");
            _currentCommand.Complete();
        }
    }

    Debug.Log($"全コマンドの実行完了");
}

開発へ与えた影響について

ここまでで、バトルリザルトをなぜリファクタリングする必要があったのか、そしてどのように改善したのかを説明しました。

実際にリファクタリングを行ってから約3ヶ月が経ちましたが、工数が10営以上も削減されるというはっきりとした効果が現れています。

さらに、バトルリザルトの実装を担当する人が変わっても、新しい担当者がバトルリザルト画面を作るのにかかる時間が1〜2営に収まるようになり、工数面でのチーム全体の効率が向上しました。

また、同様のリファクタリングを進めるような動きも進んでおり、今後の開発の現場とチームの意識へ良い影響を与えています。

おわりに

今回は「『呪術廻戦 ファントムパレード』「バトルリザルト」の大規模リファクタ ~工数10営削減までの裏側~」というテーマで、ゲーム開発におけるバトルリザルトの設計とリファクタリングの進め方について紹介させていただきました。

大規模なソーシャルゲームタイトルを運用する上でこういった改善は、より面白いゲームへアップデートするための基礎となります。

また、実際に工数が大幅に削減されたことで、ゲームとしても新たな挑戦への機会を得ることができたと考えており、今後もこういった改善を随所で行うことで、より皆様に楽しんでいただけるゲーム運営を目指していきます。

最後まで読んでいただきありがとうございました。この記事がゲーム開発で困っている方や設計の参考になれば幸いです。

今後もファンパレのさらなる進化にご期待ください!

©芥見下々/集英社・呪術廻戦製作委員会 ©Sumzap, Inc./TOHO CO., LTD.


alt_text
水田将大

株式会社サムザップ クライアントエンジニア
2024年新卒でサイバーエージェント入社、サムザップ配属
『真戦国炎舞 データベース』開発進行、『呪術廻戦 ファントムパレード』クライアント開発に従事