• ゲーム
  • Industry
  • リソース
  • コミュニティ
  • 学習
  • サポート
開発
Unityエンジン
任意のプラットフォーム向けに2Dおよび3Dゲームを構築
ダウンロードプランと価格
収益化
アプリ内課金(IAP)
ストア全体でIAPを発見し、管理する
Mediation
収益を最大化し、マネタイズを最適化する
Ad Quality
アプリのユーザーエクスペリエンスを保護する
Tapjoy
長期的なユーザーの忠誠心を構築する
すべてのマネタイズ製品
詳しく見る
詳しく見る
発見され、モバイルユーザーを獲得する
UnityベクターAI
プレイヤーを適切なゲームに接続する
Auraのオンデバイス広告
ピークエンゲージメント時にデバイス上のユーザーにリーチする
すべての成長製品
活用事例
3Dコラボレーション
リアルタイムで3Dプロジェクトを構築およびレビューする
没入型トレーニング
没入型環境でのトレーニング
顧客体験
インタラクティブな3D体験を作成する
すべての業界ソリューション
業界
製造業
運用の卓越性を達成する
小売
店内体験をオンライン体験に変換する
自動車
革新と車内体験を高める
全業界
技術ライブラリ
ドキュメント
公式ユーザーマニュアルとAPIリファレンス
開発者ツール
リリースバージョンと問題追跡
ロードマップ
今後の機能をレビューする
用語集
技術用語のライブラリ
インサイト
ケーススタディ
実際の成功事例
ベストプラクティスガイド
専門家のヒントとコツ
すべてのリソース
新機能
ブログ
更新情報、情報、技術的ヒント
お知らせ
ニュース、ストーリー、プレスセンター
コミュニティハブ
ディスカッション
議論、問題解決、つながる
イベント
グローバルおよびローカルイベント
コミュニティストーリー
Made with Unity
Unityクリエイターの紹介
ライブストリーム
開発者、クリエイター、インサイダーに参加する
Unity Awards
世界中のUnityクリエイターを祝う
すべてのレベルに対応
Unity Learn
無料でUnityスキルをマスターする
プロフェッショナルトレーニング
Unityトレーナーでチームをレベルアップ
Unity初心者向け
スタートガイド
学習を開始しましょう
Unityエッセンシャルパスウェイ
Unity は初めてですか?旅を始めましょう
ハウツーガイド
実用的なヒントとベストプラクティス
教育
学生向け
キャリアをスタートさせる
教育者向け
教育を大幅に強化
教育機関向けライセンス
Unityの力をあなたの機関に持ち込む
認定教材
Unityのマスタリーを証明する
サポートオプション
ヘルプを得る
Unityで成功するためのサポート
Success Plan
専門的なサポートで目標を早く達成する
FAQ
よくある質問への回答
お問い合わせ
私たちのチームに連絡する
プランと価格
言語設定
  • English
  • Deutsch
  • 日本語
  • Français
  • Português
  • 中文
  • Español
  • Русский
  • 한국어
ソーシャル
通貨
購入
  • プロダクト
  • Unity Ads
  • サブスクリプション
  • Unity Asset Store
  • リセラー
教育
  • 学生
  • 教育関係者
  • 教育機関
  • 認定資格試験
  • 学ぶ
  • スキル開発プログラム
ダウンロード
  • Unity Hub
  • ダウンロードアーカイブ
  • ベータプログラム
Unity Labs
  • ラボ
  • 研究論文
リソース
  • Learn プラットフォーム
  • コミュニティ
  • ドキュメント
  • Unity QA
  • FAQ
  • サービスのステータス
  • ケーススタディ
  • Made with Unity
Unity
  • 当社について
  • ニュースレター
  • ブログ
  • イベント
  • キャリア
  • ヘルプ
  • プレス
  • パートナー
  • 投資家
  • アフィリエイト
  • セキュリティ
  • ソーシャルインパクト
  • インクルージョンとダイバーシティ
  • お問い合わせ
