Upload
ryohei-tokimura
View
29.953
Download
6
Embed Size (px)
DESCRIPTION
AssetStoreで公開中のUnity用ADV制作ツール「宴」 その実装時に得られたUnityプログラムのノウハウのまとめ
Citation preview
「宴」実装時に得られたUnityプログラムのノウハウ
Unity用ADV制作ツール「宴」
http://madnesslabo.net/utage/
2014/05/19マッドネスラボ代表 時村良平
https://twitter.com/rodostw
概要
• Unityにおけるプログラム一般( C# )• これが重要!どうやって探して、どこに渡す?• シリアライズ• コルーチン• エディタ拡張
• より具体的なプログラムについて• 2D• 画面サイズに対応• プロジェクト設定• ADVエンジン(シナリオ解析)• エクセルの読み込み• リソースのDLとメモリ管理• セーブロード
「宴」
このスライドでは「宴」の実装を通して得られた
Unityプログラムのノウハウを紹介します
ADV(ビジュアルノベル)をUnityで制作するためのツール。
AssetStoreで販売中。
https://www.assetstore.unity3d.com/#!/content/15905
詳細は「宴」のサイトを!
無料体験版もあります。http://madnesslabo.net/utage/
「宴」のプログラムの構成の紹介
参考 以下のスライドでは、参考になりそうなリンクや「宴」のソースコードパスをここに記述
ADVに特化した部分
ゲームプログラム全般に汎用的に使う
サンプルコード主にUI処理を記述
Unityにおけるプログラム一般( C# )
これが重要!どうやって探してどう呼び出す?
~ コンポーネントを使いこなそう ~
どうやって探す?
~ 目当てのコンポーネントはどこにある? ~
• たとえば、GameManagerというコンポーネントからPlayerというコンポーネントを使うことを考える。
• ただのC#なら
プロパティを使うなりして、あらかじめメンバ変数にセットしておく
もちろんこれでもいい
または、メソッドの引数に持たせて呼び出す
コンポーネントならではの使い方
•一番素直なのはインスペクターを使う
GameManagerとPlayerのオブジェクトが別々の場合
ドラッグ&ドロップでGameManagerのインスペクターにPlayerを設定してやる
• インスペクターで設定するのが基本だけど
プレハブ化すると
他のオブジェクトへの参照はMissingになってしまう。
他にもスクリプトから動的にオブジェクトを作った場合にも、インスペクター経由での設定はできない
他の手段はなにがある?
• GetComponentを使うパターン
GameManagerとPlayerのオブジェクトが同じ場合
インスペクターで設定する必要はない→プレハブ化などすると、インスペクターを使ったオブジェクトの参照は不安定なので、可能ならこのほうが安全で手軽
• GetComponentInChildrenを使うパターン
Playerが子
親子構造が保障されているときや、子オブジェクトを動的に生成した場合にも
•親オブジェクトのパターン
Playerが親
親子構造が保障されているときや、親子オブジェクトを動的に生成した場合にも
Unity4.5でGetComponentInParentが
追加されたのでそっちのほうがいい
• GameObject.Findを使うパターン
名前だけは分かっている
シーン編集時には存在しないオブジェクト(動的に生成したGameObjectや、他のシーンで生成したGameObject)を参照するときなど。
速度は遅いので注意
子にあることが分かってるなら、transfom.Findというのもある。
• GameObject.FindWithTagを使うパターン
GameObject.Findと似ているが、タグを指定するのでちょっと早い模様。
ただし、タグはプロジェクト間で共有が非常にしづらいので、汎用性に欠ける
速度はちょっと遅い
タグを指定
• FindObjectOfType<T>を使うパターン
Activeであるのが必須
GameObject.Findと似ているが、型を指定できるので便利コンポーネント以外に、TextureなどのAssetの型も指定できる
速度は遅い?
今までは対象が一つの場合を想定とはいえ複数の場合も応用すればOK複数用の処理
・GetComponents
・GetComponentsInChildren
・GameObject.FindGameObjectsWithTag
・FindObjectsOfType
• ゲーム中に必ず一つという保障があるなら、シングルトンを使うパターンもあり
手軽で速度低下もないが、シングルトンで作れなくなったときに構造を大幅に変える必要がでてくるので、多用は禁物
シングルトンな処理を作るUnityのシングルトン
の書き方はいろいろあるが、一番単純なパターンがこれ
ここまでが基本
もうちょい頑張る
遅い処理はキャッシュしよう• FindやGetComponentは遅い
参考 http://docs.unity3d.com/410/Documentation/ScriptReference/index.Performance_Optimization.html
プロパティと null 合体演算子を使うとコードがスッキリする!オススメ!
一度みつけたら、変数として保存(キャッシュ)しておくと二回目以降はすぐ呼び出せる
遅い処理はキャッシュしよう• Transformは注意
Transformの==はカスタムされているのが原因
参考 http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
this.transformは内部でGetComponentしてるようなので、処キャッシュしたほうがいい。
ただし、 null 合体演算子だとバグる!
こうする
RequireComponent
• そのコンポーネントを使う場合、必ず同時に使うコンポーネントを指定できる
参考ソース Utage¥Scripts¥ADV¥AdvEngine.cs
AddComponentすると、自動的にRequireComponentで指定されてるコンポーネントもAddされる
RequireComponent
• これを使うと、安全なコードが書ける。
参考ソース Utage¥Scripts¥ADV¥AdvEngine.cs
コンポーネントがあることが保障されているのでGetComponetでnullにならない!
RequireComponent・RequireComponent・プロパティ内のnull合体演算子からのGetComponet組み合わせて使うと便利!凄くオススメ
どう呼び出す?
~ 命令を呼ぶ ~
•普通は、メソッドを呼べばいい
• コンポーネントの種類を指定したくないとき
型はPlayerとEnemyの二種類がある。C#的には継承を使うのが普通
• SendMessage
Classの型を気にせず柔軟に使える
メソッド名を指定してSendMessage
• SendMessageの真の力
TargetとFunctionをインスペクター上で変えれば、ソースコードを変えることなく好きな相手に好きな処理をさせられる
• SendMessageのさらなる力
SendMessageには引数を一つだけもたせられるただし、返り値はもてない。それでも、こんな感じで処理を繋げられる。
• SendMessage、SendMessage ・・・
引数をGgamaObjectにすると、「誰から呼ばれたか」も制約せずに柔軟に使える。引数をGgamaObjectにしても、GetComponentを使えば、コンポーネントを取得することもできる。SendMessageからSendMessageを呼ぶことも可能。
• SendMessage、SendMessage ・・・
SendMessageはやりすぎる
と、わけがわからなくなるので注意。
参照関係をエディタ上で追えなくなりがち。
どこからも呼ばれてないように見えてしまう
• プログラム内で完結させるならデリゲートを使う手もある
型安全に使える。ただし、プログラムを書かないと処理を切り替えられない。
ゲーム内で「プレイヤーと敵」のように頻繁に処理が切り替わる場合は向かないかも。ロードの終了とかのフレームをまたぐ処理に使うのがよさげ。(宴では、ユーザの独自拡張のための入り口とかによく使ってる)
SendMessage
GameObjectに対して名前指定で処理を呼ぶ。引数を一つだけ持たせられる。
• メリット• 名前指定なので、ソースコードを変えずに呼び出し先を変えるのに使える。
• たとえば、「ボタンを押されたら呼び出す処理名」などをインスペクターに登録しやすい。
• 複数のコンポーネントに対しても個別に処理をさせらる。• BroadCastを使えば子階層のオブジェクトにも処理をさせられる。
• デメリット• 返り値がもてない。• 複数処理がされる場合は、処理順が制御できない?• 非アクティブなオブジェクトには使えない。(ので、初期化処理などには使いづらい)
Func,Actionなどのデリゲード(コールバック)
C#の標準的な仕組み。
メリット• 型指定なので、ある程度複雑な処理も安全に使いやすい。
• ロード終了時など、「タイミングが来たらこの処理を呼んで」という形で使える。
• 「追加の独自拡張」を想定する場合にも使える。
• 返り値がもてる
• デメリット• プログラムとして記述しなければ使えない。
(SendMessageのように「インスペクター上に記述された文字列の処理を呼ぶ」ということはできない。)
シリアライズ
~ Unityのシリアライズは奥が深い・・・・・・ ~
シリアライズって?
• Wikiより引用
• コンピュータプログラミングにおいて、シリアライズ、もしくはシリアル化 (serialize) という用語は、次のような異なる2つの意味を有する。
• ある一つの資源を、複数の主体が利用しようとするときに、それを調整(同期)して、一つの時点では一つの主体だけがそれを利用するようにすること。この意味では逐次化という訳語が用いられる。対義語は並列化である。
• ある環境に存在しているオブジェクトをバイト列やXMLフォーマットに変換すること。この意味では直列化という訳語が用いられる。同義語にMarshallingがある。対義語は直列化復元ないしデシリアライズである。
Unityのシリアライズ
• シーンやプレハブの情報はYAMLという書式で保存している
AssetServer使っているなら、対象のAssetを選択→右クリック→Compare Binary で中身がみれる。どこを変えると、どう変わるか見てみるのも一興
参考 http://docs-jp.unity3d.com/Documentation/Manual/FormatDescription.html
Unityのシリアライズ基本
シリアライズって?
• イメージ的には「インスペクターで扱える」でOk
何がいいことあるの?
• シーンやプレハブ、Assetとしてデータを編集・保存できるようになる
• インスペクター上のGUIで編集できるようになるので楽
• シリアライズできない=Unityエディタ上では扱いづらいと考えてOK
参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html
どうすればいい?
•基本的に対象になるクラスは• MonoBehaviourを継承するクラス(コンポーネント)
• ScriptableObjectを継承するクラス
• 通常のC#クラスも[Serializable]を使えばOK
•対象になるメンバ変数は• Public
• [SerializeField]を適用すれば、privateな型でもOK
参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html
似てるけど違うコンポーネントとScriptableObjectコンポーネント
MonoBehaviourを継承するクラス
GameObjectにアタッチする
SceneビューのGameObjectにアタッチする。各オブジェクトの「機能」のイメージ
似てるけど違うコンポーネントとScriptableObject
ScriptableObject
ProjectビューにAssetとして作成される。
つまり、テクスチャなどと同じく素材・データのイメージに近い
ScriptableObjectを継承するクラス
コンポーネントと同じく、インスペクターを持つ
似てるけど違うコンポーネントとScriptableObject
• コンポーネント(MonoBehavior)• オブジェクトにアタッチする「機能」
• ScriptableObject• データの塊(独自定義のAsset)
• 宴での使用例「ADVのシナリオデータ、フォント定義、描画順定義・・・など」
・・・という感じで使い分けるとよさげ。(本来はScriptableObjectはUnityの基本データクラスみたいなものMonoBehabiorやEditorWindowもScriptableObject を継承しているハズ)
シリアライズのための記述方法まとめ
• [Serializable]• 独自定義のクラスをシリアライズ可能にする
• クラスに定義する• C#の標準的な機能
• [SerializeFiled]• 非publicなメンバ変数をシリアライズの対象にする
• 変数に定義する• Unity独自の機能
似てるのでまとめ。使っていればたぶんすぐ慣れる。
参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html
Unityのシリアライズの基本
• Publicか[SerializeFiled]か
参考 http://docs-jp.unity3d.com/Documentation/ScriptReference/SerializeField.html
簡単に書くならpublicにするだけ
[SerializeField]にすると、privateな変数もシリアライズできる
こうすれば、プロパティ(get,set)でアクセスを制御できる。C#的はpublicを使わずに、プロパティを使うのが基本。Unity的にはどっちがベターか?
Unityのシリアライズ一歩先
Dictionaryのシリアライズ
• Unity(というかC#)では、Dictionaryはシリアライズ不可( Dictionaryはジェネリックの入れ子的な使い方をしているせい。詳細は割愛)
• 使用頻度は高いので、シリアライズ可能なDictionaryを作った。• ただしキーは文字列のみとする
参考ソース Utage¥Scripts¥GameLib¥Dictionary
ジェネリックのシリアライズ
• ジェネリックを使う場合の注意
クラス定義を型ごとにすること
MyGenecricClass<int>はダメ
class MyGenecricClassInt : MyGenecricClass<int>{}
と定義すれば使えるようになる。
参考 http://answers.unity3d.com/questions/214300/serializable-class-using-generics.html
ネームスペース&デフォルト引数は禁止
• ネームスペースを使った場合、デフォルト引数を使ったメソッドを持つクラスはシリアライズができなくなる。
• デフォルト引数は使わないように修正。
• コルーチンにデフォルト引数を使っている場合はちょっと注意。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
子
親
親子参照でバグ?
• ScriptableObject内親子構造にある[Serializable]なクラスに互いへの参照をもたせた
• なぜかUnityエディタがことあるごとに遅くなった。(コンパイルで一分以上かかる)
• アプリの起動時は特に影響なし。エディタのみ?
参考ソース Utage¥Scripts¥GameLib¥StringGrid
こうすると妙に重い
コールバックで参照するというよくわからない処理にしたら解消。Unityのバグ?
Mono DLL
~ たぶん殆どの人は使わなくてもOK ~
DLL化
• C#のプログラムをDLL化して使うことができる。
•体験版などでコードを隠蔽する場合や、複数プロジェクトにまたがる共通ライブラリを作る場合に使える。
参考 http://docs-jp.unity3d.com/Documentation/Manual/UsingDLL.html
http://terasur.blog.fc2.com/blog-entry-312.html
またしてもデフォルト引数が!
• シリアライズ関係ない、通常のC#のクラスでもデフォルト引数はダメな模様。
• DLLは.Net3.5以下しか対応してないけど、デフォルト引数は3.5だとコンパイルできない模様
もうUnityでデフォルト引数は全面的に避けたほうがいいかも・・・4.5以降ならDLL使わなければいいので、ほぼ問題ない?
Missing・・・
• コンポーネントやScriptableObjectをDLL化すると、作成済みのシーンやプレハブは全部Missingになってしまう・・・(GUIDを使ったUnity内部での参照関係がおかしくなってしまうらしい)
Missing・・・
•回避するには継承を使う。
参考ソース Utage¥Scripts¥GameLib¥File¥FileIOManagerBase
DLL化したいソースはスーパークラスに記述
スーパークラスだけDLL化する
スーパークラス作って、それを継承する形にする
シリアライズとリファクタリング~ もうMissingはうんざりだ! ~
Missingを避けるための、リネームの法則
リネームしていいか?
シリアライズが関係ないクラス名やメンバー名 ○
コンポーネントとScriptableObjectのクラス名 ○
コンポーネントとScriptableObjectの[SerializeField]またはpublicなメンバー変数名
×
[Seriazible]なクラス名 ×
[Seriazible]なクラスの[SerializeField]またはpublicなメンバー変数名 ×
基本的にシリアライズが絡むなら、コンポーネントとScriptableObjectのクラス名以外は変えてはいけない。
それ以外は、リネームするとシーンやプレハブに設定された値は初期値にリセットされる。
シーンやプレハブに設定された値を残したまま、インスペクター上の表示名だけ変えたい場合は
エディタ拡張をするしかない。
シーンやプレハブに設定せずに、AddComponentなどで動的に生成する場合は、リネームしてもOK
(シーンやプレハブに設定されたメタデータの問題なので・・・)
コンポーネントのクラス名の変えかたまず、Unity上でファイル名を変える(F2でリネームできる)
次にMonodevelopなど、エディタ上でクラス名を変える
MonodevelopやVisualStudioならクラス名を
記述してる場所は全て書き換えてくれる機能がある。Monodevelopならクラス名でフォーカスして右クリックでRefactorが出てくる。
シリアライズおわり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()が始まる
コルーチンを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を渡してやるという手もある。
止め方に注意
• StopAllCoroutinesを呼んでも、呼び出したコルーチンはそのフレームの最後までは動くので注意。
IEnumerator CoLoadSub2(){while(true){if(isEnd){StopAllCoroutines(); //ここで止めても
}Debug.Log("!");//ここは呼ばれるyield return 0;
}}
エディタ拡張~ 手抜きのレシピ ~
OnValidateが便利
/// <summary>/// インスペクターから値が変更された場合/// </summary>void OnValidate(){
dataTbl.RefreshDictionary();}
リファレンス http://docs-jp.unity3d.com/Documentation/ScriptReference/MonoBehaviour.OnValidate.html
インスペクターのエディタ拡張をせずに、インスペクターで値が変えられたときの操作を記述できる。
簡単な処理ならこれでOK
参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting¥Node2DSortData.cs
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でチェックして一度だけ変更するという手もある。
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で表示してくれる。
細かい制限をかけない(エディタ拡張しないでよい)パラメーターに関してはこれで書いたほうが楽
エディタ拡張をする場合でも、なるべく簡単に書く。
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で表示
より具体的なプログラム
画面サイズに対応~ アスペクト比を変えたり、レターボックスをつけたり ~
デバイスごとにバラバラな解像度に対応
デバイスの解像度に応じて描画領域が広げるように設定もできる
レターボックス(設定したアスペクト比を越える部分は黒で塗りつぶす )
NGUIはなぜか縦長タイプに対応できないが、そこもカバー
アスペクト比の範囲を設定可能
参考ソース Utage¥Scripts¥GameLib¥Camera¥CameraManager.cs"アスペクト比を固定のままなら、全部同じ値にする(殆どはこのパターンだと思われる)
実装のコツ
参考ソース Utage¥Scripts¥GameLib¥Camera¥CameraManager.cs"
その他のカメラのViewPortを調整して描画範囲を操作する
カメラを二つ以上使う。全画面を黒で塗りつぶすだけのClearCameraを描画順が先に来るように設定
2D~ Spriteのアレな感じを使いやすく ~
Unity4.5(Spriteの改良)や
Unity4.6(uGUI)次第ではこの辺全部無駄になる可能性
あります
Unityの2DSpriteの問題点
•描画順は、ヒエラルキーを無視してしまう
たとえば、こんな風に一画面丸ごとの描画順を変えようとしても
Unityの2DSpriteの問題点
描画順は、各スプライトの一つ一つのSortingLayerかOrder In Layerを設定するしかない
親オブジェクトを一つ変えれば、子の描画順も全部変わる!・・・とかができない
Unityの2DSpriteの問題点
• そのくせ、コリジョンの優先順はZ値なのでヒエラルキーに依存する。
Z値と描画順を合わせないと、
一番手前に描かれてるスプライトにタッチ判定・・・とかができない
手前に描画するものはZ値も手前にする必要が・・・
描画順に親子関係を反映するようにした
参考ソース Utage¥Scripts¥GameLib¥2D¥2D¥Node2D.cs
親の描画順1500 親の影響を受けて、
子のグローバルな描画順は1500
子のローカルな描画順 0
カラーの値も親子関係に影響するように
親を半透明に
一つ変えるだけで、画面全部が半透明に
描画順制御のデータテーブルを作成
参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting¥Node2DSortData.cs
描画順は手作業でも設定できるし、あらかじめ用意したデータテーブルからも設定できる
描画順とZ値を
プロジェクト内で共通の値として管理できるようにした。
カスタムプロジェクト設定~ 共通設定!一元管理! ~
共通で使う値を、一元管理したい
• ゲーム内で共通で使うデータを一元管理したい• 描画順のテーブル
• ローカライズ用のキーワード
• エディタ上からもいつでもロードして使いたい
共通で使う値を、一元管理したい
参考ソース Utage¥Scripts¥GameLib¥CustomProjectSetting
カスタムプロジェクト設定を作って
起動時に読み込むようにした
しかし、どうにもバグというかスッキリしないまだ検討の余地あり•課題
• カステムプロジェクト設定といってもプロジェクト内に必ず一つと決められない。
ツールとして提供するときは、サンプル設定データが必要になる
多言語対応も考えると、もっとややこしかったり
• エディタ中でも参照するためシーンを開いていないときですら、参照したい。• 色々やり方はあるが、(初回起動時など)どうしても読み込めないときが出てきてしまう。
• この辺のノウハウが欲しい・・・
ADVエンジン~ 「宴」のADVエンジンの構造 ~
ADVエンジンの構造と流れ
シナリオのエクセルデータを解析
シナリオのコマンドデータ作成
ゲーム実行中
コマンドデータを実行していく
おおざっぱな処理の流れ
エクセルの読み込み~ とっても便利!一度使うとやめられない! ~
Npoi使えば楽
•各Cellの文字列を読み込むのは楽• あとは文字列を解析するだけ
参考 http://terasur.blog.fc2.com/blog-entry-511.html参考ソース Utage¥Assets¥Utage¥Editor¥Scripts¥ExcelParser.cs
StringGrid
•宴では、StringGridという文字列のグリッド解析クラスを用意して、エクセルやCSVの文字列を解析している。
参考 Utage¥Scripts¥GameLib¥StringGrid
インポートの設定だけちょっと面倒
• 「拡張子がエクセルファイルだったらインポート」とかやってしまうと、他の環境と衝突するかもしれない。
参考ソース Editor¥Scripts¥Menu¥ScenarioData¥AdvScenarioDataBuilderWindow.cs
プロジェクト設定的なものを作って、そこで管理しているファイルだけインポートするようにする。
インポートしたらScriptableObjectに
参考ソース Assets¥Utage¥Editor¥Scripts¥Menu¥ScenarioData¥AdvExcelImporter.cs
出力したものは、ScriptableObjectにするのが良い。
一度作ってしまえば、すごく便利!
• OnPostprocessAllAssetsを使えば、インポートは、ファイルが更新されると自動で行われる
• エクセルを編集→上書き保存→Unityエディタに戻る
自動で更新が行われる。凄く便利!
ADV用のコマンドを実行していく
~ キャラを表示したり、テキストを表示したり、音を鳴らしたり ~
コマンドの処理の流れ
参考ソース Assets¥Utage¥Scripts¥ADV¥Scenario¥AdvScenarioPlayer.cs
初期化
リソースのロード
コマンド実行
待機処理
次のコマンドへ
改ページ待ちなど
毎フレームの処理
オートセーブ
実際にはもう少し複雑
• ジャンプ命令や、既読スキップ、シーン回想への対応など
•特にセーブがらみはわりと面倒。• 「いつでもセーブできる」とした場合、演出の途中でセーブロードするのが凄く大変
• シーンの冒頭など「セーブポイント」でしかセーブできない代わりに、「動的な演出に凝れるモード」もあったほうがいいかも?
• 動的な演出についてはこのまとめがわかりやすい
http://www.slideshare.net/tunacook/ss-35094307
オートセーブ
• 基本は改ページの直後の状態をセーブデータとして記録しておく• スプライトやサウンドなど、「シーンのそのときの状態」をセーブデータとして利用する。• わりと簡単に作れる。
• コマンドを追加拡張するごとにセーブデータの拡張も必要になる可能性がある。
• 宴ではこっちを採用
• これとは別に、シナリオのページラベルをキーにして、「そのページの状態」をシナリオデータから再現できるようにするという手もある。• こっちは難しいが、柔軟に使えるという利点もある。
• コマンドを拡張しても、セーブデータ自体はいじらないですむ場合もある
IOの仕組みは後述
コマンドの処理の流れ
初期化
リソースのロード
コマンド実行
待機処理
次のコマンドへ
改ページ待ちなど
毎フレームの処理一番のポイントはこれ
オートセーブ
リソースのロード
• ロード待ちを減らすため、あらかじめ素材をロードする• 今のコマンドだけではなく、この先数ページぶんのコマンドのロードだけ先にやっておく。(いわゆる裏読み・バックグラウンドロード)
• これがADVエンジン制作の一番難易度が高いポイント・・・と思ってる
• UnityではResouces.Loadが同期しかなかったり、ロード後のリソースの初期化とかは同期だったりするので、実際はどうしてもロードによる処理落ちができる。
リソースのDLとメモリ管理~ 大量のリソースをどうさばくか ~
「宴」のAssetファイルマネージャー
•目標
• ダウンロード機能をつける
• AssetBundleを使わない(UnityBasicで動くようにする)
• なるべく早くロードする
• 必要以上のメモリを使わない
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
まとめるとこんな感じ
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
サーバー
必要になったら素材をDL
普通のロードは遅い
サーバー
デバイスストレージにキャッシュ
システムメモリにプール
シナリオを解析あらかじめロード
ロード済みファイルはすぐに解放せず、次のロードに備える。
メモリ使用量に応じて自動で解放
次ページ以降の素材をあらかじめロードし、ロード待ちを減らす
バックグラウンドでDL一度DLしたら、ローカルに保存二回目以降の余計なDLを避ける
ロードを最適化
ファイルロードの課題
• Resouces.Loadは?• ダウンロードできない
• アプリサイズが増える
• リソースの修正にはアプリ自体のアップデートが必要になる
• AssetBundleは?• UnityProじゃないとAssetBundleの作成ができない
• Unityのアップデートによっては、過去のAssetBundleと互換性が取れない
• サウンドのStream再生ができない。(メモリ消費が大きい)
• WWWは?• 毎回ダウンロードすることになる
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileWork.cs
「宴」のAssetファイルマネージャー
•目標
• WWWとResoureces.Load両方でロードできるようにする
• ロード&メモリ管理機能をつける• WWWでロードする場合はキャッシュ機能をつける
• 一度DLしたファイルはキャッシュファイルとして保存・再利用する
• ロードした後のリソースはプール機能をつけて管理する• 一度ロードしたAssetは、メモリ内に残しておいて再利用する
• メモリ不足になったら、使っていない古いAssetをアンロードする
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileWork.cs
デバイスキャッシュ
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
Tool>Utage>Open Output Foloder>Cache
キャッシュファイルを保存しているフォルダが開く
キャッシュファイルは連番でファイル名がつく。
そのリソースがバージョンアップしたら、新しいファイルを書き込んで、古いファイルは消す。
WebPlayerではファイルIOができないので、キャッシュ機能は実装できない
デバイスキャッシュ
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
ローカルに書き込む際に、圧縮・暗号化もかけられる。
符号化したシナリオファイル
バイナリエディタなどで覗いてもネタバレはしない
• 目的は、LoadFromCacheOrDownloadと殆ど同じ• つまり、ファイルのバージョン管理つきのDLとローカルへの保存
• ただし、アセットバンドルではなく.pngや.wavなど汎用的なファイルを扱う
• メリット• アセットバンドル化しないので、pngなどはサイズが小さいままDLできる
• Unityのバージョンアップに左右されない• →アセットバンドルはUnityのバージョンアップでDLしなおしになる可能性がある
• デメリット• 今のところアセットバンドルに未対応
• つまり、3DモデルなどはDLできない
• 将来的にはアセットバンドルも併用したいけど検討中。
デバイスキャッシュ
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
アンロードする際には、可能な限りこのサイズ以下になるようにする
システムメモリにプール
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
ファイルマネージャーこんな感じ
DLのタイムアウト時間
エラー時のリトライ回数
同時にロードするファイルの最大数このサイズ(MB)を越えた
ら、プールしてるリソースをアンロードする
システムメモリにプール
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
ファイルマネージャーをデバッグモードでみる
管理中のファイル情報を見れる
使用中のファイル
ロード中のファイル
ロード待機してるファイル
使用済みのファイル
(アンロードせずにプールしてある)
システムメモリにプール
参考ソース Utage¥Scripts¥GameLib¥File¥AssetFileManager.cs
プールしてあるファイルが必要になったらすぐ使える(ロードが必要なくなる)
使用し終わっても、アンロードせずに、いったん使用済みとしてプールする
メモリ管理→参照管理
• プールの仕組みを実現するには
「どのファイルが使用中なのか?」という参照管理が必須になる。
参考ソース Utage¥Scripts¥ADV¥Scenario¥Command¥AdvCommand.cs
どのobjectから参照してる
か、参照がなくなったかを常に設定する(この場合はthis)
この仕組みはテクスチャを大量に使うゲーム
(カードゲームとか)にも使えるハズ
けっこう作るのは面倒
ブラッシュアップしていってそのうち「宴」から独立させて
AssetStoreでリリースするかも?
セーブロード~ たぶんUnityではこれが正解!WebPlayerでも使える ~
PlayerPrefsは手軽だけど、柔軟には使いづらい・・・•大規模になると管理がつらい
• 数が少ないうちはいいが、セーブする要素が増えたりすると困る
• サイズ可変の配列だったり、セーブデータを複数もったり・・・
•遅いとか、レジストリをいじるとか、困る面もある。
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では使えない
(使える場合もあるようだが、非常に限定的なので非対応と思ったほうがいい)
バイナリデータを作成
• BinaryReader& BinaryWriterで手作業でバイナリ化処理を• コードを書くのは面倒だけど、バージョンアップ対応など細かい融通は利く
参考ソース Utage¥Scripts¥ADV¥Save¥AdvSystemSaveData.cs
WebPlayer対応を考慮したセーブデータのIO
参考ソース Utage¥Scripts¥GameLib¥File¥FileIOManagerBase.cs
バイナリ化できてしまえば、ファイルとしてIO(入出力)すればいい。
WebPlayerなど、ファイルIOに対応してない場合は、PlayerPrefでセーブロードする
バイナリ配列をStringに変換し、ファイルパスをキーにしてIO
WebPlayer対応を考慮したファイルIO
バイナリ化を駆使すれば、
スクリーンショット(テクスチャ)つきのセーブデータをWebPlayerで保存できる!
WebPlayerのPlayerPrefにはサイズ制限があるのでそこだけ注意
その他のプラグラム紹介
テキストの禁則処理
参考ソース Utage¥Scripts¥GameLib¥2D¥2D¥TextArea2D.cs
そういう場合に適度な改行をして読みやすくするのを「禁則処理」という
日本語を表示する場合に
句読点や括弧等が行頭や行末に
くると読みづらくなる
禁則処理
こういう日本独自の処理はおそらくUnity公式ではサポートされないと思われる
(日本人が書くしかない)
行頭に 。かっこ悪い
構文解析
• ADVを作るためには「構文解析」が色々必要になる• <Color>タグつきのテキスト
• “Point+=1”という「文字列」を数式として処理したり
• その他いろいろ
詳しくはソースを確認してほしい
「宴」のプログラム構成
ADVに特化した部分
ゲームプログラム全般に汎用的に使う
サンプルコード主にUI処理を記述
ADV特化部分
主に、テクスチャやサウンドなどの設定
ADVの多言語対応
ADV中の2D描画
ADVのシステム制御
ADVのセーブ処理
ADVのシナリオ制御ほぼ中心となる処理 UI制御
ゲームプログラム汎用部分
2D処理
カメラ・タッチ入力処理シリアライズ可能なDictionary
文字列で書かれた数式の解析
ファイルマネージャー関係サウンド処理
(ボリューム調整や、フェード処理つき)
iTween
カスタムプロジェクト設定(一元管理用データ)
エクセルやCSVなど、文字列のグリッドの解析
タグつきテキスト解析フォント制御
その他の便利処理
おしまい
Unity用ADV制作ツール「宴」http://madnesslabo.net/utage/
2014/05/19マッドネスラボ代表 時村良平
https://twitter.com/rodostw
「宴」実装時に得られたUnityプログラムのノウハウ