133
「宴」実装時に得られた Unityプログラムのノウハウ UnityADV制作ツール「宴」 http ://madnesslabo.net/utage/ 2014/05/19 マッドネスラボ代表 時村良平 https://twitter.com/rodostw

「宴」実装時に得られたUnityプログラムノウハウ

Embed Size (px)

DESCRIPTION

AssetStoreで公開中のUnity用ADV制作ツール「宴」 その実装時に得られたUnityプログラムのノウハウのまとめ

Citation preview

Page 1: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」実装時に得られたUnityプログラムのノウハウ

Unity用ADV制作ツール「宴」

http://madnesslabo.net/utage/

2014/05/19マッドネスラボ代表 時村良平

https://twitter.com/rodostw

Page 2: 「宴」実装時に得られたUnityプログラムノウハウ

概要

• Unityにおけるプログラム一般( C# )• これが重要!どうやって探して、どこに渡す?• シリアライズ• コルーチン• エディタ拡張

• より具体的なプログラムについて• 2D• 画面サイズに対応• プロジェクト設定• ADVエンジン(シナリオ解析)• エクセルの読み込み• リソースのDLとメモリ管理• セーブロード

Page 3: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」

このスライドでは「宴」の実装を通して得られた

Unityプログラムのノウハウを紹介します

ADV(ビジュアルノベル)をUnityで制作するためのツール。

AssetStoreで販売中。

https://www.assetstore.unity3d.com/#!/content/15905

詳細は「宴」のサイトを!

無料体験版もあります。http://madnesslabo.net/utage/

Page 4: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」のプログラムの構成の紹介

参考 以下のスライドでは、参考になりそうなリンクや「宴」のソースコードパスをここに記述

ADVに特化した部分

ゲームプログラム全般に汎用的に使う

サンプルコード主にUI処理を記述

Page 5: 「宴」実装時に得られたUnityプログラムノウハウ

Unityにおけるプログラム一般( C# )

Page 6: 「宴」実装時に得られたUnityプログラムノウハウ

これが重要!どうやって探してどう呼び出す?

~ コンポーネントを使いこなそう ~

Page 7: 「宴」実装時に得られたUnityプログラムノウハウ

どうやって探す?

~ 目当てのコンポーネントはどこにある? ~

Page 8: 「宴」実装時に得られたUnityプログラムノウハウ

• たとえば、GameManagerというコンポーネントからPlayerというコンポーネントを使うことを考える。

Page 9: 「宴」実装時に得られたUnityプログラムノウハウ

• ただのC#なら

プロパティを使うなりして、あらかじめメンバ変数にセットしておく

もちろんこれでもいい

または、メソッドの引数に持たせて呼び出す

Page 10: 「宴」実装時に得られたUnityプログラムノウハウ

コンポーネントならではの使い方

Page 11: 「宴」実装時に得られたUnityプログラムノウハウ

•一番素直なのはインスペクターを使う

GameManagerとPlayerのオブジェクトが別々の場合

ドラッグ&ドロップでGameManagerのインスペクターにPlayerを設定してやる

Page 12: 「宴」実装時に得られたUnityプログラムノウハウ

• インスペクターで設定するのが基本だけど

プレハブ化すると

他のオブジェクトへの参照はMissingになってしまう。

他にもスクリプトから動的にオブジェクトを作った場合にも、インスペクター経由での設定はできない

Page 13: 「宴」実装時に得られたUnityプログラムノウハウ

他の手段はなにがある?

Page 14: 「宴」実装時に得られたUnityプログラムノウハウ

• GetComponentを使うパターン

GameManagerとPlayerのオブジェクトが同じ場合

インスペクターで設定する必要はない→プレハブ化などすると、インスペクターを使ったオブジェクトの参照は不安定なので、可能ならこのほうが安全で手軽

Page 15: 「宴」実装時に得られたUnityプログラムノウハウ

• GetComponentInChildrenを使うパターン

Playerが子

親子構造が保障されているときや、子オブジェクトを動的に生成した場合にも

Page 16: 「宴」実装時に得られたUnityプログラムノウハウ

•親オブジェクトのパターン

Playerが親

親子構造が保障されているときや、親子オブジェクトを動的に生成した場合にも

Unity4.5でGetComponentInParentが

追加されたのでそっちのほうがいい

Page 17: 「宴」実装時に得られたUnityプログラムノウハウ

• GameObject.Findを使うパターン

名前だけは分かっている

シーン編集時には存在しないオブジェクト(動的に生成したGameObjectや、他のシーンで生成したGameObject)を参照するときなど。

速度は遅いので注意

子にあることが分かってるなら、transfom.Findというのもある。

Page 18: 「宴」実装時に得られたUnityプログラムノウハウ

• GameObject.FindWithTagを使うパターン

GameObject.Findと似ているが、タグを指定するのでちょっと早い模様。

ただし、タグはプロジェクト間で共有が非常にしづらいので、汎用性に欠ける

速度はちょっと遅い

タグを指定

Page 19: 「宴」実装時に得られたUnityプログラムノウハウ

• FindObjectOfType<T>を使うパターン

Activeであるのが必須

GameObject.Findと似ているが、型を指定できるので便利コンポーネント以外に、TextureなどのAssetの型も指定できる

速度は遅い?

Page 20: 「宴」実装時に得られたUnityプログラムノウハウ

今までは対象が一つの場合を想定とはいえ複数の場合も応用すればOK複数用の処理

・GetComponents

・GetComponentsInChildren

・GameObject.FindGameObjectsWithTag

・FindObjectsOfType

Page 21: 「宴」実装時に得られたUnityプログラムノウハウ

• ゲーム中に必ず一つという保障があるなら、シングルトンを使うパターンもあり

手軽で速度低下もないが、シングルトンで作れなくなったときに構造を大幅に変える必要がでてくるので、多用は禁物

シングルトンな処理を作るUnityのシングルトン

の書き方はいろいろあるが、一番単純なパターンがこれ

Page 22: 「宴」実装時に得られたUnityプログラムノウハウ

ここまでが基本

Page 23: 「宴」実装時に得られたUnityプログラムノウハウ

もうちょい頑張る

Page 24: 「宴」実装時に得られたUnityプログラムノウハウ

遅い処理はキャッシュしよう• FindやGetComponentは遅い

参考 http://docs.unity3d.com/410/Documentation/ScriptReference/index.Performance_Optimization.html

プロパティと null 合体演算子を使うとコードがスッキリする!オススメ!

一度みつけたら、変数として保存(キャッシュ)しておくと二回目以降はすぐ呼び出せる

Page 25: 「宴」実装時に得られたUnityプログラムノウハウ

遅い処理はキャッシュしよう• Transformは注意

Transformの==はカスタムされているのが原因

参考 http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/

this.transformは内部でGetComponentしてるようなので、処キャッシュしたほうがいい。

ただし、 null 合体演算子だとバグる!

こうする

Page 26: 「宴」実装時に得られたUnityプログラムノウハウ

RequireComponent

• そのコンポーネントを使う場合、必ず同時に使うコンポーネントを指定できる

参考ソース Utage¥Scripts¥ADV¥AdvEngine.cs

AddComponentすると、自動的にRequireComponentで指定されてるコンポーネントもAddされる

Page 27: 「宴」実装時に得られたUnityプログラムノウハウ

RequireComponent

• これを使うと、安全なコードが書ける。

参考ソース Utage¥Scripts¥ADV¥AdvEngine.cs

コンポーネントがあることが保障されているのでGetComponetでnullにならない!

Page 28: 「宴」実装時に得られたUnityプログラムノウハウ

RequireComponent・RequireComponent・プロパティ内のnull合体演算子からのGetComponet組み合わせて使うと便利!凄くオススメ

Page 29: 「宴」実装時に得られたUnityプログラムノウハウ

どう呼び出す?

~ 命令を呼ぶ ~

Page 30: 「宴」実装時に得られたUnityプログラムノウハウ

•普通は、メソッドを呼べばいい

Page 31: 「宴」実装時に得られたUnityプログラムノウハウ

• コンポーネントの種類を指定したくないとき

型はPlayerとEnemyの二種類がある。C#的には継承を使うのが普通

Page 32: 「宴」実装時に得られたUnityプログラムノウハウ

• SendMessage

Classの型を気にせず柔軟に使える

メソッド名を指定してSendMessage

Page 33: 「宴」実装時に得られたUnityプログラムノウハウ

• SendMessageの真の力

TargetとFunctionをインスペクター上で変えれば、ソースコードを変えることなく好きな相手に好きな処理をさせられる

Page 34: 「宴」実装時に得られたUnityプログラムノウハウ

• SendMessageのさらなる力

SendMessageには引数を一つだけもたせられるただし、返り値はもてない。それでも、こんな感じで処理を繋げられる。

Page 35: 「宴」実装時に得られたUnityプログラムノウハウ

• SendMessage、SendMessage ・・・

引数をGgamaObjectにすると、「誰から呼ばれたか」も制約せずに柔軟に使える。引数をGgamaObjectにしても、GetComponentを使えば、コンポーネントを取得することもできる。SendMessageからSendMessageを呼ぶことも可能。

Page 36: 「宴」実装時に得られたUnityプログラムノウハウ

• SendMessage、SendMessage ・・・

SendMessageはやりすぎる

と、わけがわからなくなるので注意。

参照関係をエディタ上で追えなくなりがち。

どこからも呼ばれてないように見えてしまう

Page 37: 「宴」実装時に得られたUnityプログラムノウハウ

• プログラム内で完結させるならデリゲートを使う手もある

型安全に使える。ただし、プログラムを書かないと処理を切り替えられない。

ゲーム内で「プレイヤーと敵」のように頻繁に処理が切り替わる場合は向かないかも。ロードの終了とかのフレームをまたぐ処理に使うのがよさげ。(宴では、ユーザの独自拡張のための入り口とかによく使ってる)

Page 38: 「宴」実装時に得られたUnityプログラムノウハウ

SendMessage

GameObjectに対して名前指定で処理を呼ぶ。引数を一つだけ持たせられる。

• メリット• 名前指定なので、ソースコードを変えずに呼び出し先を変えるのに使える。

• たとえば、「ボタンを押されたら呼び出す処理名」などをインスペクターに登録しやすい。

• 複数のコンポーネントに対しても個別に処理をさせらる。• BroadCastを使えば子階層のオブジェクトにも処理をさせられる。

• デメリット• 返り値がもてない。• 複数処理がされる場合は、処理順が制御できない?• 非アクティブなオブジェクトには使えない。(ので、初期化処理などには使いづらい)

Page 39: 「宴」実装時に得られたUnityプログラムノウハウ

Func,Actionなどのデリゲード(コールバック)

C#の標準的な仕組み。

メリット• 型指定なので、ある程度複雑な処理も安全に使いやすい。

• ロード終了時など、「タイミングが来たらこの処理を呼んで」という形で使える。

• 「追加の独自拡張」を想定する場合にも使える。

• 返り値がもてる

• デメリット• プログラムとして記述しなければ使えない。

(SendMessageのように「インスペクター上に記述された文字列の処理を呼ぶ」ということはできない。)

Page 40: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズ

~ Unityのシリアライズは奥が深い・・・・・・ ~

Page 41: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズって?

• Wikiより引用

• コンピュータプログラミングにおいて、シリアライズ、もしくはシリアル化 (serialize) という用語は、次のような異なる2つの意味を有する。

• ある一つの資源を、複数の主体が利用しようとするときに、それを調整(同期)して、一つの時点では一つの主体だけがそれを利用するようにすること。この意味では逐次化という訳語が用いられる。対義語は並列化である。

• ある環境に存在しているオブジェクトをバイト列やXMLフォーマットに変換すること。この意味では直列化という訳語が用いられる。同義語にMarshallingがある。対義語は直列化復元ないしデシリアライズである。

Page 42: 「宴」実装時に得られたUnityプログラムノウハウ

Unityのシリアライズ

• シーンやプレハブの情報はYAMLという書式で保存している

AssetServer使っているなら、対象のAssetを選択→右クリック→Compare Binary で中身がみれる。どこを変えると、どう変わるか見てみるのも一興

参考 http://docs-jp.unity3d.com/Documentation/Manual/FormatDescription.html

Page 43: 「宴」実装時に得られたUnityプログラムノウハウ

Unityのシリアライズ基本

Page 44: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズって?

• イメージ的には「インスペクターで扱える」でOk

Page 45: 「宴」実装時に得られたUnityプログラムノウハウ

何がいいことあるの?

• シーンやプレハブ、Assetとしてデータを編集・保存できるようになる

• インスペクター上のGUIで編集できるようになるので楽

• シリアライズできない=Unityエディタ上では扱いづらいと考えてOK

参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html

Page 46: 「宴」実装時に得られたUnityプログラムノウハウ

どうすればいい?

•基本的に対象になるクラスは• MonoBehaviourを継承するクラス(コンポーネント)

• ScriptableObjectを継承するクラス

• 通常のC#クラスも[Serializable]を使えばOK

•対象になるメンバ変数は• Public

• [SerializeField]を適用すれば、privateな型でもOK

参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html

Page 47: 「宴」実装時に得られたUnityプログラムノウハウ

似てるけど違うコンポーネントとScriptableObjectコンポーネント

MonoBehaviourを継承するクラス

GameObjectにアタッチする

SceneビューのGameObjectにアタッチする。各オブジェクトの「機能」のイメージ

Page 48: 「宴」実装時に得られたUnityプログラムノウハウ

似てるけど違うコンポーネントとScriptableObject

ScriptableObject

ProjectビューにAssetとして作成される。

つまり、テクスチャなどと同じく素材・データのイメージに近い

ScriptableObjectを継承するクラス

コンポーネントと同じく、インスペクターを持つ

Page 49: 「宴」実装時に得られたUnityプログラムノウハウ

似てるけど違うコンポーネントとScriptableObject

• コンポーネント(MonoBehavior)• オブジェクトにアタッチする「機能」

• ScriptableObject• データの塊(独自定義のAsset)

• 宴での使用例「ADVのシナリオデータ、フォント定義、描画順定義・・・など」

・・・という感じで使い分けるとよさげ。(本来はScriptableObjectはUnityの基本データクラスみたいなものMonoBehabiorやEditorWindowもScriptableObject を継承しているハズ)

Page 50: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズのための記述方法まとめ

• [Serializable]• 独自定義のクラスをシリアライズ可能にする

• クラスに定義する• C#の標準的な機能

• [SerializeFiled]• 非publicなメンバ変数をシリアライズの対象にする

• 変数に定義する• Unity独自の機能

似てるのでまとめ。使っていればたぶんすぐ慣れる。

参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html

Page 51: 「宴」実装時に得られたUnityプログラムノウハウ

Unityのシリアライズの基本

• Publicか[SerializeFiled]か

参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html

簡単に書くならpublicにするだけ

[SerializeField]にすると、privateな変数もシリアライズできる

こうすれば、プロパティ(get,set)でアクセスを制御できる。C#的はpublicを使わずに、プロパティを使うのが基本。Unity的にはどっちがベターか?

Page 52: 「宴」実装時に得られたUnityプログラムノウハウ

Unityのシリアライズ一歩先

Page 53: 「宴」実装時に得られたUnityプログラムノウハウ

Dictionaryのシリアライズ

• Unity(というかC#)では、Dictionaryはシリアライズ不可( Dictionaryはジェネリックの入れ子的な使い方をしているせい。詳細は割愛)

• 使用頻度は高いので、シリアライズ可能なDictionaryを作った。• ただしキーは文字列のみとする

参考ソース Utage¥Scripts¥GameLib¥Dictionary

Page 54: 「宴」実装時に得られたUnityプログラムノウハウ

ジェネリックのシリアライズ

• ジェネリックを使う場合の注意

クラス定義を型ごとにすること

MyGenecricClass<int>はダメ

class MyGenecricClassInt : MyGenecricClass<int>{}

と定義すれば使えるようになる。

参考 http://answers.unity3d.com/questions/214300/serializable-class-using-generics.html

Page 55: 「宴」実装時に得られたUnityプログラムノウハウ

ネームスペース&デフォルト引数は禁止

• ネームスペースを使った場合、デフォルト引数を使ったメソッドを持つクラスはシリアライズができなくなる。

• デフォルト引数は使わないように修正。

• コルーチンにデフォルト引数を使っている場合はちょっと注意。https://www.facebook.com/groups/unityuserj/permalink/620981311295146/?comment_id=621128654613745&offset=0&total_comments=7

参考 https://www.facebook.com/permalink.php?story_fbid=541323405927604&id=167184853341463

Unity4.5では問題なく使える模様http://terasur.blog.fc2.com/blog-entry-795.html

Page 56: 「宴」実装時に得られたUnityプログラムノウハウ

親子参照でバグ?

• ScriptableObject内親子構造にある[Serializable]なクラスに互いへの参照をもたせた

• なぜかUnityエディタがことあるごとに遅くなった。(コンパイルで一分以上かかる)

• アプリの起動時は特に影響なし。エディタのみ?

参考ソース Utage¥Scripts¥GameLib¥StringGrid

こうすると妙に重い

コールバックで参照するというよくわからない処理にしたら解消。Unityのバグ?

Page 57: 「宴」実装時に得られたUnityプログラムノウハウ

Mono DLL

~ たぶん殆どの人は使わなくてもOK ~

Page 58: 「宴」実装時に得られたUnityプログラムノウハウ

DLL化

• C#のプログラムをDLL化して使うことができる。

•体験版などでコードを隠蔽する場合や、複数プロジェクトにまたがる共通ライブラリを作る場合に使える。

参考 http://docs-jp.unity3d.com/Documentation/Manual/UsingDLL.html

http://terasur.blog.fc2.com/blog-entry-312.html

Page 59: 「宴」実装時に得られたUnityプログラムノウハウ

またしてもデフォルト引数が!

• シリアライズ関係ない、通常のC#のクラスでもデフォルト引数はダメな模様。

• DLLは.Net3.5以下しか対応してないけど、デフォルト引数は3.5だとコンパイルできない模様

もうUnityでデフォルト引数は全面的に避けたほうがいいかも・・・4.5以降ならDLL使わなければいいので、ほぼ問題ない?

Page 60: 「宴」実装時に得られたUnityプログラムノウハウ

Missing・・・

• コンポーネントやScriptableObjectをDLL化すると、作成済みのシーンやプレハブは全部Missingになってしまう・・・(GUIDを使ったUnity内部での参照関係がおかしくなってしまうらしい)

Page 61: 「宴」実装時に得られたUnityプログラムノウハウ

Missing・・・

•回避するには継承を使う。

参考ソース Utage¥Scripts¥GameLib¥File¥FileIOManagerBase

DLL化したいソースはスーパークラスに記述

スーパークラスだけDLL化する

スーパークラス作って、それを継承する形にする

Page 62: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズとリファクタリング~ もうMissingはうんざりだ! ~

Page 63: 「宴」実装時に得られたUnityプログラムノウハウ

Missingを避けるための、リネームの法則

リネームしていいか?

シリアライズが関係ないクラス名やメンバー名 ○

コンポーネントとScriptableObjectのクラス名 ○

コンポーネントとScriptableObjectの[SerializeField]またはpublicなメンバー変数名

×

[Seriazible]なクラス名 ×

[Seriazible]なクラスの[SerializeField]またはpublicなメンバー変数名 ×

基本的にシリアライズが絡むなら、コンポーネントとScriptableObjectのクラス名以外は変えてはいけない。

それ以外は、リネームするとシーンやプレハブに設定された値は初期値にリセットされる。

シーンやプレハブに設定された値を残したまま、インスペクター上の表示名だけ変えたい場合は

エディタ拡張をするしかない。

シーンやプレハブに設定せずに、AddComponentなどで動的に生成する場合は、リネームしてもOK

(シーンやプレハブに設定されたメタデータの問題なので・・・)

Page 64: 「宴」実装時に得られたUnityプログラムノウハウ

コンポーネントのクラス名の変えかたまず、Unity上でファイル名を変える(F2でリネームできる)

次にMonodevelopなど、エディタ上でクラス名を変える

MonodevelopやVisualStudioならクラス名を

記述してる場所は全て書き換えてくれる機能がある。Monodevelopならクラス名でフォーカスして右クリックでRefactorが出てくる。

Page 65: 「宴」実装時に得られたUnityプログラムノウハウ

シリアライズおわりUnityのシリアライズは奥が深かった・・・・・・

Page 66: 「宴」実装時に得られたUnityプログラムノウハウ

コルーチン

~ その裏技 ~

Page 67: 「宴」実装時に得られたUnityプログラムノウハウ

コルーチンからコルーチンを呼ぶ

void Load(){

StartCoroutine( CoLoad ());}

IEnumerator CoLoad(){

yield return StartCoroutine(CoLoadSub1());yield return StartCoroutine(CoLoadSub2());

}

IEnumerator CoLoadSub1(){

yield return new WWW(url1);}

IEnumerator CoLoadSub2(){

yield return new WWW(url2);}

コルーチン内部で

yield return StartCoroutine()

とする。

この例では →

CoLoadSub1()が終わってから、

CoLoadSub2()が始まる

Page 68: 「宴」実装時に得られたUnityプログラムノウハウ

コルーチンをMonovehavior以外で使う裏技public class MainClass : MonoBehaviour{

SubClass sub = new SubClass();void Load1(){

StartCoroutine(sub.CoLoad1());}void Load2(){

StartCoroutine(sub.CoLoad2(this));}

}//コルーチンをもつ通常のクラスpublic class SubClass{

//コルーチンpublic IEnumerator CoLoadSub1(){

yield return new WWW(url1);}//コルーチン内部でStartCoroutineを使うには、MonoBehaviourを渡すpublic IEnumerator CoLoadSub2(MonoBehaviour parent){

yield return parent.StartCoroutine(CoLoadSub1());yield return new WWW(url2);

}}

