UnityでC#を使うときのヒント集

はじめに

こんにちは。カヤックソーシャルゲーム事業部技術基盤チーム所属、Unityエンジニアのと申します。普段はUnity用のツールや共通ライブラリーなどの開発を担当しています。よろしくお願いいたします。

この記事はカヤックUnityアドベントカレンダー2016の7日目の記事になります。

C#言語はいろんな場面でアプリを開発することができますが、プラットフォームごとに挙動の違いや特別な注意事項などがあります。今回はUnityの環境でC#を使うときに注意すべきポイントを紹介します。

NullReferenceExceptionをなくす

NullReferenceExceptionはUnityでゲームを開発するとき一番出やすい例外です。特に実機で発生する場合は、アプリが動かなくなることもよくあります。NullReferenceExceptionは値がnullになってる変数にアクセスしようとするとき発生する例外です。ネットワークエラーやユーザー入力によるエラーなどと違って、NullReferenceExceptionが出た場合は大抵スクリプトのどこかにバグがあります。ですから対応の方針は「処理する」じゃなくて「無くす」です。

nullチェック

ポイントは変数や関数の戻り値などを使う前に、nullの可能性を考えることです。このときはnullに対して、「ありえる」と「ありえない」の2パタンがあります。List<T>.FindGetComponentなど、nullを返す可能性が十分ある場合は、変数を使う前にnullチェックをする必要があります。

例えば

var camera = GetComponent<Camera>();

// nullをチェック、処理する
if (camera == null)
{
    camera = gameObject.AddComponent<Camera>();
}

// 絶対nullじゃないから、cameraを使う...

Assertを使う

逆に、ロジック的に「この関数はnullを返すわけがない」場合もあります、こういうときにnullチェックをしても、バグを隠し、CPUを無駄にするだけなので、チェックせずに「これはバグだ」と大声で警告を出すべきです。こんなときにはUnityのAssertクラスが役に立ちます。

Assertクラスにはいろんなアサーションメソッドが用意されています。使い方は簡単、例えば

var character = CreateCharacter();

// characterはnullではないはずと宣言する
Assert.IsNotNull(character, "CreateCharacter should always return a valid character object.");

// characterを使う...

こうしたら、万が一nullが返された場合には、エラーメッセージがログされ、バグがあることもすぐわかります。そしてAssert.raiseExceptionsを設定したら、スクリプトの実行も止めてくれます。アサーションメソッドは本番ビルドから自動で削除されるので、オーバーヘッドがないのも便利です。

他にもいろんなアサーションメソッドがあるので、積極的に使えば、バグは少なくなるでしょう。

UnityEngine.Object

UnityEngine.ObjectクラスはUnityが管理しているオブジェクトの共通ベースクラスです。CameraMaterialなどの型はもちろん、MonoBehaviourScriptableObjectから派生したスクリプトクラスもUnityEngine.Objectです。UnityEngine.Objectを使うときには特別な注意事項があります。

== nullの挙動

UnityEngine.Objectobj == nullでnullと比較するときの挙動は普通のC#オブジェクトと違って、nullではないのにtrueになる場合もあります。例えば、AssetBundle.Unloadを呼んだら、nullをアサインしなくてもassetBundle == nullの結果もtrueになります。こんなことが起こる原因は、UnityEngine.Object==オペレーターをオーバーライドしているからです。== nullの意味は「破棄されたかどうか」です。

C#のオブジェクトとしてほんとにnullであるかどうかをチェックしたいときには、ReferenceEquals(obj, null)を使えばOKです。

コンストラクタはなぜ使えない

MonoBehaviourとかから派生したスクリプトクラスを書くとき、コンストラクタを使わずにAwakeOnEnableなどを使うのが基本です。C#をよく使う人にとっては不自然ですが、以下のような理由でコンストラクタを使用できません。

  1. プレーモード以外にもスクリプトのインスタンス化が行われています。シーンを編集するときなど、ゲームが実際実行されてない場合にもスクリプトのインスタンスを作っています。初期化ロジックをコンストラクタで書くと、実行されるタイミングは分かりづらいです。
  2. UnityEngine.Objectのコンストラクタは非同期でローディングスレッドで実行されています。Unityのスクリプトエンジンはシングルスレッドベースなので、別スレッドではGetComponentなどのUnity APIは使えないし、同期をとるのも複雑で、間違えやすいです。Awakeなどのメッセージはこういった問題はありません。

GC Allocを減らす

Unityは.NETと同じ、自動メモリー管理が実装されていますが、その性能はCLR標準のGCと比べて結構差があります。注意しないとゲームのパフォーマンスがかなり悪くなります。UnityのGCはヒープ上のオブジェクトが多いほどメモリー回収時間が長くなります。これはゲームのフレームレートが急に下がったりする主な原因です。

