CORETECH ENGINEER BLOG

株式会社サイバーエージェント SGEコア技術本部 技術ブログ

人が増えても遅くならないゲーム開発をするためにUnityエンジニアが実践したことまとめ

はじめに

こんにちは、株式会社サイバーエージェント SGEコア技術本部(コアテク)のエンジニアの矢野です。

私はこの一年ほど、あるモバイルゲームの新規開発に携わっていました。
このプロジェクトは、開発期間が約1年という、弊社の中では短期間でのリリースを行ったプロジェクトでした。

短期間での開発において、「最初は高速だったのに、人が増えるにつれてどんどん開発が遅くなる」という状況に陥ることは避けなければいけません。
そのため、私はサブミッション(メインは別)として、「開発効率の最適化」を掲げ、プロジェクト初期からコミットしました。
学びの多いプロジェクトだったので、特にプロジェクト初期を中心に、どのような意思決定を行い、どのような仕組みを導入したのか、その一部を紹介したいと思います。

すべてを詳細に書くと大変な分量になってしまうため、今回は概要レベルのまとめになりますが、プロジェクトの立ち上げや開発効率改善に取り組む方々の参考になれば幸いです。

プロジェクトの前提

本記事ではソフトウェアアーキテクチャやワークフローといったテーマを扱いますが、これらの最適解はプロダクトの規模やチーム構成、予算、開発期間などのコンテキストに大きく依存します。
したがって、原理主義的すぎるものなど、非建設的な議論の火種を生まないように、最初にプロジェクトの前提を記述します。もちろん建設的な議論は歓迎です。

  • プロジェクトの規模感
    • 機能重めのハイブリッドカジュアルゲーム
      • 設計もある程度しっかりする必要があるくらいの重さ
    • プロダクト規模 - 画面数:約 97 画面
      • どこまでを「画面」とするかは曖昧なので規模の参考程度に
      • 同じデザインの汎用ダイアログはまとめて1画面としてカウント
      • 3Dの背景だったりチャットのような複雑なものも1画面としてカウント
      • その他、チュートリアルやアニメーションなどもあるが画面数には反映しない
    • エンジニアの人数:多い時で1ヶ月あたりの実働8人月くらい
    • リリースまでの開発期間:約1年
  • その他
    • サーバ - クライアント方式
    • 多言語対応
    • ハイブリッドカジュアルジャンルにしては要件は多め
      • 主観になるので、上記の画面数や後述のテスト数から推察してください
      • セキュリティ対策、UIアニメーションなど非機能要件含めて多め
    • 若手が多めでベテランもいる
    • 仕様の不確実性は低いプロジェクト
    • Unityで開発

これから紹介する事例はあくまで上記の前提条件を持つこのプロジェクトにおける、さらに私の現在の実力という制約下での一つの解に過ぎません。

銀の弾丸は存在しないので、自分が置かれた制約や文化などに合わせて最適解を考えることが我々エンジニアの仕事であるという前提で、一つの事例としてお読みいただければと思います。

プロジェクト初期に負債を産まないということ

プロジェクトの初期(本開発の初期)というのはエンジニアにとってとても快適な時期です。
読み解くべき既存のコードは無いし、ビルドはすぐに終わるし、使ってみたかった最新のライブラリを導入することもできます。
目に見えるアウトプットが出しやすいし、そして一般的にですが、そのようなわかりやすいアウトプットが評価されやすい力学が組織として働きやすい時期でもあります。

それだけにこの時期は、「設計よりも今はスピード重視で」「フォルダ構成は後で決めればいい」など「とりあえず」の判断が横行しやすい時期であるとも考えられます。
このような判断は目先のことだけを考える場合には最適かもしれません。早くアウトプットが出せてわかりやすく評価されやすいし、技術的難易度も低いです。
しかし、中長期的に返済しなければならない技術的負債を生み出しているとも考えられます。

そして、負債には利子がつくものです。技術的負債にも利子がつき、その利子はプロジェクトが進むにつれて複利的に膨れ上がります。
これは例えば、汚いコードが生まれると、その上にその場しのぎのコードが継ぎ足され、解読が難しくなり、バグが生まれ、それを直すためにさらにアドホックな対応が追加されるというループが発生するということです。

さらに、この負債を返すときには最初のコードを書いた人はもうプロジェクトにいないかもしれません。
その場合は別の人が負債を返却することになります。これの発生頻度は、現実の負債のアナロジーとは大きく異なる点と言えるかもしれません。

コードの例以外にも、例えばドキュメントがなくて詳しい人に聞かないとわからない状況だったり、逆に管理すべきドキュメントが多すぎてドキュメントを読んだり書いたりするのに時間を使いすぎるといったことも考えられます。
プロジェクトの後半で開発スピードが落ちるのは、このように膨れ上がった利子の支払いに工数の大半を奪われている状態であると言えます。

本プロジェクトでは短期開発を行うために、まずこのような負債をできるかぎり生まないことを重要視しました。

情報へのアクセス経路を最適化する

さて負債を生まないために「とりあえず」にしてはいけないことの一例として、Unity プロジェクトのフォルダ構造が挙げられます。

「目的のファイルを探す」という作業は、エンジニア・非エンジニアを問わず、Unity を使う全メンバーがとても頻繁に行う作業です。
1回あたりの時間はわずか数秒〜数十秒であっても、プロジェクト全体で積み重なれば膨大な工数になります。

