「宴」実装時に得られた
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プログラムのノウハウ

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