Copyright © 2025 Unity Technologies
  • 法規事項
  • プライバシーポリシー
  • クッキーについて
  • 私の個人情報を販売または共有しないでください

「Unity」の名称、Unity のロゴ、およびその他の Unity の商標は、米国およびその他の国における Unity Technologies またはその関係会社の商標または登録商標です(詳しくはこちら)。その他の名称またはブランドは該当する所有者の商標です。

Hero background image
Last updated May 2019, 10 min. read

プロジェクトの規模が拡大するのに合わせてコードを設計する方法

このウェブページは、お客様の便宜のために機械翻訳されたものです。翻訳されたコンテンツの正確性や信頼性は保証いたしかねます。翻訳されたコンテンツの正確性について疑問をお持ちの場合は、ウェブページの公式な英語版をご覧ください。
ここをクリックしてください。

このページの内容:大規模化しつつあるプロジェクトのコードを、あまり問題を引き起こさず無理なく拡大できるように設計する効果的な戦略。プロジェクトの規模が大きくなるに従って、その設計は恒常的に調整し、クリーンアップしなければならなくなります。変更を実施する前には常に一歩引くようにして、物事を小さいパーツへと分解していき、そこからもう一度考えをまとめ上げるのが良いでしょう。

この記事は、スウェーデンのゲームスタジオ Fall Damage の CTO Mikael Kalms 氏によるものです。Kalms 氏には、20 年を越えるゲームの開発とリリースの経験があります。それでもなお、プロジェクトを安全かつ効率的に拡大できるようなコードの作成方法に大きな関心を持っています。

  • 「シンプル」から「複雑」へ
  • インスタンス、プレハブ、ScriptableObject
  • 単純なコード例のパラメーター
  • ScriptableObject の使用
  • 大型 MonoBehaviour の分割
  • MonoBehaviour から正規の C# クラスへ
  • インターフェースの使用
  • ソフトウェアのアーキテクチャ
  • ロジックとプレゼンテーション
  • データのみのクラスとヘルパークラス
  • 静的メソッド
  • オブジェクトの切り離し
  • シーンのロード
  • クリーンで制御されたシャットダウン
  • シーンファイルのマージ負担の軽減
  • コードテストプロセスの自動化
  • コンテンツテストプロセスの自動化
  • 自動化プレイスルーの作成
プロジェクトの規模が拡大するのに合わせてコードを設計する方法

「シンプル」から「複雑」へ

Unite Berlin 講演向けにチームが作成した、基本的な Pong スタイルゲームのコードサンプルを確認しましょう。上の画像からわかるように、2 つのパドルと 4 つの壁(上下、左右)があり、ゲームロジックとスコア UI が使用されています。壁とパドルに対応する単純なスクリプトがあります。

このサンプルはいくつかの重要な原則に基づいています。

  • 1 つの「オブジェクト」 = 1 つの Prefab
  • 1 つの「オブジェクト」のカスタムロジック = 1 つの MonoBehaviour
  • 1 つのアプリケーション = 内部でリンクされたプレハブを含む 1 つのシーン

これらの原則は、このような相当にシンプルなプロジェクトでは有効ですが、より拡大した規模で臨むのであれば構造の変更をしていかなければなりません。では、コードを整理するために使用できる戦略はどのようなものでしょうか?

大規模化しつつあるプロジェクトのコードを設計するコツ_コンポーネントのパラメーター

インスタンス、プレハブ、ScriptableObject

まず、インスタンス、プレハブ、ScriptableObject の違いについて理解しましょう。上に示すのは、インスペクターに表示される Player 1 の Paddle ゲームオブジェクトの Paddle コンポーネントです。

3 つのパラメーターがあることがわかります。しかし、このビューでは、ベースとなるコードがどのように動作するのかわかりません。

左パドルの入力軸はインスタンスで変更したほうが納得いくものになるでしょうか。それとも、プレハブで実行すべきところでしょうか?プレイヤーごとに入力軸は異なるので、おそらくここはインスタンスで変更したほうがよいと思われます。Movement Speed Scale はどうでしょう?インスタンスかプレハブのどちらで変更すべきものでしょうか?