StartCoroutineはMonovehaviorからしか呼べない。

IEnumerator で宣言するコルーチン自体は通常のC#のクラスでもOK。

その内部でさらにStartCoroutineをしたいなら、Monobehaviorを渡してやるという手もある。

Page 69: 「宴」実装時に得られたUnityプログラムノウハウ

止め方に注意

• StopAllCoroutinesを呼んでも、呼び出したコルーチンはそのフレームの最後までは動くので注意。

IEnumerator CoLoadSub2(){while(true){if(isEnd){StopAllCoroutines(); //ここで止めても

}Debug.Log("!");//ここは呼ばれるyield return 0;

}}

Page 70: 「宴」実装時に得られたUnityプログラムノウハウ

エディタ拡張~ 手抜きのレシピ ~

Page 71: 「宴」実装時に得られたUnityプログラムノウハウ

OnValidateが便利

/// <summary>/// インスペクターから値が変更された場合/// </summary>void OnValidate(){

dataTbl.RefreshDictionary();}

リファレンス http://docs-jp.unity3d.com/Documentation/ScriptReference/MonoBehaviour.OnValidate.html

インスペクターのエディタ拡張をせずに、インスペクターで値が変えられたときの操作を記述できる。

簡単な処理ならこれでOK

参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting¥Node2DSortData.cs