たとえば、サンプルなどでよくある、大元からファイルの種類(Prefabs, Scenesなど)ごとにフォルダを分ける構成だと、以下のような問題が起こることが予想されます。

  • UIデザイナーがUIの調整作業を行いたいとき、Scenes フォルダを開くべきか Prefabs フォルダを開くべきか直感的にわからない
  • ショップ機能のUI調整をするときに、Prefabs/Outgame/ShopTextures/Outgame/Shopという離れた階層を頻繁に行き来する必要がある

このような事態を防ぐために、本プロジェクトでは以下の点を重視して構成を決めました。

  1. エンジニア以外も含め、誰もが迷わず直感的に目的のファイルに辿り着けること
  2. 一つの作業をするときになるべく近くに関連ファイルがまとまっていること

具体的には、「今やりたい作業ごと」に、ルートに近い階層から大きくフォルダを分けるフォルダ構造にしました。
これを個人的にコンテキスト・ファーストのフォルダ構造と呼んでいます。

Assets
└── (中略・プロジェクト名など)
    ├── 3d          # 3Dモデル
    ├── Graphic     # シェーダなど、グラフィックエンジニアが使うもの
    ├── UI          # UIに関連するプレハブ・画像・シーンなど
    ├── MasterData  # マスタデータ
    ├── Scripts     # スクリプトはアセンブリの関係があるのでひとまとめ
    └── ...

このような構成することで、「UIを編集したい」と思った時にはまず迷わず UI フォルダを開けば作業が開始できます。
実際にはもっと細かく決めていますが、基本的には、作業者が迷わないことを最優先としたフォルダ構造としています。

また、このフォルダ構造の話はあくまで一例にすぎません。
重要なのは、情報へのアクセス経路を最適化し、認知のコストを他の人に支払わせないようにすることが、中長期的な生産性に繋がる、という点です。

フォルダ構造以外だと、以下の点も考えておくと良さそうです。

  • 開発用の共通言語(ユビキタス言語)
    • プランナーとエンジニアで違う言葉を使ってしまうことを防ぐことで、「あの機能のことか」と脳内で変換したり、人に聞いたりするコストを無くす
    • ドキュメントを作って終わりではなく、実際に使われるワークフローに載せるところまでやる
  • ドキュメント
    • 構造を整えて迷わずアクセスできるようにする
    • 特に初めてその情報を取得する人が自分で必要な情報を見つけられるように
    • また、書きすぎず、管理し切れる量に絞り、コードを見れば理解できる部分はコードに任せる
    • 特に「書いた人が満足して終わり」にしない
  • 略称
    • 「長いから」「打つのが面倒」「なんとなくカッコいい」などの都合で安易に略称を使い、読む側へ脳内変換コスト・解読コスト・ヒアリングコストを支払わせない
    • そのための文化の醸成
    • (もちろん、共通言語化しているものは問題ない)
  • などなど

細かいことのようですが、このように情報を整理しておくことで、開発が進んでフォルダやドキュメントなどの情報が増えたときに、「人が増えたのに遅くなる」という事態を未然にふせぐことができます。

ブルックスの法則とアウトゲーム設計

この「人が増えたのに遅くなる」という現象は、ブルックスの法則として知られています。
ブルックスの法則では、その根拠として、前節の例のようなコミュニケーションに関わるコストの他に、タスクの分解可能性を挙げています。

例えば、極端な例ですが、ゲームの全機能をたった一つの Scene に配置し、Prefab 化もせずに直接 GameObject を並べて作っていたとします。
この状態では、Unity の仕組み上、複数人が同時にその Scene を編集することは困難です。
誰かが作業している間、他の人は待機するか、後で競合した作業をマージする作業を行うことになります。そしてこのマージ作業は大抵現実的ではありません。

つまり、このプロジェクトはタスクの分解可能性が著しく低い状態であり、人を増やしても並行作業ができず、むしろ待ち時間や競合解決で速度が落ちてしまいます。

これは極端な例としても、実際のプロジェクトでは「ある機能を触ると違う機能が壊れる」「ロジックと表示が密結合していて、UIデザイナーが自分で調整作業をできない」などの形で、並行作業の阻害要因が発生しがちです。

そこでいわゆるアウトゲーム部分については、いくつか設計上の工夫をしました。
まず、ビジネスロジック(見た目に関係しないロジック)の実装が重めなプロジェクトだったため、プレゼンテーションロジック(見た目に関係するロジック)の実装担当者とビジネスロジックの実装担当者を分けられるように設計しました。
その上で、ビジネスロジックはゲーム内の機能単位で分割し、それぞれ独立して別の担当者が実装、そしてテストが行えるようにしました(もちろん、依存関係のある機能にはロジックにも依存が発生しますが)。

また、ビューはビジネスロジックの実装と並行に行えるように、ドメインモデルを参照しない形に設計しました。
さらに、後述しますが、プレゼンターにも依存せず、ビュー単体で動作確認・UIデザイナーによる調整などが行える仕組みにしました。

構成図