Paddle コンポーネントを表すコードを見てみましょう。

大規模化しつつあるプロジェクトのコードを設計するコツ_コード内のパラメーター

単純なコード例のパラメーター

少し立ち止まって考えてみると、このプログラムではさまざまなパラメーターが異なる方法で使用されていることに気付きます。InputAxisName はプレイヤーごとに個別に変更する必要があります。MovementSpeedScaleFactor と PositionScale は両方のプレイヤーで共有する必要があります。次に、インスタンス、プレハブ、ScriptableObject を使用するときに指針となる戦略を示します。

  • 必要なものは一度だけですか?プレハブを作成し、インスタンス化します。
  • インスタンス固有の変更があるオブジェクトなど、複数回必要になりそうなものについては、その後、プレハブを作成し、それをインスタンス化して、いくつかの設定をオーバーライドします。
  • 複数のインスタンスにわたって確実に同じ設定を適用したいのであれば、代わりに ScriptableObject を作成し、そこからソースデータを作成します。

Paddle コンポーネントで ScriptableObject をどのように使用しているかについては、次のコード例をご覧ください。

大規模化しつつあるプロジェクトのコードを設計するコツ_ScriptableObject の使用

ScriptableObject の使用

こういった設定は PaddleData 型の ScriptableObject に移動しているわけですから、この Paddle コンポーネントにはその PaddleData に対する参照のみが存在しています。どういう結果になっているのかをインスペクターで見てみれば、PaddleData と 2 つの Paddle インスタンスという、2 つのアイテムがありますね。軸名と個々のパドルがそれぞれ指している共有設定のパケットは引き続き変更できます。この新しい構造では、様々な設定の背後にある意図をより簡単に見ることができるようになっています。

大規模化しつつあるプロジェクトのコードを設計するコツ_単一責任の原則

大型 MonoBehaviour の分割

これが実際の開発中のゲームであった場合、個々の MonoBehaviour がどんどん大きくなっていくのがわかると思います。いわゆる「単一責任の原則」に基づいて作業することで、これらをどう分割できるのかを見てみましょう。つまり、各クラスはそれぞれ単一のオブジェクトを処理していなければならないと規定されているということです。正しく適用すれば、「特定のクラスは何をするのか」と「何をしないのか」という質問に簡単に答えられるはずです。これにより、チームのすべての開発者が個々のクラスの役割を簡単に理解できるようになります。これはどのようなサイズのコードベースでも適用できる原則です。上の画像のような簡単な例を見てみましょう。

これはボールのコードです。あまりそのようには見えませんが、よく見てみると、ボールの初速度ベクトルを設定するためにデザイナーが使用する速度と、ボールのその時点の速度を管理するために自作した物理シミュレーションで使用する速度が存在していることがわかります。

わずかに異なる 2 つの目的のために同じ変数を再利用しているということです。ボールが動き始めると、すぐに初速に関する情報が失われます。

自作の物理シミュレーションは FixedUpdate() での動きだけではなく、ボールが上下の壁に当たったときのリアクションにも対応します。

OnTriggerEnter() コールバックの奥には Destroy() 操作があります。ここで、トリガーロジックがゲームオブジェクト自体を削除します。大規模なコードベースでは、エンティティがそれ自体を削除することを許可するのは珍しく、多くの場合オーナーがその対象のオブジェクトを削除します。

ここが利用できるところです。対象を小さなパーツに分割できます。これらのクラスには、ゲームロジック、入力処理、物理シミュレーション、プレゼンテーションなど、多種多様な役割があります。

これらの小さなブロックを作成する方法を以下に示します。

  • 汎用ゲームロジック、入力処理、物理シミュレーション、プレゼンテーションは MonoBehaviour、ScriptableObject または生の C# クラス内に配置できます。
  • インスペクターでパラメーターを公開するためには、MonoBehaviour または ScriptableObject を使用できます。
  • エンジンイベントハンドラおよび GameObject の寿命の管理は、MonoBehaviour 内に配置する必要があります。