Page 72: 「宴」実装時に得られたUnityプログラムノウハウ

OnValidate+MarkAsChangedパターン

参考ソース Utage¥Scripts¥GameLib¥2D¥2D¥Node2D.cs

/// <summary>/// インスペクターから値が変更された場合/// </summary>protected virtual void OnValidate(){

MarkAsChanged();}

/// <summary>/// 毎フレームの最後の更新/// </summary>protected virtual void LateUpdate(){if ( CachedTransform.parent != lastParent || hasChanged ){//構造に変化があったRefresh();

}}

インスペクターでの変更だけなく、・スクリプト内部からのsetなどで変更されうる・パラメーターが多く、かつ更新による負荷が大きいなど、ある程度複雑になる場合は

「変更があった」というフラグのみ設定して、LateUpdateやUpdateでチェックして一度だけ変更するという手もある。

Page 73: 「宴」実装時に得られたUnityプログラムノウハウ

EditorGUILayout.PropertyFieldを使うと楽

参考ソース Utage¥Editor¥Scripts¥Inspector

UtageEditorToolKit.PropertyField(serializedObject, "engine", "Engine");UtageEditorToolKit.PropertyField(serializedObject, "isAutomaticPlay", "Is Automatic Play");UtageEditorToolKit.PropertyField(serializedObject, "startScenario", "Start Scenario Label");