また、このようないわゆるアーキテクチャの側面以外に関しても、後から人が増えてもスムーズに動けるようにするために、プロジェクト初期に以下のようなことを決めました。

  • モデルへのアクセスの方針
    • ドメインモデルの読み書き経路はわかりやすく統一する
    • ビジネスロジックとプレゼンテーションロジックの担当者のコミュニケーションを明確にするため
    • データの整合性を担保して、画面を跨いだときや複数画面で表示される情報などの管理を正しく行うため
  • 画面遷移の実装方法
    • MVP などの GUI アーキテクチャはあくまで一つの画面や表示要素が関心ごとなので、画面遷移はその外側で制御する
    • Android の「戻る」ボタンに無理なく対応できる設計にしておく
    • 仕組みは何でもいいのですが、今回は私が個人でリリースしている OSSUnity Screen Navigator」を使用しました
  • グローバルアクセスの方針
    • staticやシングルトンは、インスタンスを差し替えられないので、テストの阻害要因となるため原則禁止
    • グローバルアクセスしたいモジュールにはグローバルなサービスロケーター経由でアクセスする
    • ただし、サービスロケーターをアンチパターンとする一般的な主張の背景を踏まえた上で節度を持って使用する
    • ストレージ保存システム、課金システムなど妥当性があるものに限る
  • 学習コストの最適化方針
    • 特に大人数での分業が予想される部分について、学習コストを考慮して費用対効果を最適化することを意識する
    • 想定されるメンバーのスキルセットを踏まえて、学習に時間がかかるであろうライブラリやアーキテクチャの導入は慎重に行う(それだけの効果が見込めるなら良い)
    • 触る人数が少ない部分はそれほど気にしなくていい
    • また、これはあくまで開発効率という観点における注意点であり、育成など別の観点がある場合はこの限りではない
  • メッセージング(イベント通知)の方針
    • ビジネスロジックで発生したイベント(あるいはドメインモデルの変更イベント)やビューで発生したイベントをやり取りする方法を決めておく
  • アセンブリの利用方針
    • 意図しない参照を防ぐために、またコンパイル時間を短縮するために、アセンブリを分ける
    • 分割しすぎると管理が困難になるデメリットもあるため、メリットを最大化するようにバランスを考える
    • アセンブリ外からアクセスする必要がないクラスやメソッドは internal にして、外部とのインターフェースを明確にする
  • など

詳細な部分や差し替えられる部分については後から誰かが決めれば良いので、プロジェクト初期としては枠組み・土台となる部分を中心に決めておきました。

また、もちろんこれらは一人で決めたわけではなく、他のエンジニアの方に相談させていただいて決めました。
ありがたいことにこの相談の過程でいくつかの方針が良い方向に変わりました。

ビジネスロジックのテストとシフトレフトの重要性

前節で触れた通り、本プロジェクトではビジネスロジックのテストを作成しています。
本プロジェクトではサーバサイドにも多くのロジックを持っていますが、クライアントサイドのビジネスロジックだけでも882件(執筆時点)のテストが存在しています。

Test Runner 実行画面

これにより、リリースしてからもロジック自体を起因としたバグは少なく、また従来と比べてデバッグにかかる工数も大幅に抑えられたように思います。

これは良いことですが、テストを書くメリットはそれだけではありません。
分業の観点で言えば、ビジネスロジックの実装担当者は、テストを実行することで自分の実装が正しいことを他者の実装に依存しない形でテストすることができます。
これにより、ビューが未完成であってもビジネスロジックの動作確認をすることができるので、ビューの実装と並行・分業して実装を進めることができます。

また、このような並行作業という観点の他に、「シフトレフト」の面からも効果的です。
テストを書かない場合、バグは開発工程の後半、つまり実装完了後の実機プレイでようやく発見されることになります。
この段階でのバグ修正は、以下のような理由から、初期段階での修正に比べてコストがずっと大きくなります。

  • 「修正 → ビルド → 実機転送 → プレイ確認」という工程を繰り返す必要があり、軽微な修正でも多くの時間を要する
  • バグチケットの起票、PMによるスケジュール再調整、開発者がチケット内容を理解する時間やコンテキストスイッチ、これらに伴うコミュニケーション、さらにこれらに時間を取られた結果発生した遅延のリカバリーのための会議など、開発以外のオーバーヘッドが各所で発生
  • 開発後半になるほど機能は密結合し、修正の影響範囲が予測しづらくなり、既存のコードを読んだり実装担当者に確認をしたりといった作業が必要になる
  • さらに、「当時の実装者が既にいない」といった事態が重なると、調査コストが跳ね上がる

これのような状況を防ぐために、バグの検出と修正をプロセスの早い段階(左側)に移動させる「シフトレフト」が重要だと考え、そのための手段の一つして、ビジネスロジックではテストを作成しています。

なお、テストを書かない理由として「テストを書く時間がない」という話をよく聞きます。
この主張の妥当性はプロジェクトのコンテキストにより異なるので本記事では論じませんが、上記のようなシフトレフトしないことによるコストの増大が起こりうるプロジェクトの場合でいえば、「テストを書かないから余計に時間が無くなる」と言えそうです。
したがって、本プロジェクトとしては、初期からテストを書くことを決め、それが実現できる設計とワークフローを構築しました。

ビューのテストを諦めない

Unity におけるビュー(GUI)のテストは、一般的に自動化の難易度が高いです。
その理由は色々ありますが、一つの大きな理由として、以下のように、正しさの判定に人間が必要だということが挙げられます。

  • 見た目を判定する必要があるもの
    • テキストが赤い色になっているか
    • 画像がズレていないか
  • インタラクションが必要なもの
    • ボタンをクリックされた時にイベントが呼ばれるか
    • スクロールした時に画面外にあったUIがアニメーション付きで出現するか

