この記事は?
※この記事は CCS Advent Calender 2017 3日目の記事です。
前記事: 完成度をワンランク上げるデジ絵加工技術
はじめに
こんにちは、CCS OBあるいは__エンドレスシラフ__のNicolaiです。
昨年のアドベントカレンダーで書いた記事が非常にポエムだったので、
よ~しまた愚にもつかねえポエム書くぞ~書くぞ~と思っていましたが、
謎のポエマーだと思われるのも正直きついものがあるのでやめました。
その代わり今回は割と実用寄りというか、
__エンドレスシラフの「∀kashicforce」というゲームで実際に使われた技術__について取り留めなくお話します。
どういうゲームなのかは上のリンクを見てください。
上記ゲームはUnityを使って作られてるので、Unityメインの話です。
あとワークアラウンド的技術や運用で回避する方法、一時しのぎなどのよくない話とか諸々…
タイトルに__【雑】__と書いた通りかなり雑な記事です。
解説でソースコードが出てくる箇所がありますが、全部載せると分量がやばいので
エッセンス部分だけに抽出して載せるに留めます。
かなりとっ散らかってるけど許してね。
追記:
重要なことを書き忘れてた。
基本的に∀kashicforceはWindows向けの同人ゲームなので、WindowsのStandalone Playerビルド前提の話になります。
モバイルとかWebGLとか向けビルドの話は知見がないので知りません!!!!!注意!!!!
#【一読の前に注意】
この記事中で言及する「Unity」は、特に記載がない限り
__「Unity5.0.0f4」というクッソ古いversion__のことを指します。
なんでそんな古いバージョンについて言及するかというと、上記のバージョンが
∀kashicforceで使ってたバージョンのUnityだからです。
開発始めた当時は最新版だったんだよ!!!
何が言いたいかというと、記事中で「Unityここがダメ!!!」みたいな話が出てきたら、
それは__最新版のUnityでは直ってる可能性がある__ということです。
そういう直ってる箇所あったら教えてください。
自動アップデータの実装
∀kashicforceでは、ゲーム起動時に更新差分があった場合に
__自動で差分パッチをダウンロードしてくる仕組み__があります。
仕組み自体はかなり単純で、以下のような感じで動作しています。
-
udm差分ファイル作成ツール などを用いて差分パッチを作成し、
差分パッチの名前にバージョン番号をつけて自前サーバのどこかに上げておく。 - 例:Akashicforce_patch_v110.exe
- 自前サーバのどこかに、最新版のバージョン番号が書かれたテキストを上げておく。
- ゲームのローカルディレクトリに、バージョン番号の書かれたテキストを配置する
- ゲーム起動時のスクリプトで、バージョン番号テキストをサーバから読み取ってくる。
C#では以下のような感じ(適当にコードのエッセンス部分だけ載せる)
WebClient wc = new WebClient();
Stream st;
string url = ServerName + FileName;
try
{
st = wc.OpenRead(url);
}
catch (WebException exc)
{
return -1;
}
Encoding enc = Encoding.GetEncoding("Shift_JIS");
StreamReader sr = new StreamReader(st, enc);
string new_version = sr.ReadToEnd();
sr.Close();
st.Close();
return int.Parse(new_version);
- 同様にローカル側のバージョンを読み取り、サーバ側から読み取った最新バージョン番号と比較。
ローカルよりもサーバ側バージョンが新しいならば、サーバからパッチをDLする。
__DLするパッチの名前はのようにバージョン番号のついた名前で特定__する。 - 例:ローカルバージョンがver1.01、サーバ側最新バージョンがver1.03ならば、ver1.02とver1.03をDLする。
名前は例えばAkashicforce_patch_v102.exeとかつけておくと特定できる。 - DLしてきたパッチを起動し、
Application.Quit
でゲームを終了する。
起動するのはバージョン番号の一番若いパッチ。
string path = Application.dataPath + "/../" + exe_name;
var exProcess = new Process();
exProcess.StartInfo.FileName = path;
//外部のプロセスを実行する
exProcess.Start();
Application.Quit(); // ゲーム終了
-
差分パッチが実行されるので、更新時テキストとかでどこのフォルダにパッチを適用すればいいかなどをフォロー 。
下は差分パッチ実行時の表示。
-
udm差分ファイル作成ツールは__「差分適用後に実行するコマンド」__という設定項目が存在するため、
それを用いて__次のバージョン番号のパッチを連鎖的に起動__する。
ちなみに指定した名前のコマンドやファイルが存在しない場合には何も行わないという仕様らしい。
ざっとこんな感じの流れです。
ちなみに__ネットワーク接続自体がない場合にはバージョン確認などを行わず、そのまま起動する__ようにします。
最後のパッチ適用のあたりをもっと効率化できるとほんとうはいいんですけどね。
例えば差分適用も裏で勝手にやってくれるとか。
そこまではちょっと力量不足とかいろいろあってやってません。
開発中はUnityのバージョンは固定しよう
ワークアラウンドみたいな話です。
開発中にUnityのバージョンを変えるのはお勧めしません。
というのも__Unityのプロジェクトコンバータにバグがあり、__
大きめのUnityプロジェクトをアップコンバートしようとすると
Invalid AABB aabb
という謎のエラーが出まくったり、IsFinite(outDistanceAlongView)
というエラーが出たり、
バグが修正されたパッチリリースをインストールしようとしたら
別のところでエンバグしていたとかいう実績があるからです。
詳しくは↓とか。
参考:
https://gamedev.stackexchange.com/questions/105921/invalid-aabb-aabb
http://cdecrement.blog.fc2.com/blog-entry-205.html
https://issuetracker.unity3d.com/issues/game-object-transform-dot-position-is-incorrect-after-object-dot-instantiate-after-awake-function-call
まあ上の現象自体はもう解消されている話なのかもしれませんが、
このような__ユーザーレベルでは解消不能の不具合に出くわす可能性がある__
ということは頭に留めておくべきかと思います。
__「新機能を追加することは、バグを埋め込むこと」__という格言(?)もあります。
安易に魅力的な新機能に釣られてホイホイバージョンを変更するのは、特に開発中真っただ中のプロジェクトにおいては大きなリスクがあることは意識しましょう。
__これは何もUnityに限った話ではなく、使用しているアセットやミドルウェア、ツールなどすべてにおいて言える__ことです。
ただし、テスト環境が潤沢にそろっており、__テストを全てパスすることで品質を担保できる__場合は上記の限りではありません。
また、モックアップの作成時や、~1か月程度で完成する規模のプロジェクトの場合、
__むしろ新しい機能を積極的に試していく__ことの方が重要だと考えます。
ちなみに
∀kashicforceの場合、Unity5.0.0f4を使用して作成していましたが、
途中でバージョンを上げようとして全然ダメだった記憶があります
(たしか5.1へのコンバートは成功して5.2へコンバートしたらダメだったような気がする)
StreamingAssets以下のファイルを暗号化せよ
Unityで外部リソースを用意したい場合、__StreamingAssetsディレクトリ__を用意して、
その中のファイルを参照するという手段を取ることがあります。
参考: https://docs.unity3d.com/jp/540/Manual/StreamingAssets.html
例えば、∀kashicforceではムービーファイルや内部設定データをStreamingAssetsに配置しています。
しかし、__StreamingAssetsディレクトリに配置したファイルは全く暗号化とか何もされない__ため、
ユーザーがデータを書き換え放題という問題があります。
最悪のケース、ゲームのシナリオデータをStreamingAssetsに平文で配置してしまったらもはや目も当てられません…
そこで、前もって__暗号化ツールでStreamingAssetsディレクトリ以下のデータを暗号化__しておき、
ゲーム側でデータを読み込む際に復号してから使用するというアプローチをとります。
手っ取り早いのは__AES暗号化方式__で暗号化してしまうことです。
詳しくは以下のブログの暗号化そのまんまでいいんじゃないでしょうか。
__初期化ベクトルだけ変えておく__のを忘れないようにNE!(ユーザーの復号防止のため)
まあ、一番いいのは__StreamingAssetsにそういう危険なデータを置かない__ことなんですが…。
実際、後からテキスト類を差し替えたい場合とかに結構重宝するので捨てがたいんですよね~
nullの判定にnull合体演算子(??)を使用してはいけない
ワークアラウンド話です。
UnityのUnityEngine.GameObject
をDestroy
すると1フレーム後に参照がnullになりますが、
こいつは__nullを自称しているnullではないObjectである__ことが知られています。
そして上記のObjectは__==演算子でnullと比較するとtrueが返りますが、??演算子での比較結果はtrueが返りません。__
(??演算子が適切にオーバーライドされてないらしい?)
?.演算子は試してません。
つまり、nullチェックのつもりでnull合体演算子を用いると思わぬところでバグったり、
意図しない挙動を起こす可能性があります。
例えば__nullチェックしてnullならば新しいGameObjectをInstantiate
する__というケースにnull合体演算子を用いる場合ですね。
いつまで経ってもnullにならないので、いつまで経ってもInstantiate
されません。
要注意です。
// やばい
cache_obj = instantiated_obj ?? GameObject.Instantiate(orig_obj);
// 冗長だがOK
if (instantiated_obj != null) {
cache_obj = instantiated_obj;
} else {
cache_obj = GameObject.Instantiate(orig_obj);
}
あと、∀kashicforceで引っかかったわけではないですが、
__System.Object
にキャストしたオブジェクトをnullチェック__する場合にも罠があるようです。
OnCollisionEnterが呼ばれない問題
よくある問題一覧:
-
Collision
とCollider
は違うんだよ! -
Collision2D
とCollider2D
とCollision
とCollider
は全部違うものなんだよ! - 関数ごとに必要な引数型が全部違うんだよ!
- 関数の引数型が違っててもUnityは警告を出してくれないんだよ!
-
OnTriggerEnter
を呼ぶときはIsTriggered
にチェックしてなくちゃいけないし
OnCollisionEnter
を呼ぶときはIsTriggered
は未チェックじゃなければいけないんだよ! - 3Dの衝突の時には
rigidbody
、2Dの衝突の時にはrigidbody2D
がアタッチされていないといけないんだよ! - 勿論関数名をタイポしてたりしても警告は出ないんだよ!
と、__めちゃくちゃたくさん罠がある__ので注意!
あとrigidbody
はCollider
より先にアタッチしとかないと動作しないとかそういうなんかがあったような…。
(よく思い出せない)
iTweenを1つのオブジェクトに対して複数同時に使うとなんか挙動がおかしくなるんだが?
ポーズ画面の実装
- DOTweenをUnityにインストールします
- ポーズしたい時に
Time.timeScale = 0f
にします -
Time.deltaTime
とかTime.fixedDeltaTime
などの値が0になります - 上記の値を使用して動き制御しているオブジェクトが停止します
- 逆に言うと
Time.deltaTime
に依存しないで移動するオブジェクトの動きを止めることはできません
ただ、大抵そういうのは__意図していない挙動であることが圧倒的に多い__ため、
意図したものでない限りTime.deltaTime
を掛けて制御しましょう - Unity上部「Tools→DOTween Utility Panel」を開きます。
ユーティリティパネル上部の「Preference」をクリックし、「TimeScale Independent」のチェック設定を有効にします。
- これで、DOTweenを使用した制御は
Time.timeScale
に依存しなくなります。
つまりポーズ画面で、__通常のキャラクターは動かないけど、
メニュー画面のUIとかカーソルだけDOTweenを使って動かす__ということが実現できます。
下のような感じ
「suspended」が表示されている間(ポーズ画面中)は、カーソルとUI以外は動いていません。
解除するとオブジェクトが動き出します。
当たり前ですがDOTweenを使って動かしてるオブジェクトは容赦なくポーズ中も動くので注意しましょう。
Time.timeScale = 0f;
の時のコルーチンの中断の挙動
Time.timeScale=0の時に、コルーチンの中断方法によって止まる止まらないの挙動が変わります。
-
yield return null
→ 止まらない -
WaitForSeconds
→止まる -
WaitForFixedUpdate
→止まる -
WaitForEndOfFrame
→止まらない
つまり、上の項のようなやり方でポーズを実装した場合、
ポーズ中にyield return null
とWaitForEndOfFrame
で動作制御しているオブジェクトは停止しません。
別途コルーチン内でTime.timeScale
の値を見て制御しましょう。
成果物をリリースビルドする前にUnityのキャッシュをクリアせよ
Unityは内部で素材キャッシュのような仕組みを持っているらしく、
__しかもそのキャッシュのデータがビルド時に優先的に使用されてしまう__という問題があります。
大抵は問題ないのですが、__エクスプローラから直にUnityプロジェクト内の素材ファイルなどの差し替えを行った__場合、
「プロジェクト内の素材自体は差し変わっているが
実際のゲーム画面、およびビルド成果物には差し替え前のファイルが使われ続ける」という現象が発生します。
つまり、意図しないファイルがビルド成果物に含まれることがあるということですね。
大問題です。
解決法ですが、割と単純ですべてのアセットを再インポートすると解決します。
ただし再インポートはめちゃめちゃ時間かかるので注意しましょう。
System.Text.EncodingでShift-JISを使用するとエラーになる
参考: System.Text.Encoding で Shift JIS を使いたい
ざっくり言うとUnity本体のインストールディレクトリの/Mono/lib/mono/2.0/
にあるI18*.*.DLL
を
Assetディレクトリにインポートすると解決します。
直接関係ないですが、リソースを内部でzip化しておいて、Ionic.Zip.dll
を使用して
ゲーム実行時に動的に解凍する場合にも同様の問題が発生します。
(この場合Encoding name 'IBM437' not supported.
とかいう一見意味不明のエラーが発生してめっちゃ焦る)
この場合も同じようにI18*.*.DLLを配置しましょう。
参考: Unity C# Zip化したアセットバンドル(ファイル)を解凍
ネイティブプラグイン
C#から利用できない、あるいは利用しづらい機能(例えば生ポインタなど、メモリをそのまま扱いたいケース)や、
既存のC/C++言語のライブラリをC#で利用したい場合は、__ネイティブプラグイン__の仕組みを利用します。
まあこれは実際は、ほとんどUnityとは関係ないC <--> C#間の一般的なマーシャリング(P/Invoke)の話ですが…。
∀kashicforceでは、ムービーのデコードにネイティブプラグインを使用しています。
TCP/IP通信のコア部分もネイティブプラグイン化していますが、こちらは結局未使用です…。
ちなみにムービプラグインの内部では__libvpxを使用したリアルタイムVP9デコード__を行っていますが、
∀kashicforce開発時点ではVP9をUnity上でデコードしてテクスチャにバインドするのは
ある意味世界初の技術だったと言えます(ドヤアアア
なお現在はどうなのか知らない。多分公式のMovieTextureとかがVP9サポートしてるやろ(ホジ
ネイティブプラグインとムービ再生の話は正直言ってかなり踏み込んだ話なので、ザラっと流しで要点だけ書きます。
雑な記事で書くには割と重めなので…。
詳しくはググるか私に聞いてください。というかどっか別の機会で踏み込んだ記事書きます多分
マーシャリング
以下のように、C#側とC/C++側で共有したい構造体/クラスについて
[StructLayout(LayoutKind.Sequential)]
で構造体のレイアウトをそろえる必要があります。
C#のクラスではメンバが宣言した順でメモリ的に連続に並ぶとは限りません。
(コンパイラ依存で並べ替えが発生する可能性がある)
構造体だとデフォルトでSequentialだった気がするのですが、
C#の場合構造体/クラスは値型と参照型になってるため使い分けが異なり、
今回はクラスを用いたいため明示的に指定しています。
[StructLayout(LayoutKind.Sequential)]
public class ESMoviePlugin_MovieInfo
{
public int width;
public int height;
public int stride;
public int now_frame;
public int databuf_size;
public float fps;
public DECODE_STATUS status;
};
[StructLayout(LayoutKind.Sequential)]
public class ESMoviePlugin_FrameBuffer
{
public IntPtr frame_y;
public IntPtr frame_u;
public IntPtr frame_v;
public int num_frame;
public bool is_used;
};
typedef struct ESMoviePlugin_MovieInfo
{
int width;
int height;
int stride;
int now_frame;
int databuf_size;
float fps;
DECODE_STATUS status;
};
typedef struct ESMoviePlugin_FrameBuffer
{
uint8_t* frame_y;
uint8_t* frame_u;
uint8_t* frame_v;
int num_frame;
bool is_used;
};
DECODE_STATUS
はenumなのであまり気にしないでください。
で、Marshal.StructurePtr()
でIntPtr
にしてネイティブ側に受け渡しします。
参考: https://msdn.microsoft.com/ja-jp/library/2zhzfk83(v=vs.110).aspx
decode_intptr = Marshal.AllocHGlobal(Marshal.SizeOf(decode_framebuf));
Marshal.StructureToPtr(decode_framebuf, decode_intptr, false);
Marshal.AllocHGlobal
だとコピーになるじゃん…という人は大人しくGCHandle
(下の方参照)使いましょう。
IntPtr
ですが、ネイティブ側では普通に構造体型のポインタ(ESMoviePlugin_MovieInfo*
)で受け取ってよいです。
もっといいやり方(dllexport
した公開関数にInAttribute/OutAttribute
をつける)があるけど、
どうせ受け渡すのは連続メモリ領域なんだからポインタで扱った方が早いよね!(えー
勿論ですが、C#側に公開しなくていい構造体についてはマーシャリングする必要性はありません。
生ポインタを取り扱う
ネイティブ側との生ポインタの扱いはIntPtr
経由で取り扱います。
C#側で確保したメモリをC/C++側でいじる場合、GCの対象から外したいため、以下のようにPinnedMemory
化します。
今回のケースではデコーダが読み書きするメモリ(つまりテクスチャの領域)ですね。
public ESMovie_DecodePtr(int size)
{
// YUVテクスチャのため注意
decode_buf = new byte[size];
// GCHandleでbyte配列のポインタを取得
decode_hn = GCHandle.Alloc(decode_buf, GCHandleType.Pinned);
decode_ptr = decode_hn.AddrOfPinnedObject();
}
AddrOfPinnedObject
で実際のアドレスがIntPtr
で取れます。
ちなみにToIntPtr
メソッドは用途が全然違い、GCHandle.Alloc
したハンドルのアドレスしか取得できません。
ムービ再生プラグインでは、マーシャリングしたESMoviePlugin_FrameBuffer
構造体のframe_y
,frame_u
,frame_v
メンバに
それぞれY,U,Vテクスチャとして使用するメモリを割り当てています。
もちろん使わなくなったらちゃんと破棄しましょうねー
public void DestroyPtr()
{
decode_buf = null;
decode_hn.Free();
decode_ptr = IntPtr.Zero;
}
デコードしたデータはTexture2D
のLoadRawTextureData
で読み込ませます。
その名の通りRawなバイト列を何も考えずに読み込めるので超便利です。
tex_Y.LoadRawTextureData(decode_Y.decode_buf);
tex_U.LoadRawTextureData(decode_U.decode_buf);
tex_V.LoadRawTextureData(decode_V.decode_buf);
tex_Y.Apply();
tex_U.Apply();
tex_V.Apply();
おわりに
他にもなんか色々書くことあるけど気力と時間が無くなりました(雑)
他に書いた方がよかったかもしれない話リスト
- Mercurial + TortoiseHGによるUnityプロジェクトのバージョン管理
- ADX2LEを利用したループ・フェードイン・フェードアウトなどの管理 / ネットワーク経由での効果音同期再生
- LoadScene時にアニメーションを使う
- 使ってるアセットについての話
- etcetc...
今回の∀kashicforceは色々知見が得られたので、雑メモじゃなくてちゃんとまとめたいところですね…。