『呪術廻戦 ファントムパレード』渋谷事変マップのアウトゲーム開発 ~新規表現と保守性の両立~

alt_text

目次

渋谷事変マップとは

弊社で開発・運用中のタイトル『呪術廻戦 ファントムパレード(以下、ファンパレ)』に、渋谷事変マップという新しい見た目のメインクエストを2025年3月に追加しました。渋谷事変で起きている様々な出来事に対してゲームならではの表現を実現する改修となっており、既存のメインクエストに新しい要素が複数追加されています。

この渋谷事変マップは大きな開発でありながらサーバー側の開発が全く無く、根幹のデータ構成に関しては既存のものを使って表示のみを渋谷事変用に変更しているのが大きな特徴となっています。

渋谷事変マップ作成までのハードル

渋谷事変開発は、大きく分けて3つのハードルがありました。

1.通常メインクエストマップと渋谷事変マップの両立

2.データ構成を変えずに行う渋谷事変マップの表現

3.通常メインクエストに存在しないクエストクリア後の演出やマップ背景の変化の実現

本記事ではこの3つをそれぞれ解説していきます。

通常メインクエストマップと渋谷事変マップの両立

渋谷事変マップはメインクエストの一種であることから、今まで使われていた通常メインクエストマップと渋谷事変マップをシームレスに行き来することとなりました。

章選択のボタンを押した時やマップ上で矢印ボタンにより渋谷事変マップの対象クエストから通常メインクエストマップに遷移するパターンなど、様々なケースを想定すべく設計を考えました。

『呪術廻戦 ファントムパレード』の画面基盤設計としてDisplayという単位で画面が分かれており、例えばホーム画面はHomeDisplay、メインクエスト画面はQuestSelectDisplayという画面のクラスになっています。

このDisplay間を移動する際に、UI基盤側でそのDisplayを使用する際に必要なオブジェクトの生成やトランジションが行われており、これは画面遷移全般のフローとして定められています。

また、Displayから直接描画処理をまとめたDisplayViewを呼び出す形になっており、ユーザーからのアクションはViewに届きますが、コールバックを通してPresenterから反映され、Displayはマスターデータから構築されるため、名称などは違いますがMVPの構築となっています。

alt_text
通常のメインクエストのDisplay構造

開発当初、第一に今までの通常メインクエストのコードに「渋谷事変なら〇〇する」という分岐を入れることを検討しましたが、クラスの肥大化が著しくなることは明らかだったため行いませんでした。

通常メインクエストマップと渋谷事変マップはほぼ見た目が違うマップであり、それぞれ違うDisplayとして管理する方針を考えていました。

しかし、この方針だと前述した「UI基盤側でそのDisplayを使用する際に必要なオブジェクトの生成やトランジション」が画面遷移時に必ず行われてしまい、僅かな時間ではありますが通常メインクエストマップと渋谷事変マップ間の遷移にラグが生じてしまうことが分かりました。

そこで、Displayとしてはクエスト画面として唯一のものを使い、Viewとして通常メインクエストマップと渋谷事変マップを切り替える方針で進めました。

結果として、通常メインクエスト画面のDisplayであったQuestSelectDisplayを2つの画面を切り替えるための機構とし、IDisplayControllerを介して、共通の処理を呼び出す形にしました。

Display基盤は全てOutGameDisplayBaseという親クラスを継承しており、そこにある共通の処理はこのIDispayControllerを介して呼ばれます。

また、あまり使用想定はありませんが全ての画面で必ず呼び出したい処理があった際はQuestSelectDisplay側から呼び出すことも可能です。

alt_text
渋谷事変マップのDisplay構造

実際のコードとして説明すると、まずIDisplayControllerは下記のようなクラスになっており、Display基盤において呼び出しが必須なメソッドとDisplay切替時のメソッドを定義しています。

public interface IDisplayController<T> where T: OutGameDisplayChangeBaseData
    {
        public Action<T, Action> OnSwitchDisplay { get; set; }

        public UniTask OnDisplayChanged(T data);
        
        public UniTask OnPrepareAsync(T param, CancellationTokenSource cts);
        
        public UniTask OnShowAsync(CancellationTokenSource cts);
        
        public UniTask OnHideAsync(CancellationTokenSource cts);

        public void OnStackedDialog();

        public void OnUnstackedDialog(bool shouldUpdate);

        public bool ShouldBackInDisplay();
    }

QuestSelectDisplayでは、UI基盤であるOutGameDisplayBaseから呼ばれるメソッド内でどちらの画面を開くかを判定し、その画面内の画面構築処理を呼び出します。

遷移パラメーターと呼んでいる画面を遷移するためのデータ群を開きたい画面に渡すことで画面構築を行います。