これらは人間の目や手を使って手動で動作確認を行わないと正しさの判定が難しいものになります。
そこで思いつくのが、Unity エディタ上または実機上で、実際にプレイしながら人間が動作確認をする方法です。

しかしながら、ゲームのビューというものは構成も状態も複雑なものが多く、パターンも含めてプレイして確認するのはとても非効率です。
試しに下図のような、ちょっとしたショップ機能の1商品のビューを考えてみます(本プロジェクトとは関係のないビューです)。

商品のビュー

構成としてはそこまで複雑ではないので、各要素を並べるだけならすぐに実装できそうです。
しかしながらこのビューは状態が多く、以下の要素を制御する必要があります。

  • 背景色の種類(2色の背景色を制御)
    • 緑: 商品が無料の場合
    • 紫: ジェムショップの場合、ただし無料商品は緑
    • 黄色: コインショップの場合、ただし無料商品は緑
    • 青: 上記以外のショップの場合(無料商品も青)
  • アイコン
    • コイン: 商品により6種
    • ジェム: 商品により6種
    • その他のアイテム
  • 商品名
  • 価格タイプ($などの単位表示が変わる)
    • 無料
    • ジェム
    • リアルマネー
  • 価格
  • バッジの種類、有無
  • クリックされた時にイベントが発行されること

これらの要素は組み合わせて使われるので、パターンはさらに多くなります。
また、最大文字数、最小文字数の時など、境界値で見た目が破綻しないかを確認する必要もあります。
さらに、アイコン画像などが存在しない場合にどういう挙動になるのかも確認しておいた方がいいかもしれません。

このようなことを考えると、これらを正確に実装できているかを実機でデバッグするのはやはり非効率であると考えられます。

また、シフトレフトについて考えると、少なくとも本プロジェクトについては、ビジネスロジックとは明確に切り離されているべきだと言えます。
これによりビジネスロジックの開発が完了していなくてもビューの開発を行うことができるため、分業可能性が高まります。

さらに、UIデザイナーがUIを調整したり、アニメーションをつけたり、またサウンドをつけたりするワークフローについても、効率的に行えるものを考えないといけません。

ここまでの話をまとめると、以下の要件を満たせればビューを効率的に開発・動作確認をすることができると考えられます。

  • ビューが持つ各状態における見た目を確認したい
  • その状態の制御が正しくできているかロジックを確認したい
  • インタラクションの制御を確認したい
  • 独立して開発したい
  • エンジニア以外がアニメーションやエフェクトなどをつけやすい形にしたい

これを達成するために、本プロジェクトでは以下の方針で実装を行いました。

  • ビューはビジネスロジック(など)を参照しないような設計にする
  • ビューにダミーデータを設定して開発・動作確認できるシーンを作成する
    • ダミーデータはInspectorから設定
    • このシーンでサウンドやアニメーションなども開発・確認を行う
  • 上記のシーンは最低でも画面単位、ショップの商品の例のような複雑なビューはもっと細かく分割する

下図は、実際にこのシーンで動作確認を行なっている様子です。

Inspector からダミーデータを入れている図

また、開発効率を高めるためには、このシーンは手軽に作れることが重要です。
エディタ拡張により、たとえば以下のようなコードを書くだけで、上図の Inspector が初期値付きでいい感じに表示されるようにしています。

using System;
using UnityEngine;

namespace Example
{
    [Serializable]
    internal sealed class RegularShopProductPanelDemoPresenter : MenuDemoPresenter<RegularShopProductPanel>
    {
        public void Setup(
            RegularShopProductPanelColorType colorType = RegularShopProductPanelColorType.Green,
            string iconResourceKey = "Components/Icon_ShopItem/ShopItem_s_CoinPack_4.png",
            string productName = "Basket of Coin",
            int amount = 1,
            RegularShopProductPriceType priceType = RegularShopProductPriceType.Gem,
            int priceAmount = 100,
            string badgeText = "Best Value")
        {
            var price = new RegularShopProductDisplayPrice(priceType, priceAmount);

            view.Setup(colorType, iconResourceKey, productName, amount, price, badgeText, OnClick);
            return;

            void OnClick()
            {
                Debug.Log($"デモ商品クリック: {productName} (価格: {priceAmount}, 数量: {amount})");
            }
        }
    }
}

ちなみに、ダミーのサーバレスポンスJSONを入れられるようにしてプレゼンテーションロジック全体をテストできるようにする方法も考えました。
しかし、その場合はビジネスロジックの実装がビュー実装の先行タスクになり、分業可能性が低下するため、本プロジェクトでは見送りました。

インゲームは柔軟な初期設計に

さて、次にいわゆるインゲームの話に移ります。

本プロジェクトにおけるアウトゲームと比べた時のインゲームの大きな特徴として、インゲームにはアウトゲームほど物量が存在しないという点があります。
したがって、アウトゲームほど多人数での並行開発を考える必要はありませんでした。

しかしながら一方で、インゲームは面白さや手触りのイテレーションを高速に回せる必要があります。
初期段階では想定し切れないことも多く、その状態で詳細に設計をし過ぎてしまうと、このような調整スピードが落ちてしまう恐れがありました。

これら2点の理由から、インゲームに関してはアウトゲームよりも軽い設計に留めました。
ある意味担当者任せとも言えます。インゲーム担当エンジニアがとても頑張ってくれました。ありがとうございます。