多くのゲームにとって、多くのコードを MonoBehaviour の外側に出すことにできるだけ労力や時間をかけることに、価値はあると思います。これを実行する 1 つの方法は、ScriptableObject を使用する方法です。この方法については、すでに有用なリソースがいくつか出回っています。

大規模化しつつあるプロジェクトのコードを設計するコツ_MonoBehaviour の C# クラスへの移行

MonoBehaviour から正規の C# クラスへ

MonoBehaviour から正規の C# クラスに移動するというやり方もありますが、この方法のメリットは何でしょう?

正規の C# クラスには、コードを小さなコンポーネント化できるブロックに分割するという点においては Unity 独自のオブジェクトよりも有用な言語機能が備わっています。さらに、正規の C# コードは Unity の外部でネイティブ .NET コードベースと共有できます。

一方、正規の C# クラスを使用している場合、エディターはこのオブジェクトを認識せず、インスペクターにネイティブでは表示できません。

この方法では責任の種類ごとにロジックを分割する必要があります。ボールの例へ戻って考えてみると、ここでは単純な物理シミュレーションを BallSimulation という名前の C# クラスに移動してしまっています。このクラスが実行する必要があるジョブと言えば、物理演算の統合とボールが物体に当たったときのリアクションの処理くらいです。

しかし、ボールのシミュレーションにおいて、実際に当たった物体に基づいて判定を下すことは理にかなっているでしょうか?これはゲームロジックで処理することのように見えますね。つまるところ、ボールには何らかの形でシミュレーションを制御するロジック部分があり、そのシミュレーション結果が MonoBehaviour にフィードバックされているということになります。

上の再構成されたバージョンを見てみると、大きな変更点の 1 つとして、Destroy() 操作が深いレイヤーに埋め込まれなくなったことがわかります。この時点で、MonoBehaviour に残っている明確な役割領域はほんのわずかです。

これにできることは他にもありますFixedUpdate() 内の位置更新ロジックを見てみると、そのコードは位置を送信する必要があり、そこから新しい位置が返されることがわかります。ボールのシミュレーションでは、ボールの位置自体は実際には保持していないのです。指定されたボールの位置に基づき、その瞬間のシミュレーションを実行して結果を返しています。

大規模化しつつあるプロジェクトのコードを設計するコツ_インターフェースの使用

インターフェースの使用

インターフェースを使用すれば、そのボールの MonoBehaviour 部分の、必要とされるパーツのみをシミュレーションと共有できることがあります(上の画像を参照してください)。

もう一度コードを見てみましょう。Ball クラスは単純なインターフェースを実装します。LocalPositionAdapter クラスを使用すると、Ball オブジェクトへの参照を別のクラスに渡すことができます。Ball オブジェクト全体ではなく、LocalPositionAdapter アスペクトだけを渡します。

また、BallLogic はゲームオブジェクトを破棄するタイミングを Ball に通知する必要があります。Ball はフラグを返す代わりに、BallLogic のデリゲートを提供することがあります。これが再構成されたバージョンで最後にマークされた行が実行する内容となります。洗練された設計になりましたね。よく見かけるロジックばかりがたくさんありますが、各クラスは目的が細かく限定されています。

これらの原則を使用することで、一人で作業しているプロジェクトの構造を整えることができます。

大規模化しつつあるプロジェクトのコードを設計するコツ_ソフトウェアアーキテクチャ

ソフトウェアのアーキテクチャ

少し規模の大きいプロジェクトのソフトウェアアーキテクチャソリューションを見てみましょう。Ball ゲームを例にとると、BallLogic や BallSimulation など、さらにに特化したクラスをコードに導入すれば、階層構造を構築できるようになるはずです。

MonoBehaviour には他のすべてのロジックがまとめあげられているため、そのすべてを把握している必要がありますが、ゲームのシミュレーション部分は必ずしもロジックのしくみを把握している必要はありません。ただシミュレーションを実行するだけです。時には、ロジックがシミュレーションに信号を送り、それに応じてシミュレーションが反応することもあります。

