C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について
- 2016-08-05
UnityにおいてList<T>のforeachは厳禁という定説から幾数年。しかしなんと現在Unityが取組中のコンパイラアップデートによって解決されるのだ!ついに!というわけで、実際どういう問題があって、どのように解決されるのかを詳しく見ていきます。
現状でのArrayのforeachとListのforeach
まずは現状確認。を、Unityのプロファイラで見てみます。以下の様なコードを書いて計測すると……。
var array = new int[] { 1, 2, 3, 4, 5 };
var list = new List<int> { 1, 2, 3, 4, 5 };
// ボタンを叩いて計測開始
button.OnClickAsObservable().Subscribe(_ =>
{
Profiler.BeginSample("GCAllocCheck:Array");
foreach (var item in array) { }
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:List");
foreach (var item in list) { }
Profiler.EndSample();
// プロファイラでそこ見たいのでサッと止める。
Observable.NextFrame(FrameCountType.EndOfFrame).Subscribe(__ =>
{
EditorApplication.isPaused = true;
});
});
Unityのプロファイラは使いやすくて便利。というのはともかく、なるほどListは40B消費している(注:Unity上でコンパイラした時のみの話で、普通のC#アプリなどでは0Bになります。詳しくは後述)。おうふ……。ともあれ、なぜListのforeachでは40Bの消費があるのか。ってところですよね。foreach、つまりGetEnumeratorのせいに違いない!というのは、半分合ってて半分間違ってます。つまり100%間違ってます。
GetEnumeratorとforeach
foreachはコンパイラによってGetEnumerator経由のコードに展開されます。
// このコードは
foreach(var item in list)
{
}
// こう展開される
using (var e = list.GetEnumerator())
{
while (e.MoveNext())
{
var item = e.Current;
}
}
GetEnumerator、つまり IEnumerator<T> はクラスなので、ヒープに突っ込まれてるに違いない。はい。いえ。だったらArrayだって突っ込まれてるはずじゃないですかー?
// こんなコードを動かしてみると
Profiler.BeginSample("GCAllocCheck:Array.GetEnumerator");
array.GetEnumerator();
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:List.GetEnumerator");
list.GetEnumerator();
Profiler.EndSample();
そう、むしろArrayは32B確保していてListはむしろ0なのだ。どっちも直感的には変てこ。
配列とforeachの最適化
配列をforeachで回すとコンパイラが、forループに展開します。
// このコードは
foreach (var item in array)
{
}
// こうなる
for (int i = 0; i < array.Length; i++)
{
var item = array[i];
}
ちなみに配列のループを回すときは明確にLengthを使うと良いです。というのも、配列の境界チェック(自動で入る)が実行時に消せます。
// こっちよりも
var len = array.Length;
for (int i = 0; i < len; i++)
{
var item = array[i];
}
// こっちのほうが速い
for (int i = 0; i < array.Length; i++)
{
var item = array[i];
}
詳しくはArray Bounds Check Elimination in the CLRをどうぞ。ようするに基本的には配列はforeachで回しておけばおk、indexを別途使う場合があるなら、Lengthで回すことを心がけるとベター。というところでしょうか。(もっというと配列の要素は構造体であると、更にベターなパフォーマンスになります。また、配列は色々特別なので、配列 vs Listで回す速度を比較すれば配列のほうがベタベターです)
List<T>のGetEnumeratorへの最適化
list.GetEnumeratorが0Bの理由は、ここにクラスライブラリ側で最適化が入っているからです。と、いうのも、List<T>.GetEnumeratorの戻り値はIEnumerator<T>ではなくて、List<T>.Enumeratorという構造体になっています。そう、特化して用意された素敵構造体なのでGCゴミ行きしないのだ。なので、これをわざとらしくtry-finallyを使ったコードで回してみると
Profiler.BeginSample("GCAllocCheck:HandConsumeEnumerator");
var e = list.GetEnumerator();
try
{
while (e.MoveNext())
{
var item = e.Current;
}
}
finally
{
e.Dispose();
}
Profiler.EndSample();
0Bです。そう、理屈的にはforeachでも問題ないはずなんですが……。ここでちゃんと正しくforeachで「展開された」後のコードを書いてみると
using (var e = list.GetEnumerator())
{
while (e.MoveNext())
{
var item = e.Current;
}
}
40B。なんとなくわかってきました!?
using展開のコンパイラバグ
「List<T>をforeachで回すとGCゴミが出るのはUnityのコンパイラが古いせいでバグッてるから」というのが良く知られている話ですが、より正しい理解に変えると、「構造体のIDisposableに対するusingの展開結果が最適化されていない(仕様に基づいていない)」ということになります。この辺の話はECMA-334 C# Language Specificationにも乗っているので、C#コンパイラの仕様に対するバグと言ってしまうのは全然良いのかな?
どういうことかというと、現状のUnityのコンパイラはこういうコードになります。
var e = list.GetEnumerator();
try
{
while (e.MoveNext())
{
var item = e.Current;
}
}
finally
{
var d = (IDisposable)e; // ここでBoxing
d.Dispose(); // 本来は直接 e.Dispose() というコードでなければならない
}
そう、全体的に良い感じなのに、最後の最後、Disposeする時にIDisposableにボックス化してしまうので、そこでGCゴミが発生するというのが結論です。そして、これは最新のmonoコンパイラなどでは直っています、というか2010年の時点で直ってます。どんだけ古いねん、Unityのコンパイラ……。
40Bの出処
ゴミ発生箇所は分かったけれど、せっかくなのでもう少し。サイズが40Bの根拠はなんなの?というところについて。まずは色々なもののサイズを見ていきましょうー。
// こんなのも用意した上で
struct EmptyStruct
{
}
struct RefStruct
{
public object o;
}
class BigClass
{
public long X;
public long Y;
public long Z;
}
---
// 色々チェックしてみる
Profiler.BeginSample("GCAllocCheck:object");
var _0 = new object();
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:class");
var _1 = new BigClass();
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:int");
var _2 = 99;
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:int.boxing");
object _3 = 99;
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:emptyStruct");
var _4 = new EmptyStruct();
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:emptyStruct.boxing");
object _5 = new EmptyStruct();
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:bool.boxing");
object _6 = true;
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:float.boxing");
object _7 = 0.1f;
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:double.boxing");
object _8 = 0.1;
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:refStruct.boxing");
object _9 = new RefStruct();
Profiler.EndSample();
なるほどなるほど。当たり前ですがstructのままのは0B。EmptyStructやboolなど最小1バイトのboxingは17B(ほえ?)、int(4バイト)が20Bでdouble(8バイト)や参照を一個持たせた(IntPtr - 64bit環境において8バイト)構造体が24B。classにlongを3つめたのが40B。そしてobjectが16B。つまり。つまり、最小が16Bで、そこからフィールドのそれぞれの要素のサイズが加算されるということです。
この16 bytesがどこから来ているかというと、オブジェクトのヘッダです。ああ、なるほどそういう……。
さて、これを踏まえてListのEnumeratorのフィールドを見てみると
public struct Enumerator : IEnumerator, IDisposable, IEnumerator<T>
{
private List<T> l;
private int next;
private int ver;
private T current;
ヘッダ16B + IntPtrの8B + intの4B + intの4B + Tがintの場合は4B = 36B。40じゃないじゃん、ってところは、32以降は8Bずつ埋まってくっぽ、実質33Bだと40B, 41Bだと48Bという感じ。といったところから40Bの消費になっていたということですね!
Experimental Scripting Previews
ついにコンパイラアップデートのPreviewがやってきた!Experimental Scripting Previewsにて、コンパイラのアップデートプロジェクトも始まっています。そして今のところ5.3.5p8-csharp-compiler-upgradeが配られています。
というわけで早速、冒頭の配列とListのforeachをかけてみると……
Profiler.BeginSample("GCAllocCheck:Array");
foreach (var item in array) { }
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck:List");
foreach (var item in list) { }
Profiler.EndSample();
やった!これで問題nothingですね!(実際は計測時は初回にListのほうに32B取られててあれれ?となったんですが、コンパイル後のIL見ても正常だし、まぁ二回以降叩いたのは↑画像の通りになったので、よしとしておこ……)
まとめ
で、現状はList<T>の列挙はどうすればいいのか、というと、まぁforでindexerでアクセスが安心の鉄板ではある。ForEachが内部配列に直接アクセスされるので速い説はなくはないですが、ForEachだとラムダ式のキャプチャに気を使わないと逆効果なので(詳しくはUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方)、基本的には普通にforがいいと思います(なお、キャプチャのないように気を使えば、ForEachのほうが速度を稼げる余地はあります。理論上、正常になったforeachよりも良い場合があるため)
理想的にはforeachであるべきだし、改革の時はまもなく!(5.5に↑のコンパイラアップグレードは入るっぽいですよ)。ちなみに、あくまでコンパイラのアップグレードなだけで、フレームワークのアップデートや言語バージョンのアップデートは今は含まれてはいない。段階的にやっていく話だと思うので、とりあえずはコンパイラがより良くなる、というだけでも良いと思ってます。というか全然良いです。素晴らしい。
UniRx 5.4.0 - Unity 5.4対応とまだまだ最適化
- 2016-08-03
UniRx 5.4.0をリリースしました!ちょうどUnity 5.4もリリースされたので、5.4向けの修正(Warning取り除いただけですが)を出せて良かった。というわけで5.4正式対応です。リリースは前回が5月だったので3ヶ月ぶりです。5.2 -> 5.3も3ヶ月だったので、今のとこ3ヶ月スパンになってますが偶然です。
何が変わったのかというと
Add: Observable.FrameInterval
Add: Observable.FrameTimeInterval
Add: Observable.BatchFrame
Add: Observable.Debug(under UniRx.Diagnostics namespace)
Add: ObservableParticleTrigger and OnParticleCollisionAsObservable, OnParticleTriggerAsObservabl(after Unity 5.4) extension methods
Add: UniRx.AsyncReactiveCommand
Add: ReactiveCommand.BindToOnClick, `IObservable<bool>.BindToButtonOnClick`
Add: UniRx.Toolkit.ObjectPool, AsyncObjectPool
Add: UniRx.AsyncMessageBroker, asynchronous variation of MessageBroker
Add: ObserveEveryValueChanged(IEqualityComparer) overload
Add: `Observable.FromCoroutine(Func<CancellationToken, IEnumerator>)` overload
Add: ObservableYieldInstruction.IsDone property
Add: IPresenter.ForceInitialize(object argument)
Improvement: Where().Select(), Select().Where() peformance was optimized that combine funcs at internal
Improvement: MicroCoroutine performance was optimized that prevent refresh spike
Improvement: Observable.Return performance was optimized that reduced memory cost
Improvement: Observable.Return(bool) was optimzied perofmrance that allocate zero memory
Improvement: Observable.ReturnUnit was optimzied perofmrance that allocate zero memory
Improvement: Observable.Empty was optimzied perofmrance that allocate zero memory
Improvement: Observable.Never was optimzied perofmrance that allocate zero memory
Improvement: Observable.DelayFrame performance was optimized
Improvement: UnityEqualityComparer.GetDefault peformance was optimized
Improvement: AddTo(gameObject) dispose when ObservableTrigger is not activated
Improvement: AddTo(gameObject/component) performance was optimized by use inner CompositeDisposable of ObservableDestroyTrigger
Improvement: `FromCoroutine<T>(Func<IObserver<T>, IEnumerator>)` stops coroutine when subscription was disposed
Improvement: ReactiveCollection, ReactiveDictionary implements dispose pattern
Fix: ToYieldInstruction throws exception on MoveNext when reThrowOnError and has error
Fix: ObserveEveryValueChanged publish value immediately(this is degraded from UniRx 5.3)
Fix: Prevent warning on Unity 5.4 at ObservableMonoBehaviour/TypedMonoBehaviour.OnLevelWasLoaded
Fix: Remove indexer.set of IReadOnlyReactiveDictionary
Breaking Changes: Does not guaranty MicroCoroutine action on same frame
Breaking Changes: UniRx.Diagnostics.LogEntry was changed from class to struct for performance improvement
相変わらずへっぽこな英語はおいといてもらえるとして、基本的にはパフォーマンス改善、です。
前回紹介したMicroCoroutineを改良して、配列をお掃除しながら走査する(かつ配列走査速度は極力最高速を維持する)ようになったので、より安定感もましたかな、と。その他メモリ確保しないで済みそうなものは徹底的に確保しないようになど、しつっこく性能改善に努めました。あと新規実装オペレータに関しては性能に対する執拗度がかなり上がっていて、今回でいうとBatchFrameはギチギチに最適化した実装です。既存オペレータも実装甘いものも残ってはいるので、見直せるものは見なおしてみたいですねえ。
また、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、PhotonWireではUniRxもクライアント側でかなり使っているので、その辺もちょっと話したいなと思っていますので、Photonに興味ある方もない方も是非是非。Photon固有の話も勿論しますが、普通にUnityとリアルタイム通信エンジンについての考えや、UniRx固有の話なども含めていきますので。
Debug
Debugという直球な名前のオペレータが追加されました。標準では有効化されていなくて、UniRx.Diagnostics
というマイナーな名前空間をusingするようで使えるようになります。実際どんな効果が得られるのかというと
using UniRx.Diagnostics;
---
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Normal").Subscribe();
subject.OnNext(1);
subject.OnNext(10);
subject.OnCompleted();
}
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
var subject = new Subject<int>();
var d = subject.Debug("DebugDump, Cancel").Subscribe();
subject.OnNext(1);
d.Dispose();
}
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
var subject = new Subject<int>();
subject.Debug("DebugDump, Error").Subscribe();
subject.OnNext(1);
subject.OnError(new Exception());
}
シーケンス内で検出可能なアクション(OnNext, OnError, OnCompleted, OnSubscribe, OnCancel)が全てコンソールに出力されます。よくあるのが、何か値が流れてこなくなったんだけど→どこかで誰かがDispose済み(OnCompleted)とか、OnCompletedが実は呼ばれてたとかが見えるようになります。
超絶ベンリな可視化!ってほどではないんですが、こんなものがあるだけでも、Rxで困ったときのデバッグの足しにはなるかなー、と。
BatchFrame
BatchFrameは特定タイミング後(例えばEndOfFrameまでコマンドまとめるとか)にまとめて発火するという、Buffer(Frame)のバリエーションみたいなものです。都度処理ではなくてまとめてから発火というのは、パフォーマンス的に有利になるケースが多いので、そのための仕組みです。Bufferでも代用できなくもなかったのですが、Bufferとは、タイマーの回るタイミングがBufferが空の時にスタートして、出力したら止まるというのが大きな違いですね。その挙動に合わせて最適化されています。
// BatchFrame特定タイミング後にまとめられて発火
// デフォルトは0フレーム, EndOfFrameのタイミング
var s1 = new Subject<Unit>();
var s2 = new Subject<Unit>();
Observable.Merge(s1, s2)
.BatchFrame()
.Subscribe(_ => Debug.Log(Time.frameCount));
Debug.Log("Before BatchFrame:" + Time.frameCount);
s1.OnNext(Unit.Default);
s2.OnNext(Unit.Default);
実装的には、まとめる&発火のTimerはコルーチンで待つようにしているのですが、今回はそのIEnumeratorを手実装して、適宜Resetかけて再利用することで、パイプライン構築後は一切の追加メモリ消費がない状態にしてます。
Optimize Combination
オペレータの組み合わせには、幾つかメジャーなものがあります。特に代表的なのはWhere().Select()でしょう。これはリスト内包表記などでも固有記法として存在するように、フィルタして射影。よくありすぎるパターンです。また、Where().Where()などのフィルタの連打やSelect().Select()などの射影の連打、そして射影してフィルタSelect().Where()などもよくみかけます(特にWhere(x => x != null)みたいなのは頻出すぎる!)。これらは、内部的に一つのオペレータとして最適化した合成が可能です。
// Select().Select()
onNext(selector1(selector2(x)));
// Where().Where()
if(predicate1(x) && predicate2(x))
{
onNext(x);
}
// Where().Select()
if(predicate(x))
{
onNext(selector(x));
}
// Select().Where()
var v = selector(x);
if(predicate(v))
{
onNext(v);
}
と、いうわけで、今回からそれらの結合を検出した場合に、内部的には自動的にデリゲートをまとめた一つのオペレータに変換して返すようになっています。
MessageBroker, AsyncMessageBroker
MessageBrokerはRxベースのインメモリPubSubです。AndroidでOttoからRxJavaへの移行ガイドのような記事があるように、PubSubをRxベースで作るのは珍しいことではなく、それのUniRx版となってます。
UniRxのMessageBrokerは「型」でグルーピングされて分配される仕組みにしています。
// こんな型があるとして
public class TestArgs
{
public int Value { get; set; }
}
---
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });
// AsyncMessageBroker is variation of MessageBroker, can await Publish call.
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
// show after 3 seconds.
return Observable.Timer(TimeSpan.FromSeconds(3))
.ForEachAsync(_ =>
{
UnityEngine.Debug.Log(x);
});
});
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
.Subscribe(_ =>
{
UnityEngine.Debug.Log("called all subscriber completed");
});
AsyncMessageBrokerはMessageBrokerの非同期のバリエーションで、Publish時に全てのSubscriberに届いて完了したことを待つことができます。例えばアニメーション発行をPublishで投げて、Subscribe側ではそれの完了を単一のObservableで返す、Publish側はObservableになっているので、全ての完了を待ってSubscribe可能。みたいな。文字だけだとちょっと分かりにくいですが、使ってみれば結構簡単です。
UniRx.Toolkit.ObjectPool/AsyncObjectPool
UniRx.Toolkit名前空間は、本体とはあんま関係ないけれど、Rx的にベンリな小物置き場という感じのイメージでたまに増やすかもしれません。こういうのはあまり本体に置くべき「ではない」とも思っているのですが、Rxの内部を考慮した最適化を施したコードを書くのはそこそこ難易度が高いので、実用的なサンプル、のような意味合いも込めて、名前空間を隔離したうえで用意していってもいいのかな、と思いました。
というわけで、最初の追加はObjectPoolです。ObjectPoolはどこまで機能を持たせ、どこまで汎用的で、どこまで特化させるべきかという範囲がかなり広くて、実装難易度が高いわけではないですが、好みのものに仕上げるのは難しいところです。なのでまぁプロジェクト毎に作りゃあいいじゃん、と思いつつもそれはそれで面倒だしねー、の微妙なラインなのでちょっと考えつくも入れてみました。
// こんなクラスがあるとして
public class Foobar : MonoBehaviour
{
public IObservable<Unit> ActionAsync()
{
// heavy, heavy, action...
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
}
}
// それ専用のPoolを<T>で作る
public class FoobarPool : ObjectPool<Foobar>
{
readonly Foobar prefab;
readonly Transform hierarchyParent;
public FoobarPool(Foobar prefab, Transform hierarchyParent)
{
this.prefab = prefab;
this.hierarchyParent = hierarchyParent;
}
// 基本的にはこれだけオーバーロード。
// 初回のインスタンス化の際の処理を書く(特定のtransformに下げたりとかその他色々あるでしょふ)
protected override Foobar CreateInstance()
{
var foobar = GameObject.Instantiate<Foobar>(prefab);
foobar.transform.SetParent(hierarchyParent);
return foobar;
}
// 他カスタマイズする際はOnBeforeRent, OnBeforeReturn, OnClearをオーバーロードすればおk
// デフォルトでは OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false) が実行されます
// protected override void OnBeforeRent(Foobar instance)
// protected override void OnBeforeReturn(Foobar instance)
// protected override void OnClear(Foobar instance)
}
public class Presenter : MonoBehaviour
{
FoobarPool pool = null;
public Foobar prefab;
public Button rentButton;
void Start()
{
pool = new FoobarPool(prefab, this.transform);
rentButton.OnClickAsObservable().Subscribe(_ =>
{
// プールから借りて
var foobar = pool.Rent();
foobar.ActionAsync().Subscribe(__ =>
{
// 終わったらマニュアルで返す
pool.Return(foobar);
});
});
}
}
基本的に手動で返しますし、貸し借りの型には何の手も入ってません!Rent後のトラッキングは一切されてなくて、手でReturnしろ、と。まあ、9割のシチュエーションでそんなんでいいと思うんですよね。賢くやろうとすると基底クラスがばら撒かれることになって、あまり良い兆候とは言えません。パフォーマンス的にも複雑性が増す分、どんどん下がっていきますし。
どこがRxなのかというと、PreloadAsyncというメソッドが用意されていて、事前にプールを広げておくことができます。フリーズを避けるために毎フレームx個ずつ、みたいな指定が可能になっているので、その完了がRxで待機可能ってとこがRxなとこです。
それと同期版の他に非同期版も用意されていて、それは CreateInstance/Rent が非同期になってます。
MessageBrokerと同じくAsyncとそうでないのが分かれているのは、Asyncに統一すべき「ではない」から。統一自体は可能で、というのも同期はObservable.Returnでラップすることで非同期と同じインターフェイスで扱えるから。そのこと自体はいいんですが、パフォーマンス上のペナルティと、そもそもの扱いづらさ(さすがにTのほうがIObservable[T]より遙かに扱いやすい!)を抱えます。
sync over asyncは、UniRx的にはバッドプラクティスになるかなあ。なので、同期版と非同期版とは、あえて分けて用意する。使い分ける。使う場合は極力同期に寄せる。ほうがいいんじゃないかな、というのが最近の見解です。
なお、Rent, Returnというメソッド名はdotnet/corefxのSystem.Buffersから取っています。
AsyncReactiveCommand
というわけでこちらもsync/asyncの別分けパターンで非同期版のReactiveCommandです。ReactiveCommandは何がベンリなのか分からないって話なのですが、実はこっちのAsyncReactiveCommandはかなりベンリです!
public class Presenter : MonoBehaviour
{
public UnityEngine.UI.Button button;
void Start()
{
var command = new AsyncReactiveCommand();
command.Subscribe(_ =>
{
// heavy, heavy, heavy method....
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
// after clicked, button shows disable for 3 seconds
command.BindTo(button);
// Note:shortcut extension, bind aync onclick directly
button.BindToOnClick(_ =>
{
return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
});
}
}
interactableの状態をコード実行中、というかつまりIO<T>
が返されるまでfalseにします。連打防止でThrottleFirstがよく使われますが、それをより正確にコントロールしたり、また、引数にIReactiveProperty[bool]を渡せて、それを複数のAsyncReactiveCommandで共有することで、特定のボタンを実行中は他のボタンも実行できない、のような実行可否のグルーピングが可能になります(例えばグローバルでUI用に一個持っておけば、ゲーム中でUIは単一の実行しか許可されない、的なことが可能になる)
PresenterBase再考
PresenterBase、Obsoleteはつけてないのですけれど、GitHub上のReadMeで非推奨の明言を入れました。賢い基底クラスは悪。なのです。POCO。それはUnityにおいても何事においても例外ではない。その原則からするとPresenterBaseは賢すぎたのでナシ of the Year。動きはする、動きはするんですが……。
Model-View-Presenterパターン自体の否定ではなくて(それ自体は機能するとは思っています、ただし関心がModelにばかり向きがちですが、Viewは何か、Presenterは何か、についてもきちんと向き合わないとPresenterが奇形化するかなー、というのは実感としてある。ViewであるものをPresenterとして表現してアレゲになる、とか)、PresenterBaseというフレームワークのミスかな、とは。です。
とりあえずいったん初期化順序が気になるシーンは手でInitializeメソッド立てて、それをAwake, Startの代わりにして、呼ばせる。いじょ。みたいな素朴な奴で十二分かなー、とオモッテマス。結局。メリットよりもデメリットのほうが大きすぎたかな。反省。
この辺りに関してはアイディアはあるので、形にするまで、むー、ちょっと味噌汁で顔洗って出直してきます。
まとめ
あんまり大きな機能追加はなく細々とした変化なんですが、着々と良くはなっているかな、と!
Rxに関してもバッドプラクティスを色々考えられるというか反省できる(おうふ……)ようになっては来たので、どっかでまとめておきたいですね。油断するとすぐリアクティブスパゲティ化するのはいくないところではある。強力なツールではあるんですが、やりすぎて自爆するというのは、どんなツールを使っても避けられないことではあるけれど、Rxがその傾向はかなり強くはある。
まぁ、sync over asyncはいくないです。ほんと(思うところいっぱいある)。
というわけかで繰り返しますが、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、よければそちらも是非是非です。
LightNode 2 - OWINからASP.NET Coreへの移植実例
- 2016-07-28
ASP.NET Core以前に.NET Coreをガン無視している昨今。というのも、.NET Coreというかようするところ最近の.NETは横、つまりクロスプラットフォームへの広がりなんですよね。それ自体は素晴らしく良いことではあるのですが、縦、つまり機能面での拡充があるのかどうかというと、あんまない気がしています。それは、クロスプラットフォームいうても基本的にはWindowsでしか現状/当分は使わないんだよなー、という私みたいな人間にとってはあまり興味を引かれるものではないのであった。
とかっていつまでも言ってるのもアレなので、とりあえずLightNode(という私の作ってるOwinで動くMicro REST Framework、ようはASP.NET Web APIみたいなやつ)をASP.NET Coreに移植してみました。アプリケーションの移植じゃなくてライブラリの移植なので、むしろ楽です。LightNodeはガチガチにOwinのみで構築していたので、ほとんど単純な置換のみでOKでした。
ASP.NET Coreで動作させるだけなら、OWIN - ASP.NET Coreのブリッジを使うという手もありますが、今回は完全にASP.NET Core向けに書き直しました。せっかくやるなら、ちゃんとしっかりしたものにしたいですしね、HttpContextのほうが望ましいのにIDictionaryなEnvironmentが露出してたりすると嫌じゃん。そんなわけでつまり、OWINに関連する部分は完全にASP.NET Core仕様に変わったので、互換性はありません。
ASP.NET Coreライブラリ開発の準備と移植手順
準備としてVisual Studio 2015 Update 3と.NET Core SDKを入れればOK。が、しかしいきなり.NET Core SDKがUpdate 3が入ってねーよエラーが出てインストールできなくて泣いた。世の中厳しい。Forumによるとそういう事例多数。対応としては「DotNetCore.1.0.0-VS2015Tools.Preview2.exe SKIP_VSU_CHECK=1」で叩けば入るよって話で、そうして叩くことによってようやく準備OK。幸先は悪い。
そうして入ったらテンプレートに.NET Core系があるので、とりあえずClass Library(.NET Core)を作る。参照してるのは .NETStandard Library 1.6.0。この辺良く分からないんですが、corefx/.NET Platform Standardによると.NET 4.6.3ぐらいに相当するそうで。ふーむ、まぁASP.NET Core系がnetcoreapp 1.0で1.6と同じところらしいので、このままでOKっぽい。気がする。とりあえず。UWPとかが視野の場合はちょっと話は違うのでしょうけれど。
次にASP.NET系のライブラリをNuGetで入れる。のですが、どの参照をいれればいいのかがまず分からない:) 今回はOwin的なMiddlewareを作りたかったんですが、ここはMicrosoft.AspNetCore.Http.Abstractionsが最適のようですね。これでようやくスタートライン。
既存のLightNodeのコードを突っ込むと当然激しくコンパイルエラーが出るのでここからチマチマと直していきました。まず目につくのがリフレクション関連で、IsEnumとかTypeに生えてる判別系のメソッドが片っ端から動いてません。誰しもが通る.NET Coreの洗礼!これは、type.GetTypeInfo() によるTypeInfoのほうにIsEnumなどなどが生えてるので、ひたすらGetTypeInfoを書き加えるだけの簡単なお仕事をします。GetTypeInfoの嫌なところはSystem.Reflection名前空間への拡張メソッドとして実装されてるので、IntelliSenseに出てこなくてイラッとする率が高いこと……。まぁ、あと実際にひたすらGetTypeInfoを書きまくるのは面倒くさいので、Typeへの拡張メソッドとして GetTypeInfo().IsEnum とかコンパイルエラー出てるものだけ定義してやることで作業量低減(まぁプロパティは()を書かなきゃいけないのでアレですけど。拡張プロパティはよ)
また、Parallel.ForEach がない!これは.NETStandardには含まれてないそうなので、別途System.Threading.Tasks.ParallelをNuGetから拾ってくる。なんかこう、標準に入ってて当たり前だろ、みたいに思うものが別添えになってるの、不思議な感覚ですね。これだともはやReactive Extensionsが標準にないとかImmutable Collecitonsが標準にないとか、どうでもというか全く大したことない話に見えます。なんせParallel.ForEachすらないんだから!(ところでNuGetのVersion History見ると結構細かくアップデートされてはいるんですが、いったいなにが変わったのかRelease Note出して欲しくはある……)
AppDomain.CurrentDomain.GetAssemblies もない!対象アセンブリ内からControllerがわりのクラスを引っ張ってきたくて、読み込み済みのAssemblyからGetTypesして全部検査したい、というのをやりたいわけですが、ないんですねえ。そして実際、これの代替は今のところないらしい……(というかAppDomainが今のところない)。フレームワーク系の常套手段なのに……。Loaderがどうのこうのとか、あとASP.NET Core側で特化した何かはありそうな気配を感じなくもなかったんですが、今回はGetAssembliesじゃなくても回避可能なので(一手間ではあるんですが、外側からその対象AssemblyのTypeを渡してさえくれればAssembly拾えてGetTypesできる)、Typeを渡してもらう方式のみに制限することでとりあえず回避しました。
ここから先はASP.NET Core的なところ。 IDictionary[string, object]
を HttpContext
に変える。そしてAppFunc
をRequestDelegate
に変える。だけの簡単なお仕事。OWINとASP.NET Coreの差異はそれだけだし中身一緒なので、機械的に置き換えていくだけ。OWINに関してはどうだのこうだのと一悶着あったりなかったりで色々ありましたが、一番下のレイヤーで触ってる限りは、概念はほんと完全に一緒なので無駄なことは全くなかったですね。上のレイヤーで触っていても、それはそれで何も考えず置き換えられるはずなので、実際のとこOWINは良かったと思ってます。新しい、ASP.NET CoreのHttpContextは、昔のHttpContextというよりかは、OWINのEnvironmentそのものだったりしますからね。
これでコンパイル通ったので、実行確認のためASP.NET Core Web Application(.NET Core)テンプレートから新規プロジェクトを作成。ASP.NET Core的なテンプレートはもちろんEmptyで。Startupに以下のを書いて
using LightNode;
using LightNode.Server;
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseLightNode(typeof(Startup));
}
}
public class Toriaezu : LightNodeContract
{
public string Echo(string x)
{
return x;
}
}
http://localhost:15944/Toriaezu/Echo?x=hoge
にアクセスでhogeが出力される。おー、ちゃんとできてますね!当たり前っちゃあ当たり前でしょうけれど、あまりに意外にすんなり動いたので普通に感動した。いやあ、いいじゃんASP.NET Core。
Swagger Included
LightNode 1の時はSwaggerは別添えだったんですが、今回はとりあえず一緒に突っ込んじゃいました。JSON.NET使ったJSON出力とかも同梱です(というかデフォルトがそれになってます)。まぁSwaggerに関してはDependencyが増えるわけでもないしいいじゃんといえばいいじゃん、なので。いいかな、と。LightNodeのSwagger統合はビュー(HTMLとかCSSとか画像とか)がDLLに埋め込んでやってたんですが、そうしたリソースを.NET Coreで埋め込むにはどうすればいいのか。今まではPropertiesで埋めてったんですが、.NET Coreではproject.jsonに書くのが正解のようですねー。
"buildOptions": {
"embed": [
"Swagger/SwaggerUI/**"
]
}
buildOptions.embedで指定できるようで、ああ、なるほど、これはこれで知ってれば凄く楽なので、全然良いですね。良いです。いいじゃん.NET Core。
というわけでサクッとSwagger統合も果たせた。
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.Map("/api", builder =>
{
builder.UseLightNode(typeof(Startup));
});
app.Map("/swagger", builder =>
{
var xmlName = "AspNetCoreSample.xml";
var xmlPath = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), xmlName);
builder.UseLightNodeSwagger(new LightNode.Swagger.SwaggerOptions("AspNetCoreSample", "/api")
{
XmlDocumentPath = xmlPath,
IsEmitEnumAsString = true
});
});
}
}
うーん、全然いけますね、じゃあ次行こう、次。
Glimpse not Included
LightNodeのウリはSwagger統合とGlimpse統合、特にGlimpseへの診断情報表示は力を入れていて(Glimpseへのハックも含めて)他にここまでやってるフレームワークはないほどでした。ので、当然ASP.NET Coreでもやりたいわけですが、んー、そもそもGlimpseがまだASP.NET Coreに本対応してない……。2.0 Betaで一応対応してるということで、あるだけマシか?と思いきや、かなり古いもので全然動かない。というかGitHubでの開発も(1.x系も2.x系も)なんかもうほとんど動いてない……。メイン開発者2名がMicrosoftに転職ということで、ASP.NET Core対応含めてよりアクティブになるのかなー、とか思ってたら、まさかの大失速……。多分、Microsoft内では別のことやっていて、そっちが忙しくて以前よりもなお作業できなくなってるんでしょうね。しかし、うーん、残念だなあ。
ASP.NET Coreに移れない/移りたくない理由があるとしたら、このGlimpseが全然対応してないってことでしょうかねえ。Glimpse自体はほんと素晴らしいので、なんとか再生してくれればいいのですけれど。
感想
.NET CoreにせよASP.NET Coreにせよ、結構コマンド操作がフィーチャーされてて、ゆとりな私には辛いものがあるんですが、さすがに1.0、普通に書いてる限りは、Visual Studio使ってる限りは特にコマンドの必要性もなく、それなりに快適に書けますね。安定してねえー、とか不満に思うことも全然ないので、もう普通に良さそう。いや、思ってたよりも全然いい感じだった。
さて、じゃあASP.NET CoreでもLightNode使おうぜ!になるかというと、うーん、とりあえずまずは普通にASP.NET Core MVCでいいでしょう(笑)。時代がねー、ちょっと違いますからね。LightNodeも3年前ですから。まぁ、でも設計思想とか全然古くなってないというかむしろASP.NET Core MVCがようやく追いついてきたかな、ぐらいの勢いだとは思ってます!例えばASP.NET Core MVCのFilterは完全にLightNodeのフィルターと一緒ですからね。
// ASP.NET Core MVC
public class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// do something before the action executes
await next();
// do something after the action executes
}
}
// LightNode
public class SampleFilterAttribute : LightNodeFilterAttribute
{
public override async Task Invoke(OperationContext operationContext, Func<Task> next)
{
// do something before the action executes
await next();
// do something after the action executes
}
}
Filters vs. Middlewareの話なんかも、それは3年前に全部考えきって実装し実践してますから(ホジホジ。という話なんで、まぁ全然LightNode 2もいいじゃないでしょうか。LightNodeは他に、密接に統合されたクライアント自動生成などもありますしね。かわりにMVC + Web API的な、Razorのビューを返すコントローラーとWeb APIコントローラーとの統合、みたいなのができてないのは痛み。ここ馴染ませられないのは普通に不便だということを最近良く感じてるので、ASP.NET Core MVCいいですね。いいですね。
OWINベースで書いたものの移行はそこそこすんなり行けるだろうなあ、という感触はなんとなく掴めた気がします。逆に、やっぱASP.NET MVC 5あたりからの移行は厳しそう。厳しいでしょう。どうするんでしょうね、どうしようかな、参りましたね……。
ともあれせっかくの新しい.NETの幕開けなので、もう少しポジティブに情報掴んで行こうかなー、という気にはなれたのでめでたしめでたし。
BigQueryを中心としたヴァルハラゲートのログ分析システム
- 2016-07-16
というタイトルでGoogle for Mobile | Game Bootcampで発表しました。4月なので3ヶ月遅れでスライド公開です。
なんかあまり上手く話せなかったな、という後悔がなんかかなり残ってます:) スライドもフォント細くて吹き出しの文字が見辛いな!とりあえず、WindowsでBigQueryなシステムとしては一つの参考例にはなるのではないかなー、と思います。第一部完。
第二部はEtwStreamへの移行と、BigQuerySinkのOSS公開かなー、というところなんですがまだまだまだまだまだ先っぽいのでアレでコレでどうして。できれば誰もが秒速でASP.NETアプリケーションのログをBigQueryに流し込める、みたいな状況にしたいのですけれどねえ、そこはまだまだ遠いかなー、ですね。そのへんの.NETのエコシステムは弱いと言わざるをえない。けれどまぁ、地道に補完していきたいと思ってます。
Japan VR Hackathonに参加し、AMD賞受賞しました
- 2016-06-29
Japan VR Hackathonに参加してきました!の結果が昨日発表されまして、AMD賞(Best Graphics)を受賞しました。やったー。事前に決めた5人チームでの参加で、大賞取る!という気概でやってたので、入選できて良かったです。
今回作ったのは「Clash of Oni Online」というゲームで、HTC Vive用のVRゲームです。テーマである"日本らしさ"を(一応)イメージした(一応)和風の装い。
飛んで来る岩を
吹っ飛ばす
という、VRバッティングセンター。ViveはVRというだけじゃなくてコントローラーがあるのがいいですねー。
今回、2日間 31時間で242コミット(最初のコミットが2016/06/18 09:13で 最後のコミットが 2016/06/19 16:34)。時間制限のなかでは、ちゃんとゲームしてる(ゲーム性的にもとりあえず爽快に全部打ち倒すパターンと一球一球を狙い撃ちしないパターンを用意)し、グラフィックもまぁ見栄えがするレベルで、オンライン協力プレイも実装(ただしデモ時はオフラインモード)したのは結構頑張った。ハッカソン系参加が全員初めての割には綺麗に収められた感あります。
チーム編成と最終的な役割は
- 私(プログラマ):プロジェクトセットアップ、敵ボス挙動、サーバーセットアップ、エフェクト発注、雑進行管理、プレゼンスライド作成、動画撮影
- プログラマ:マルチプレイプログラム、シーン管理プログラム
- プログラマ:Viveプログラム、エフェクト組み込み
- プログラマ:企画、地形エディット、サウンド、アセット検索、デモ
- テクニカルアーティスト:アセット検索、敵モーション作成、ライティング、エフェクト
という感じでした。全員ほぼViveのプログラミング経験はなし(SDK入れて雰囲気掴んだことはあります程度)
最初に入れたアセットは
- UniRx - ないと無理
- LINQ to GameObject - ベンリ、だけど今回は別にあまり使わず
- PhotonWire - マルチプレイ前提なので。合わせてサーバー側プロジェクトもセットアップ
- SteamVR Plugin - Viveがターゲットなので
- The Lab Renderer - 使ったことなかったので結局あんま余裕なく使えなかった...
という編成から随時アセット追加追加。
タイムライン
1週間ぐらい前に参加を決める。3日前ぐらいに「2人マルチ協力プレイ」「背中合わせにして立ちまわる(時代劇にある格好いい感じのアレ)雑魚戦 + デカい鬼を撃退するボス戦というアクションゲーム」「グラフィックで魅せる」を軸にする。というのを決定。和風です、和風。コミュニケーション手段としてSlack(チャット)を前日に立てる。Unityのバージョンを5.4.0b21に決めて全員にインストールしておいてもらうように。持ち込み物としてHTC Viveを2台用意。当日、会場ついてからGitHub(リポジトリ管理)のPrivateリポジトリを立てて全員招待。
雑魚戦+ボス戦といっても、作りきれるか怪しいので(実際ボス戦で手一杯だった、そりゃそうだ)、ボス戦から先に作っていくように。最初に決めていた役割分担は
- 私(プログラマ):マルチプレイ
- プログラマ:ボスプログラム、プレイヤー行動プログラム
- プログラマ:ボスプログラム、プレイヤー行動プログラム
- プログラマ:地形アセット購入/組み込み・サウンド購入/組み込み、パーセプションニューロン触る(使えそうなら使う)
- テクニカルアーティスト:モデルやらモーションやらエフェクトやら
でした。まぁマルチプレイといってもUnity側のプログラムがある程度できないとやることもないんで、まずは自分で持ってきたViveを自前PCのSurface Bookに繋ごうとしたらSurface Bookの出力をViveがうまく認識してくれなくて最終的に諦め(Forumとか見た限りだとノートPCの出力端子とのトラブル事案は結構多い模様なのでshoganaiね)。ということでViveのプログラミングは他の人に完全に任せることにして、ボスのプログラムを作っていこうかなー、ということに。ボス戦は、崖のような場所にボスが立っている(下半身は見せない)というイメージが共有されたので
迫り来るシリンダー撃退ゲーとして作成。雑に作ったこのシーンは、初日ずっとフル活用されることになったのだった。動画系は常時GifCamで撮ってSlackにあげてました。イメージが瞬時に共有されますし、良い内容だとテンションも上がりますし。
この時点でLeanTweenを追加。マルチプレイできるようにするので、非確定要素をいれないように、というのとそんな凝ることもないしなので弾は全部トゥイーンで制御しようかと。トゥイーンライブラリは色々ありますが、今のとこ私が選ぶならDOtweenかLeanTweenかなぁ。普通だったらDOtween選びます。ただ、今回はLeanTweenにしました、ちょっと慣れておこうかな、と思って。LeanTweenは複数のTweeenの制御とかの補助が入ってないんですが、その辺はUniRxで制御させたので全く問題なし。基本的に完了などのイベントをObservable化すれば既存トゥイーンライブラリとUniRxの統合は容易ですし、かなりいい具合にコントロールできます。実際今回は色々な挙動をそれで組みました。ところで、今ひっそりとUniRx前提のハイパフォーマンスでリアクティブなトゥイーンライブラリを作っているので、それが完成したら基本的にそれしか使いません:) まぁ、というのもあって色々なトゥイーンライブラリを試しているというのもあります。
このシーンをプレイヤー行動プログラム側に渡して、VIVEで弾き返したり防いだりを作ってもらう感じに。パーセプションニューロン触るマンはパーセプションニューロンを触りつつサウンド探しを、テクニカルアーティストはボスのモデル(これ自体は買ったもの)のUnityへのインポートとテクスチャ調整とモーション付けを、私は弾のバリエーションを作ってました。
豪速球を投げ込むバッティングセンター的イメージ。手前で弾が伸びてくるので振りにくい。二者択一(手前のキューブはプレイヤーAとプレイヤーBです、マルチ協力プレイだから!)でどっちに来るか分からないので、なんとなく緊迫感あってゲーム的でもあるよね、ということで最終的なボス行動にも採用。
ぶわーっと全方位に出すのが欲しい(VIVEのデモゲームのThe LabにあるXortexという360度シューティングのボス弾のような)というオーダーを受けて。中々いい感じに派手なので、これも採用。
あとはボツ案的な弾を作ったり、その他、この時点ではまだ夢膨らみんぐで、ボスの行動も腕をばちーんと振り下ろしてきてそれを斬撃の連打で防いで弾き返す(協力プレイなので、片方が防いでたらそれに加勢しないとダメ、とか)、などを想定したコードを準備したりAIは少しリッチにしようかな、とBehaviorTreeのライブラリ書いてたり(ノードエディタなしの基本的なランタイムだけ)、プレイヤープログラム作るチームは盾で防御する処理(最終的に削られたけれど最初は刀と盾の装備のつもりだった)をやってたら、夜0時。うーむ、時間が過ぎるのは早い。
この辺でさすがに未だにシリンダーとキューブが相手で完成形が全く見えてないのはヤヴァいでしょうということで、シーン統合しましょう祭り。特にテストで大量のアセットを抱えていたマップ作るマンがGitHubに中々Pushできないなどなどトラぶりつつも、2時ぐらいにようやく一段落。マップにモーション付きボスモデル配置して、とりあえず弾を出るようにして、でこんな具合に。
色々アレですが、しかし中々格好よくてテンションあがりますね!その他プレイヤーのほうも入れこんだりなんなりで床に転がって仮眠とって、朝。キューブにも岩を当てはめてついに出来上がったのは……!
んー、悪くない。悪くないんだけど、和ではない。雲南省(適当なイメージ)とかそういう中国の高山っぽい気すらする。ここで実際完成させる仕様を概ね確定。
- マップは明確に和テイストが出るものにリテイク
- ボスは殴り攻撃などなし、弾のみ
- 盾はなし、弾を打ち返してボスにダメージ与えて、一定回数食らわせたらクリア
- マルチプレイは諦めないので作業は並走、ただし最終的にはシングルプレイが完全にプレイできるの優先
私は、ボスのモーションが二種あって、殴りつけてくるつもりでつけてもらったモーションはボスが弾を投げ飛ばしてくる(つもり)な雰囲気に適当に調整(タイミングは適当にdelayかけて目視で合わせただけでジャストとは程遠いんですが、まぁなんとなくそれっぽく見えなくもないのでヨシとした)したり、もう一個の大技っぽくやってくるモーションは、なんか岩を抱え込んでる感じにできそうな気がしたので、適当にそれっぽく位置合わせして破裂させてみることに。
うん、それっぽい。地面にめり込んでってるのとかも、まぁ全然気にならないし。このボス行動は今回のハッカソンで私的な私が作った中では一番よくやりましたしょうでした。全部、偶然素材が揃っただけなんですがうまく噛み合ったということで。
この後は、マップを和テイストに差し替えて常時マップブラッシュアップ、ゲームの要素が確定したので、各種のヒットエフェクトを作ってもらって当て込みや効果音、プレイ感向上のための弾の動きなどの調整、そして諦めてないマルチプレイなどなどを時間ギリギリまで使ってなんとか完成……!(実際、最後の30分でボスのダメージエフェクトがつき、最後の15分前でボスが死ぬようになった程度にギリギリだった)
マルチプレイに関しては、Viveのセンサーが干渉してうまく二台プレイの調整ができなかったのと、もう一台のデスクトップPCを会場の無線LANに繋げなくて、というネットワーク的な問題で断念。いちおう、プログラム側はマルチ想定で動作するように最後まで組んでました、いやほんと。サーバー側、AzureのVMも一時的なものなのでということで、かなりマシンパワーの強いものに変更したりしたんですけどね、というわけでここをお見せできなかったのは残念。なので、最後の5分でマルチプレイ用のログイン待機処理を消して、リリースビルド完成。お疲れ様ー。
完成形
出来上がったものは、マップリテイクによって城が追加されたことにより「城下まで迫ってきた赤鬼を、手に持つサムライブレードにより撃退し、城を守る」という設定に。なっていた。完全後付けで。ゲーム名は特に何も考えもなく直前で「Clash of Oni Online」に大決定。
ハッカソンでの評価は特に会場でのプレゼンはなく、体験してもらって審査員が表を付けてく形式とのことで、あとはデモマンがいい感じに来場者に説明してるのを横目に私は会場を見学する:)最後の一秒までドタバタと調整を続けていた割には、目立ったバグもなくスムースにプレイできてて良かった良かった。
最終的には当日の審査ではなく後日の審査ということなので、プレゼン資料を作ったり動画を撮ったりして
結果待ち……!そして発表……!受賞……!やったね!
反省点とか
当初の想定よりもViveのルームスケールを活かしてない、直立不動のスタイルになったのは、ちょっと想定外。弾を避けたりとか、近寄ったりとかもうちょっとだけアクティブなのをイメージしていたので、しょうがないといえばしょうがないのだけれど、次に何か作るのだったら動くタイプのを作りたいですねぇ。
効果音が足りなかったり、割れてたり。効果音足りないのは、岩の音を、ボス撃破音とか足すべき箇所はいっぱいですよねー。マップリテイクで時間が取れなかったのがその辺の敗因か。Viveコントローラーと刀の位置が微妙にあってなかったり、足が地面に設置してなかったりといった、プレイヤーに対する調整も甘め。shoganai。この辺はViveプログラミングにて慣れてれば、スッと合わせられる話だと思うので、経験値を積もう。ボスの全方位弾が実は全方位じゃなくて左に寄ってるのは普通にロジックのバグ……。リテイク後のマップのクオリティが急ぎで用意しただけあってリテイク前に比べると低い(雑に光源足すためだけの灯籠を並べるとかしたかった)、ボスを遠方に置く形になってしまったのでスケール感が出なかった、など。
とか、まぁアラはいくらでも見つかりますが、基本的にはよくやったと思ってる……!よ!チームメンバーが全員、より良くするために自分の仕事を探して作りきっていったというのは純粋なハッカソンの楽しさという感じで、疲労困憊だけど達成感はありますね。
それとViveでのプログラミングは、結構ゲーム作成入門(Unity入門)にいいかもですね。3Dのプレイヤーの操作ってモーションつけたり色々ハードル高いですが、Viveならすぐに手の動きがキャプチャされて自由に動かせるアクションが作れるので、よくあるシューティングとかブロック崩しとか作っていくよりも楽しいんじゃないかな。(今のVR経験値が少ない現状なら)VRで空間を見て、Viveコントローラーで自由に操作できるというのは、それだけで楽しい体験を作れちゃいますしね。
そんなわけで、家でもViveを設置したしGeforce GTX 1080搭載PCも買ったので、ちょいちょいとVive用に何か作っていきたいという気持ちを強くしました。ので、ちょいちょいと出していければいいですねー。
ObserveEveryValueChanged - 全てをRx化する拡張メソッド
- 2016-06-07
ブードゥーの秘術により、INotifyPropertyChanged不要で、値の変更を検知し、IObservable化します。例えばINotifyPropertyChangedじゃないところから、WidthとHeightを引き出してみます。
using Reactive.Bindings.Extensions;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.ObserveEveryValueChanged(x => x.Width).Subscribe(x => WidthText.Text = x.ToString());
this.ObserveEveryValueChanged(x => x.Height).Subscribe(x => HeightText.Text = x.ToString());
}
}
なるほど的確に追随している。ソースコードはGitHub上に公開しました。
ReactivePropertyと組み合わせることで、そのままバインダブルに変換することも可能です。
public class MyClass
{
public int MyProperty { get; set; }
}
public partial class MainWindow : Window
{
MyClass model;
public IReadOnlyReactiveProperty<int> MyClassMyProperty { get; }
public MainWindow()
{
InitializeComponent();
model = new MyClass();
this.MyClassMyProperty = model.ObserveEveryValueChanged(x => x.MyProperty).ToReadOnlyReactiveProperty();
}
}
ついでにokazukiさんが、ReactiveProperty v2.7.3に組み込んでくれましたので(今のところ).NET版では是非是非に使えます。UWP用とかXamarin用とかもきっとやってくれるでしょう(他人任せ)
仕組み
CompositionTarget.Renderingに引っ掛けて、つまり毎フレーム監視を走らせています。もともとUniRxのために作った機構を、そのままWPFに持ってきました。CompositionTarget.Renderingは、アニメーション描画などでも叩かれている比較的低下層のイベントで、これより遅いと遅れを人間が検知できちゃうので影響が出るし、これより早くても視認できないので意味がない。という、ぐらいの層です。こういった用途ではベストなところ。
毎フレーム監視がありかなしか。ゲームエンジンだと、そもそもほとんどが毎フレームごとの処理になっているので違和感も罪悪感もないのですけれど、全てがイベントドリブンで構築されている世界にそれはどうなのか。もちろん、原則はNoです。素直にINotifyPropertyChangedを書くべきだし、素直にReactivePropertyを書くべきでしょう。
ただ、アニメーションでも使われるしデバイスのインプット(LeapMotionとか)もその辺に引っ掛けるようなので、ここにちょっとプロパティに変更があるかないかのチェック入れるぐらい別にいいじゃん(どうせCPU有り余ってるんだし)、みたいな開き直りはあります。かなり。割と。
ObserveEveryValueChangedは、毎フレーム回っているような低下層の世界から、イベントドリブン(リアクティブ)な世界に引き上げるためのブリッジとしての役割があります。そう思うと不思議と、よく見えてきませんか?ただ「毎フレームポーリングかよ、ぷぷw」とかって一笑するだけだと視野が狭く、もう少しだけ一歩踏み込んで考えてみると思考実験的に面白い。私はコード片に意思を詰め込んでいくのが好きですね。哲学といってもいいし、ポエムでもある。そこには幾重も意味が込められています。
PhotonWire - Photon Server + Unityによる型付き非同期RPCフレームワーク
- 2016-05-31
というのを作りました。Unityでネットワークマルチプレイヤーゲーム作るためのフレームワーク。といっても、100%自作じゃなくて、基本的にPhoton Serverというミドルウェアの上に乗っかるちょっと高級なレイヤーぐらいの位置づけです。去年の9月ぐらいに作った/作ってるよ、というのは発表していたのですが、それからかれこれ半年以上もpublicにしてないベイパーウェアだったのですが(グラニ社内では使ってました)、重たい腰を上げてやっと、やっと……、公開。
謳い文句は型付き非同期RPCフレームワークで、サーバー側はC#でasync/awaitを使ったメソッド実装、Unity側はそこから生成されたUniRx利用のメソッドを呼ぶだけで相互に通信できます。それでなにができるかというと、Visual StudioでUnity -> Server -> Unityと完全に一体化してデバッグできます。全部C#で。もうこれだけで最強でしょう。他は比較にならない。勝った。終わった。以上第一部完。
真面目に特徴、強みを上げると、以下のような感じです。
- 完全タイプセーフ。サーバー-サーバー間は動的プロキシ、クライアント-サーバー間は事前クライアント生成。
- IDLレス。C#サーバーコードのバイナリを元にして、クライアントコードを生成するので、普通にサーバーコードを書くだけ。面倒なIDLはゴミ箱ぽい。
- 高性能。サーバーはasync/awaitで、クライアントはUniRxにより完全非同期で駆動。特にサーバーのC#コードはIL直書きも厭わずギチギチに最適化済み。
- 事前生成シリアライザによるMsgPackでのシリアライズ/デシリアライズ。デシリアライズは更にマルチスレッド上で処理してUniRxでメインスレッドにディスパッチするのでフレームレートに一切影響を与えない。
- Visual Studioとの完全な統合。高いデバッガビリティと、Analyzer利用を前提にしたフレームワーク構成はVS2015時代の新しい地平線。
- 外部ツール「PhotonWire.HubInvoker」により外からAPIを直接叩ける。
HubInvokerは私にしては珍しく、ちゃんと見た目にこだわりました。これの外観の作り方はMaterial Design In XAML Toolkitでお手軽にWPFアプリを美しくで記事にしてます。
Photon Serverを選ぶ理由
Unityでもネットワーク系は色々な選択肢があると思います。
- UNET
- PUN + Photon Cloud
- Photon Server(SDK直叩き)
- モノビットエンジン
- WebSocketで自作
- MQTTで自作
このあたりは見たことある気がします。そのうちUNETは標準大正義だしAPIもProfilerも充実してる感なのですが、uNet Weaver Errorがムカつくので(コンパイルができなくなるという絶望!特にUniRx使ってると遭遇率が飛躍的に上昇!)、それが直らないかぎりは一ミリも使う気になれない。というのと、サーバーロジックを入れ込みたいどうしてもとにかくむしろそれがマスト、な状況の時にというか割とすぐにそうなると思ってるんですが、Unity純正だと、逆にUnityから出れないのが辛いかな、というのはありますね(ロードマップ的にはその辺もやるとかやらないとかあった気がしますが、まぁ遠い未来ということで)。Unity外で弄れるというのは、サーバーロジックだけじゃなくHubInvokerのようなツールを作れるっていうのも良いところですね。大事。なので、標準大正義は正しくも選べないのです。
モノビットはよく知らないので。C++でサーバーロジックは書きたくないなあ、今はC#も行けるんですっけ?
自作系は、あんまりそのレイヤーの面倒は見たくないので極力避けたい。別に動くの作るのはすぐでも、まともにちゃんと動き続けるの作るのは大変なのは分かりきってる話で。トラブルシュートも泣いちゃう。そこに骨を埋める気はない。あと、自作にするにしてもプロトコルの根底の部分で安定してるライブラリがあるかないかも大事で(そこまで自作は本当に嫌!)、Unityだとただでさえそんなに選択肢のないものが更に狭まるので、結構厳しい気がするのよね。実際。
Photonといって、Photon Cloudの話をしているのかPUN(Photon Unity Network)の話をしているのか、Photon Serverの話をしているのか。どれも違く、はないけれど性質は違うのだから一緒くたに言われてもよくわからない。さて、PUN。PhotonのUnityクライアントは生SDKが低レイヤ、その上に構築されたPUNが高レイヤのような位置づけっぽい感じですが、PUNは個人的にはないですね。秒速でないと思った。PUNの問題点は、標準のUnity Networkに似せたAPIが恐ろしく使いづらいこと。標準のUnity Network自体が別に良いものでもなんでもないレガシー(ついでにUnity自体も新APIであるUNETに移行する)なので、それに似てて嬉しい事なんて、実際のとこ全くないじゃん!もうこの時点でやる気はないんですが、更にPhoton Serverで独自ロジック書いたらそこははみ出すので生SDK触るしかないのだ、なんだ、じゃあいらないじゃん?Client-Client RPCも別になくてもいいし、というかなくていいし。
Photon Server。C++のコアエンジンってのは言ってみればASP.NETにおけるIISみたいなもので、開発者は触るところじゃない、直接触るのはサーバーSDKとクライアントSDKだけで、つまり両方ピュアC#。その上では普通にC#でガリガリと書ける。いいじゃん。両方ピュアC#というのが最高に良い。サーバーはWindowsでホストされる。それも最高に良い。プロトコルとかはゲーム専用で割り切ってる分だけ軽量っぽい。うん、悪くないんじゃないか。
また、ホスティングは結構優秀です。まず、無停止デプロイができる(設定でShadowCopy周りを弄ればOK)。これ、すっごく嬉しい。この手のは常時接続なのでデプロイ時に切断するわけにもいかないし、これ出来ないとデプロイの難易度が跳ね上がっちゃいますからねぇ。また、1サーバーで擬似的に複数台のシミュレートなどが可能です。実際、グラニでは6台構成クラスタのシミュレートという形で常に動かしていて、どうしても分散系のバグを未然に防ぐには重要で、それがサクッと作れるのは嬉しい。脚周りに関しては、かなり優秀と思って良いのではないでしょうか。
PhotonWireの必要な理由
Photon Serverがまぁ悪くないとして、なんでその上のレイヤーが必要なのか。これは生SDKを使ったコードを見てもらえれば分かるかしらん。
// 1. クライアント送信
var peer = new CliendSidePeer(new MyListener());
peer.OpCustom(opCode:10, parameter:new Dictionary<byte, object>());
// 2. サーバー受信
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
switch (operationRequest.OperationCode)
{
case 10:
// Dictionaryに詰まってる
var parameter = operationRequest.Parameters;
HogeMoge(); // なんか処理する
// 3. 送り返す
this.SendOperationResponse(new OperationResponse(opCode:5), sendParameters); //
break;
// 以下ケース繰り返し
default:
break;
}
}
public class MyListener : IPhotonPeerListener
{
// 4. クライアント受信
public void OnOperationResponse(OperationResponse operationResponse)
{
// 返ってきたレスポンス
switch (operationResponse.OperationCode)
{
case 5:
// なんかする
break;
}
}
}
問題点は明白です。原始的すぎる。byteパラメータ、Dictionaryを送って受け取りそれをswitch、送り返してやっぱswitch。こうなると当然長大なswitchが出来上がってカオスへ。また、クライアント送信とクライアント受信がバラバラ。コネクションで送信した結果を受け取るのが、独立した別のListenerで受け取ることになる、となると、送信時にフラグONで受信側でフラグチェック祭り、Listener側のフラグ制御が大変。送信したメッセージと戻り受信メッセージだという判別する手段がないので、並列リクエストが発生するとバグってしまう。
これをPhotonWireはHubという仕掛け(とUniRx)で解決します。
ようするにMVCのControllerみたいな感じで実装できます、ということですね。また、逆に言えば、PhotonWireはそんなに大きな機能を提供しません。あくまで、このswitchやちょっとしたシリアライゼーションを自動化してあげるという、それだけの薄いレイヤーになっています。なので、PhotonWireによるコードが素のPhoton Serverによるものと少し異なるからといって、あまり警戒する必要はありません。実際、薄く作ることは物凄く意識しています。厚いフレームワークは物事の解決と同時に、別のトラブルを呼び込むものですから……。
ちなみにPhotonWireを通すことによる通信のオーバーヘッドは4バイトぐらいです。それだけで圧倒的に使いやすさが向上するので、この4バイトは全然あり、でしょう。
Hub
HubというのはASP.NET SignalRから取っています。というか、PhotonWireのAPIはSignalRからの影響がかなり濃いので、ドキュメントはSignalRのものを漁れば20%ぐらいは合ってます(全然合ってない)
// Unityクライアント側で受け取るメソッド名はインターフェイスで定義
public interface ITutorialClient
{
[Operation(0)]
void GroupBroadcastMessage(string message);
}
[Hub(100)]
public class Tutorial : PhotonWire.Server.Hub<ITutorialClient>
{
// 足し算するだけのもの。
[Operation(0)]
public int Sum(int x, int y)
{
return x + y;
}
// 非同期も行けます、例えばHTTPアクセスして何か取ってくるとか。
[Operation(1)]
public async Task<string> GetHtml(string url)
{
var httpClient = new HttpClient();
var result = await httpClient.GetStringAsync(url);
// PhotonのStringはサイズ制限があるので注意(デカいの送るとクライアント側で落ちて原因追求が困難)
// クラスでラップしたのを送るとPhotonの生シリアライズじゃなくてMsgPackを通るようになるので、サイズ制限を超えることは可能
var cut = result.Substring(0, Math.Min(result.Length, short.MaxValue - 5000));
return cut;
}
[Operation(2)]
public void BroadcastAll(string message)
{
// リクエスト-レスポンスじゃなく全部の接続に対してメッセージを投げる
this.Clients.All.GroupBroadcastMessage(message);
}
[Operation(3)]
public void RegisterGroup(string groupName)
{
// Groupで接続の文字列識別子でのグループ化
this.Context.Peer.AddGroup(groupName);
}
[Operation(4)]
public void BroadcastTo(string groupName, string message)
{
// 対象グループにのみメッセージを投げる
this.Clients.Group(groupName).GroupBroadcastMessage(message);
}
}
async/awaitに全面対応しているので、同期通信APIを混ぜてしまっていて接続が詰まって死亡、みたいなケースをしっかり回避できます。属性をペタペタ張らないといけないルールは、Visual Studio 2015で書くとAnalyzerがエラーにしてくるので、それに従うだけで良いので、かなり楽です。
プリミティブな型だけじゃなくて複雑な型を受け渡ししたい場合は、DLLを共有します。
// こんなクラスをShareプロジェクトに定義して、Server側ではプロジェクト参照、Unity側へはビルド済みDLLをコピーする
public class Person
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
// サーバーがこんなふうに戻り値を返して
[Operation(1)]
public Person CreatePerson(int seed)
{
var rand = new Random(seed);
return new Person
{
FirstName = "Yoshifumi",
LastName = "Kawai",
Age = rand.Next(0, 100)
};
}
// Unity側では普通に受け取れる
proxy.Invoke.CreatePersonAsync(Random.Range(0, 100))
.Subscribe(x =>
{
UnityEngine.Debug.Log(x.FirstName + " " + x.LastName + " Age:" + x.Age);
});
プロジェクトの構成はこんな感じ。シームレス。
また、オマケ的に、Unity側でのエディタウィンドウではコネクションの接続状況と送受信グラフがついてきます。UNETの立派なProfilerに比べるとショボすぎて話にならないんですが、ないよりはマシかな、と。
サーバー間通信
Photon Serverはサーバーとサーバーを接続してクラスタを作れるのですが、その通信もHubを使ったRPCで処理しています。
// ServerHub(呼ばれる方)
[Hub(54)]
public class MasterTutorial : PhotonWire.Server.ServerToServer.ServerHub
{
[Operation(0)]
public virtual async Task<int> Multiply(int x, int y)
{
return x * y;
}
}
// ClientHub(呼ぶ方)
[Hub(99)]
public class Tutorial : Hub
{
[Operation(0)]
public async Task<int> ServerToServer(int x, int y)
{
var val = await GetServerHubProxy<MasterTutorial>().Single.Multiply(x, y);
return val;
}
}
この見た目、直接呼んでるかのように書けるサーバー間通信は、実行時には以下のように置き換わってネットワーク呼び出しに変換されています。
なので、ServerHubはかならず戻り値はTaskじゃないとダメです(Analyzerが警告してくれます)。昔はこの手の処理を、メソッド呼び出しのように隠蔽する場合って、同期的になっちゃって、でもネットワーク呼び出しなので時間かかってボトルネックに、みたいなパターンが多かったようですが、今はTask[T]があるので自然に表現できます。このへんも含めてTask[T]が標準であることの意味、async/awaitによる言語サポートは非常に大きい。
この辺りの詳しい話は以下のスライドに書いています。
ネットワーク構成
PhotonWireは特に何の既定もしません。Photonが自由に組める通り、どんな組み方もできるし、どんな組み方をしてもPhotonWireでの呼び出しに支障は出ません。
のはいいんですが、その時、ClientPeer, InboundS2SPeer, OutboundS2SPeerの3種類のPeerを持つように、PhotonWireもまたHub, ServerHub, ReceiveServerHubとそれぞれに対応する3種のHubを持っています。3つ、これは複雑で面倒。
しかしPhotonWireはネットワークの複雑さの隠蔽はしません。やろうと思えばできますが、やりません。というのも、これ、やりだすと泥沼だから。賢くやりたきゃあAkkaでもなんでも使ってみればよくて、自分で書いたら一生終わらない。Photonのネットワークは本当に全然賢くなくて、ただたんに直結で繋いでるという、それだけです。そんなんでいい、とまではいいませんが、そうなら、それに関しては受け入れるべきでしょうね。勘違いしちゃあいけなくて、フレームワークは複雑さを隠蔽するもの、ではないのです。
ともあれ、最低限の賢くなさなりに、スケールしそうな感じに組み上げることは可能なので、全然良いとは思ってますよ!
できないこと
ポンと貼り付けてtransformが自動同期したり、いい感じに隙間を補完してくれたりするものはありません。ただ、Client-Server RPCがあれば、それは、その上で実装していくものだと思うので(いわゆるNantoka ToolkitとかNantoka Contribの範疇)、しゃーないけれど、自前で作ろうという話にはなってきますね。↑のネットワーク構成の話も、隠蔽とまではいかなくても、決まった構成になるのだったらそれなりにバイパスするいい感じのユーティリティは組んでいけるだろうから、その辺のちょっとした増築は、やったほうがいいでしょう。
まとめ
現状実績はないです(今、公開したばかりですからね!)。ただ、グラニで開発中の黒騎士と白の魔王というタイトルに投下しています。
半年以上は使い続けているので、それなりには叩かれて磨かれてはいるかなあ、と。大丈夫ですよ!と言い切るには弱いですが、本気ですよ!とは間違いなく言えます。DLLシェアや自動生成周りが複数人開発でのコンフリクトを起こしがちで、そこが改善しないと大変かなー、というところもありますが、全般的にはかなり良好です。
ちょっと大掛かりだったり、Windows/C#/Visual Studioベッタリな、時代に逆行するポータビリティのなさが開き直ってはいるんですが、結構使い手はあると思うので試してみてもらえると嬉しいですね!あと、大掛かりといっても、知識ゼロ状態からだったら素のPhoton Server使うよりずっと楽だと思います。そもそもにPhotonWireのGetting Startedのドキュメントのほうがよほど親切ですからねぇ、Visual Studioでのデバッグの仕方とかも懇切丁寧に書いてありますし!
VR時代のマルチプレイヤーって結局どうすんねん、と思ってたんですが、Project TangoのサンプルがPhotonだしAltspaceVRもPhotonっぽいので、暫くはPhotonでやってみようかなー。という感です。
MarkdownGenerator - C#におけるAPI Reference生成のためのドキュメントツール
- 2016-05-23
APIリファレンス作りたい?Sandcastle。以上。終了。あるいはdotnet/docfxが良いのではないでしょうか。こいつはdotnet配下にあるように、MSの今後のOSS系のはこれでドキュメント生成されていく可能性があります。
というのは置いといて、私的には実のところ、あんまり重要視していませんでした、ドキュメントツール。.chmにはいい思い出がなくて、というか別に見ないじゃん?htmlで出力してもなー、なんかゴチャゴチャしてて汚いしなー。一方でJavaScriptなんかは様々な格好良くフォーマットされた形式で色々出てるのであった。いいじゃん。いいね。
さて、もう一つ。HTMLで出力しても置き場にこまる。GitHub Pagesにはそんないい思い出がない。別にあんなところをフロントにするよりもリポジトリのアドレス直のほうが断然いいじゃん、みたいな。というわけでアレだ、GitHub Wikiだ。あそこをゴミ置き場にすればいいんだ。という発想で、まずUniRxのリファレンスをGitHub Wikiに置いてみました。
UniRxは、namespaceを意図的にある程度平ったくしてるので、ちょっとごちゃってますが、まぁまぁいいんじゃない?それなりに見れる。悪くはない。少なくともないよりは100億倍良い。
私的にはIntelliSenseがドキュメントだ!みたいな意識がそれなりにあって、最初のチュートリアルみたいなドキュメントがあったら、あとはそれを手がかりにあとはIntelliSenseでなんとかしようぜ、的なところが。実際Roslynなんかはそんな感じがする。Getting Startedはそれなりに厚い、けど全貌からは程遠い。でもAPIドキュメントはない。さあ、IntelliSenseで宝探しだ。って。肯定もしないけれど否定もしない、そういうのも今風よね。でも、まあこの程度のAPIリファレンスでも生成してやると、それはそれで良いな、って思ったのだ。です。
MarkdownGenerator
生成は自家製こんそーるあぷりで行ってます。というわけで公開しました。
dllとxmlを渡すとmdと目次用のHome.mdをばらまくので、GitHub Wikiに投げ込みます。そう、GitHub Wikiはご存じの方も多い通り、それ自体がgitで管理されててCloneできるのです。さいこー。というわけでそのままPushするだけ。Good。完璧。これなら、CIなんかでフックして毎回生成して投げ飛ばしてあげてもいい。よね。
生成結果のStyleはちょっとまだまだ試行錯誤中。まあでも割とこんなもんでいいんちゃうんちゃうん?ユースケースの9割ぐらいはカバーできているでしょう。それ以上はノイズということで。
その他ツール
Sandcastleはそもそも出力をカスタマイズできるので、もう少し真面目というかガッチリしたものが必要ならば、maxtoroq/sandcastle-mdあたりを使ってMarkdownを出力してやると良いでしょう。これなら、きっちりとSandcastleで出力される情報が全部そのまま入ってるので、ちゃんとしてる感は圧倒的に高いです。また、繰り返しますけれどDocFXは今からやるなら最有力候補な気がします。DLLからじゃなくてRoslynでプロジェクトファイルから解析したりとか今風。あくまでstatic file generatorなのでmdじゃなくてhtml出力なので、Wikiに投げ飛ばす用途には向かないのと、ちょっと複雑、使いこなすのは難しい、かな、まあ相応には良さそうかとは。
MarkdownGeneratorは、ちゃんとしてないなりに、私的に重要視してる情報がパッと一覧で見やすくする、ということを重視しているので……。あと、なんかSandcastle使いたくないんだよねー、心理的に。なんだろうね、レガシー臭するからなのかな。食わず嫌いなだけって話でもあるのだけれど。
何れにせよ、GitHubであってもなくてもいいだろうけれど、API Referenceを投げ飛ばす場としては、そういうところ(どういうところ?)がいいですね。独立してるよりもリポジトリに近い場所のほうが素敵度は高い。気がする。あとはなんのかんのでGitHubに慣れきってるというのもあって、GitHubにあると情報がスムースに受け取れる気がするんだよね。これもなんでだろうね。でもそういうのってあるよね。
Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映
- 2016-05-14
UniRx 5.3.0をリリースしました!今回のアップデートは、内部的な最適化、です。最適化は、もうそろそろあんまやるところ残ってないよね、なんて思ってたんですが、じっくり考えるとそんなことなく割とあったので埋めました。それが表題のコルーチンの省メモリと高速化です。使い方次第ではありますが、場合によっては今回のアップデートでものすごく恩恵に授かる人もいればそこそこの人もいるかもです。ともあれ基本的に内部的に変更してるだけなので、入れるだけでそれなりに高速化したりする可能性がそれなりにあります。
前回が2月だったので3ヶ月ぶりですね。あまりオペレータ追加がないので、次はオペレータ追加に集中したい気もする。なんか優先的に欲しいのあればリクエストもどうぞ(Observable.Windowとかいい加減そろそろ入れろよって話なんですが)
MicroCoroutine
今回の大きい変化はMicroCoroutine(と、自称してる)の導入です。特に大量にEveryUpdateやEveryValueChangedを呼んでるシチュエーションにおいて10倍、というのは場合によりで大雑把なのですが、相当速くなります。
void Start()
{
// Start 10000 Coroutines
for (int i = 0; i < 10000; i++)
{
// Standard Unity Coroutine
// StartCoroutine(Counter());
// Use UniRx 5.3 - MicroCoroutine
MainThreadDispatcher
.StartUpdateMicroCoroutine(Counter());
}
}
IEnumerator Counter()
{
while (true)
{
count++;
yield return null;
}
}
こんな10000個、単純なコルーチンを起動するコードがあったとして
大きく違いがでます。ちょっと恣意的すぎではあるんですが、UniRxはコルーチンを簡単にかけるが故に、これに近いシチュエーションってのが意図せず起こりがちではありました。また、Resources.LoadAsyncなど非同期系APIからの取得に関しても、一時的に多くのコルーチンを起動するシチュエーションはあり得るのではないでしょうか。
性能改善した理由は、基本的にはUnityの公式ブログUPDATE()を10000回呼ぶで紹介されていることの話で、10000個のUpdateは遅くて、配列に詰めて直接ループで呼ぼうぜ、と。どうせUpdate内のメソッドは呼ばれてC#の領域で実行されるんだから、マネージド(C#)-アンマネージド(C++)の繋ぎのレイヤーは純粋にオーバーヘッドになってくるよ、と。なるほどそうだねそりゃそうだねぇ。それはStartCoroutineにも言えて、というかコルーチンのほうがもっと性能劣化度が大きいんですよね。
この記事は非常に素晴らしくて、大量にモノ出して速度遅くなってるのがスクリプト起因なら、マネージャー立ててまとめて、あとUpdateに限らずマネージド-アンマネージドの繋ぎをやってる部分が遅いだろうからそこを適切に取り除ける限り除けば、全然まだまだそれなりに捌ける余裕は残ってるぜ。ということで、むしろ希望に満ちていていい感じです。実際、ハイパフォーマンスを謳うDOTweeenとかのライブラリもそんな感じですね、動かすものそれぞれにUpdateするコンポーネントを挿したりはしない、中央管理で動かすのだ、と。
さて、UniRxでは幾つかのメソッドはコルーチン依存でループを回しています。Observable.EveryUpdateとかEveryValueChangedとか。少しに使う分にはいいんですが、気楽に使えるが故に、大量に使うと、10000個とまではいかなくてもやっぱり、それぞれがコルーチンを起動することによるマネージド-アンマネージドオーバーヘッドがそのまま乗っかってきてしまいます。というわけで、やはりコルーチン自前管理の道を進むしかない……。幸い、自前管理で問題になる機能面での低下に関しては、UniRx自体がコルーチンを凌ぐだけの機能を提供しているので、気にしないでよし。というわけで純粋にいかにコルーチン(IEnumerator)を高速に回転させ、高速にメンテナンスするかにだけ集中すればよし。
回転させるのはforループ回すだけの話なんですが、マネージャー作ろうぜ、となった時に、Listに詰めるのはいいんですが、面倒くさいのは削除。削除は要注意で、単純にListのRemoveやって済ませたりするのは結構アレです(Removeは相当高コストな操作です)。かといってDictionaryやSet、LinkedListでやるなんていうのは論外で(列挙の性能が死ぬので本末転倒)、基本的に配列で頑張るべきなんですが、さてはて。結局、その辺のめんどーを見るのがめんどーだからUpdateやStartCoroutineでぶん回すのだ。割と本気で。
ではどうしたか、というと、UniRxのMicroCoroutineのアプローチはRemoveしない。です。しない。空いた部分はnullで埋めて純粋にスキップするだけにする。多少の空きなら、いちいち削るよりもスキップさせたほうが速い。しかし、それだけだとブヨブヨと膨らみ続けてしまうので、xフレーム毎に空きスペースに詰めなおして小さくします。縮める際も前の方に整列させるんじゃなくて、空きスペースに対して後ろから埋めるようにするので、順番はグチャグチャになります。その代わり余計な配列へのセットが発生しないので速い。そして膨らんだ配列は放置して膨らんだままにします、終端のインデックスだけ記録して管理するのみ(ところでアセットストアにアップデート申請出してから気づいたのですが、この配列の使い方なら定期的なお掃除じゃなくて、動かしながら埋めるようなコードにするのも可能っぽい感、なので次回アップデートでそうします)
というわけで、UniRxのMicroCoroutineは中央集権的なので多少膨らむことが許される(でしょう!)ことを利用して、とにかく高速にコルーチンを捌く、ということだけに集中してます。ので速い。下手に自前管理するよりも速いかもしれませんし、Updateで監視するよりもObserveEveryValueChangedのほうがむしろ速い、Rxで書いたほうが速い、みたいな逆転現象も全然発生しうるような話になります。
ObserveEveryValueChanged
EveryUpdate
EveryFixedUpdate
EveryEndOfFrame
NextFrame
TimerFrame
IntervalFrame
DelayFrame
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame
この辺りのメソッドを使った場合、内部の実装がMicroCoroutineに差し替わったので自動的に恩恵に預かれます。コルーチン -> Observable変換に関しては FromMicroCoroutine が追加されました。基本的にはFromCoroutineと一緒なのですが、MicroCoroutineではyield returnするのはnullだけにしてください、それ以外には対応してません(UnityEditor上ではWarning出して警告します)。MicroCoroutineの制約はそれなんですが、なんだかんだで、8割ぐらいはyield return nullだけで成立するんちゃうんちゃうん、みたいな。賢くやろうとすればもう少しは出来なくもないんですが、シンプルで高速なコルーチンの回転を損ねちゃうのでナシ。IEnuemrator.Currentの呼び出しや、その型チェックすら省きたい。残り2割ぐらいなら普通にStartCoroutineすればいいじゃん、ということで。実際、UniRxの↑のメソッドはそれでかなり置き換えることが出来る、ということを発見できたので、全面的に導入する気になったのです。
また、最悪待ちたい場合は、isDoneのループを回すようにToYieldInstruction経由でIObservableを待てるので、大抵のことはなんでもできます。
IEnumerator MicroCoroutineWithToYieldInstruction()
{
var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
while (!(www.HasResult || www.IsCanceled || www.HasError)) // 3つもプロパティ並べるのダルいので次回アップデートでIsDoneを追加します予定
{
yield return null;
}
if (www.HasResult)
{
UnityEngine.Debug.Log(www.Result);
}
}
もっとプリミティブに直接利用したい場合は、StartCoroutineの代わりにMainThreadDispatcherに3つ生やしてあります。
MainThreadDispatcher.StartUpdateMicroCoroutine
MainThreadDispatcher.StartFixedUpdateMicroCoroutine
MainThreadDispatcher.StartEndOfFrameMicroCoroutine
それぞれがコルーチンを消費するタイミングで、まぁ普通はStartUpdateMicroCoroutineを使えばよいでしょふ。もし大量のStartCoroutineがプログラム中にあるのなら、これに差し替えるだけで本当にすっごく速くなるでしょう。ほんと。
SubscribeWithState
ここから先はUniRxのアップデートの話だけ。そして本当にMicro Micro Microな最適化であんま意味はないんですが、まず、SubcribeWithStateを追加しました。これによって何が変わるか、というと、例えば……
// Before
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
return source.Subscribe(x => text.text = x);
}
// After
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x);
}
という感じの使い方ができます。どういう違いが出るのかというと、以前にUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方という記事の中で説明したのですが、ラムダ式はその中身によってコンパイル時に生成されるコードがかなり変わってきます。で、最速なのはそのメソッド内だけで完結していて外部の変数等には一切触っていない状態。onNextはActionなので、副作用かける際にどうしても外部変数をキャプチャしてしまうことが多いんですよね。そこでSubscribeWithStateを使うと、必要な変数を閉じ込めることができるので最速ゴミなしの形で記述できます。
ただまぁ、これやると、じゃあSelectやWhereなんかもState取れたほうがいいんですか?(理屈上はそうです)、とか、ああクロージャ殺さなきゃ死ね死ね死ね、とか思ったりしそうなのですけれど、Subscribeの回数ってパイプライン内の実行頻度に比べれば圧倒的に少なくなるはずなんですよね。だから全体のバランスで見たら無視できるといっても過言ではないはず、特にクロージャでちょっとゴミが出る程度の話は。
なのであんま神経質にやることはないんですが、↑のSubscribeToTextのようなそんな手間もかからないし、UIとかシーンの初期化時にいっぱい登録される可能性があるようなものでライブラリ的な部分でカバーできる質のものならば、少しだけ気を使ってあげると気は安らぐかもしれません。
ReactiveCommand
ReactiveCommandは.NET版のReactiveProeprtyにあった、最後のパーツなんですが、どうなんでしょうね、本来はViewModelのレイヤーのためなんですが、UnityだとPresenterにUI要素がセリ出してきてるのでイマイチベンリかどうか分からなくて入れてなかったんですが。一応、こんな風に使えます。
public class Player
{
public ReactiveProperty<int> Hp;
public ReactiveCommand Resurrect;
public Player()
{
Hp = new ReactiveProperty<int>(1000);
// If dead, can not execute.
Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
// Execute when clicked
Resurrect.Subscribe(_ =>
{
Hp.Value = 1000;
});
}
}
public class Presenter
{
public Button resurrectButton;
Player player;
void Start()
{
player = new Player();
// If Hp <= 0, can't press button.
player.Resurrect.BindTo(resurrectButton);
}
}
buttonのinteractableとonClickが抽象化されたもの、って感じですね。
その他
リリースノートから。
Add : ReactiveCommand
Add : MainThreadDispatcher.StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine
Add : Scheduler.MainThreadFixedUpdate, MainThreadEndOfFrame
Add : ToYieldInstruction(cancellationToken)
Add : Observer.Create(onNext/onNext, onError/onNext, onCompleted) overload
Add : IReadOnlyReactiveProperty.SkipLatestValueOnSubscribe
Add : Observable.WhenAll overload (IObservable<Unit>(params IObservable<Unit>[] sources), this becomes breaking changes)
Add : Observable.FromMicroCoroutine
Add : Observable.AsSingleUnitObservable
Add : Observable.SubscribeWithState
Add : Observable.CreateWithState
Add : Disposable.CreateWithState
Improvement : Use MicroCoroutine on `ObserveEveryValueChanged`, `EveryUpdate`, `EveryFixedUpdate`, `EveryEndOfFrame`, `NextFrame`, `TimerFrame`, `IntervalFrame`, `DelayFrame`, `SampleFrame`, `ThrottleFrame`, `ThrottleFirstFrame`, `TimeoutFrame`
Improvement : Performance improvement for Observable.Range, Repeat when scheduler is Scheduler.Immediate
Improvement : Use Time.unscaledDeltaTime in IgnoreTimeScaleMainThreadScheduler
Fix : ReadOnlyReactiveProperty(source, initialValue) does not publish initial value on subscribe
Fix : IReadOnlyCollection has set indexer
Fix : Ambigious property of IReactiveCollection.Count, Indexer
Fix : Throw invalid error when ObservableWWW.LoadFromCacheOrDownload failed.
Breaking Changes : Added IReadOnlyReactiveProperty.HasValue
Breaking Changes : AsyncConvertsion scheduler to Scheduler.MainThread on WebGL build(WebGL doesn't support ThreadPool)
Other : Update UniRxAnalyzer 1.4.0.1 https://www.nuget.org/packages/UniRxAnalyzer
ToYieldInstructionはUniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成で説明しているのですが、Unity 5.3以降のCustomYieldInstuctionを応用したもので、IObservableをコルーチンで処理できるようにするやつで、結構お薦め機能です。MicroCoroutineで回すための補助にもなりますし。
SchedulerにMainThreadFixedUpdateとMainThreadEndOfFrameを足しました。ObserveOnやTimerなどで、その辺の細かい制動をしたい方にどうぞ。
(ReadOnly)ReactivePropertyへのSkipLatestValueOnSubscribe拡張メソッドの追加。これは、(UniRxの)ReactivePropertyはSubscribe時に必ず値をプッシュするようになってるんですが、そういった初期値を無視したいって局面は少なからずあるんですよね。Rx.NET用のReactivePropertyでは、コンストラクタでReactiveProeprtyModeとして、None | RaiseLatestValueOnSubscribe | DistinctUntilChanged を指定できるようなデザインを選んでいるのですが(というのも、Viewにデータバインディングするため構築時の初期値はnullであることが確定している、というシチュエーションが割とあるため)、UniRxのReactivePropertyではSubscribe側が選ぶというデザインにしています。この辺はフレームワークの性質の違いに合わせてるのですが、ともあれ、初期値を無視したい場合は rxProp.SkipLatestValueOnSubscribe().Subscribe() としてもらえれば。
Observable.WhenAllを、IObservable[Unit][]が相手の場合はIObservable[Unit]を返すようにしました。これは、別にUnit[]が返されても何の意味もないからというのと、それによって余計な配列確保をしないという最適化も入れています。この方が絶対に良いんですが、しかし戻り値の型が変わってしまったので破壊的変更にはなっています。最初から気づいておけば良かったですね、すびばせん。
AsSingleUnitObservableは LastOrDefault().AsUnitObservable() みたいな変換をかけるやつで、Async的な保証をかけるのにベンリというあれそれ。
あとは、んー、使ってる人は、うちの社内以外にないのでは疑惑も感じてますが、UniRxAnalyzerを更新してます。コンストラクタにIObservableを突っ込んでいた場合に誤検出していたのを修正しています。
これ、Visual Studio 2015を使って開発している人は絶対に入れたほうがいいですよ!Subscribe忘れて発火しないのに気づかなかったー、みたいなポカミスが圧倒的に防げますので。
まとめ
性能面でより気にせずにカジュアルに色々使えるようになった、というのはいいことかなー。性能面で問題出た際に「そういう使いかた想定してないから」といった却下の仕方って、あんましたくないですからね。聞いてていいものでは全くない。デザインとしてカジュアルに使えるようになっているなら、性能もちゃんと担保していかないし、そういうのが頻発するならライブラリの設計が悪い。と、思ってるので、今回のでよりちゃんと自然に使えるようになったかな、と。ObserveEveryValueChangedは個人的には最高にクールな機能だと思ってるので、気兼ねなく使って欲しいし、やっと本当に気兼ねなく使えるようになりました。
ObservableUpdateTrigger(UpdateAsObservable), Observable.EveryUpdate, Observable.EveryGameObjectUpdate とUpdateのハンドリングも3択、性能特性も三者三様。混乱との対話!別に特に何をレコメンドすることもなく、まあ素直に書くならUpdateTriggerが素直でよく。自身のUpdateループで周りますしね。EveryUpdateはMicroCoroutineなので性能特性的には良さげ、どうせAddTo(this)するならループのライフサイクルもUpdateTriggerと別に変わりはしないし(UpdateTriggerだとDisableでUpdateが回らなくなるので、まぁその辺で挙動に違いは出る)。EveryGameObjectUpdateはMainThreadDispatcherのSubjectに積まれるもので、UpdateTriggerが使える状況なら非推奨かな、あんまりSubjectに頻繁にAdd, Removeするのは性能特性的に悪手なので。UpdateTriggerもSubjectが駆動するのですが、性質的にグローバルじゃないのでAdd, Removeは局所化されるからそこまででは、に通常はなるでしょう、的な。
そんなこんなで、少なくともRxが性能面のネックでー、と言われるのは悔しい話なので、大きいものから小さいものまで、最適化ネタは常に考えてます。利用事例としても、結構ヒットしてる某社の某ゲーム(とは)や最近でた前作に続いてヒットの予感のする某ゲーム(とは)など、かなり使いこなしてる事例もあって(個人的にはとても感動した!)、ちゃんと実用的といってもいいレベルになってると思われます。弊社の開発中タイトルである黒騎士と白の魔王でもガッツリ使っているので、ご興味ある方は中途採用は絶賛募集中です:) 当たり前ですがドッグフーディングは凄く大事で、さすがにデカいバグは出てこないにしても軽微なものはちょいちょい上がってくるので、日々、堅牢さは担保されているな、とかかんとか。あと、使いすぎてるほどに使いすぎてるので、常に性能面でネックになってはいけない、性能面でネックになってはいけない、とマントラを唱えるプレッシャーになってるのもいいことです、多分きっと。
今回のアップデートでツメが甘かった案件としてはAsyncOperation.AsObservableやObservableWWWが内部的にまだFromCoroutine利用なので、FromMicroCoroutineに可能なら差し替えようかな、と。効果のほどとしては、やっぱり場合によりけりですが、初期化とかで大量に回る時は大きく変わるかも。しれない。ともあれ次回アップデートにご期待を。ただyield return wwwやasyncOperationした場合とyield return nullでisDoneチェックする場合とで、戻ってくるタイミングが異なるので、そこのルールを統一させないとかなあ。
Microsoft MVP for Visual Studio and Development Technologies(C#)を再々々々々受賞しました
- 2016-04-01
今年の受賞で、6年目です。エキスパタイズとしてはVisual Studio and Development Technologiesという微妙な感じなので、C#です。C#です、と言わせてください。
春はあけぼの、というわけで会社も新たなステージに入りました。原点を振り返りつつも、新たな創業として、より強固な姿を掲示できればいいかな、と思います。
去年に私は
私が主に力を入れているのはUnityと、そのReactive Extensions実装のUniRxで、特にUniRxはかなりヒットさせられたとは思います。が、まだまだ兆しといったところなので、確固たるものにしなければならない。また、それを基盤にして、C#の強さというのを、ただの今までの.NETコミュニティにだけに留まらず、幅広い世界に届ける、伝えていきたい。
と言ってましたが、それは達成できました(いやほんと)。しかし、よくよく考えると、なんだか小粒です。今年はもう少し大きなヴィジョンで行きたいですね。最近のMicrosoftのメッセージとしても、C#の強さがより一層際立ってきました。それをしっかりと補強し、力強く、Real World C#というのを示し続けていきます。そう、Real Worldで。どういうことかといえば、そういうことです。今年はね。
引き続き主戦場はC#とUnityです。Unityも.NET Foundation加入やHololensやVRの最有力の開発環境であったりと、変わらず非常に魅力的で、強力な環境ですす。いろいろな形で、刺激的なことを掲示し続けられればいいかな、と思っています。
そんなわけで引き続き、今年もよろしくお願いします。
Roslyn Analyzerでコンフィグを読み込ませて挙動を変更する
- 2016-03-30
方法。が、欲すぃ。例えば採番する時に0ベースなのか1ベースなのかプロジェクトによって変えたい。例えばCodeFix時の名前変更のルールを先頭アンスコ付けるのか付けないのかを自由に変えさせたい。NotifyPropertyChangedGenerator - RoslynによるVS2015時代の変更通知プロパティの書き方の時は、専用のAttributeを使うので、その中のインターフェイスを書き換えてコンフィグ代わりにしてね、という方法を取ったのですが、専用の属性が使えなきゃ適用できない手法で、全然汎用的っぽくないし、当然ながら全然イケてない。
ではどうするか。実は、Additional Filesという仕組みが用意されているので、それを用いることでコンフィグを読みこませることができます!詳細はroslyn/Using Additional Files.mdに書かれていますが、任意のテキストファイルを色々な手法(コマンドラインの引数やcsprojの定義としてなど)でプロジェクトに設定し、Analyzer側では AnalyzerOptions.AdditionalFiles で読めます。
では、やってみましょう。
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AdditionalFileAnalyzer : DiagnosticAnalyzer
{
static DiagnosticDescriptor Rule = new DiagnosticDescriptor("AdditionalFileAnalyzer", "AdditionalFileテスト", "AdditionalFiles:{0}", "Usage", DiagnosticSeverity.Error, isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(Analyze, SymbolKind.NamedType);
}
private static void Analyze(SymbolAnalysisContext context)
{
// AnalyzerOptionsにAdditionalFilesがある。
// CodeFixContextの場合はProjectに生えてるので、 context.Document.Project.AnalyzerOptions で取得可能。
var additionalFiles = context.Options.AdditionalFiles;
// Pathから引っ掛けて取る
var config = additionalFiles.FirstOrDefault(x => System.IO.Path.GetFileName(x.Path) == "config.json");
if (config != null)
{
// GetText().ToString()で文字列が取れるので、あとはJsonConvertでデシリアライズするなりなんなりどうぞご自由に……。
var text = config.GetText().ToString();
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Symbol.Locations[0], text));
}
else
{
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Symbol.Locations[0], "JSONが見つかってないぞ"));
}
}
}
これでJSONが見つかってないぞ、と怒られまくります。ファイルを追加するには、まずプロジェクトの適当なところにconfig.jsonを足して、Build Actionを AdditionalFiles に変更します。が、変更しようとするとやっぱり怒られるので、しょうがないからcsprojを手動で書き換えます。
<ItemGroup>
<AdditionalFiles Include="config.json" />
</ItemGroup>
これで、以下の様な結果が得られます。
うん、ちゃんと読めてる!
コンフィグとして使うには、まぁイマドキのJSONでコンフィグだったらJSON.NETでJsonConvert.DeserializeObjectなんかでサクッと復元してやるのが楽でしょう。外部DLL読み込むのが嫌だという場合は、XMLにしてLINQ to XMLやXmlSerializer使うのも全然お手軽でいいとは思います。
コンフィグなんてほぼほぼ固定になるのに毎回デシリアライズ走らせるのは嫌だなあ、って場合はstaticオブジェクトにキャッシュしちゃうのも悪くない手だと思います。その場合は、書き換えた場合はプロジェクト再読み込み(or VS再起動)になってしまいますが、実用的には全然問題ないはず。といいたいのですが、複数のプロジェクト跨ぎで共有されちゃうと厄介だったりするので注意が必要なので、まぁちょっとDeserializeするぐらい大したことねーよということで、毎度Deserializeするのも全然構わないかな、とは。
このコンフィグを読み込む手法はStyleCopAnalyzer/Configuration.mdでも採用されているし、これがスタンダードのやり方だと思って良さそうです。
SerializableDictionary - Unityで高速に辞書復元するためのライブラリ
- 2016-03-21
という、ScriptableObjectとかJsonUtilityとか、そもそもSerializeFieldとかでシリアライズできるDictionaryを作りました。
もともとDictionaryはシリアライズできないのですが、ISerializationCallbackReceiverを用いてシリアライズ/デシリアライズのタイミングでKeyの配列、Valueの配列に戻してやるなどで保存すること自体は全然可能でした。のですが、速度的には問題あるな、というのに直面しました。
その前に、JSONから復元するのがまず遅かった。じゃあMsgPackやProtobufに変更したら速いかといえば、別にそこまでそうではなかった。これはつまり、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。ではScriptableObject化すればどうだろう、確かにデシリアライズのプロセスがUnityネイティブ(実体は不明)と化して、確かに速い。が、そこからDictionaryに変換してやるのをC#で書いたらやっぱりそこが遅い。
遅い、というとアレで、量次第ですけどね。今回、量がやたら多かったので結構かなり相当引っ張られてた。初期化のタイミングなどで大量のDictionaryを捌くような場合に、無視できない程度に結構引っかかる遅さを醸し出してる。結局、配列からであっても、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。という悲しい現実をつきつけられるのであった。
というわけで、SerializableDictionaryはDictionaryの内部構造をシリアライズすることで、ネイティブプロセスのみで完結して爆速で復元します。
SerializableDictionaryではSerializableDictionary, SerializableLookup(MultiDictinary), SerializableTupleの3つを提供します。今のとこアセットストアに公開するつもりはそんなないので、使いたい場合はソースコードをZipでダウンロードするなりGitで落とすなりしてプロジェクトに投げ込んでください。
SerializableDictionary
例えばキーがint、値がstringの辞書を保存したい場合は、まず、継承したクラスを作ります。
[Serializable]
public class IntStringSerializableDictionary : SerializableDictionary<int, string>
{
}
わざわざ継承しなきゃいけない理由は、ジェネリックな型はシリアライズできないからです!しょうがないね。別にゆうてそんなに大量の型があるわけでもないでしょうし、素直にそれぐらいは作りましょう。あとは、普通に使えば普通にシリアライズ可能になってます。メデタシメデタシ。
インスペクタに表示するためのPropertyDrawerも用意してあります(こちらも定義しないとインスペクタに何も表示されなくて不安になる)。使う場合は、SerializableDictionaryPropertyDrawerを継承した型を一つ作って、そこに属性でひたすらぶら下げます。
#if UNITY_EDITOR
[UnityEditor.CustomPropertyDrawer(typeof(IntStringSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntDoubleSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntIntStringSerializableDictionary))]
public class ExtendedSerializableDictionaryPropertyDrawer : SerializableDictionaryPropertyDrawer
{
}
#endif
これを定義すれば、インスペクタ上では
なんとKeyとValueの確認しか用意されてなくて、エディット不能!ただのDump!うーん、気が向いたらエディット可能にします。そのうち(多分やらない)。
複数キーの辞書
Int + Intの組み合わせでキーにしたいとか、辞書にはよくあるケースです。そして、そういったよくあるケースではKeyにTupleを使うことが多いです。が、UnityにはTupleはありません。UniRxにTupleがあります、が、それはシリアライズ可能ではありません(Genericだからねー、structなので継承もできない)。と、いうわけで、辞書のキーにしたいよね専用にSerializableTupleを用意しておきました。使う場合はもちろんまずは継承してジェネリックを消すとこからはじめます。
[Serializable]
public class IntIntTuple : SerializableTuple<int, int>
{
public IntIntTuple()
{
}
public IntIntTuple(int item1, int item2)
: base(item1, item2)
{
}
}
[Serializable]
public class IntIntStringSerializableDictionary : SerializableDictionary<IntIntTuple, string>
{
}
あとは普通にキーに使ってもらえれば、普通に使えます。ちょっと手間ですが、そこまで多いわけでもないでしょうし我慢できる範囲内。だといいかな。
SerializableLookup
ILookupは、Keyに対してValue側が複数になっている辞書です。Dictionary[Key, Value[]] みたいなイメージ。通常のILookupはLINQのToLookup経由でしか作成できない、Readonlyなシロモノです。これ、非常に便利な型でして、よく使います。ToLookupしらない人は覚えましょう。ただ、勿論シリアライズできないのでAddを加えたSerializableLookupを用意しました(Removeはありません!つまりBuilderのほうがイメージ近いかもしれません。Removeがない理由は実装しててバグッたからとりあえず消してるだけなのでそのうち入れるかもしれないかもしれない)
使い方はDictionaryと同様。
[Serializable]
public class IntIntSerializableLookup : SerializableLookup<int, int>
{
}
#if UNITY_EDITOR
[UnityEditor.CustomPropertyDrawer(typeof(IntIntSerializableLookup))]
public class ExtendedSerializableLookupPropertyDrawer : SerializableLookupPropertyDrawer
{
}
#endif
ちなみに中身は面倒くさいんでSerializableDictionaryの一部を改変して辻褄合わせてるだけなので、実効速度的な意味ではToLookupで生成したものに比べるとやや劣るかなー、といったところ。まぁハッシュキーの衝突具合とかにもよるので、いうほどそこまでではないと思います。実装の雑さは気にしてるのでそのうち直したい(絶対やらない)
TrimExcess
ListにせよDictionaryにせよ、任意個数をAdd可能なものは、内部である程度余分なバッファを持っています。しかし、ScriptableObjectなどにしてAssetBundleに載せたい場合は、その後の追加なども特になく個数は固定である可能性も少なくないはずです。と、いうわけで、TrimExcessメソッドを呼ぶことで余分なバッファを切り落とすことができます。もし個数が固定であることが見えているなら、事前にSerialize前に呼んであげておくことで、メモリ節約につながります。
Unityでシリアライズ可能なもの
内部構造の話なのですが、その前にUnityでシリアライズ可能なものの制限についておさらい。
- [Serializable]のついた非ジェネリックな具象型
- UnityEngine.Objectを継承した型
- public、または[SerializeField]のついたインスタンスフィールド
- int, float, double, bool, stringなどのプリミティブなデータ型
- 配列、もしくはList[T]
Dictionaryに非対応なのは勿論ですが、Nullable[T]に非対応が割と痛かったりするかな!また、トップレベル以外でnullをサポートしていなかったりして、ちょっと複雑な型を作った場合、nullを入れたと思ったら全部0が入った謎データに置き換わっていた、とかが生じます。それらの制限の回避策としては、SerializableDictionaryと同じようにSerializableNullableのようなそれっぽい似非な型を自前で作ってあげればなんとかなります。nullのほうも同様にNullableClass(なんじゃそりゃ)を作ってあげることにより、nullかそうでないかの区別を可能にできます。面倒くさくはあるんですが、どうしても必要な場合はそうして回避できなくもないよ、ということで。
SerializableDictionaryは、Dictionaryの内部構造を、Unityでシリアライズ可能な範囲(ようするにひたすら単純な配列まみれにする)に修正することで実現しています。オリジナルのDictionary自体はdotonet/corefxのものです。
ハッシュコードを永続化することの安全性
ハッシュコードを永続化することは、推奨されないことが明言されています。というのも、そのオブジェクトに対するハッシュコードが一意であるかが、どのスコープまで保たれるかというのは、全くもって不明瞭だからです。参照型などはアプリケーション起動毎に異なってなにの役にも立たなくなる、では数字は?文字列は?保証はないんですねー。
実用的な意味では、問題ないと判断しても構わないと思っています。しかし、まず、monoと.NET Frameworkのような環境が違うもので生成したもの同士の互換性はないと考えたほうがいいでしょう(実際ない)。また、.NET Framework内だけでも、バージョンが異なれば、違うハッシュアルゴリズムが使われることにより異なるハッシュコードが使われる可能性は全然あります。今後も、Unityのバージョンアップ+(もしあるのなら)monoのバージョンアップが発生した際などは、互換性が崩れる可能性があります。最悪そうなった場合は、任意のComparerを挟み込めるようになっているので、そこで互換性を保ったハッシュコードを返してやることにより、一応大丈夫とはいえます。一応。
とはいえまあ、実用的な意味では大丈夫でしょう。タブンネ。まぁしかし、この辺グレーゾーンなきらいもあるから、Unity公式でサポートってのは難しいんじゃないかなあ、というのはしょうがないかなー。
まとめ
Unityって結局どこで動いているものなのよ、ってのを改めて突きつけられた感じがしました。C++のエンジンがあくまでも主だな、と。また、シリアライズを通して考えると、一見不思議なMonoBehaviourやpublic fieldなども納得がいくように見えてきて、ようするにネイティブとの境界線を接続している場所なんですね。COMとdynamicでやり取りするように、ネイティブレイヤーとフィールドでやり取りする。そう思えば、何もかも腑に落ちてきた気がします(悟り!)。
C#のレイヤーでいかに仕事をさせないかがキモで、そのためにC#を書く。ってのも、まぁ悪くない話だし、Unityアプリケーションとしての整合感やパフォーマンスが最も求めるべきことなのだ。というのは認識しておきたいな、なんて改めて思わさせられました。
Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方
- 2016-01-06
Happy boxing! UniRxの前回リリース(UniRx 5.0.0)でパフォーマンス向上を果たしたと書きましたが、まだやり残したことがありました。それがボックス化(boxing)の殺害です。ボックス化は単純に言うと、せっかくの値型が箱に入っちゃってGCゴミが発生してGCがーーー、というもの。避けれるなら避けるべし。あ、ちなみに今回の内容は特に別にUnityに限らないふつーのC#の話です。
それと、というわけかでUniRx 5.1.0リリースしました、アセットストアから落とせます。基本的な内容は以下に解説するボックス化を徹底的に殺害したことによるパフォーマンス向上です。
ボックス化とジェネリクス
GCって、別に見えてるnewだけで発生するわけでもありません。見えてるものを警戒するのは大事ですが、見えないものを見てないのは片手落ち感が否めない。そんな見えないものの代表例がボックス化です。実際どういう時に発生するのかというと
var x = (object)10;
みんな大好きint(ValueType)がobject(ReferenceType)に!これがボックス化の害です。なるほど、避けたほうが良さそうだ。とはいえこんなのやらないって?ですよね。ではこれは?
void Hoge(object o)
{
}
Hoge(10);
まぁまぁやらないかもしれませんが、まぁまぁやるといえなくもないです。というかやる時はあります。ではこれは?
bool IsSame<T>(T t1, T t2)
{
return t1.Equals(t2);
}
一見何も悪くないのですが、実は悪いです。どこが?
public virtual bool Equals(Object obj);
ここが。ようするにEqualsはobjectになった後に比較されてしまうのです。というわけでボックス化が発生します。ジェネリクスは基本的にボックス化を避けれるのですが、一部のObjectに生えてるメソッド、というかようするにEqualsですが、を触る場合、気をつけないとうっかりしがちです。他に t1.GetType() と書いてもボックス化が発生します。その場合、 typeof(T) と書くことで避けられます。
EqualityComparer<T>を使う
ボックス化を避けた比較を行うインターフェイスにIEquatable<T>があります。
public interface IEquatable<T>
{
bool Equals(T other);
}
これを使い、つまり
bool IsSame<T>(T t1, T t2) where T : IEquatable<T>
{
return t1.Equals(t2);
}
にすればボックス化は避けれる問題なし。ではあるんですが、これでは不便すぎます(さすがにintとかはIEquatable<T>を実装してはいますが、普通の参照型はほとんど実装していないでしょう)。同じなのかどうかとりあえずチェックしたい、Equalsを普通に呼びたいケースは沢山あります。そこでEqualsを外部から渡せるIEqualityComparer<T>インターフェイスと、デフォルト実装を取得するEqualityComparer<T>.Defaultが使えます。
bool IsSame<T>(T t1, T t2)
{
return EqualityComparer<T>.Default.Equals(t1, t2);
}
EqualityComparer<T>.Defaultは、TがIEquatable<T>を実装していればTがIEquatable<T>のEquals(T other)を、実装してなければEquals(object other)を呼んで比較します。これによりめでたく値型のボックス化が避けれました!UniRxでもDistinct、DistinctUntilChanged、ObserveEveryValueChanged、そしてReactivePropertyのSetValueでボックス化が発生していたのですが、UniRx 5.1.0からは発生しなくなっています。なんで今まで発生していたのかというと、EqualityComparer<T>がiOS/AOTで怪しくてあえて避けてたんですが、5.0.0からAOTサポートはきってIL2CPPのみにしたので無事性能向上を果たせました。
UnityとIEquatable<T>
Unityにおいては、それだけでメデタシではなく、もう少し話に続きがあります。Unityにおける代表的な値型であるVector2やRectなどは、全て、IEquatable<T>を実装して、いません。へー。==はオーバーライドされているので、素のままで扱って比較している限りは問題ないのですが、ジェネリックの要素として、また、DictionaryのKeyとして使った場合などでもボックス化が発生しています。
これが地味に困る話で、UniRxにおいてもObserveEveryValueChangedなどでVector2などが流れてくるたびにボックス化が発生したらちょっとよろしくない。
そこで、その対策として今回のUniRx 5.1.0では UnityEqualityComparer.Vector2/Vector3/Vector4/Color/Rect/Bounds/Quaternion というものを用意しました。これら代表的なUnityの値型に関しては、専用のEquals/GetHashCodeを実装してあります。また、 UnityEqualityComparer.GetDefault[T] により、それらが型から取り出せます。普通にUniRxを使っている範囲では(Distinct、DistinctUntilChanged、ObserveEveryValueChangedなど) IEqualityComparer の取得は UnityEqualityComparer.GetDefault[T] を通すようにしているため、極力ボックス化が発生しないようになっています。
ラムダ式と見えないnew
ボックス化、見えないGCゴミの話を書いたので、ついでにもう一つ見えないゴミを発生させるラムダ式について。ラムダ式は実際のところコンパイラ生成の塊みたいなもの、かつ、中身によってかなり生成物が変わってきます。ざっと6通りのパターンを用意してみました。
static int DoubleStatic(int x)
{
return x * 2;
}
int DoubleInstance(int x)
{
return x * 2;
}
void Run()
{
var two = int.Parse("2");
Enumerable.Range(1, 1).Select(DoubleStatic); // 1
Enumerable.Range(1, 2).Select(DoubleInstance); // 2
Enumerable.Range(1, 3).Select(x => x * 2); // 3
Enumerable.Range(1, 4).Select(x => x * two); // 4
Enumerable.Range(1, 5).Select(x => DoubleStatic(x)); // 5
Enumerable.Range(1, 6).Select(x => DoubleInstance(x)); // 6
}
どんな感じになるか想像できました?では、答え合わせ。ちょっと簡略化しているので正確にはもう少しこんがらがった機会生成になっていますが、概ねこんな感じになってます。
static Func<int, int> cacheA;
static Func<int, int> cacheB;
internal static int LambdaA(int x)
{
return x * 2;
}
class Closure
{
internal int two;
internal int LambdaB(int x)
{
return x * two;
}
}
internal static int LambdaC(int x)
{
return DoubleStatic(x);
}
internal static int LambdaD(int x)
{
return DoubleInstance(x);
}
void Run()
{
var two = int.Parse("2");
// 1 - Select(DoubleStatic)
Enumerable.Range(1, 1).Select(new Func<int, int>(DoubleStatic));
// 2 - Select(DoubleInstance)
Enumerable.Range(1, 2).Select(new Func<int, int>(DoubleInstance));
// 3 - Select(x => x * 2)
if(cacheA != null)
{
cacheA = new Func<int, int>(LambdaA);
}
Enumerable.Range(1, 3).Select(cacheA);
// 4 - Select(x => x * two)
var closure = new Closure();
closure.two = two;
Enumerable.Range(1, 4).Select(new Func<int, int>(closure.LambdaB));
// 5 - Select(x => DoubleStatic(x))
if(cacheB != null)
{
cacheB = new Func<int, int>(LambdaC);
}
Enumerable.Range(1, 5).Select(cacheB);
// 6 - Select(x => DoubleInstance(x))
Enumerable.Range(1, 6).Select(new Func<int, int>(LambdaD));
}
それぞれ似ているような違うような、ですよね?一つ一つ見ていきましょう。
パターン1、パターン2はメソッドを直接突っ込む場合。この場合、実際のところはデリゲートを生成して包んでます。そしてこのデリゲートはGCゴミになります。なります。全く見えないんですが地味にそうなってます。と、いうわけで、それを回避するには静的メソッドなら静的フィールドに静的コンストラクタででも事前に作ってキャッシュしておく、インスタンスメソッドの場合は、もし使うシーンがループの内側などの場合は外側で作っておくことで、生成は最小限に抑えられるでしょう。
パターン3は、恐らく最もよく使うラムダ式の形式で、使う値が全てラムダ式の中だけで完結している場合。この場合、自動的に静的にキャッシュを生成してそれを未来永劫使いまわしてくれるので、非常に効率的です。一番良く使う形式が効率的というのは嬉しい、遠慮無くどんどん使おう。
パターン4も、まぁよく使う形式、でしょう。ローカル変数をラムダ式内で使っている(キャプチャ)した場合。この場合、普通にクラスがnewされて、そこにラムダ式内部で使われる値を詰め込み、その自動生成のクラスのインスタンスメソッドを呼ぶ形に変換されます。というわけで、パターン4は見た目は人畜無害ですが、中身はそれなりのゴミ発生器です!いや、まぁたかがクラス一個。であり、されどクラス一個。画面上に大量に配置されるGameObjectのUpdateなどで無自覚に使っていたりすると危なっかしいので、それなりに気を留めておくと精神安定上良いでしょう。
パターン5、パターン6は内部でメソッドを使っている場合。ちなみにここではメソッドにしましたが、フィールドやプロパティでも同じ生成結果になります。抱え込む対象がstaticかinstanceかで変わってきて、staticの場合ならキャッシュされるので少しだけ有利です。
なお、この挙動は現時点でのVisual Studio 2015のC#コンパイラによって吐かれるコードであり(Unityの今のmonoもほぼ一緒、のはず、です、確か多分)、将来的にはそれぞれもう少し効率的になるかもしれません(メソッドを直接突っ込む場合のキャッシュとかは手を加える余地がある気がする)。とはいえ原理を考えたら、外部変数をキャプチャするラムダ式はどうやってもこうなるしかなさそうだったりなので、大筋で変わることはないと思います。
まとめ
正直なところ今回書いたのは細かい話です!別に気にしすぎてもしょうがないし、というかこんなの細部まで気にして避けながら書くのは不可能です。ギチギチに避けてラムダ式禁止だのLINQ禁止だの言い出すなら、早すぎる最適化の一種で、かなり愚かしい話です。が、ゲームの中にはひじょーにタイトな部分は存在するはずで、そこで無自覚に使ってしまうのも大きなダメージです。私だってタイトになることが想定されるUpdateループの中でLINQを貫くならやめろバカであり、普通にペタペタとforで書けとは思いますよ。
あんまりゼロイチで考えないで、柔軟に対処したいところですねえ。どこに使うべきで、使うべきでないか。まぁその見極めがむつかしいから全面禁止とかって話になるのは実際のところ非常によくわかる!のですが、それこそプロファイラで問題発見されてからでもいいじゃん、ぐらいの牧歌的な考えではいます。いやだって、そんなたかがLINQやラムダ式ぐらいであらゆるところがボトルネックになるわけないぢゃん?そんなのより大事なとこ沢山あるでしょう。それに比べたらLINQを普通に使えることのほうが、UniRxを普通に使えることのほうが100億倍素晴らしい。もちろん、地味な積み重ねでダメージが出てくるところであり、そして一個一個は地味だったりするから見つけづらくて辛いとかって話もありつつ。
そんなわけでUniRxは、かなり厳し目に考慮しながら作っているので、比較的概ね性能面でも安心して使えるはずです!まだもう少しやれることが残ってはいるんですが、ちょっと踏み込んで書いてみると謎のuNET weaver errorに見舞われて回避不能で死んでいるので、当面はこの辺が限界です(ほんとuNET絡みのエラーはなんとかして欲しい、理不尽極まりない)。とはいえ、何かネタがあれば継続してより良くしていきますので、よろしくおねがいします。
そういえば第一回Unityアセットコンテストでは、セミファイナリスト頂きました。ほぼほぼスクリプトのみの地味 of 地味なアセットであることを考えると全然上等で、嬉しい話です。
2015年を振り返る
- 2015-12-31
振り返るシリーズ第四弾。去年の目標は
テーマは「クライアントサイドとサーバーサイドをC#で統一することのメリットの実証」「さらにリアルタイムネットワークもC#で統一」「のためのヒットアプリケーションの創出」です。指向はあんま変わってないんですが、より具体的に。来年は動く年かな、といったところ
ようするに会社(グラニ/CTO)でゲーム出して実証する。であり、結果としては……。はい。なので、本当に来年こそはね。というところです。
C#
今年はVS2015のリリースということもあり、かなりRoslynに傾倒しました。その集大成としてのまとめは実例からみるC#でのメタプログラミング用法集にスライドで出してますが、その後にもAnalyzerだけではなく、Roslyn C# Scriptingによる実行できるコンフィグの手法と実活用例やWorkspace APIを使ったプロジェクトコードからのT4生成での応用例といった形で、ただたんに触ってみた、ではなくて、実用的にどう使えるのか、の応用例をきっちり掲示できたんじゃないかと思います。これらの話は実際に使っているもので、便利、かつ世界が広がったのは確かなので、Roslynはこれからどんどん応用的に使われるといいかな。
また、いつになくライブラリ書いてました。会社で必要だから作ってったという面も大きいんですけれど、こうして並べると、実際結構やりましたね、えらいえらい。
まずAPIサーバーとしてLightNodeという自家製フレームワークを採用しているので、Glimpse対応とSwagger対応を入れてます。この対応は大正解で、もはやGlimpseとSwaggerなしでAPI開発していくのは無理ゲーとすら思える。超絶便利。Glimpseは主要開発者がMicrosoft入りしてより専任して開発することになったのと、Swaggerはもはやウェッブ標準といってもいい立ち位置を確立したということで、どちらもメジャーなテクノロジとなったことも含めて、技術選定にも成功したと言えるでせう。ASP.NET 5がまだまだ時間かかるので様子見したのも含めて、現状での最適解ではあるんじゃないかな、と。
RedisライブラリのCloudStructuresもGlimpse対応とStackExchange.Redis対応によって、相当リッチなものになりました。特にGlimpseのは相当気合入れたので充実してて実用度超絶高い。とはいえ本体部分は原則的には薄いラッパーなので、粛々とやってきます、というところですね。
LINQ to BigQueryはLINQPad Driverを作りました!これで実用度が飛躍的に上がりました。世間的にもデータ分析はBigQueryで決まりだよね!という流れが出来上がった年なわけですが、ここも乗り遅れずというか、むしろ引っ張る側に回れたのではないかと自負するところです。あとLINQPadにも相当詳しくなった。
今年最後の新顔はEtwStreamで、↑でLINQPadに詳しくなった結果、応用例が頭に浮かんで一気に実用に載せられました。ETW(Event Tracing for Windows)のビューアーとしてまともに使える世界唯一の解といっても過言ではない(むしろ今までのフベンサがヤバすぎた……)。年末にはRoslyn C# Scriptingによる実行できるコンフィグというコンセプトを打ち出したOut-of-Process Serviceも追加していて、来年はこれの稼働実績を作っていきたいところです。
変わり種だけど誰にとっても実用度100億なのはOpen on GitHubというVS拡張で、もはやこれなしでぎっはぶでコミュニケーションを取るのは無理なのでは疑惑もあるほどに神拡張。地味に12/29にVer 1.4.0のアップデート出してます。
そしてUniRx。今年はお陰様で大躍進の年で、GitHubスター順でも、GitHubで公開されてるUnity用ライブラリでは世界4位と、中々に中々の感じで、いやほんと良かった。
年の始めではReacitvePropertyやPresenterBaseの導入によるModel-View-(Reactive)Presenterというコンセプトの確立が地味に大きめ(ちなみに私は設計とかってこのレベルの話こそ最も大事だと思ってて、コードを小奇麗にするようなレベルの話は設計、ですかねえ?とは思ってる)。真ん中でObservableTriggers、そして年末駆け込みのUniRx 5.0で完全書き直しによるパフォーマンスとデバッガビリティの向上。もはやUniRxなしで書けと言われると困りすぎるぐらいに必要不可欠な存在となれました。
UniRxが成功したポイントは、Reactive Extensions自体が素晴らしいコンセプトで実績もある、というのも勿論そうなんですが、UniRx自体の、Unityへフィットさせるための繋ぎ方の工夫の面も大きいとは自負してます。あまり頭でっかちにならずに柔軟に作り込めたのが良かった。ReacitvePropertyなんかも、実装はシンプルですが、(.NET版を作った時の)コンセプト立証と実装には普通に時間かけてたし、そもそもコロンブスの卵的なところもありましたし、更にそのUnity化でも、削ぎ落とし方には相当神経使ってます(ゴテゴテ足すだけが設計ではない)。
また、お陰様でUnityのスクリプティングと、そしてRxには詳しすぎるほどに詳しくなれました、というかRxに関しては実際隅から隅まで理解した……。
現在未公開だけど予定があるものとしてはPhotonWireというUnity/Photon Server用の非同期RPCフレームワークがあります。これは来年の初頭に公開できればな、ですかねー。
お仕事
書いたライブラリは基本的に会社でフルに使ってるものなので、成果っちゃあ成果です。が、集中力散漫な年でした。成果として些かライブラリ過多になったのは、それが時間の捻出が比較的しやすい(土日とかにちょっと徹夜して気合いれればグッと形にできたりするんで)というのが大きいですね。細切れな時間で、いまいちうまくプロダクトそのものにコミットできなかったのは減点度大きめで、非常に良くない。ライブラリ側の成果と相殺してプラマイゼロと言いたくはあるんですが、私的な理想像とは離れたところにあるので、まぁ、60点ぐらい……(ちょっと甘め)
いろいろと白髪が増える、ハゲる。といったぐらいな感じです、うみぅ。来年は100点目指します。
ゲームとか音楽とか
今年はライブにまぁまぁ行ったんですが、年末のKing Crimsonの来日講演が震えすぎたのでそれが全て。生きててよかったというか生きてるうちに生で聴けることがあるなんて……!内容もばっちしで、いやはやホンモノは永遠にホンモノであり続ける、というかホンモノであり続けようとする姿勢に感服するばかりです。この編成での本気のライブ版が早く出てほすぃ。
面白かったといえばSquarepusherの来日講演も見に行ったのですが、実に面白かった。サイトでは360度動画が公開されてますが、そうした全方位動画やプロジェクションマッピング、VRの未来すら感じられて、思い出すと行けて本当に良かったなぁ。
ゲームはDownwellがオモシロイッス。
漫画は、iPhone 6s Plusを買ってから、スマフォで本や漫画が十分に読めるようになってしまって革命的に体験が変わった。いやあ、大画面スマフォ(ファブレット)は良いですね!一度体験すると、もう小さいのには戻れない。というわけで基本Kindleで買ってiPhoneで読むというインスタントな生活になりました。今年読んだ中だとヴァーチャル・レッドが良かったかなぁ、終始、陰鬱な空気が流れていて息苦しいんだけどどこか心地良くもある。気に入りすぎてKindleで読んだ後に実書籍のほうも買ったんですが装丁がよく出来てて、そちらも満足度高し。
来年
シンプルに、全力で仕事してゲーム出す。ですね。それ以外なし。もちろん、それはC#を全方面で活かした実証結果として成り得るものです。絶対。これは絶対。
私個人としてはヌルヌルとC#でのスクリプティングにだけ篭もりすぎた感あるので、UniRxも一段落したことだし(ちなみに、もう少し性能向上のためのアップデートが控えていてというか現在審査中なので年明け早々にそれはリリースされるでしょう)、シェーダーをそれなりにすらすら書ける程度にはグラフィック処理もできるようにってのは年頭の宿題にしておきます。
UniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成
- 2015-12-21
UniRx(Reactive Extensions for Unity)のVer 5.0が昨日、AssetStoreにリリースされました。前回が4.8.2で6月なので、半年ぶりで、今回はメジャーアップデートとなります。現在の最新であるUnity 5.3(の新機能)に対応というのもあります、が、今回の目玉は書き直しです。半年間なにやっていたかというと、書き直そう!いよいよやっと重い腰を上げてスタックトレースに優しいコードにしよう!と思い立って始めてみたもののメンドウくささが極まって挫折して放置。してたんですが、先月ぐらいに、いい加減に手を付けたくて、ちょっとうちの会社の仕事時間を貰ってゴリゴリ進めてやっと終わりました。
とりあえず分かりやすい成果としては、スタックトレースです。
var rp = new ReactiveProperty<int>();
rp.Where(x => x % 2 == 0)
.Select(x => x * x)
.Take(10)
.Subscribe(x => Debug.Log(x));
rp.Value = 100;
という人畜無害なコードがあるとして、以前のスタックトレースはこうです。
言ってることはわからんでもないコンパイラ生成の何かと、多量の中間物で埋まっていて、実に読み取りにくい。この程度のメソッドチェーンならまだマシで、もっと長大で、複雑なオペレータが絡んでる場合は困難極まってました。私も何度文句を言われて平謝りしたか分からないぐらいです。しかし、今回のバージョンからはこうです。
自動生成コードなし、中間物ナシ。圧倒的な読みやすさ!また、これはそのまま、書いたとおりに動いているということの証左でもあります。実行パイプラインの無駄がスタックトレースに出ているままに皆無になったので、パフォーマンスにも寄与しています(書き換えた今では、もはや前のが厚すぎた説はありますけれど、それはまぁ言わんといてください……)
実装はかなりメンドウで、ラムダ式を使うと問答無用でコンパイラ生成のクラスが吐かれてしまうので、ひたすら名前付きのクラスを作っていくお仕事をしました(一個のオペレーターにつき2~3のクラスを要求する、オーバーロードがあればその分だけ……)。また、Unityのコンソールの出力に合わせた細かい調整を施すことによって(+通常のスタックトレースへの吐かれ方に対しても調整して)作りました。すっかりスタックトレースのことを考えたプログラミングができる脳みそが出来上がったんですが、基本的に面倒くさ度100なので、ふつーのゲーム側のコードでは考えたくないしやりたくもないしやらなくていいと思ふ。
性能改善
じゃあ前のは遅かったのかよ、と言われると、うーん、そんなでもないですよ?、とは言いたいのですけれど、まぁカタログスペック的には実際3~10倍ぐらい速くなってます。これはねぇ、例えばMySQL 5.7が5.6の3倍速い!なるほど、じゃあ5.6はゲロ遅なのか?そうじゃあないっしょー、みたいな話なのですが、実際速くなったのは誰にとっても私にとっても嬉しい話です。
しかし、パフォーマンス低いとか気になるとか、漠然とした話で、何も言ってないに等しいんです。もちろん、3~10倍速くなったというのも何も言っちゃあいないです。プログラムの抱えている範囲に対して広すぎる、漠然としすぎていて何ら指標になっちゃいません。というのは気をつけてください。Rxのパフォーマンスを測るにあたって、フェーズ的に3つあって、
- Observableを構築するフェーズ(さすがにこれはほとんど無視していい)
- Subscribe = Observerを構築するフェーズ
- OnNext
それぞれは独立して考える必要があります。また、ReactivePropertyはSubscribeと同時にOnNextも一回入るのでSubscribe + OnNextである、などなどがあるので、どこをどう測りたいかを明確にし、どう測るかを考えないとザルな結果になります。
基本的に、Rxのチェーンの寿命は長いのでOnNextの性能を最重要視して見るべきです。ここの区別は非常に大事です、長ければチェーン構築コストは相対的に無視できる範囲に収まるのでマイクロな結果で想像するのは違うってものです。が、初回に大量にSubscribeが発生するといった、ローディング的な意味合いでは、Subscribeのフェーズも鑑みる必要があります。
んで、これもザックリとしすぎでアレなんですが、OnNextは3~5倍ぐらい、Subscribeに関しては10~20倍速くなりました。OnNextは全体的なパイプラインの最適化のオペレーターの実装調整が効いてるんですが、Subscribeは抜本的に最適化/単純化したので、以前と全然違う結果になってます。これは、社内で大量のSubscribeがシーンロード初回に発生するという事案がありまして、Subscribeを改善しない限りロード長過ぎで終わぽ、だったのでなんとかしました、はい、すびばせん今まで手付かずで……(ちなみに本家Rx.NETとやり方変えてるので本家Rx.NETよりも速い)
あとのところはオペレーター次第です。WhereとかSelectとか、単純な奴は実装変わってないんで大差ないんですが、一部のメソッドの実装が素朴でしょっぱかったので、そういうのはきっちり直してるので以前のと全然性能変わってきてます。特にObserveOnが顕著かな。また、Observable.IntervalやTimerなどの一部の時間系メソッドも構造がガラッと変わってるので(MainThreadScheduler/ThreadPoolSchedulerが使われる場合には最適化パスを通るようにしてる)、かなり良好な結果が得られるのではないかと。
全体的にGCゴミも減ってます。まだもう少し減らせるポイントが残ってるので、次のマイナーアップデートではその辺の処理をする予定デス。
リリースノート
今回の。
破壊的変更:
iOS/AOTサポートは切りました。IL2CPPしかサポートしません。
Unit/Tuple/CancellationToken/TimeInterval/Timestampedをclassからstructに変えました。
MainThreadDispatcher.Postのメソッドシグネチャが変わり、T stateを要求します。
ObservableMonoBehaviour/TypedMonoBehaviourがObsoleteになりました。
AotSafe Extensions(WrapValueToClass)を消しました。
InputField.OnValueChangeAsObservableをOnValueChangedAsObservableにリネームしています(Unity 5.3の場合。Unity 5.3でInputField側で同様の変更が入っているため)
Subscribe in SubscribeでのException Durabilityを保証します。
追加メソッド/クラス:
Observable.ForEachAsync
Observable.Take(duration)
Observable.Aggregate
Observable.Zip(T3~T7)
Observable.CombineLatest(T3~T7)
Observable.Start(function, timeSpan)
Observable.ToYieldInstruction in Unity 5.3
Observable.DoOnError
Observable.DoOnCompleted
Observable.DoOnTerminate
Observable.DoOnSubscribe
Observable.DoOnCancel
Observable.CreateSafe
Progress
StableCompositeDisposable
MultilineReactivePropertyAttribute
その他色々修正:
色々色々(詳しくはGitHubのとこの正式なリリースノート見てくだしあ)
破壊的変更といっても、直撃することはないんじゃないかなあ、と思ってます。ただ社内ではUnit/Tupleのstructへの変更で引っかかったりはしました(想定外にもnullが代入されている場合があった!)。それは適切にdefault使うのと、Tupleに関してはTuple?にするなりする程度で対応はできます。struct化はAOTサポートを切ることで躊躇いなくできるようになって、ヨイことだなー、と。コードも全体的にAOTサポートのための余計なコードを順次切り落としています(パフォーマンスロスに繋がっていたので)。その辺はIL2CPPバンザイ、ですかねえ。
vs IL2CPP - Runtime UnitTest Runnner
IL2CPP万歳と言ったそばから言うのもアレですが、IL2CPP苦しい……。コンパイル死ぬほど遅いし、というのはおいておいても、まだ地雷は埋まっていて、たまに踏んで死ぬんですよね。その場合IL2CPPのバグなんで報告して直してもらうってことになるんですが、それはそれとして、なんで死ぬのかがAOTの場合は想像ついたし対処も比較的容易だったんですが、IL2CPPは踏むまで地雷かどうかを察知することが不能な上に、踏んだら踏んだで、何を踏んだからこうなったかがイマイチ分からなくて最小ケース作ってバグレポも辛いケースもちらほら。
とはいえ、それなりに安定してきてるのは確かだと思います。偉い。そこは賞賛されるべき。
のはいいんですが、実行するまで分からないじゃ(特にライブラリ側としては)困るので、iOS実機でユニットテストを動かしたいと思いました。Unity 5.3からEditor Test Runnerなども標準で入ってきましたが、端的に言えば、欲しいのはそれじゃない。実機で動かしたいの!エディターでの実行はどうでもいいの!
エディター上での実行も大事なんですが、元々UniRxは.NET用ライブラリとしても動くように設計されていて、ユニットテストも.NET用ライブラリとしてMSTestで書かれている(!)という特殊な環境なので、エディターでのテストサポートは完全に不要なのです。いや、だってVSのテストランナー使ったほうがやりやすいじゃん?
そうやってユニットテスト自体は書かれてるし、さすがに実機用に別のを書きなおすのは不可能なので、このユニットテストを実機で動かせるように持ってければそれでいいんだよねー。
ここで出てくるのがRoslyn。Roslynを使ってユニットテストプロジェクト内のユニットテストを、ソースコードのファイル単位ではなく、解析可能な構文木単位で取得し、T4 Text Templateで整形して吐き出せちゃえばいいんだ、という合わせ技で運搬することに成功しました。VS2015だから出来るハック、VS2015最高……。さすがにコード持ってくだけではMSTestの実体がなくて動かないんですが、そこは適当にモック(Shim)を用意して回避しました。
エクストリーム雑なUI。エラーが出た場合は赤くなってExceptionを表示します。これで、ちゃんとiOS/IL2CPPで全部パスしてるのを確認済みです。
ちなみにこのRoslyn + T4でコード生成するテクニック、今回のように別プロジェクトをターゲットにして運搬するというのもいいんですが、自プロジェクトを対象にすることもできます。T4で生成するためのコードのタネって、今まではT4側に書くしかなくて面倒だったんですが、もうその制限はありません。ありとあらゆるソースコードがコード生成のためのタネとして使えます。メタプログラミングの扉をまた一つ開いてしまった。
このテクニックは私の発明じゃなくてRoslynをT4テンプレート内で使う - ぷろじぇくと、みすじら。から拝借してますので、気になる人はそちらの記事をどうぞ。l
Unhandled Exception Durability
UniRx 5.0の変更のうち、ちょっとだけ重要なのがUnhandled Exception Durabilityというコンセプト。です。これは、Rxでイベントハンドリングするのはいいんだけどエラーでるとイベント購読が吹っ飛ぶの困るんだよねー、に対するUniRxからの回答ということで。内容ですが、Subscribe in Subscribe時の例外を外側に伝搬「しない」ことを保証しています(逆に言えば実は4.8では保証されてなくて解除されたりしてました。ちなみにRx.NETでも保証されてなくて解除されたりされなかったりします、ここはUniRx独自で挙動を明言する形に倒しています)。伝搬しない、というのは握りつぶすという意味ではなくて、ObservableのDispose処理を行わない、という意味です(例外自体はグローバルに飛ぶのでUnityのConsoleにExceptionが表示されるし、ログイベントでちゃんと捉えられます)
button.OnClickAsObservable().Subscribe(_ =>
{
// もし内側でエラーが発生しても、外側のOnClickがデタッチされることはない
ObservableWWW.Get("htttp://error/").Subscribe(x =>
{
Debug.Log(x);
});
});
エラーハンドリングは難しい問題で、RxJavaのErrorHandlingの章を読んでも別にそんなワカラナイよね、とかって感じではある。UniRxでは Retry/OnErrorRetry でハンドルできなくはなく、まぁそれがスタンダードなRx WayではあるんですがRxJS の Operators (6) - Observable のエラーハンドリングのまとめコメント「これで本当にエラーハンドリングに十分なのか不安です。」とあるように、実に不安です。
で、入力用のハンドラーが吹っ飛ぶのは致命傷なので、どうしても救いたいその辺のとこに関してはSubscribe in Subscribeで処理するのがいいんじゃないかなー、というのを提唱します。入力イベントを合成したいって局面も多いと思うので、それはそれで合成してもらったうえで(そして、その合成パイプラインに関してはエラーが出ないよう厳重に作る!)、それを入力ストリームだと考えて、そこから先はSubscribe in Subscribe。あまり格好の良いものではないのも事実ですが、現実的っちゃあ現実的かなー、と。ちなみにこの挙動を保証するのはUniRxだけだと思うので他のRx系に持ってっても動きません(多分)
なお、Subscribe in Subscribeでの例外で解除されないのは最上流がHot Observableのものだけです。HotとColdに関してはRxのHotとColdについてなどを参照するといいと思いますが、とりあえず具体的にHotなのはUniRxデフォルトでは FromEvent/Subject/ReactiveProperty/ObservableTriggers/UnityUI.AsObservable です。ようはイベント的なやつです。Coldなのは Return/Interval/Timer/FromCoroutine などで、これらは例外で解除されます(そうじゃないとTimerとか無限に動き続けられても危なくて困るでしょ?FromCoroutineだって途中でエラーが出てる状態なのに回られても困るでしょ?)
CustomYieldInstuction
書き直しはいいんだけど、何か新機能ないと寂しいよなー、ということで、Unity 5.3用に一つ入れました。Unityブログでもカスタムコルーチンとして紹介されていますが、Unity 5.3からCustomYieldInstructionが搭載されました。というわけでUniRxもUnity 5.3以上ならToYieldInsturctionメソッドが使えるようになっています。
IEnumerator TestNewCustomYieldInstruction()
{
// Rx Observableをyield returnで待ちます.
yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
// スケジューラを変える(Time.scaleを無視する)とかも当然可能
yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();
// 戻り値を得る場合はObservableYieldInstructionを変数に取れば、Result/Errorで受け取れます
var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
yield return o;
if (o.HasError) { Debug.Log(o.Error.ToString()); }
if (o.HasResult) { Debug.Log(o.Result); }
// 当然こういう長めのものだって自由に書けます
yield return this.transform.ObserveEveryValueChanged(x => x.position)
.FirstOrDefault(p => p.y >= 100)
.ToYieldInstruction();
}
今までもToAwaitableEnumerator/StartAsCoroutineというメソッドで同様なことを出来るようにしていたのですが、ToYieldInsturctionのほうが効率的だし、使いやすいです。ToYieldInsturctionによるObservable->Coroutine変換のオーバーヘッドはないといっても過言ではない!Unity 5.3最高!
ちなみに、このToYieldInsturctionはCustomYieldInstructionクラスを実装してません。Unity 5.3のカスタムコルーチン対応というのは、yield returnでIEnumeratorを受け取ると毎フレームMoveNextを呼び出して待機する、というのが正しい話です。CustomYieldInstructionはあくまでIEnumerator実装のためのちょっとしたヘルパーなので、別にそれにこだわる必要はありません、ということで普通に独自の軽量なIEnumerator実装を刺しています。
ちなみに実行されるタイミングはCustomYieldInstructionの説明によると after MonoBehaviour.Update and before MonoBehaviour.LateUpdate だそうなので、実行タイミング調整のネタに使えるかもしれません。
まとめ
実際のトコver 2.0なんですが、諸事情で4始まりなのでver 5.0です!Unityのメジャーバージョンと偶然揃ったしいっか、という気がしますね!今回のコードはかなり自信あって、パフォーマンスがー、な局面であってもお薦めできます。どうせ、ライトウェイトを冠した超機能限定版の同じようなものを実装するなら、性能面であっても素直にUniRxを使ったほうがいいでしょう。と、言えます。言えます。
今月頭に書いたUnity 5.3のMulti Scene EditingをUniRxによるシーンナビゲーションで統合するなどのように、UniRxを前提に置くことで、やれることが大幅に広がります。根底から入れれば全体のプログラミングの世界観が(良くも悪くも)大きく変わります。が、まぁそれはエキセントリックすぎるということであれば、触りは単純なところからでも全然アリかな、とは。思います。特に非同期/マルチスレッド関連は、変なライブラリ入れるよりもずっと良いでしょう。
ところで半年前、今年6月に第一回UniRx勉強会を開催しましたが、第二回の需要ってありますか?もしありましたら、その前に発表者が必要!なので、是非話したい!人は、私のTwitterかメールかに連絡ください。開催するにも発表者いなければ開催もなにもないですからね……!
ついでにもはや触れちゃいけない扱いの気がしなくもないUnity アセットコンテストというのに応募していたのですが結果発表……。