さて、アウトゲームより軽い設計に留めるとはいえ、後から破綻しないように最低限の設計だけは行いました。

まず、ライフサイクルと実行順の制御です。
Unity では、例えば複数の MonoBehaviour に Start や Update メソッドを書いた場合、その処理順序は不定です。

public class Player : MonoBehaviour
{
    // Enemy の Update よりも先に呼ばれるかもしれないし、後かもしれない
    void Update()
    {
    }
}

public class Enemy : MonoBehaviour
{
    // Player の Update よりも先に呼ばれるかもしれないし、後かもしれない
    void Update()
    {
    }
}

Script Execution Order 機能で管理することもできますが、細かく管理する場合には煩雑になりがちです。

しかしながら実際には、細かく正確に処理順を制御しなければならない場面が多いです。
例えば、味方と敵がダメージを与え合うバトルゲームでは、「お互いのHPが同時に(同じフレームで)ゼロになった時」の挙動を正確に実装するために、処理順を厳密に制御する必要があります。
これを行わないと、上記の状況で「特定のプラットフォームでは敵を倒せるのに、他のプラットフォームでは味方が倒される」など非常に厄介なバグが生まれます。
他にも、1フレームだけ表示がずれたり、入力したはずの処理が1フレームだけ遅れたりという再現の難しいバグにも繋がります。

このような状況を防ぐために、ゲームループや各オブジェクトのライフサイクル、処理順を Unity に任せない形で管理する仕組みを実装しました。
以下のようにゲームループを回して、Tick メソッド内で各オブジェクトを適切な順番で更新することで、処理順を正確に制御することができます。

// 簡略化した実装イメージです
public async UniTask ExecuteAsync()
{
    Enter();

    while (true)
    {
        Tick(_deltaTimeProvider.GetDeltaTime());
        
        if (_isExitScheduled)
        {
            break;
        }

        await UniTask.Yield();
    }

    Exit();
}

また関連する実装として、インゲームが持ちうる状態の定義と状態遷移の仕組み、またそのライフサイクルを管理するための実装も併せて行いました。

その他には以下のようなことを決めておきました。

  • アーキテクチャ
    • 今回はビジネスロジックを分離しやすい仕様だったので分離
    • ただし Transform など、Unity の特性上テストしづらいものについてはビジネスロジックから除外して、Actor レイヤーとして分割
  • テスト
    • ビジネスロジックについてはテストを記述できるように
    • Actor についてはダミーデータで動かせるデモシーンを作成
      • ゲーム内の要素ごとに細かく開発・動作確認できる仕組み
  • 初期化
    • データの受け渡しインターフェースと、任意のデータを使って独立してインゲームを動作させる仕組み
  • 概念と用語の整理
    • インゲームに出てくる概念を整理・ユビキタス言語を決めた

なお、プロジェクトの初期に便宜上「インゲーム」「アウトゲーム」という言葉で領域を分割しましたが、これについてはよくありませんでした。
というのも、インゲーム内には「ポーズダイアログ」などのUIが表示されるのですが、これは果たしてインゲームなのかアウトゲームなのか、混乱を招いてしまったためです。
機能の性質で分けるべきところを、見た目上の単位で分けてしまった点は次回のプロジェクトに向けた反省点の一つです。

作り直しを防ぐLookDev

アセットの作り直しは、プロジェクトの工数を圧迫する大きな要因です。
特に、開発の終盤になってから以下のような問題が発覚して大規模な作り直しが発生することは避けなければなりません。

  • キャラクターモデルを実機で動かしたら重すぎて、すでに制作済みの全キャラクターについてポリゴン数の削減作業が発生
  • リッチな表現のためにシェーダを作り込んだが、重すぎたのでシェーダを大幅に変更
  • テクスチャ解像度や枚数の管理が甘く、実機でメモリ不足(OOM)によりクラッシュしたため全テクスチャを描き直し
  • 開発途中の仕様変更で「敵の出現数を増やしたい」となったが、描画負荷の限界で実現できないため背景(環境)のモデルなど他の箇所を作り直し
  • 仕上げ段階でポストエフェクトを追加したところ、パフォーマンスの空きがないので絵作りから見直し

これらの問題の中には、ポリゴン数などのように「事前にルールを決めて発生自体を防げる問題」と、仕様変更のように「発生自体は防げないが、なるべく発生を抑制しつつバッファも作っておくべき問題」の二つがあります。

まず前者については、一度に描画するキャラクター数や背景の密度、エフェクトの量といった描画物を決め、頂点数やテクスチャサイズ、ドローコール数などの定量的なレギュレーションに落とし込むことで、処理落ちやクラッシュを防ぐことができます。

次に後者については、仕様変更や演出のリッチ化要望は開発を進めていれば必ず発生するものであり、これ自体はプロダクトの価値を高めるために必要なことです。
しかしながら、「面白くするためならどれだけ何をしてもいい」にしてしまうと、あくまで最悪のケースの仮定ではありますが、多くの作り直しが発生し、その結果デスマーチに陥り、健全な危機感を持つメンバーが離脱してしまい、さらには無理を通し乗り越えることを美徳としてそれに過剰適応した組織体制や文化が残り、最初に戻る、という悪循環が発生してしまう可能性があります。
このような事態を引き起こさないために、プロジェクト初期段階では以下の動きをすることが必要です。

  • 決められることは可能な限り全て決めておく
    • 「なんとなく決めてない」だけのこともあるので、何が決まれば決まるのか、誰が決めれば決まるのかを握りに行く
    • あくまでプロダクトの価値が最終目的なので、必ずしも決め切ることを目的とせず、場合によっては「決め方を決める」ことをゴールとする
  • その上で、すぐに決められないものについては、後から何がどの程度変わりうるのかを想定し、バッファを設けておく
    • ポリゴン数やテクスチャサイズなどにあらかじめ想定した分の余裕を持たせるなど