入力は、他と切り離された自己完結できる場所で処理させておくと便利です。ここで入力イベントが生成され、ロジックに渡されます。次に起こることがなんであれ、シミュレーション次第です。

これは入力とシミュレーションでうまく機能します。ただし、プレゼンテーションに関わる部分(例:特別なエフェクトをスポーンするロジック、スコアカウンターの更新)においては問題が発生する可能性があります。

ロジックとプレゼンテーション

プレゼンテーションでは他のシステムで何が発生しているかは把握していなければなりませんが、そういったシステムに完全にアクセスできるようになっている必要はありません。可能であれば、ロジックとプレゼンテーションは切り離すようにしてください。コードベースを、ロジックのみと、ロジックとプレゼンテーションの 2 つのモードで実行できるようになるところまでこぎつけるようにしてください。

ときには、プレゼンテーションを適切な時点で更新できるようにロジックとプレゼンテーションをつなぐ必要があります。それでも、プレゼンテーションを正確に表示するために必要なものだけを提供することが目標であるので、それ以外の理由はありません。こういったアプローチにより、構築しているゲームが全体的に複雑にならないように、2 つの部分を自然に区切れるようになります。

データのみのクラスとヘルパークラス

ときには、データに対するロジックと処理すべてを同じクラスに組み込んではいない、データのみを含むクラスも有効です。

データを所有せず、渡されたオブジェクトを処理する目的の関数を含むクラスを作成するのも、良い方法です。

静的メソッド

静的メソッドの優れた点は、どのグローバル変数にもアクセスしないと推定される場合であればメソッドに影響を与える可能性がある任意のスコープを、メソッドを呼び出したときに渡される引数の内容から一目で識別できることです。わざわざメソッドの実装から確認しなくてよいのです。

このアプローチは関数型プログラミングの分野にも関わってきます。コアのビルディングブロックとしては、何かを関数へ送信すると、その関数が結果を返したり、出力パラメーターの 1 つを変更したりする、ということになります。このアプローチをお試しください。従来のオブジェクト指向プログラミングで実行するよりも、バグの数が少なくなる可能性があります。

オブジェクトの切り離し

オブジェクト間にグルーロジックを挿入することで、オブジェクトを切り離すこともできます。Pong スタイルのサンプルゲームを再び取り上げます。Ball ロジックと Score プレゼンテーションはどのようにして互いに通信するのでしょうか?ボールに何かが起きたときに Ball ロジックが Score プレゼンテーションに通知するにしても、Score ロジックが Ball ロジックに問い合わせるにしても、どうにかして互いに通信する必要があります。

この場合バッファーオブジェクトを作成する手段があります。ロジックが書き込み、プレゼンテーションが読み込む、ストレージエリアの提供だけを目的とするものです。または、それらの間にキューを配置して、ロジックシステムがキューにアイテムを入れ、プレゼンテーションがキューからそのアイテムを読み込めるように設定することもできます。

ゲームの規模が大きくなったときにプレゼンテーションからロジックを切り離す方法としてお勧めなのが、メッセージバスを使用する方法です。メッセージングのコアの原理は、送信者と受信者のいずれも相手の情報がなく、メッセージバス/システムを認識していることです。さて、Score プレゼンテーションは、スコアが変わったというイベントをメッセージシステムを通じて受け取る必要があります。その後、ゲームロジックがポイントの変更をプレイヤーに示すイベントをメッセージシステムにポストします。システムを切り離す必要がある場合は、UnityEvents を使用すると便利ですが、独自にコーディングすることもできます。これにより、用途に合わせて別個のバスを作成できます。

シーンのロード

LoadSceneMode.Single の使用をやめて、代わりに LoadSceneMode.Additive を使用します。

シーンをアンロードする必要があるときは、明示的なアンロードを使用します。遅かれ早かれ、シーンの遷移中にはいくつかのオブジェクトをライブ状態へ維持しておかなければいけないのです。