メモリー自動回収のタイミングはランダムではなく、動的メモリー確保の時のみです。これはProfiler上のGC Allocで確認できます。最適化方法はUnityマニュアルに書いてありますが、初心者では気づきにくい罠もあります。

配列を返すUnity API

Unityのビルトイン型は一見普通のC#クラスですが、実際にはデータを持っていないエンジン内部オブジェクトのブリッジです。だからGetComponents<T>()Mesh.verticesなど配列を返すAPIは、毎回C#側で新しい配列を確保することになります。Updateの中とかで毎フレームこういうAPIにアクセスすると、GC回収の頻度は大きく上がります。

対策は配列を確保しないAPIを使うか配列をキャッシュして使いまわすかです。例えば

private List<Component> components = new List<Component>();

void Update()
{
    // 配列を確保していない
    GetComponents<Component>(components);
}
// 頂点データのキャッシュ
private List<Vector3> vertices;

void Start()
{
    vertices = new List<Vector3>(mesh.vertices);
}

void Update()
{
    // 頂点データを編集...
    vertices[0] = new Vector3(x, y, z);
    ...

    // データをUnityに渡す
    mesh.SetVertices(vertices);
}

foreachループの問題

C#でよく使われているforeachループも実際にはメモリーを確保しています。Updateなどで毎フレーム実行すると、フレームレートはかなり下がることもあります。過去には標準の.NETもこの問題に引っかかっていましたが、現在は直っています。Unityが使っているMonoコンパイラーが古いので、5.4までのバージョンではまだこの問題が修正されていません。5.5正式版が出るまでは、forループを使うか、手動でforeachを展開するしかありません。

Dictionary<TKey, TValue>の場合の展開方法を紹介します。

void Update()
{
    // Update中にforeach、GCが頻繁に発生
    foreach (var pair in dictionary)
    {
        var key = pair.Key;
        var value = pair.Value;
    }
}

void Update()
{
    // 手動でforeachを展開

    // デフォルトのenumeratorは値型なので、大丈夫
    var enumerator = dictionary.GetEnumerator();

    try
    {
        while (enumerator.MoveNext())
        {
            var pair = enumerator.Current;

            var key = pair.Key;
            var value = pair.Value;

            // ループの中身
        }
    }
    finally
    {
        enumerator.Dispose();
    }
}

IL2CPPとAOTコンパイル

IL2CPPはUnityが作ったC#のAOTコンパイルプラットフォームです。IL2CPPが誕生する前にはMono AOTでiOSなどAOTコンパイルが必要なプラットフォームをサポートしていました。Mono AOTでは、Linqがエラーになりやすいとか、C#のeventが使えないとかいろんな問題がありましたが、IL2CPPの時代では大抵直りました。しかしAOTコンパイルの根本的な制限はずっと存在しているので、注意しないといけません。回避策はUnityマニュアルにありますので、確認しておけば問題ありません。

コードストリッピング

IL2CPPによってもたらされた新しい問題もあります。IL2CPPはILコードをC++コードに変換する技術です。最終的に取得したバイナリコードのサイズはMono AOTよりかなり大きくなりました。アプリのサイズを減らすためにはバイナリコードのストリッピングが必須になります。つまり使われていないコードをアプリから削除するのです。

使われているかどうかは静的解析で判断するので、C#のリフレクションで動的に使われているコードは間違えて削除されるかもしれません。そしてこんなエラーが出ます。

MissingMethodException: Method not found: 'Default constructor not found...ctor() of <SomeType>.'

こういうコードを残すためにIL2CPPにヒントを渡さないといけません。方法は二つあります。

[Preserve] // 削除されないようにする
class ClassOnlyUsedByReflection
{

}

コードサイズ

IL2CPPがコンパイルしたバイナリコードはMonoより大きくなるので、C#上でもコードサイズを意識する必要があります。

  • インターフェースの使用を控えます。
  • ジェネリックと値型の組み合わせに注意します。値型は型ごとにコードが生成されますが、参照型なら使いまわせます。

まとめ

.NET世界の常識がUnityの世界では通用しないこともよくあるので、C#を勉強する以外に、Unityのマニュアルやブログなどをよくチェックするのも大事です。

明日の内容は非同期処理です。担当は清水になります。

お楽しみに。

【Unity】ScriptableObjectの基本と応用について

今回はScriptableObjectの基本機能の解説と、Editor拡張と合わせた応用事例の紹介をします。

こんにちは、ソーシャルゲーム事業部所属エンジニアのぴーちんです!!宜しくお願いします