要は、作り直しを防ぐためにはできる限り不確実性への対処をしておく動きが重要ということです。

これを実現するため、本プロジェクトでは LookDev として検証フェーズを初期に設けて、検証と合意形成を行いました。
LookDev と呼ばれる工程の定義はプロジェクトにより微妙に異なる可能性がありますが、本プロジェクトでは「処理負荷などの非機能要件もクリアした上で最終的な見た目を決める工程」と定義しています。

具体的には以下の内容を行いました。

  • 動作を保証するモバイル端末の基準決め
    • どの端末でどの程度のFPSで動くことを目指すのか
    • AndroidiOSともに最低保証とするラインをCPUやGPUの観点から決める
    • ビジネス的な観点が多分に含まれるので、ビジネスサイドと調整
  • プロトタイプの作成と絵作り
    • 実際のゲーム画面に近いプロトタイプ(ゲームとして動かなくて良い)を作成し、ビジュアルの方向性を決める
    • コアテクのグラフィックスチームとTAの方に協力していただき、統合スタイライズドレンダリングシステム「SIRIUS」を使用(後述)
    • 本プロジェクトではこの段階で最終系にかなり近い絵作りをすることができた
  • パフォーマンス検証
    • プロトタイプを実機で動作させ、CPU、GPU、メモリそれぞれの負荷状況を計測
    • 本プロジェクトは描画するオブジェクトの数が多かったので、シェーダのチューニングやポストエフェクトの取捨選択、頂点数のレギュレーション決め、品質設定機能の設定値決めなどを行う必要があった
  • ワークフローの確立
    • アセットの制作からUnityへのインポート、確認までのパイプラインを整備(他のエンジニアの方にやっていただきました)

上記の SIRIUS については私が所属しているコアテクのグラフィックスチームが開発している、いろんなプロジェクトで汎用的に使えるスタイライズドレンダリングシステムです。
詳細は以下の講演をご参照ください。

www.youtube.com

また、頂点数やテクスチャサイズといったレギュレーションは、決めて終わりではなく、それを守れるワークフローを整えることも重要です。
守れないルールに効果はなく、それどころかルールを決めるための工数の分だけ開発効率が低下するためです。
本プロジェクトではインポートパイプラインで Unity に入れる前にテクスチャをコンバートしたりしていたため出番はありませんでしたが、守れるワークフローを作るためのツールとして、決めたレギュレーションをチェックできる Asset Regulation Manager というツールをコアテクからリリースしています。

Asset Regulartion Manager 画面

こちらは OSS として公開していますので、よろしければ使ってみてください。

github.com

Unityエディタのパフォーマンスチューニングに向き合う

開発を進めていると、想定していたフレームレートが達成できないなど、ランタイムのパフォーマンスが問題になることがあります。
このようなランタイムのパフォーマンスについては、実機でプレイすればすぐにわかるという意味で、課題として挙がりやすいものであるといえます。
これに対して、「Unity エディタ自体のパフォーマンスチューニング」は課題にあがりづらいですが、開発効率という観点では非常に重要です。

例えば、開発が進むと以下のような時間が増えていくことがよくあります。

  • スクリプトが増えることによるコンパイル待ち時間
  • アセットが増えることによるインポート待ち時間、アセットリフレッシュ待ち時間
  • Playボタンを押してからゲームが始まるまでの待ち時間
  • エディタ拡張が増えることによる各所エディタ操作時の待ち時間

これらは開発者が待てば済む問題であるため、「なんか遅いけど仕方ない」というように、修正の優先度が下がりがち、あるいは放置されがちです。
しかし、これらの待ち時間はプロジェクト全体で見ると到底無視できない時間になり得ますし、また待ち時間自体以外にも、待ち時間により開発者の集中力が途切れるなど目に見えないコストが発生します。
そこで本プロジェクトでは、できる限り待ち時間が増えないように、プロジェクト初期から開発中にかけていくつかの対応を行いました。

まず行ったのはアセンブリの分割です。
Unity のデフォルトでは、すべてのスクリプトAssembly-CSharp.dll にまとめられますが、このままでは1行だけコードを変更した場合でもプロジェクト全体の再コンパイルが走ってしまいます。
Assembly Definition File を使ってアセンブリを分割することで、そのアセンブリおよびそのアセンブリを参照するアセンブリのみがコンパイルされます。

Assembly Definition File の Inspector

アセンブリは実装が進んでからでは分割が困難になるため、プロジェクト初期の設計段階で分割しておくべきです。
ただし分割しすぎるとそれはそれで管理が困難になるため、適切な粒度を保つことも重要です。

なお、「Compilation Visualizer for Unity」という OSS を使うと、アセンブリごとのコンパイル時間を計測できるのでおすすめです。

light11.hatenadiary.com