DontDestroyOnLoad の使用も止めてください。オブジェクトの生存期間を制御できなくなります。実際、LoadSceneMode.Additive でオブジェクトをロードすれば、DontDestroyOnLoad を使用する必要がなくなります。生存期間が長いオブジェクトを、生存期間が長い特別なシーンへ変えてください。

クリーンで制御されたシャットダウン

私はこれまでいろいろなゲームを作ってきましたが、どんなゲームを作るときにも役立った習慣は、クリーンで制御されたシャットダウンをサポートすることでした。

アプリケーションを終了する前に、ほぼすべてのリソースをアプリケーションが解放できるようする。可能であれば、グローバル変数は引き続き割り当てず、ゲームオブジェクトを DontDestroyOnLoad でマークしないようにしてください。

オブジェクトをシャットダウンする方法に特定の順番があれば、エラーを見つけ、リソースのリークを特定するのが簡単になります。また、これにより、再生モードを終了したときに Unity エディターが不安定な状態になりません。再生モードを終了するとき、Unity はドメインを完全には再ロードしません。クリーンシャットダウンを実行できれば、エディターでゲームを実行した後に、エディターや編集モードのスクリプティングが不安定な動作を起こす可能性が低くなります。

シーンファイルのマージ負担の軽減

これは、Git、Perforce、Plastic などのバージョン管理システムを使用して実現できます。すべてのアセットをテキストとして保存し、オブジェクトをプレハブ化してシーンファイルから切り離します。最後に、シーンファイルを複数の小さなシーンに分割します。ただし、これには他にツールが必要になる場合があることに注意してください。

コードテストプロセスの自動化

近いうちに 10 人以上のチームになることが見込まれている場合は、プロセス自動化のために何か措置を講じなければならなくなると思われます。

クリエイティブなプログラマーであれば、ユニークな部分により時間をかけるため、できるだけ多くの繰り返し部分を自動化に回したいと考えるものでです。

コードのテストを作成することから始めましょう。具体的に言えば、オブジェクトを MonoBehaviour から正規のクラスに移動しようとする場合、ロジックとシミュレーション向けユニットテストを構築するためにユニットテストフレームワークを使用するのが簡単です。すべての場合に有効とは限りませんが、後で他のプログラマーがコードにアクセスしやすくなります。

コンテンツテストプロセスの自動化

テストはコードのテストだけではありません。コンテンツもテストする必要があります。チームにコンテンツクリエイターがいる場合は、制作したコンテンツをすぐに検証できる標準化された方法があると便利です。

プレハブを検証したり、カスタムエディターで入力した一部のデータを検証したりするテストロジックは、コンテンツクリエイターが簡単に利用できるようにする必要があります。エディターでボタンをクリックするだけですぐに検証を実行できるようになれば、時間の節約につながるのですぐにありがたみがわかります。

次の段階としては、Unity Test Runner をセットアップすることがあります。これでオブジェクトを定期的かつ自動的に再テストできるようになります。指定したすべてのテストも実行されるように、これをビルドシステムの一部としてセットアップすることをお勧めします。お勧めの方法は、通知をセットアップしておくことです。そうすれば問題が発生したときにチームメンバーが Slack またはメールで通知を受け取れるようになります。

自動化プレイスルーの作成

自動化プレイスルーには、制作したゲームをプレイできるAIの作成と、エラーのログ記録が関与します 。簡単に言えば、AI がエラーを 1 つ見つけるごとに、ユーザーがエラーを見つけるために費やす必要がある時間が減るということです。

このケースでは、同じマシンに約 10 個のゲームクライアントをセットアップし、詳細設定を最小限に抑えて、すべてを実行します。クライアントがクラッシュしたら、ログを確認します。こういったクライアントの 1 つがクラッシュするまでにかかった時間のひとつひとつが、言ってしまえばバグを見つけるために自分たちでゲームをプレイしたり、他のユーザーにプレイしてもらったりする必要をなくして節約できた時間だということです。つまり、実際に自分たちや他のプレイヤーと共にテストプレイをしたときには、ゲームが面白かったかどうか、どこに表示の問題があったかなどにフォーカスできたということです。