public static void PropertyField(SerializedObject serializedObject, string propertyPath,string label, params GUILayoutOption[] options)

{SerializedProperty property = serializedObject.FindProperty(propertyPath);if (property == null){

Debug.LogError(propertyPath + " is Not Found");}else{

EditorGUILayout.PropertyField(property, new GUIContent(label), options);}

}

メンバ変数名 インスペクターに表示する名前

EditorGUILayout.PropertyFieldを使うと、stringやMonobehavior、Vector3、enumなどの型によらず適切なGUIで表示してくれる。

細かい制限をかけない(エディタ拡張しないでよい)パラメーターに関してはこれで書いたほうが楽

エディタ拡張をする場合でも、なるべく簡単に書く。

Page 74: 「宴」実装時に得られたUnityプログラムノウハウ

Property Drawerを使う

リファレンス http://docs-jp.unity3d.com/Documentation/Components/editor-PropertyDrawers.html

参考ソース Utage¥Editor¥Scripts¥Attribute¥EnumFlagsAttributeDrawer.cs

EnumFlagsという、フラグを表示するProperty Drawerを作成しておく

こう書くと

[System.Flags]enum DebugOutPut{Log = 0x01,Waiting = 0x02,CommandEnd = 0x04,

};[SerializeField][EnumFlags]DebugOutPut debugOutPut = 0;