protected override async UniTask OnPrepareAsync(CancellationTokenSource cts)
{
    // 遷移パラメーターの取得
    _param = OutGameDisplayManager.Instance.GetChangeData<Param>() ?? _param;
    
    // どちらのディスプレイを開くか判定
    _currentQuestDisplayType = GetQuestDisplayType(_param);
    _currentDisplay = GetDisplay(_currentQuestDisplayType);

    await base.OnPrepareAsync(cts);

    // 画面構築処理
    await _currentDisplay.OnPrepareAsync(_param, cts);
}

したがって、実際の画面構築はそれぞれの画面(QuestSelectController、ShibuyaIncidentController)側に処理を書くことになります。

そして通常メインクエスト側から渋谷事変マップに移動したい場合、またはその逆の時は、IDisplayControllerに定義されているOnSwitchDisplayを呼び出します。

例えば、渋谷事変マップ側でのチャプター変更メソッド中に移動先が渋谷事変マップではない時は以下のようにOnSwitchDisplayを呼び出して画面を遷移します。

このコールバックにはQuestSelectDisplay側から画面切り替え時の登録をしておき、移動したい際にはそのコールバックを呼び出すだけで互いのマップを行き来できます。

// チャプター変更メソッド
private async UniTask<bool> ChangeChapter(int chapterNumber)
{
    // チャプターマスターの取得
    var chapterMaster = MasterDataManager.MainQuestChapter.GetByChapterNumber(chapterNumber, _chapterType);
   
    // 移動しようとしている章が渋谷事変でない場合は通常のクエストへの遷移処理を行う
    if (!chapterMaster.IsShibuyaIncidentChapter())
    {
        OnSwitchDisplay?.Invoke();
    }

    // 以下通常のチャプター変更処理
    ・・・
}

つまり、

  • 画面の切り替えや処理の発火に責任を持つクラス(QuestSelectDisplay)
  • 画面内のデータの持ち方や画面構築方法に責任を持つクラス(QuestSelectController、ShibuyaIncidentController)
  • 画面内の表示処理に責任を持つクラス(QuestSelectDisplayView、ShibuyaIncidentDisplayView)

に分かれたということになります。

とはいえ、ここまで話しましたが今回の手法は決して画面設計時のベストプラクティスではありません。

ただ、元々のQuestSelectDisplayやそのViewはあわせて5000行を超えるような肥大化されたクラスで、これらの既存部分に対して変更を加えながら、新規画面である渋谷事変マップを実装するのは至難の業でした。

そこで、QuestSelectDisplayViewはそのままに、QuestSelectDisplayはQuestSelectControllerという新しいクラスに丸ごと移植できる今回の方式を取ったことで、影響範囲を気にせず渋谷事変マップ開発に集中することができました。

画面遷移基盤に変更を手を加えたくないが基盤にしたがったような挙動を再現しつつ、自由に画面遷移を行いたいという時に、その画面構築メソッドごとインターフェースで確約することで実装した一例として参考にしていただけますと幸いです。

渋谷事変用のデータ構成

渋谷事変マップで既存のメインクエストマップと大きく操作として変わるのは時間帯の移動の演出です。

このデータ構成に関しては、メインクエストの既存の仕組みを拡張した使い方をすることで実現しました。

まず、前提の説明ですが、メインクエストでは「章」と「エリア」と「クエスト」の区分けが存在します。

  • 「章」→そのまま〇章という表示の通りで章の中に複数のエリアを持つ
  • 「エリア」→章の中で一画面に表示されているクエスト群
  • 「クエスト」→実際にプレイする1話のクエスト

となっています。

章が複数のエリアを持っており、エリアが複数のクエストを持っているというイメージが分かりやすいかもしれません。

今まではエリア内のクエストを全てクリアしたら次のエリアに進めるという流れで分かりやすい一本道のルートに見えていたかと思います。

しかし、渋谷事変では画面左のスクロールビューで「時間帯移動」が可能となっており、これによって表示されるクエストの内容は変化します。

どちらも一本道ではあるものの一見通常メインクエストマップと渋谷事変マップでは明らかに構造が違うように見えるかと思います。

ここで内部的には「エリア」=「時間帯」と考え、今までエリアはそこにある全てのクエストをクリアしないと次のエリアにいけなかったものを、クリアしたクエストに紐づいた次のクエストが存在するエリア(時間帯)に移動できるというロジックに変更されています。

これによって、今までと全く同じデータ構造で違うプレイ体験を実現しました。

したがって、実は渋谷事変のメインクエスト画面は渋谷事変ではない9章以前のメインクエストデータでも(その他の調整データは必要ですが)動作させることができます。

また、渋谷事変では既存のメインクエストと違い、実際にクエストを選ぶ前に時間帯内の地点(例:渋谷ヒカリエ・渋谷ストリーム)を選択することが出来ます。

これによってどの時間帯どの場所で何が起きているか分かりやすくするようにしていますが、これによって今まではファーストビューで取得したクエスト情報を元にそのまま表示すればよかったものが、地点ごとに紐づいたクエストを表示する必要がでてきました。