さて、次にアセットのインポート時間などについて考えます。
本プロジェクトでは、リリース時点で3Dモデルが1000個以上存在しており、それぞれについてテクスチャを2枚ずつ使用していました。
このテクスチャは 4096 x 4096 px で作られていましたが、前述の LookDev の結果、ランタイムで使用するテクスチャは 512 x 512 px にすることにしました。
Unity では、大きいサイズのテクスチャをインポートしたとしても、テクスチャのインポート設定で実際にアプリに入れるときの仕様に合わせてリサイズおよび圧縮を行うことができます。

インポート設定の画面

しかし、そもそも Unity に必要以上に大きなテクスチャを入れてしまうと、インポート時間やプラットフォーム切り替え時間、またリモートにあるテクスチャをバージョン管理ツールで取得するための時間など、あらゆる時間が長くなってしまいます。
それを防ぐためには、Unity Accelerator を使ったり、必要なファイルだけを取得できるバージョン管理ツールを使ったりするという手もあります。
今回のプロジェクトでは、Unity にインポートする前にリサイズ処理をするパイプラインを作成することで、そもそも必要最低限のサイズのテクスチャだけをインポートすることにしました。
これにより、インポート時やリモートからの取得時などに無駄な時間がかかるのを防ぐことができました。

ちなみに Unity 2021 でこの辺りのインポート時間の高速化や分析のためのツールが入ったので、こちらも知っておくと便利です。

light11.hatenadiary.com

また、開発が進みいろんなエンジニアがいろんなエディタ拡張ツールを作るようになると、それらによりパフォーマンスが低下する可能性もあります。
Unity にはランタイムのパフォーマンス計測を行う Profiler というツールがありますが、これを Edit Mode で起動すればエディタのパフォーマンス計測もできます。
本プロジェクトでは開発中に何度かこれを使ってパフォーマンスをチェックしました。

Profiler の画面

最後にドメインリロードの無効化対応です。これは他のエンジニアの方が提案・実装してくれました。ありがとうございます。
Unity では開発中に「Play ボタンを押して動作確認をする」という行為をかなり頻繁に行いますが、この時に発生する待ち時間は開発が進めば進むほど長くなります。
Unity の設定からドメインリロードを無効化することで、このときの待ち時間を短くすることができます。
注意点として、ドメインリロードを無効化すると static なメモリ領域がクリアされないため、以下のようにして明示的にクリア処理を行う必要があります。

using UnityEngine;

public class Game
{
    public static int Score;

    // Playボタンが押されたタイミングで強制的に初期化する
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Init()
    {
        Score = 0;
    }
}

本プロジェクトは上述のとおり、static は原則禁止としていたため、そこまでの追加工数を必要とせずにこの仕組みを導入できたと思います。

工数見積りに工数をかけすぎない

開発効率を考える上で、前節の Unity エディタの待ち時間と同じように見過ごされがちな問題として「過度な見積もりにかける工数」があります。

まず、多くのプロジェクトにおいて、実装が見積もり時間を超過する現象は日常的に発生します。
これは必ずしもエンジニアの能力不足というわけではなく、タスクに含まれる不確実性が原因となることが多いです。

例えば「使ったことのないライブラリの導入」や「前例のない機能の実装」などは、実際に手を動かさない限り正確な工数は分かりません。
正確な工数がわからないままに見積もりを出してコミットメントすると、上記のように遅れるという結果になりがちです。

仮に、これを実態に基づいて見積もるなら、「最短3日で終わるが、最長15日かかる」といった広いレンジを設けることになります。
レンジが広いように見えますが、「不測の事態にかかる工数」と「発生確率」のグラフはロングテールな分布なので、最悪のケースを正しく想定するならむしろ最長はもっと多めに見積もっておいた方がいいことも多いです。
しかしながら、見積もりの目的はスケジュール策定やリソース配分であることが多いため、このような広いレンジの見積もりは情報としての価値が低いという判断になりがちです。
そこで、無理やり「精度」を上げて、「8日〜10日」のような数字を出すこともあるかもしれませんが、これは精度をあげているどころか不確実性というリスクを隠蔽しているだけに過ぎません。
これはもはや見積もりと呼べる代物ではなく、願望です。

詳細な事前調査を行えばより正確な見積もりが出せますが、それ自体にも工数がかかる上に、正確に見積もれる頃には実装がほぼ終わっているということもよくあるので、これを行うべきかは目的次第であるといえます。
また、未来の見積りになればなるほどその精度は低下します。これは有名なバリー・ベーム氏の不確実性コーンが示すところでもあります。

約束は開発を遅らせるという話もあります。

bufferings.hatenablog.com

特に、もはや前時代的なのかもしれませんが、タスクごとに細かく厳密な工数管理をし、納期にコミットさせるというプロジェクトマネジメント手法を採っているケースで、このような事態が発生しがちなように思います。

ではもういっそ見積もりなどしない方がいいのかというと、そういうわけにもいきません。
スケジュールや人員計画、経営的な判断などをできるだけ正確に行いたい場合には、やはり見積もりは必要です。
正確ではないにしろざっくり把握したいのであれば、少なくともざっくりとした見積もりが必要です。

このように、何をどこまでどの程度の精度で見積もるかは、生産性を考える上で非常に難しい問題であり、簡単に決められることではありません。
しかし、何も決めずに闇雲に進めると、「振り返ってみたら見積もりやスケジュール調整、それに伴う会議にとても多くの時間を使っていた」という事態になりかねません。