Property Drawerを使うと、コンポーネントごとにエディタ拡張をしなくても、

特定の表示パターンを指定できる。

Enumとして表示されてしまう

フラグ操作に適したUIで表示

Page 75: 「宴」実装時に得られたUnityプログラムノウハウ

より具体的なプログラム

Page 76: 「宴」実装時に得られたUnityプログラムノウハウ

画面サイズに対応~ アスペクト比を変えたり、レターボックスをつけたり ~

Page 77: 「宴」実装時に得られたUnityプログラムノウハウ

デバイスごとにバラバラな解像度に対応

デバイスの解像度に応じて描画領域が広げるように設定もできる

レターボックス(設定したアスペクト比を越える部分は黒で塗りつぶす )

NGUIはなぜか縦長タイプに対応できないが、そこもカバー

Page 78: 「宴」実装時に得られたUnityプログラムノウハウ

アスペクト比の範囲を設定可能

参考ソース Utage¥Scripts¥GameLib¥Camera¥CameraManager.cs"アスペクト比を固定のままなら、全部同じ値にする(殆どはこのパターンだと思われる)

Page 79: 「宴」実装時に得られたUnityプログラムノウハウ

実装のコツ

参考ソース Utage¥Scripts¥GameLib¥Camera¥CameraManager.cs"

その他のカメラのViewPortを調整して描画範囲を操作する

カメラを二つ以上使う。全画面を黒で塗りつぶすだけのClearCameraを描画順が先に来るように設定

Page 80: 「宴」実装時に得られたUnityプログラムノウハウ

2D~ Spriteのアレな感じを使いやすく ~

Page 81: 「宴」実装時に得られたUnityプログラムノウハウ

Unity4.5(Spriteの改良)や

Unity4.6(uGUI)次第ではこの辺全部無駄になる可能性

あります

Page 82: 「宴」実装時に得られたUnityプログラムノウハウ

Unityの2DSpriteの問題点

•描画順は、ヒエラルキーを無視してしまう

たとえば、こんな風に一画面丸ごとの描画順を変えようとしても

Page 83: 「宴」実装時に得られたUnityプログラムノウハウ

Unityの2DSpriteの問題点

描画順は、各スプライトの一つ一つのSortingLayerかOrder In Layerを設定するしかない

親オブジェクトを一つ変えれば、子の描画順も全部変わる!・・・とかができない

Page 84: 「宴」実装時に得られたUnityプログラムノウハウ

Unityの2DSpriteの問題点