そこで、地点IDという概念を追加し、メインクエストにそれぞれ振られているIDに対してそれぞれ地点IDを紐づけました。


/// <summary>
/// 渋谷事変マップマスター
/// </summary>
[Serializable]
public partial class IncidentMapMasterClientDto : ClientMasterBaseDto
{
    /// <summary> ID </summary>
    public int Id { get; set; }

    /// <summary> 対象のメインクエストID </summary>
    public int MainQuestId { get; set; }

    /// <summary> 対象の地点ID </summary>
    public int IncidentPlaceId { get; set; }

}

こちらは至ってシンプルで、今まではメインクエストの一話ずつが単独で存在していましたが、この地点という概念によって、メインクエストの数話がこの地点に内包された状態を表現できます。

また、これは表示上の話のみなのでユーザーデータやマスターデータの持ち方としては既存と変える必要がありません。

他にもこのように既存にも存在していたメインクエストIDを起点として、様々なマスタデータを構築し、渋谷事変の表現を行っています。

クエストクリア後演出、マップ変形演出について

マップ変形演出の紹介

渋谷事変マップでは全体的な見た目が変わっていることの他に、メインクエストクリア後な特殊な演出やクエストの進行度に応じたマップの変形演出も行われています。

現在は、演出として

  • “帳”
  • 極ノ番「隕」

が実装されています。

極ノ番「隕」では、「隕」が落ちた後は渋谷事変マップとエリア内の詳細マップのどちらも変形した状態で表示されるようになっています。

alt_text
変化前の渋谷事変マップ

alt_text
変化後の渋谷事変マップ

また、ストーリー内で領域展開「伏魔御廚子」を放った後にも変形演出が入り、BGMも専用のものに変化します。

マップ変形演出の仕組み

これらの仕組みとして、まずクエスト進行度を進捗値としてそれぞれのメインクエストに1から始まる番号(シーケンス)を振っています。

そして同時にユーザーデータとしてどのクエストまでクリアしたかをシーケンスで保持しています。

したがって、このシーケンスを元に以下のようなマスタで開始シーケンスと終了シーケンスの間にあるデータを取ってきて、そこにある地形変化グループというIDを取得します。


/// <summary>
/// 渋谷事変地形変化マスタ
/// </summary>
[Serializable]
public partial class IncidentLandformChangeMasterClientDto : ClientMasterBaseDto
{
    /// <summary> ID </summary>
    public int Id { get; set; }

    /// <summary> 地形変化グループ </summary>
    public int SituationGroup { get; set; }

    /// <summary> 開始シーケンス </summary>
    public int StartSequence { get; set; }

    /// <summary> 終了シーケンス </summary>
    public int EndSequence { get; set; }
}

その後は、この地形変化グループを元に表示したいリソース名が指定の地形変化グループの時変化するようにマスターデータが入っているとリソース読み込みのパスを差し替えてマップ変形を実現しています。


/// <summary>
/// 渋谷事変地形変化時の詳細マップマスタ
/// </summary>
[Serializable]
public partial class IncidentLandformChangeDetailMapMasterClientDto : ClientMasterBaseDto
{
    /// <summary> ID </summary>
    public int Id { get; set; }

    /// <summary> 地形変化グループ </summary>
    public int SituationGroup { get; set; }

    /// <summary> 変化前の詳細マップリソース名 </summary>
    public string BeforeChangeResourceName { get; set; }

    /// <summary> 変化後の詳細マップリソース名 </summary>
    public string AfterChangeResourceName { get; set; }
}

以上のように、どのように地形変化をしているかをIDとして持ち、そこからリソースの差し替えを行うことで、ストーリー状況に応じた背景変化、BGM変化を実現しています。

おわりに

今回はご紹介しきれませんでしたが、

  • 渋谷事変時系列
  • 地点に紐づくキャラアイコン
  • その他背景がそのストーリー地点の位置の背景になっている
  • 渋谷事変の地点内のクエスト一覧表示画面での画面トランジション

など、より自然に様々な渋谷事変を楽しめる工夫が随所に組み込まれています。

これらの機能を考えたプランナーの方々や、素晴らしい演出、UIを作っていただいたデザイナーの方々、ゲーム内のストーリーを描いていただいたADV、シナリオチームなど多くの方々が創意工夫を詰め込んだ施策となっています。

この場を借りて関係者の方々、そしてプレイしてくださっているユーザーのみなさまに感謝申し上げます。

ぜひ、実際に渋谷事変をプレイして体験してもらえると幸いです。

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


alt_text
鈴木 晴

株式会社サムザップ クライアントエンジニア
2023年新卒でサイバーエージェント入社、サムザップ配属
アウトゲームテックリードとして『呪術廻戦 ファントムパレード』クライアント開発に従事