本プロジェクトでは、上記の内容を踏まえて、以下の点に気をつけました。

  • 見積もりの目的を確認し、必要な部分だけ見積もる
  • やってみないと分からない部分があることを認め、無理な数字合わせに無駄な時間を使わない
  • 遠い未来の見積もりはせず、やるとしてもざっくり「予想」する程度
    • 言葉の力は強く、一人歩きする恐れがあるため、この場合に見積もりという言葉は使わない
  • 実装のイメージが具体的にできない場合は、すぐ見積もりせずに調査タスクを先に実行する
  • 見積もる場合は、具体的に作業イメージができる粒度(30分〜3時間くらい)まで分解して見積もる

結果的に、見積もり自体の工数やスケジュールの引き直し工数、それに伴う会議の工数など、多くの工数を削減し、その時間を実装にあてることができたと思います。

エンジニア以外も使うツールだからこそ直感的に効率的に

さて長くなってきたので、まだ書きたいことはあるものの、最後にツールの生産性について触れて本記事を締めくくりたいと思います。

ゲーム開発の過程では多様なツールを開発します。
その中には、以下のように、エンジニアが作業を効率化するためのツールだけでなく、プランナーやデザイナー、QAといったエンジニア以外の職種の方々が使うツールも数多くあります。

より多くのメンバーがより多くの回数使用するツールであるほど、UXを洗練しないと、プロジェクト全体として考えた時に無視できないほどの工数をロスすることになります。

例えば、開発中に使用するデバッグメニューについて考えてみます。
「ゲームをクリア状態にする」「所持金をMAXにする」「無敵モードをONにする」といった機能のことです。
こういったデバッグメニューは、プロジェクトにもよりますが、開発が進むと数百個といった数になり得ます。

極端な悪い例として、仮にこれらのメニューが何の意味的なまとまりもなく1つの画面に数百個のボタンとして羅列されていたら、テスターの方は毎回テストのたびに画面をスクロールして目的のメニューを探すことになります。
一回あたり数十秒〜数分のロスが発生し、これが1日に一人当たり何十回も繰り返されることになります。

このような事態を防ぐために、デバッグメニューは構造化を行ったり、検索機能を設けたりする必要があります。
本プロジェクトでは、これを行うために Unity Debug Sheet を導入しています。

Unity Debug Sheet のサンプル画面

これは私が個人でリリースしているOSSです。
宣伝のようになってしまいますが、目的が正しく達成できれば何でも良いです。

github.com

ちなみに構造化や検索機能のメリットとして特定の項目が「無いこと」がわかるという点も挙げられます。
目的のデバッグ項目がまだ存在しない時に、担当エンジニアに項目追加依頼をするわけですが、その項目が「存在しない」ことがはっきりとわからないと心理的に依頼しづらいものです。
具体的には、以下のような非効率が発生します。

  • 実は特定のステージまでワープできる機能があったが、見当たらなかったので手動で1時間かけてプレイして到達した
  • ガチャの排出回数を制御するデバッグ機能が見つからず、手作業で100回ガチャを回して、101回目の特殊挙動を確認した

構造化や検索機能を作っておくことで、明らかに「無いこと」がわかるので、このような事態を防ぐことができます。
早めに整理しておかないと、プロジェクトが進めば進むほど、項目の存在の証明が悪魔の証明と化していくので、早めの整理が肝心です。

また、このようなデバッグメニューを使いやすいものにするには、構造化だけではなく、言葉選びも非常に重要です。
例えば、「ジェムを付与するデバッグメニュー」の名前が「Execute AcquireGemUseCase」だったらどうでしょうか。
実装したエンジニア本人には理解できますが、他の人、特にエンジニア以外にはかなりわかりづらい項目名になっています。
「ジェムを増やす」というメニュー名にした方が誰がみてもずっと直感的です。

良いUXと悪いUXの違いについては、以下の記事がとても参考になります。
エンジニア(もちろん私も含め)がついやってしまいがちな、実装都合のUIを避けるための視点が詰まっています。

qiita.com

なお、デバッグメニューのようにランタイムで使うツールだけでなく、Unityエディタ上で使うツールについても同様です。
本プロジェクトの例を挙げると、マスタデータの管理ツールを下図のようなエディタ拡張として作成しました。

マスタデータの管理ツールの画面

大きなミスなく運用できているので悪くないのではないかと思っています。

まとめと宣伝

本プロジェクトでは、開発効率という観点のもと、プロジェクトの立ち上げ期から様々な取り組みを行いました。
もちろん今回の内容が全てのプロジェクトに当てはまるわけではありませんが、参考になる部分があれば幸いです。

最後に宣伝になりますが、私の所属している株式会社サイバーエージェント SGEコア技術本部(コアテク)では以下のエンジニアを募集しています。

コアテクの仕事についてはつい先週Automaton様にインタビューしていただいた記事もありますので、よろしければこちらもご参照ください。

automaton-media.com

また、個人的に開発期間が約1年という短期開発のプロジェクトは、課題の発見と実践のサイクルが回しやすく、実際にやってみてとても魅力的に感じました。
弊社は大規模なプロジェクトも多いですが、本プロジェクトのような規模のプロジェクトも開発しており、今後の注力領域の一つとなっております。
コアテクに限らずエンジニアを募集しています(そして全然足りていない)ので、興味がある方はぜひ、以下のページもご参照ください。

creator.game.cyberagent.co.jp