• そのくせ、コリジョンの優先順はZ値なのでヒエラルキーに依存する。

Z値と描画順を合わせないと、

一番手前に描かれてるスプライトにタッチ判定・・・とかができない

手前に描画するものはZ値も手前にする必要が・・・

Page 85: 「宴」実装時に得られたUnityプログラムノウハウ

描画順に親子関係を反映するようにした

参考ソース Utage¥Scripts¥GameLib¥2D¥2D¥Node2D.cs

親の描画順1500 親の影響を受けて、

子のグローバルな描画順は1500

子のローカルな描画順 0

Page 86: 「宴」実装時に得られたUnityプログラムノウハウ

カラーの値も親子関係に影響するように

親を半透明に

一つ変えるだけで、画面全部が半透明に

Page 87: 「宴」実装時に得られたUnityプログラムノウハウ

描画順制御のデータテーブルを作成

参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting¥Node2DSortData.cs

描画順は手作業でも設定できるし、あらかじめ用意したデータテーブルからも設定できる

描画順とZ値を

プロジェクト内で共通の値として管理できるようにした。

Page 88: 「宴」実装時に得られたUnityプログラムノウハウ

カスタムプロジェクト設定~ 共通設定!一元管理! ~

Page 89: 「宴」実装時に得られたUnityプログラムノウハウ

共通で使う値を、一元管理したい

• ゲーム内で共通で使うデータを一元管理したい• 描画順のテーブル

• ローカライズ用のキーワード

• エディタ上からもいつでもロードして使いたい

Page 90: 「宴」実装時に得られたUnityプログラムノウハウ

共通で使う値を、一元管理したい

参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting

カスタムプロジェクト設定を作って

起動時に読み込むようにした

Page 91: 「宴」実装時に得られたUnityプログラムノウハウ

しかし、どうにもバグというかスッキリしないまだ検討の余地あり•課題

• カステムプロジェクト設定といってもプロジェクト内に必ず一つと決められない。

ツールとして提供するときは、サンプル設定データが必要になる

多言語対応も考えると、もっとややこしかったり

• エディタ中でも参照するためシーンを開いていないときですら、参照したい。• 色々やり方はあるが、(初回起動時など)どうしても読み込めないときが出てきてしまう。

• この辺のノウハウが欲しい・・・

Page 92: 「宴」実装時に得られたUnityプログラムノウハウ

ADVエンジン~ 「宴」のADVエンジンの構造 ~

Page 93: 「宴」実装時に得られたUnityプログラムノウハウ

ADVエンジンの構造と流れ

シナリオのエクセルデータを解析

シナリオのコマンドデータ作成

ゲーム実行中

コマンドデータを実行していく

おおざっぱな処理の流れ

Page 94: 「宴」実装時に得られたUnityプログラムノウハウ

エクセルの読み込み~ とっても便利!一度使うとやめられない! ~

Page 95: 「宴」実装時に得られたUnityプログラムノウハウ

Npoi使えば楽

•各Cellの文字列を読み込むのは楽• あとは文字列を解析するだけ

参考 http://terasur.blog.fc2.com/blog-entry-511.html参考ソース Utage¥Assets¥Utage¥Editor¥Scripts¥ExcelParser.cs

Page 96: 「宴」実装時に得られたUnityプログラムノウハウ

StringGrid

•宴では、StringGridという文字列のグリッド解析クラスを用意して、エクセルやCSVの文字列を解析している。

参考 Utage¥Scripts¥GameLib¥StringGrid

Page 97: 「宴」実装時に得られたUnityプログラムノウハウ

インポートの設定だけちょっと面倒

• 「拡張子がエクセルファイルだったらインポート」とかやってしまうと、他の環境と衝突するかもしれない。

参考ソース Editor¥Scripts¥Menu¥ScenarioData¥AdvScenarioDataBuilderWindow.cs

プロジェクト設定的なものを作って、そこで管理しているファイルだけインポートするようにする。

Page 98: 「宴」実装時に得られたUnityプログラムノウハウ

インポートしたらScriptableObjectに

参考ソース Assets¥Utage¥Editor¥Scripts¥Menu¥ScenarioData¥AdvExcelImporter.cs

出力したものは、ScriptableObjectにするのが良い。

Page 99: 「宴」実装時に得られたUnityプログラムノウハウ

一度作ってしまえば、すごく便利!

• OnPostprocessAllAssetsを使えば、インポートは、ファイルが更新されると自動で行われる

• エクセルを編集→上書き保存→Unityエディタに戻る

自動で更新が行われる。凄く便利!

Page 100: 「宴」実装時に得られたUnityプログラムノウハウ

ADV用のコマンドを実行していく

~ キャラを表示したり、テキストを表示したり、音を鳴らしたり ~

Page 101: 「宴」実装時に得られたUnityプログラムノウハウ

コマンドの処理の流れ

参考ソース Assets¥Utage¥Scripts¥ADV¥Scenario¥AdvScenarioPlayer.cs

初期化

リソースのロード

コマンド実行

待機処理

次のコマンドへ

改ページ待ちなど

毎フレームの処理

オートセーブ

Page 102: 「宴」実装時に得られたUnityプログラムノウハウ

実際にはもう少し複雑

• ジャンプ命令や、既読スキップ、シーン回想への対応など

•特にセーブがらみはわりと面倒。• 「いつでもセーブできる」とした場合、演出の途中でセーブロードするのが凄く大変

• シーンの冒頭など「セーブポイント」でしかセーブできない代わりに、「動的な演出に凝れるモード」もあったほうがいいかも?

• 動的な演出についてはこのまとめがわかりやすい

http://www.slideshare.net/tunacook/ss-35094307

Page 103: 「宴」実装時に得られたUnityプログラムノウハウ

オートセーブ