この記事はカヤックUnityアドベントカレンダー2016の6日目の記事になります。

ScriptableObjectの基本と応用について

ScriptableObjectは、Unityによってシリアライズされたデータ群をアセットとして保持する為の仕組みです。 Prefabと一見似てる様に思われますが、Prefabの様にシーン上に配置する使い方は出来ません。

詳細は公式のマニュアルを見ると良いでしょう。

ScriptableObjectの作り方

ここでコードを交えて作り方の紹介

ScriptableObjectにしたい場合は、 ScriptableObjectを継承する必要があります。 今回はScriptableObjectにしたいclassと、そのScriptableObjectのinstanceを作成するEditorToolを以下に書いてみました。

using UnityEngine;
using UnityEditor;

public class TestScriptableObject : ScriptableObject
{
    public string imageUrl;

    public Texture2D icon;
}
using UnityEngine;
using UnityEditor;


public class TestScriptableObjectCreater : Editor
{
    [MenuItem ("Test/CreateScriptableObject")]
    static void Create()
    {
        var instance = CreateInstance<TestScriptableObject>();

        AssetDatabase.CreateAsset(instance, "Assets/00TestScriptableObject/TestScriptableObject.asset");
        AssetDatabase.Refresh();
    }
}

UnityのMenuにある Test→CreateScriptableObject を選択すると、Assets/00TestScriptableObject/TestScriptableObject.asset が生成されました。

f:id:pchin:20161206102223p:plain

以上がScriptableObjectを作る方法でした。

Editor拡張と組み合わせる

ScriptableObjectは主にUnityのEditor拡張機能と組み合わせて使う場合が多いです。 例えば、以下の様な場合があります。

  • ツールの設定ファイル
  • マスターデータ

ツールの設定ファイルとしての使用例

今回はFacebook SDK for Unityを参考にして紹介しましょう。

FacebookのAPIを使用するための設定項目をScriptableObjectで設定可能な設計になっています。 SDKなどの様にコードをDLLにまとめるケースが多いと、設定ファイルを外出し出来て、しかもGUIで設定を行えるのは便利ですね。

f:id:pchin:20161206102221p:plain

マスターデータ

ScriptableObjectをモックゲームのマスターデータとして扱う場合もあります。

社内の本番環境での使用例はまだありませんが、小規模なゲームやモックを作る場合での使用はアリだと思います。 以下の様なオススメする理由があります。

  • ScriptableObjectはUnityEngneの内部(C++)で実装されてるので読み込みが早い
  • UnityEditor上でデータをInspectorから編集可能なので、ゲームを実行して確認して修正するサイクルを早く回せる

以下はサウンドのマスターデータを格納してるScriptableObjectの例です。 小規模なゲームプロジェクトで、サウンドに関するマスターデータをScriptableObjectに保持される事例を紹介します。

f:id:pchin:20161206102218p:plain

UnityのシリアライズされたAssetは人間が読みやすいテキスト形式にする事が可能です。

yaml形式ならば、Unity以外の場所でもデータの検証がしやすいので便利ですね!

データの更新があった場合でも、行単位でgitのdiffが出るのでレビューもしやすいです。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: c5a9d5d57f4c8457a9b5d4cbc08c8ccc, type: 3}
  m_Name: sound
  m_EditorClassIdentifier: 
  sheetName: "\u30DE\u30B9\u30BF\u30FC\u30C7\u30FC\u30BF\uFF08\u30E2\u30C3\u30AF\u7528\uFF09"
  worksheetName: sound
  dataArray:
  - id: 1
    category: 1
    soundname: battle_normal01_loop
    filename: battle_normal01_loop.wav
    channel: 0
    minvolume: 40
    maxvolume: 40
    minpitch: 100
    maxpitch: 100
    priority: 0
    limitrepeatms: 0
    loop: 1
    startdelayms: 0

Asset Serializationの設定について

Unityでシリアライズされたアセットは、標準設定だとバイナリ形式で保存されるので人間が読めない事が多いです。 弊社では、基本的にすべてのアセットをyaml形式で保存する設定を有効にしています。

この設定は、 Edit->ProjectSettings->Editor から開けるWindowで行えます。 ここで Asset Serializaton の項目で ForceText を選択すると、人間が読める形式でアセットが保存されます。

f:id:pchin:20161206102215p:plain

この事に関する詳細は、公式マニュアルでも説明されています。

おわりに

如何でしたでしょうか? ScriptableObjectという手段を知ってるだけでも、技術的な問題解決の引き出しが増えるのでご活用ください。

明日はUnityでC#を使うときのヒント集についての記事になります。 担当は権くんになります。 お楽しみに。