
4年前に最初の記事
「50 Unity Tips」を公開しました。 そのほとんどが依然として関連しているという事実にもかかわらず、次の理由で多くが変更されました。
- ユニティが良くなりました。 たとえば、FPSカウンターを信頼できるようになりました。 プロパティドロワーを使用する機能により、カスタムエディターを記述する必要が減りました。 プレハブを使用する方法は、事前定義されたネストされたプレハブとその代替の要求が少なくなりました。 スクリプト可能なオブジェクトがより使いやすくなりました。
- Visual Studioとの統合が改善され、デバッグがはるかに簡単になり、「サル」デバッグの必要性が減少しました。
- サードパーティのツールとライブラリが改善されました。 Asset Storeには多くのアセットが表示されており、視覚的なデバッグやロギングなどの側面が簡素化されています。 独自の(無料の) 拡張プラグインのコードの大部分は、私の最初の記事で説明されています(そして、その大部分はここで説明されています)。
- バージョン管理の改善。 (しかし、もっと効率的に使用することを学んだだけかもしれません)。 たとえば、プレハブ用に複数のコピーまたはバックアップコピーを作成する必要はありません。
- 私はより経験を積んでいます。 過去4年にわたって、私はUnityで多くのプロジェクトに取り組みました。その中には、多数のゲームプロトタイプ 、 Father.IOなどの完成したゲーム、メインのUnity グリッドアセットなどがあります。
この記事は、上記のすべてを考慮して改訂された元の記事のバージョンです。
ヒントに進む前に、最初に短いメモを残します(最初の記事と同じです)。 これらのヒントは、すべてのUnityプロジェクトに適用されるわけではありません。
- 彼らは小さなチームの一員としてプロジェクトに取り組んだ私の経験に基づいています(3〜20人)。
- 構造化、再利用性、コードの明快さ、その他の側面には代償があります。それは、チームの規模、プロジェクトの規模、プロジェクトの目標に依存します。 たとえば、 これらすべてをgamejamに使用するわけではありません 。
- 多くのヒントを使用するのは好みの問題です(異なる場合もありますが、ここにリストされているヒントのいずれかに対しては優れた手法です)。
UnityのWebサイトには、プロジェクトでの作業に関する推奨事項もあります(ただし、それらのほとんどはプロジェクトの生産性の向上を目的としています)
(それらはすべて英語です) 。
作業プロセス
1.最初から、スケールを決定し、同じスケールですべてを作成します。 そうしないと、後でアセットをやり直さなければならない場合があります(たとえば、アニメーションは常に正しくスケーリングされません)。 3Dゲームの場合、おそらく1メートルに相当する1ユニティ単位を取るのが最善です。 ライティングと物理学を使用しない2Dゲームの場合、通常は1ピクセル(「動作」解像度)に相当する1ユニティ単位が適切です。 UI(および2Dゲーム)の場合、動作解像度(HDまたは2xHDを使用)を選択し、この解像度でスケーリングするすべてのアセットを作成します。
2.各シーンを実行します。 これにより、ゲームを開始するためにシーンを切り替えることなく、テストプロセスを高速化できます。 これは、すべてのシーンで必要なシーンのダウンロード間で渡される永続オブジェクトを使用する場合は注意が必要です。 これを実現する1つの方法は、送信されたオブジェクトをシングルトンにすることです。これにより、シーンにないオブジェクトは自動的にロードされます。 シングルトーンについては、別のヒントで詳しく説明します。
3.ソース管理を使用して、それを効果的に使用する方法を学びます。- アセットをテキストとしてシリアル化します。 実際、これによりシーンとプレハブの互換性は向上しませんが、変更の追跡は簡単になります。
- シーンとプレハブを共有する戦略をマスターします。 通常、複数の人がシーンやプレハブで作業するべきではありません。 小さなチームでは、シーンまたはプレハブで作業を開始する前に、全員に作業しないように依頼するだけで十分な場合があります。 物理的なトークンを使用して、現在シーンで作業しているユーザーを示すと便利な場合があります(テーブルに対応するトークンがある場合のみ、シーンで作業できます)。
- タグをブックマークとして使用します。
- 分岐戦略を選択し、それに固執します。 シーンとプレハブの接続をスムーズにすることは不可能であるため、分岐は非常に複雑になる可能性があります。 どの分岐方法を選択しても、シーンとプレハブを共有するための戦略と連携する必要があります。
- サブモジュールは注意して使用してください。 サブモジュールは再利用可能なコードをサポートする優れた方法ですが、いくつかの危険があります。
- 通常、異なるプロジェクトのメタファイルは同じではありません。 これは通常、MonoBehaviourまたはスクリプト化可能なオブジェクトを使用しないコードでは問題になりませんが、MonoBehaviourおよびサブモジュールを使用するスクリプト化可能なオブジェクトではコードが失われる可能性があります。
- 複数のプロジェクト(1つまたは複数がサブモジュールを使用)で作業している場合、すべてのプロジェクトのコードを安定させるために、異なるプロジェクトに対してpull-merge-commit-pushを繰り返し実行する必要があるときに、「更新の雪崩」が発生することがあります(このプロセス中に他の誰かが変更を加えると、雪崩が継続する可能性があります)。 この影響を最小限に抑える1つの方法は、サブモジュールに関連するプロジェクトからサブモジュールに変更を加えることです。 同時に、サブモジュールを使用するプロジェクトは常にプルする必要があり、プッシュする必要はありません。
4.常にコードからテストシーンを分離します。 一時的なアセットとスクリプトのコミットをリポジトリにコミットし、作業が終了したらそれらをプロジェクトから削除します。
5.ツール(特にUnity)を同時にアップグレードします。 Unityは、現在のバージョン以外のプロジェクトを開くときに接続を維持するのにはるかに優れていますが、チームメンバーが異なるバージョンで作業している場合、接続が失われることがあります。
6.サードパーティのアセットをクリーンなプロジェクトにインポートし、そこから使用する新しいパッケージをインポートします。 プロジェクトに直接インポートすると、アセットが問題を引き起こす場合があります。
- 衝突(ファイルまたは名前)が発生する可能性があります。特に、 Pluginsフォルダーのルートにファイルを含む資産、または例で標準資産を使用する資産の場合です。
- それらが乱れ、ファイルがプロジェクト全体に散らばっている場合があります。 これは、使用しないことに決めて削除したい場合に特に問題になります。
資産をより安全にするには、次の手順を使用します。
1.新しいプロジェクトを作成し、アセットをインポートします。
2.サンプルを実行し、動作することを確認します。
3.アセットをより適切なフォルダー構造に整理します。 (通常、アセットを自分のフォルダー構造に適合させません。しかし、すべてのファイルが同じフォルダーにあり、プロジェクトの既存のファイルを上書きできる重要な場所にファイルがないことを確認します。)
4.サンプルを実行し、引き続き機能することを確認します。 (コンポーネントを移動したときに資産が「壊れた」ことがありましたが、通常、この問題は発生しません。)
5.次に、不要なコンポーネント(例など)を削除します。
6.アセットがまだコンパイル中であり、プレハブにすべての接続があることを確認します。 未リリースのものがまだある場合は、テストします。
7.ここで、すべての資産を選択し、パッケージをエクスポートします。
8.プロジェクトにインポートします。
7.ビルドプロセスを自動化します。 これは小規模なプロジェクトでも役立ちますが、特に次の場合に役立ちます。
- ゲームの多くの異なるバージョンを構築する必要があります。
- 異なるレベルの技術的経験を持つ他のチームメンバーにアセンブリを作成する必要がある、または
- ビルドする前にプロジェクトに小さな変更を加える必要があります。
これを行う方法については、
Unity Builds Scripting:Basic and advanced capabilitiesを参照してください。8.設定を文書化します。 ほとんどのドキュメントはコード内にある必要がありますが、それ以外のドキュメントを作成する必要があります。 開発者に設定用のコードを調べさせることは、時間を浪費することを意味します。 文書化された設定により、効率が向上します(文書が最新の状態に保たれている場合)。 以下を文書化します。
- タグを使用します。
- レイヤーの使用(コリジョン、カリング、レイキャスティングの場合-どのレイヤーに含めるかを示します)。
- レイヤーのGUI深度(上に配置する必要があるもの)。
- シーン設定。
- 複雑なプレハブの構造。
- 選択されたイディオム。
- ビルドのカスタマイズ。
一般的なコードのヒント
9.すべてのコードをネームスペースに配置します。 これにより、独自のライブラリとサードパーティコードのコード競合を回避できます。 ただし、重要なクラスとのコードの競合を回避しようとするときは、名前空間に依存しないでください。 他の名前空間を使用する場合でも、Object、Action、またはEventクラスを名前として使用しないでください。
10.アサーションを使用します。 ステートメントは、コード内の不変条件をテストするのに役立ち、論理的なバグを取り除くのに役立ちます。 クレームは、
Unity.Assertions.Assertクラスを介して利用できます。 条件をチェックし、正しくない場合はコンソールにメッセージを書き込みます。 ステートメントが役立つ理由がわからない場合は、「
アサーションを使用したプログラミングの利点(別名assertステートメント)」を参照してください。
11.テキストの表示以外に文字列を使用しないでください。 特に、オブジェクトまたはプレハブを識別するために文字列を使用しないでください。 例外があります(Unityには名前を介してのみアクセスできる要素がまだあります)。 そのような場合は、AnimationNamesやAudioModuleNamesなどのファイルに定数などの文字列を定義します。 そのようなクラスが管理不能になった場合、ネストされたクラスを使用して、AnimationNames.Player.Runのようなものを導入します。
12. InvokeとSendMessageを使用しないでください。 これらのMonoBehaviourメソッドは、他のメソッドを名前で呼び出します。 名前で呼び出されるメソッドは、コード内で追跡するのが困難です(「使用法」は見つかりません。SendMessageのスコープは広く、追跡がさらに困難です)。
コルーチンとアクションC#を使用して、独自のバージョンのInvokeを簡単に作成できます。
public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time) { return monoBehaviour.StartCoroutine(InvokeImpl(action, time)); } private static IEnumerator InvokeImpl(Action action, float time) { yield return new WaitForSeconds(time); action(); }
次に、MonoBehaviourで次のように使用できます。
this.Invoke(ShootEnemy);
(
追加:誰かが
Unityイベントシステムの一部
である
ExecuteEventクラスを代替として使用することを提案しました。これまでのところ、私はそれについてあまり知りませんが、さらに調査する価値があるようです。)
13.ゲームをプレイするときに、スポーンされたオブジェクトが階層を難読化しないようにしてください。 ゲームのプレイ中にオブジェクトを見つけやすくするために、シーン内のオブジェクトをそれらの親として設定します。 空のゲームオブジェクト、またはシングルトン(この記事の後半を参照)を動作なしで使用して、コードで簡単にアクセスできます。 このオブジェクトにDynamicObjectsという名前を付けます。
14. nullを有効な値として使用する場合は正確に指定し、可能な場合は使用しないでください。NULL値は、無効なコードを探すときに役立ちます。 ただし、nullを無視する習慣になると、誤ったコードが正常に実行され、長い間エラーに気付かないでしょう。 さらに、各レイヤーはnull変数を無視するため、コードの奥深くで宣言できます。 有効な値としてnullを使用しないようにします。
私は次のイディオムを好みます:nullをチェックせず、問題が発生したときにコードが抜け落ちるようにします。 再利用可能なメソッドでは、変数のnullをチェックし、エラーにつながる可能性のある他のメソッドに変数を渡す代わりに例外をスローすることがあります。
場合によっては、nullが有効であるため、異なる方法で処理されることがあります。 そのような場合、値がnullになる理由を示すコメントを追加する必要があります。
インスペクターで構成された値には、一般的なスクリプトがよく使用されます。 ユーザーは値を指定できますが、指定しない場合はデフォルト値が使用されます。 これを行う最良の方法は、Tの値をラップするOptional ‹T›クラスを使用することです(これは、Nullable ‹T›に少し似ています)。特別なプロパティレンダラーを使用して、チェックボックスをレンダリングし、チェックボックスがオンの場合のみチェックボックスを表示できます。 (残念ながら、ジェネリッククラスを直接使用することは不可能です。Tの特定の値に対してクラスを拡張する必要があります。)
[Serializable] public class Optional { public bool useCustomValue; public T value; }
コードでは、次のように使用できます。
health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;
追加:多くの人々は、構造体を使用する方が良いと言っています(ゴミを作成せず、nullにすることはできません)。 ただし、これは、インスペクターで使用できるフィールドに適用するような方法で、非ジェネリッククラスの基本クラスとして使用できないことを意味します。
15.コルーチンを使用する場合、コルーチンを効果的に使用することを学びます。 コルーチンは、多くの問題を解決する便利な方法です。 ただし、デバッグは困難であり、その助けを借りれば、コードを簡単にカオスに変えることができます。
理解する必要があります:
- コルーチンを並行して実行する方法。
- コルーチンを順次実行する方法。
- 既存のコルーチンから新しいコルーチンを作成する方法。
- CustomYieldInstructionを使用して独自のコルーチンを作成する方法。
16.拡張メソッドを使用して、共通のインターフェースを共有するコンポーネントを操作します。 (
追加: GetComponentやその他のメソッドもインターフェイスで機能するようになったため、このアドバイスは冗長です)特定のインターフェイスを実装するコンポーネントを取得したり、そのようなコンポーネントを持つオブジェクトを検索したりすると便利な場合があります。
以下の実装では、これらの関数の汎用バージョンの代わりにtypeofを使用します。 汎用バージョンはインターフェースでは機能せず、typeofは機能します。 以下のメソッドは、ジェネリックメソッドでラップします。
public static TInterface GetInterfaceComponent(this Component thisComponent) where TInterface : class { return thisComponent.GetComponent(typeof(TInterface)) as TInterface; }
17.拡張メソッドを使用して、構文をより便利にします。 例:
public static class TransformExtensions { public static void SetX(this Transform transform, float x) { Vector3 newPosition = new Vector3(x, transform.position.y, transform.position.z); transform.position = newPosition; } ... }
18.より柔軟なGetComponentの代替手段を使用します。 RequireComponentを使用して依存関係を強制的に追加するのは不快な場合があります。特にエイリアンクラスでGetComponentを呼び出す場合は、常に可能または受け入れられるとは限りません。 または、オブジェクトが見つからない場合にエラーメッセージを表示する必要がある場合は、次のGameObject拡張機能を使用できます。
public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour { T component = obj.GetComponent(); if(component == null) { Debug.LogError(" " + typeof(T) + ", ", obj); } return component; }
19.同じことをするために異なるイディオムを使用しないでください。 多くの場合、物事を行うためのさまざまな慣用的な方法があります。 そのような場合、1つのイディオムを選択して、プロジェクト全体に使用します。 理由は次のとおりです。
- 一部のイディオムは互換性が不十分です。 あるイディオムを使用すると、別のイディオムに適さない方向に開発が指示されます。
- プロジェクト全体で1つのイディオムを使用すると、プロジェクト参加者は何が起こっているかをよりよく理解できます。 同時に、構造とコードがより理解しやすくなり、エラーの可能性が減少します。
イディオムグループの例:
- コルーチンは有限状態マシンです。
- 組み込みのプレハブ-接続されたプレハブ-神のプレハブ。
- データ共有戦略。
- 2Dゲームの状態にスプライトを使用する方法。
- プレハブの構造。
- 産卵戦略。
- オブジェクトを見つける方法:タイプ/名前/タグ/レイヤー/リンク別。
- オブジェクトをグループ化する方法:タイプ/名前/タグ/レイヤー/リンクの配列。
- 他のコンポーネントのメソッドを呼び出す方法。
- オブジェクトのグループ/自己登録を検索します。
- 実行順序の制御(Unityの実行順序設定を使用-yield-logic-Awake / StartおよびUpdate / Late Update-手動による方法-任意のアーキテクチャ
- マウスを使用したゲーム内のオブジェクト/位置/目標の選択:選択マネージャーはローカルの自治です。
- シーンを変更するときのデータストレージ: PlayerPrefsを使用するか、新しいシーンをロードするときに破棄されない( 破棄する)オブジェクトを使用します。
- アニメーションを組み合わせる(ブレンド、追加、レイヤー化)方法。
- 入力処理(中央-ローカル)
20.一時停止を簡単に処理できるように、独自の時間クラスを作成して維持します。 Time.DeltaTimeとTime.TimeSinceLevelLoadをラップして、一時停止と時間スケールを制御します。 クラスを使用するには規律が必要ですが、特にさまざまなタイマー(たとえば、インターフェイスアニメーションやゲームアニメーション)で実行する場合、すべてが非常に簡単になります。
追加: Unityは
unscaledTimeとunscaledDeltaTimeをサポートし、多くの状況でネイティブ時間クラスを冗長にします。 しかし、グローバル時間のスケーリングが、望ましくない方法で記述しなかったコンポーネントに影響を与える場合、依然として有用です。
21.更新が必要なユーザークラスは、グローバルな静的時間にアクセスできません。 代わりに、Updateメソッドのパラメーターとして時間差を受け取る必要があります。 これにより、上記の一時停止システムを実装するとき、またはカスタムクラスの動作を高速化または低速化するときに、これらのクラスを使用できます。
22. WWW呼び出しを行うために共通のフレームワークを使用します。 サーバーとの通信量が多いゲームでは、通常、何十ものWWW呼び出しがあります。 生のWWW Unityクラスを使用するかプラグインを使用するかに関係なく、ボイラープレートのように機能する薄いレイヤーを上に書くと便利です。
私は通常、Callメソッド(GetとPostとは別に)、CallImplおよびMakeHandlerコルーチンを定義します。 本質的に、Callメソッドは、MakeHandlerメソッドを使用して、パーサーからの「スーパーハンドラー」、成功時および失敗時のハンドラーを作成します。 また、CallImplコルーチンを呼び出します。これはURLを形成し、呼び出しを行い、完了するまで待機してから、「スーパーハンドラー」を呼び出します。
これは次のようなものです。
public void Call<T>(string call, Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure) { var handler = MakeHandler(parser, onSuccess, onFailure); StartCoroutine(CallImpl(call, handler)); } public IEnumerator CallImpl<T>(string call, Action<T> handler) { var www = new WWW(call); yield return www; handler(www); } public Action<WWW> MakeHandler<T>(Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure) { return (WWW www) => { if(NoError(www)) { var parsedResult = parser(www.text); onSuccess(parsedResult); } else { onFailure(" "); } } }
このアプローチにはいくつかの利点があります。
- 大量の定型コードの記述を回避します
- 最初に必要な要素を処理することができます(たとえば、ロードUIコンポーネントの表示、特定の一般的なエラーの処理など)。
23.テキストがたくさんある場合は、ファイルに入れます。 インスペクターの編集フィールドには入れないでください。 Unityエディターを開かずに、特にシーンを保存することなく、すばやく変更できるようにします。
24.ローカライズを計画している場合は、すべての行を1か所に分けます。 これを行うにはいくつかの方法があります。 それらの1つは、各行にpublicタイプの文字列フィールドを持つTextクラスを定義することです。デフォルトでは、たとえば英語が設定されます。 他の言語は子クラスになり、類似言語でフィールドを再初期化します。
より複雑な方法(大量のテキストまたは多数の言語に適しています)は、スプレッドシートを読み取り、選択した言語に基づいて目的の行を選択するロジックを作成することです。
クラス設計
25.検査されたフィールドの使用方法を決定し、それを標準にします。 2つの方法があります:フィールドをパブリックにするか、フィールドをプライベートにして[SerializeField]としてマークします。 後者は「より正確」ですが、利便性は劣ります(この方法はUnity自体にはあまり普及していません)。 選択したものが何であれ、それを標準にして、チームの開発者がパブリックフィールドの解釈方法を理解できるようにします。
- 検査されたフィールドは公開されています。 この場合、パブリックとは次のとおりです。「変数は、アプリケーションの実行中に設計者が安全に変更できます。 コードでその値を設定しないでください。
- 検査されたフィールドはプライベートであり、Serializableとしてマークされます。 この場合、publicとは、「コード内でこの変数を安全に変更できる」ことを意味します(したがって、それらの多くは存在せず、MonoBehavioursおよびScriptableObjectsにはパブリックフィールドはありません)。
26.インスペクターで構成する必要がない限り、コンポーネント変数を公開しないでください。 そうでない場合、特に彼らが何をしているかが明確でない場合は、デザイナーによって変更されます。 まれに、これを回避できない場合があります(たとえば、一部のエディタースクリプトが変数を使用する必要がある場合)。 この場合、
HideInInspector属性を使用して、インスペクターで非表示にする必要があります。
27.プロパティドロワーを使用して、フィールドをより使いやすくします。 プロパティドロワーを使用して、インスペクターでコントロールを構成できます。 これにより、データのタイプと挿入保護に最適なコントロールを作成できます(たとえば、変数の値を制限する)。
Header属性を使用してフィールドを整理し、
Tooltip属性を使用してデザイナーに追加のドキュメントを提供します。
28.カスタムエディターではなくプロパティドロワーを優先します 。 プロパティドロワーはフィールドタイプごとに実装されます。つまり、実装にかかる時間が大幅に短縮されます。 また、繰り返し使用する方が便利です。型の実装後は、任意のクラスの同じ型に使用できます。 カスタムエディターはMonoBehaviourに実装されているため、再利用が難しく、より多くの作業が必要です。
29.デフォルトでは、MonoBehavioursを「封印」します(封印された修飾子を使用します)。 一般に、MonoBehaviours Unityは継承にはあまり便利ではありません。
- UnityがStartやUpdateなどのメッセージメソッドを呼び出す方法は、これらのメソッドがサブクラスでどのように機能するかを複雑にします。 注意しないと、間違った要素が呼び出されるか、基本メソッドの呼び出しを忘れます。
- カスタムエディターを使用する場合、通常はエディターの継承階層をコピーする必要があります。 誰かがクラスの1つを拡張する必要がある場合、独自のエディターを作成するか、作成したものに制限する必要があります。
継承が
必要な場合、これを回避できる場合はUnityメッセージメソッドを使用しないでください。 引き続き
使用する場合は、仮想化しないでください。 必要に応じて、メッセージメソッドから呼び出される空の仮想関数を定義できます。この仮想関数は、子クラスがオーバーライドして追加のアクションを実行できます。
public class MyBaseClass { public sealed void Update() { CustomUpdate(); ...
これにより、クラスが誤ってコードをオーバーライドすることを防ぎますが、Unityメッセージを有効にすることはできます。 問題になるため、この順序は好きではありません。 上記の例では、クラスが独自の更新を完了した直後に、子クラスが操作を実行する必要がある場合があります。
30.インターフェイスをゲームロジックから分離します。 一般に、インターフェイスコンポーネントは、使用されるゲームについて何も知らないようにする必要があります。 表示するデータをそれらに渡し、ユーザーがUIコンポーネントと対話するときにチェックされるイベントをサブスクライブします。 インターフェイスコンポーネントは、ゲームロジックに従う必要はありません。 入力データをフィルタリングして、その正確性をチェックできますが、基本ルールを実装するべきではありません。 多くのパズルゲームでは、フィールド要素はインターフェイスの拡張であり、ルールを含めるべきではありません。 (たとえば、チェスの駒は、許可される動きを計算すべきではありません。)
入力情報も、この情報に基づいて動作するロジックから分離する必要があります。 移動の必要性について俳優に通知する入力コントローラーを使用し、俳優はいつ移動するかを決定します。
ユーザーが特定のリストから武器を選択できるようにするUIコンポーネントの簡単な例を次に示します。 これらのクラスがゲームに関して知っているのは武器クラスだけです(武器クラスはこのコンテナが表示するデータの有用なソースであるためです)。 ゲームはコンテナについても何も知りません。 彼女はOnWeaponSelectイベントを登録するだけです。
public WeaponSelector : MonoBehaviour { public event Action OnWeaponSelect {add; remove; }
31.構成、ステータス、およびサポート情報を分離します。- 構成変数は、そのプロパティを介してオブジェクトを定義するためにオブジェクトで構成された変数です。 たとえば、 maxHealth 。
- 状態変数は、オブジェクトの現在の状態を完全に決定する変数です。 これらは、ゲームが保存をサポートしている場合に保存する必要がある変数です。 たとえば、 currentHealthです。
- 簿記変数は、速度、利便性、および遷移状態に使用されます。 それらは状態変数から完全に決定できます。 たとえば、 previousHealth 。
これらのタイプの変数を分離することにより、ネットワークを介して送信、受信する必要があるもの、保存する必要があるもの、変更することができることを理解できます。 このような分離の簡単な例を次に示します。
public class Player { [Serializable] public class PlayerConfigurationData { public float maxHealth; } [Serializable] public class PlayerStateData { public float health; } public PlayerConfigurationData configuration; private PlayerState stateData;
32.タイプpublicのインデックス配列を使用しないでください。 たとえば、次のように武器の配列、弾丸の配列、粒子の配列を定義しないでください。
public void SelectWeapon(int index) { currentWeaponIndex = index; Player.SwitchWeapon(weapons[currentWeapon]); } public void Shoot() { Fire(bullets[currentWeapon]); FireParticles(particles[currentWeapon]); }
ここでの問題は、むしろコードにあるのではなく、インスペクターでのエラーのないセットアップの複雑さにあります。
3つの変数すべてをカプセル化するクラスをより適切に定義し、そこから配列を作成します。
[Serializable] public class Weapon { public GameObject prefab; public ParticleSystem particles; public Bullet bullet; }
このようなコードは見栄えがよくなりますが、さらに重要なことは、インスペクターでデータを設定するときにエラーを起こすことがより困難になることです。
33.非シーケンス構造には配列を使用しないでください。 たとえば、プレーヤーには3種類の攻撃があります。 それぞれが現在の武器を使用しますが、異なる弾丸と異なる動作を生成します。
3つの箇条書きを配列に詰め込み、このタイプのロジックを使用できます。
public void FireAttack() {
列挙型はコードできれいに見えるかもしれません...
public void WindAttack() {
...しかし、検査官ではありません。
名前を使用して、そこに書き込むコンテンツを理解できるように、個別の変数を使用することをお勧めします。 クラスを作成して、すべてを快適にします。
[Serializable] public class Bullets { public Bullet fireBullet; public Bullet iceBullet; public Bullet windBullet; }
これは、他の火、氷、または風のデータがないことを意味します。
34.データを直列化可能なクラスにグループ化し、インスペクターですべてがより便利に見えるようにします。 一部のアイテムには、多数の設定が含まれている場合があります。 適切な変数を見つけるのは悪夢です。 生活を簡素化するには、次の手順に従ってください。
- 変数のグループに個別のクラスを定義します。 それらを公開してシリアライズ可能にする
- メインクラスで、上記で定義した各タイプのパブリック変数を定義します。
- これらの変数をAwakeまたはStartで初期化しないでください。 これらはシリアライズ可能であるため、Unityがそれら自体を処理します。
- 定義でデフォルト値を割り当てることにより、デフォルト値を指定できます。
これにより、インスペクターで管理しやすい変数のグループが作成されます。 [Serializable] public class MovementProperties
35.パブリックフィールドに使用されていない場合でも、MonoBehavior以外のクラスをSerializableにします。これにより、デバッグモードのときにインスペクターでクラスフィールドを表示できます。これは、ネストされたクラス(プライベートまたはパブリック)でも機能します。36.コードのインスペクターで構成されたデータを変更しないようにしてください。インスペクターで構成された変数は構成変数であり、状態変数としてではなく、アプリケーションの実行時に定数として扱われる必要があります。この規則に従うと、コンポーネントの状態を元の状態にリセットするメソッドを簡単に記述でき、変数の機能を明確に理解できます。 public class Actor : MonoBehaviour { public float initialHealth = 100; private float currentHealth; public void Start() { ResetState(); } private void Respawn() { ResetState(); } private void ResetState() { currentHealth = initialHealth; } }
パターン
パターンは、標準的な方法を使用して一般的な問題を解決する方法です。Robert Nistrom の本「Game Programming Patterns」(オンラインで無料で入手可能)は、ゲーム開発時に発生する問題の解決にパターンがどのように適用されるかを理解するための貴重なリソースです。Unity自体には多くのこのようなパターンがあります。Instantiateはプロトタイプパターンの例です。MonoBehaviourは「テンプレートメソッド」パターンのバージョンであり、UIとアニメーションは「オブザーバー」パターンを使用し、新しいアニメーションエンジンはステートマシンを使用します。これらのヒントは、特にUnityでのパターンの使用に関連しています。37.便宜上、シングルトーン(「長い」パターン)を使用します。 次のクラスは、それを継承するクラスを自動的にシングルトンにします。 public class Singleton<T> : MonoBehaviour where T : MonoBehaviour { protected static T instance;
例えば、管理者のために有用なシングルトンParticleManager、AudioManager又はGUIManager。(多くのプログラマーは、クラス名が間違っているか、互いに関係のないタスクが多すぎることを示すため、XManagerと漠然と呼ばれるクラスに反対しています。一般的に、私は彼らに同意しています。 、ゲームで同じタスクを実行するため、これらのクラスは実際にはイディオムです)- , (, Player). , . GameManager ( God ;-) ).
- static public, . GameManager.Player GameManager.Instance.player.
他のヒントで説明したように、シングルトーンは、シーンのダウンロードとグローバルデータの保存の間に転送されるデフォルトのスポーンポイントとオブジェクトの作成に役立ちます。38.状態マシンを使用して、異なる状態で異なる動作を作成したり、状態を変更するときにコードを実行したりします。ライトステートマシンには多くの状態があり、各状態に対して、更新アクションだけでなく、状態に入ったときまたは状態になったときに実行されるアクションを指定できます。これにより、コードがクリーンになり、エラーが発生しにくくなります。ステートマシンが役立つことを示す良い兆候:Updateメソッドのコードには、その動作を変更するifまたはswitchコンストラクト、またはhasShownGameOverMessageなどの変数が含まれます。 public void Update() { if(health <= 0) { if(!hasShownGameOverMessage) { ShowGameOverMessage(); hasShownGameOverMessage = true;
状態が増えると、このタイプのコードは混乱する可能性があり、状態マシンはそれをより明確にします。39. UnityEventタイプのフィールドを使用して、インスペクターでオブザーバーパターンを作成します。 UnityEventクラスを使用すると、ボタンのイベントと同じUIを使用して、インスペクターで最大4つのパラメーターを受け取るメソッドをバインドできます。これは、入力を扱うときに特に役立ちます。40.オブザーバーパターンを使用して、フィールド値がいつ変化するかを判断します。変数が変更されたときにのみコードが実行されるという問題は、多くの場合ゲームで発生します。ジェネリッククラスを使用して、この問題に対する標準ソリューションを作成しました。これにより、変数の変更イベントを登録できます。以下は健康の例です。作成方法は次のとおりです。 health = new ObservedValue(100); health.OnValueChanged += () => { if(health.Value <= 0) Die(); };
これで、たとえば次のように、すべての場所で値をチェックせずにどこでも変更できます。 if(hit) health.Value -= 10;
ヘルスが0を下回ると、Dieメソッドが呼び出されます。詳細な議論と実装はこの投稿で見ます。41.プレハブにアクターパターンを使用します。(これは「非標準」パターンです。基本的な考え方は、 Kieran Lord のプレゼンテーションから取られています。)俳優は、プレハブの主要コンポーネントです。通常、これはプレハブの「個別性」を提供するコンポーネントであり、より高いレベルのコードが最も頻繁に相互作用するコンポーネントです。アクターは、同じオブジェクト(および場合によっては子オブジェクト)に対して、他のコンポーネント(ヘルパー)を使用してジョブを実行します。Unityメニューからボタンオブジェクトを作成すると、SpriteおよびButtonコンポーネント(およびTextコンポーネントを持つ子)を含むゲームオブジェクトが作成されます。この場合、アクターはButtonになります。メインカメラには通常、カメラコンポーネントに接続されたいくつかのコンポーネント(GUIレイヤー、フレアレイヤー、オーディオリスナー)もあります。カメラはここでは俳優です。アクターが適切に機能するには、他のコンポーネントが必要になる場合があります。アクターコンポーネントの次の属性を使用して、プレハブの信頼性と有用性を高めることができます。 [RequiredComponent(typeof(HelperComponent))] [DisallowMultipleComponent] [SelectionBase] public class Actor : MonoBehaviour { ...
42.ランダムでパターン化されたデータストリームジェネレーターを使用します。(これは非標準のパターンですが、非常に便利です。)ジェネレーターは乱数ジェネレーターに似ています。これは、特定のタイプの新しい要素を取得するために呼び出されるNextメソッドを持つオブジェクトです。設計プロセス中に、ジェネレーターを変更して、幅広いパターンとさまざまな種類のランダム性を作成できます。これらは、要素が必要なコードの部分とは別に新しい要素を生成するロジックを保存できるため、コードがよりきれいになるため、便利です。以下に例を示します。
var generator = Generator .RamdomUniformInt(500) .Select(x => 2*x);
既にジェネレーターを使用して、障害物の生成、背景色の変更、手続き型音楽、単語ゲームのように文字列を生成して単語を作成しています。次の設計を使用すると、ジェネレーターを使用して、さまざまな間隔で繰り返されるコルーチンを正常に制御できます。 while (true) {
ジェネレーターの詳細については、この投稿をお読みください。プレハブとスクリプト可能なオブジェクト
43.すべてにプレハブを使用します。シーン内でプレハブ(またはプレハブの一部)ではないゲームオブジェクトのみがフォルダーである必要があります。一度だけ使用される一意のオブジェクトであっても、プレハブである必要があります。これにより、シーンの変更を必要としない変更を簡単に行うことができます。44.プレハブをプレハブにバインドします。インスタンスをインスタンスにバインドしないでください。プレハブをシーンにドラッグすると、プレハブとの関係が保存されますが、インスタンスへのリンクは保存されません。可能な場合はプレハブにスナップすると、シーンをセットアップするコストが削減され、シーンを変更する必要性が減少します。可能な限り、インスタンス間の接続を自動的に確立します。インスタンスをリンクする必要がある場合は、プログラムでリンクを確立します。たとえば、プレハブプレーヤーは、起動時にGameManagerに自分自身を登録できます。また、GameManagerは、起動時にプレハブプレーヤーを見つけることができます。45.他のスクリプトを追加する場合は、プレハブのルートでグリッドを作成しないでください。グリッドからプレハブを作成する場合、最初にグリッドを空のゲームオブジェクトに親にし、それをルートにします。スクリプトをグリッドではなくルートにバインドします。したがって、インスペクターで構成された値を失うことなく、グリッドを別のグリッドに簡単に置き換えることができます。46.転送された構成データには、プレハブではなくスクリプト可能なオブジェクトを使用します。その場合:- シーンは小さくなります
- 1つのシーン(プレハブインスタンス)を誤って変更することはできません。
47.これらのレベルにはスクリプト可能なオブジェクトを使用します。レベルデータは多くの場合XMLまたはJSONで保存されますが、代わりにスクリプトオブジェクトを使用することにはいくつかの利点があります。- これらはエディターで編集できます。データの検証が容易になり、この方法は技術者以外の設計者にとってより便利です。さらに、カスタムエディターを使用して、編集をさらに簡単にすることができます。
- データの読み取り/書き込みおよび解析について心配する必要はありません。
- 分離と埋め込み、および結果のアセットの管理がより簡単になります。そのため、大規模な構成からではなく、ビルディングブロックからレベルを作成できます。
48.スクリプト可能オブジェクトを使用して、インスペクターで動作を構成します。スクリプトオブジェクトは通常、構成データに関連付けられていますが、データとして「メソッド」を使用することもできます。敵のタイプがあり、各敵が何らかの種類のSuperPowers超大国セットを持っているシナリオを考えます。それらを通常のクラスにして、Enemyクラスでリストを取得できますが、カスタムエディターがないと、インスペクターで異なる超大国(それぞれ独自のプロパティを持つ)のリストを構成できません。しかし、これらの超大国資産を作成する(ScriptableObjectとして実装する)場合、成功します!仕組みは次のとおりです。
public class Enemy : MonoBehaviour { public SuperPower superPowers; public UseRandomPower() { superPowers.RandomItem().UsePower(this); } } public class BasePower : ScriptableObject { virtual void UsePower(Enemy self) { } } [CreateAssetMenu("BlowFire", "Blow Fire") public class BlowFire : SuperPower { public strength; override public void UsePower(Enemy self) {
このパターンを使用する場合、次の制限を忘れないでください。- . NotImplementedExceptions , . Abstract , .
- Generic . generic , generic.
49.スクリプト可能なオブジェクトを使用して、プレハブを特殊化します。2つのオブジェクトの構成が一部のプロパティでのみ異なる場合、通常2つのインスタンスがシーンに挿入され、これらのプロパティはインスタンスに設定されます。通常、プロパティの別個のクラスを作成することをお勧めします。これは、2つのタイプ間で異なる場合があり、スクリプトオブジェクトの別個のクラスです。これにより、柔軟性が向上します。- 特殊化クラスから継承して、さまざまなタイプのオブジェクトの特定のプロパティを作成できます。
- シーンの設定がより安全になります(すべてのプロパティを設定して目的のタイプのオブジェクトを作成するのではなく、目的のスクリプト可能なオブジェクトを選択するだけです)。
- アプリケーションの実行中、コードを介してこれらのオブジェクトを管理する方が簡単です。
- 2つのタイプのインスタンスが複数ある場合、変更を行うときにそれらのプロパティが一定であることを確認できます。
- 構成変数のセットを、組み合わせて一致させることができるセットに分割できます。
このようなセットアップの簡単な例を次に示します。 [CreateAssetMenu("HealthProperties.asset", "Health Properties")] public class HealthProperties : ScriptableObject { public float maxHealth; public float resotrationRate; } public class Actor : MonoBehaviour { public HealthProperties healthProperties; }
多数のスペシャライゼーションを使用すると、スペシャライゼーションを通常のクラスとして定義し、そのリストを、それを適用できる適切な場所(たとえば、GameManager)に関連付けられたスクリプト可能なオブジェクトで使用できます。安全性、速度、利便性を確保するには、もう少し「接着剤」が必要です。以下は、可能な最小の使用例です。 public enum ActorType { Vampire, Wherewolf } [Serializable] public class HealthProperties { public ActorType type; public float maxHealth; public float resotrationRate; } [CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")] public class ActorSpecialization : ScriptableObject { public List healthProperties; public this[ActorType] { get { return healthProperties.First(p => p.type == type); }
50. CreateAssetMenu属性を使用して、ScriptableObject作成をAsset / Createメニューに自動的に追加します。デバッグ
51. Unity.52. IDEのデバッガーを効率的に使用する方法を学びます。たとえば、Visual StudioでのUnityゲームのデバッグを参照してください。53.経時的な値の変化のグラフを描くビジュアルデバッガーを使用します。物理学、アニメーション、およびその他の動的プロセスのデバッグ、特に不規則に発生するエラーに非常に便利です。このエラーをチャートで確認し、エラー時に変化する他の変数を追跡できます。また、目視検査により、値が頻繁に変更されたり、明確な理由なく逸脱したりするなど、特定の種類の奇妙な動作が明らかになります。Monitor Componentsを使用しますが、他にも視覚的なデバッグツールがあります。54.コンソールで便利な録音を使用します。カテゴリごとに色分けされた出力を可能にし、これらのカテゴリに従って出力をフィルタリングできるエディタ拡張を使用します。Editor Console Proを使用しますが、他の拡張機能があります。55.特に、アルゴリズムと数学コードのテストには、Unityテストツールを使用します。たとえば、Unityテストツールのチュートリアルや、Unityテストツールを使用した光速での単体テストの投稿を参照してください。56. Unityテストツールを使用して、大まかなテストを実行します。 Unityテストツールは、正式なテストに適しているだけではありません。また、シーンを開始せずにエディターで実行される便利な「大まかな」テストにも使用できます。57.キーボードショートカットを使用してスクリーンショットを撮ります。多くのバグは視覚的表示に関連しており、スクリーンショットを撮ることができれば、それらを報告するのがはるかに簡単です。スクリーンショットが上書きされないように、理想的なシステムにはPlayerPrefsカウンターが必要です。スクリーンショットは、従業員が誤ってリポジトリにコミットしないように、プロジェクトフォルダに保存する必要はありません。58.キーボードショートカットを使用して、重要な変数のスナップショットを印刷します。ゲーム中に調査可能な予期しないイベントが発生した場合に、情報を登録できます。もちろん、変数のセットはゲームによって異なります。ヒントは、ゲームで発生する典型的なエラーです。たとえば、プレーヤーと敵の位置、またはAI俳優の「思考の状態」(たとえば、彼が従おうとしている方法)。59. , . 例:
誤ってデバッグオプションをリポジトリにコミットしないように注意してください。これらのオプションを変更すると、チーム内の他の開発者を混乱させる可能性があります。60.デバッグホットキーの定数を定義し、1つの場所に保存します。デバッグキーは、ゲーム入力とは異なり、通常1か所で処理されません。ホットキーの競合を回避するには、最初に定数を定義します。別の方法は、デバッグ機能があるかどうかに関係なく、すべてのキーを1か所で処理することです。 (このアプローチの欠点は、このクラスがこのためだけにオブジェクトへの追加の参照を必要とする可能性があることです)。61.手続き型メッシュ生成の場合、頂点に小さな球を描画またはスポーンします。 これにより、三角形とUVを使用してグリッドを表示する前に、頂点が適切な場所にあり、適切なサイズであることを確認できます。性能
62.パフォーマンスに関する一般的な設計および構造のガイドラインに注意してください。- 多くの場合、このようなヒントは神話に基づいており、テストでテストされていません。
- テストによって推奨事項が検証されることもありますが、テストの品質は低くなります。
- ヒントは品質テストによってテストされますが、非現実的であるか、異なるコンテキストで適用できます。(たとえば、配列の使用が一般的なリストよりも高速であることを簡単に証明できます。ただし、実際のゲームのコンテキストでは、この違いはほとんど常に重要ではありません。あなたの場合は役に立たない。)
- 時々、アドバイスは正しいが、すでに時代遅れです。
- 推奨事項が役立つ場合があります。しかし、妥協が必要な場合があります。時には遅いが、時間通りに完了するゲームは、速いが遅れるよりも優れています。高度に最適化されたゲームには、cな遅延リリースコードが含まれる可能性が高くなります。
- 上記のプロセスよりも速く真の問題の原因を見つけるには、パフォーマンスのヒントを検討することが役立ちます。
63.できるだけ早く、ターゲットデバイスで定期的にゲームのテストを開始します。デバイスにはさまざまなパフォーマンス特性があります。彼らにあなたに驚きを与えさせないでください。問題について早く学ぶほど、問題をより効果的に解決できます。64.プロファイラーを効果的に使用して、パフォーマンスの問題の原因を追跡する方法を学びます。65.必要に応じて、より正確なプロファイリングのためにサードパーティのプロファイラーを使用します。 Unityプロファイラーは、何が起こっているのか明確に把握できないことがあります。プロファイルフレームが不足したり、ディーププロファイリングがゲームの速度を低下させ、テスト結果が意味をなさないことがあります。この場合、独自のプロファイラーを使用しますが、別のプロファイラーをAsset Storeで見つけることができます。66.パフォーマンス改善の効果を測定します。パフォーマンスを改善するために変更を行う場合は、それを測定して、変更によって実際にパフォーマンスが改善されることを確認します。変更が測定されていないか重要でない場合は、破棄します。67.パフォーマンスを向上させるために、読みにくいコードを記述しないでください。例外:- プロファイラーによってコードに問題が見つかり、変更後の改善を測定しました。改善は非常に優れているため、サポートの使いやすさを減らす価値があります。
または
- あなたは何をしているかを正確に知っています。
命名標準とフォルダー構造
68.文書化された命名規則とフォルダー構造に従います。標準化された命名とフォルダ構造のおかげで、オブジェクトを検索して理解しやすくなります。ほとんどの場合、独自の命名規則とフォルダー構造を作成する必要があります。以下に例を示します。命名の一般原則
- スペードをスペードと呼びます。 鳥は鳥と呼ばれるべきです。
- 発音して覚えられる名前を選択してください。Mayaゲームを作成している場合、レベルにQuetzalcoatisReturn(Return of Quetzalcoatl)という名前を付けないでください。
- . , . - buttonHolder buttonContainer .
- Pascal case, : ComplicatedVerySpecificObject. , , (. « »).
- (WIP, final).
- : DVamp@W DarkVampire@Walk.
- -: Die, DarkVampire@Die, DarkVampire@Death.
- : DarkVampire, VampireDark; PauseButton, ButtonPaused. , , Button. [ , . , , . , .]
- . , , PathNode0, PathNode1. 0, 1.
- , . , Bird0, Bird1, Bird2 Flamingo, Eagle, Swallow.
メイン名と要素の「アスペクト」を説明する部分の間にはアンダースコアを使用します。 例:
- GUIボタンの状態EnterButton_Active、EnterButton_Inactive
- テクスチャ DarkVampire_Diffuse、DarkVampire_Normalmap
- スカイボックス JungleSky_Top、JungleSky_North
- LODグループDarkVampire_LOD0、DarkVampire_LOD1
この規則を使用して、異なるタイプの要素を区別しないでください。たとえば、Rock_Small、Rock_LargeはSmallRock、LargeRockと呼ばれるべきです。構造
シーン図、プロジェクトフォルダー、およびスクリプトフォルダーには、同様のテンプレートが必要です。以下に使用できる例を示します。フォルダー構造
MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire
...
Structures
Buildings
...
Props
Plants
...
...
Resources
Actors
Items
...
Prefabs
Actors
Items
...
Scenes
Menus
Levels
Scripts
テスト
Textures
UI
効果
...
UI
MyLibray
...
Plugins
SomeOtherAsset1
SomeOtherAsset2
...
Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu
...
World
Ground
Props
Structures
...
Gameplay
Actors
Items
...
Dynamic Objects
Debug
Gameplay
Actors
Items
...
Framework
Graphics
UI
...