• 基本は改ページの直後の状態をセーブデータとして記録しておく• スプライトやサウンドなど、「シーンのそのときの状態」をセーブデータとして利用する。• わりと簡単に作れる。

• コマンドを追加拡張するごとにセーブデータの拡張も必要になる可能性がある。

• 宴ではこっちを採用

• これとは別に、シナリオのページラベルをキーにして、「そのページの状態」をシナリオデータから再現できるようにするという手もある。• こっちは難しいが、柔軟に使えるという利点もある。

• コマンドを拡張しても、セーブデータ自体はいじらないですむ場合もある

IOの仕組みは後述

Page 104: 「宴」実装時に得られたUnityプログラムノウハウ

コマンドの処理の流れ

初期化

リソースのロード

コマンド実行

待機処理

次のコマンドへ

改ページ待ちなど

毎フレームの処理一番のポイントはこれ

オートセーブ

Page 105: 「宴」実装時に得られたUnityプログラムノウハウ

リソースのロード

• ロード待ちを減らすため、あらかじめ素材をロードする• 今のコマンドだけではなく、この先数ページぶんのコマンドのロードだけ先にやっておく。(いわゆる裏読み・バックグラウンドロード)

• これがADVエンジン制作の一番難易度が高いポイント・・・と思ってる

• UnityではResouces.Loadが同期しかなかったり、ロード後のリソースの初期化とかは同期だったりするので、実際はどうしてもロードによる処理落ちができる。

Page 106: 「宴」実装時に得られたUnityプログラムノウハウ

リソースのDLとメモリ管理~ 大量のリソースをどうさばくか ~

Page 107: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」のAssetファイルマネージャー

•目標

• ダウンロード機能をつける

• AssetBundleを使わない(UnityBasicで動くようにする)

• なるべく早くロードする

• 必要以上のメモリを使わない

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

Page 108: 「宴」実装時に得られたUnityプログラムノウハウ

まとめるとこんな感じ

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

サーバー

必要になったら素材をDL

普通のロードは遅い

サーバー

デバイスストレージにキャッシュ

システムメモリにプール

シナリオを解析あらかじめロード

ロード済みファイルはすぐに解放せず、次のロードに備える。

メモリ使用量に応じて自動で解放

次ページ以降の素材をあらかじめロードし、ロード待ちを減らす

バックグラウンドでDL一度DLしたら、ローカルに保存二回目以降の余計なDLを避ける

ロードを最適化

Page 109: 「宴」実装時に得られたUnityプログラムノウハウ

ファイルロードの課題

• Resouces.Loadは?• ダウンロードできない

• アプリサイズが増える

• リソースの修正にはアプリ自体のアップデートが必要になる

• AssetBundleは?• UnityProじゃないとAssetBundleの作成ができない

• Unityのアップデートによっては、過去のAssetBundleと互換性が取れない

• サウンドのStream再生ができない。(メモリ消費が大きい)

• WWWは?• 毎回ダウンロードすることになる

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileWork.cs

Page 110: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」のAssetファイルマネージャー

•目標

• WWWとResoureces.Load両方でロードできるようにする

• ロード&メモリ管理機能をつける• WWWでロードする場合はキャッシュ機能をつける

• 一度DLしたファイルはキャッシュファイルとして保存・再利用する

• ロードした後のリソースはプール機能をつけて管理する• 一度ロードしたAssetは、メモリ内に残しておいて再利用する

• メモリ不足になったら、使っていない古いAssetをアンロードする

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileWork.cs

Page 111: 「宴」実装時に得られたUnityプログラムノウハウ

デバイスキャッシュ

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

Tool>Utage>Open Output Foloder>Cache

キャッシュファイルを保存しているフォルダが開く

キャッシュファイルは連番でファイル名がつく。

そのリソースがバージョンアップしたら、新しいファイルを書き込んで、古いファイルは消す。

WebPlayerではファイルIOができないので、キャッシュ機能は実装できない

Page 112: 「宴」実装時に得られたUnityプログラムノウハウ

デバイスキャッシュ

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

ローカルに書き込む際に、圧縮・暗号化もかけられる。

符号化したシナリオファイル

バイナリエディタなどで覗いてもネタバレはしない

Page 113: 「宴」実装時に得られたUnityプログラムノウハウ

• 目的は、LoadFromCacheOrDownloadと殆ど同じ• つまり、ファイルのバージョン管理つきのDLとローカルへの保存

• ただし、アセットバンドルではなく.pngや.wavなど汎用的なファイルを扱う

• メリット• アセットバンドル化しないので、pngなどはサイズが小さいままDLできる

• Unityのバージョンアップに左右されない• →アセットバンドルはUnityのバージョンアップでDLしなおしになる可能性がある

• デメリット• 今のところアセットバンドルに未対応

• つまり、3DモデルなどはDLできない

• 将来的にはアセットバンドルも併用したいけど検討中。

デバイスキャッシュ

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

Page 114: 「宴」実装時に得られたUnityプログラムノウハウ

アンロードする際には、可能な限りこのサイズ以下になるようにする

システムメモリにプール

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

ファイルマネージャーこんな感じ

DLのタイムアウト時間

エラー時のリトライ回数

同時にロードするファイルの最大数このサイズ(MB)を越えた

ら、プールしてるリソースをアンロードする

Page 115: 「宴」実装時に得られたUnityプログラムノウハウ

システムメモリにプール

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

ファイルマネージャーをデバッグモードでみる

管理中のファイル情報を見れる

使用中のファイル

ロード中のファイル

ロード待機してるファイル

使用済みのファイル

(アンロードせずにプールしてある)

Page 116: 「宴」実装時に得られたUnityプログラムノウハウ

システムメモリにプール

参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs

プールしてあるファイルが必要になったらすぐ使える(ロードが必要なくなる)

使用し終わっても、アンロードせずに、いったん使用済みとしてプールする

Page 117: 「宴」実装時に得られたUnityプログラムノウハウ

メモリ管理→参照管理

• プールの仕組みを実現するには

「どのファイルが使用中なのか?」という参照管理が必須になる。

参考ソース Utage¥Scripts¥ADV¥Scenario¥Command¥AdvCommand.cs

どのobjectから参照してる

か、参照がなくなったかを常に設定する(この場合はthis)

Page 118: 「宴」実装時に得られたUnityプログラムノウハウ

この仕組みはテクスチャを大量に使うゲーム

(カードゲームとか)にも使えるハズ

Page 119: 「宴」実装時に得られたUnityプログラムノウハウ

けっこう作るのは面倒

Page 120: 「宴」実装時に得られたUnityプログラムノウハウ

ブラッシュアップしていってそのうち「宴」から独立させて

AssetStoreでリリースするかも?

Page 121: 「宴」実装時に得られたUnityプログラムノウハウ

セーブロード~ たぶんUnityではこれが正解!WebPlayerでも使える ~

Page 122: 「宴」実装時に得られたUnityプログラムノウハウ

PlayerPrefsは手軽だけど、柔軟には使いづらい・・・•大規模になると管理がつらい

• 数が少ないうちはいいが、セーブする要素が増えたりすると困る

• サイズ可変の配列だったり、セーブデータを複数もったり・・・

•遅いとか、レジストリをいじるとか、困る面もある。

Page 123: 「宴」実装時に得られたUnityプログラムノウハウ

C#のシリアライズは・・・iOSで使えない

• セーブデータはやはりバイナリで作りたいところだが・・・

• C#のクラスを自動でバイナリ化してくれるBinaryFormatterを使う?

参考ソース http://forum.unity3d.com/threads/140606-iOS-Basic-BinaryFormatter

public static void SaveToBinaryFile(object obj, string path)

{

FileStream fs = new FileStream(path,

FileMode.Create,

FileAccess.Write);

BinaryFormatter bf = new BinaryFormatter();

//シリアル化して書き込む

bf.Serialize(fs, obj);

fs.Close();

}

iOSでは使えない

(使える場合もあるようだが、非常に限定的なので非対応と思ったほうがいい)

Page 124: 「宴」実装時に得られたUnityプログラムノウハウ

バイナリデータを作成

• BinaryReader& BinaryWriterで手作業でバイナリ化処理を• コードを書くのは面倒だけど、バージョンアップ対応など細かい融通は利く

参考ソース Utage¥Scripts¥ADV¥Save¥AdvSystemSaveData.cs

Page 125: 「宴」実装時に得られたUnityプログラムノウハウ

WebPlayer対応を考慮したセーブデータのIO

参考ソース Utage¥Scripts¥GameLib¥File¥FileIOManagerBase.cs

バイナリ化できてしまえば、ファイルとしてIO(入出力)すればいい。

WebPlayerなど、ファイルIOに対応してない場合は、PlayerPrefでセーブロードする

バイナリ配列をStringに変換し、ファイルパスをキーにしてIO

Page 126: 「宴」実装時に得られたUnityプログラムノウハウ

WebPlayer対応を考慮したファイルIO

バイナリ化を駆使すれば、

スクリーンショット(テクスチャ)つきのセーブデータをWebPlayerで保存できる!

WebPlayerのPlayerPrefにはサイズ制限があるのでそこだけ注意

Page 127: 「宴」実装時に得られたUnityプログラムノウハウ

その他のプラグラム紹介

Page 128: 「宴」実装時に得られたUnityプログラムノウハウ

テキストの禁則処理

参考ソース Utage¥Scripts¥GameLib¥2D¥2D¥TextArea2D.cs

そういう場合に適度な改行をして読みやすくするのを「禁則処理」という

日本語を表示する場合に

句読点や括弧等が行頭や行末に

くると読みづらくなる

禁則処理

こういう日本独自の処理はおそらくUnity公式ではサポートされないと思われる

(日本人が書くしかない)

行頭に 。かっこ悪い

Page 129: 「宴」実装時に得られたUnityプログラムノウハウ

構文解析

• ADVを作るためには「構文解析」が色々必要になる• <Color>タグつきのテキスト

• “Point+=1”という「文字列」を数式として処理したり

• その他いろいろ

詳しくはソースを確認してほしい

Page 130: 「宴」実装時に得られたUnityプログラムノウハウ

「宴」のプログラム構成

ADVに特化した部分

ゲームプログラム全般に汎用的に使う

サンプルコード主にUI処理を記述

Page 131: 「宴」実装時に得られたUnityプログラムノウハウ

ADV特化部分

主に、テクスチャやサウンドなどの設定

ADVの多言語対応

ADV中の2D描画

ADVのシステム制御

ADVのセーブ処理

ADVのシナリオ制御ほぼ中心となる処理 UI制御

Page 132: 「宴」実装時に得られたUnityプログラムノウハウ

ゲームプログラム汎用部分

2D処理

カメラ・タッチ入力処理シリアライズ可能なDictionary

文字列で書かれた数式の解析

ファイルマネージャー関係サウンド処理

(ボリューム調整や、フェード処理つき)

iTween

カスタムプロジェクト設定(一元管理用データ)

エクセルやCSVなど、文字列のグリッドの解析

タグつきテキスト解析フォント制御

その他の便利処理

Page 133: 「宴」実装時に得られたUnityプログラムノウハウ

おしまい

Unity用ADV制作ツール「宴」http://madnesslabo.net/utage/

2014/05/19マッドネスラボ代表 時村良平

https://twitter.com/rodostw

「宴」実装時に得られたUnityプログラムのノウハウ