読者です 読者をやめる 読者になる 読者になる

diary

日記です

UnityでEditor拡張から高速にC#コードを書いたり消したりする時のお作法

TL;DR

  • 書いてる最中からコンパイルされてEditor拡張が走ってるアセンブリが猛然と破棄されたりする
  • EditorApplication.LockReloadAssemblies, EditorApplication.UnlockReloadAssembliesで囲め
  • AssetDatabase.StartAssetEditing, AssetDatabase.StopAssetEditingで囲め

どういうことか

Unityを使ってるとそのだるさから自動生成によるコード生成を行ったり、パッケージ管理がだるすぎて思わずパッケージマネージャを自作してしまうことがあると思う。

そのような場合には高速にスクリプトからC#コードを生成したり削除したりするわけだが、Unityはせっかちなので生成してるそばからコンパイルを始めて、実行中のコードがアセンブリごと破棄されたり、不完全なコンパイルがされたり、AssetDatabaseが一時的にぶっ壊れたりする。死ね

そのような場合には適切なAPIを各位やっていく必要がある

対処

AssetDatabaseの更新をスクリプトのコード編集中だけロックしてあげればいい。ついでにアセンブリリロードもロックしておくと安心できる。ちなみに超ステートフルなAPIとして実装されてるので、きっちり対応させて呼ばないとUnityが実質固まったり、いくらまってもあたらしいアセンブリがロードされなくなったりする。死ね

我々には文明があるので、IDisposableを実装する形で実装して、usingで適切にやっていけばよい

実装

以下のようなコードを書いた上で、高速にC#コードを書いたり消したりするコードブロックをusingで囲んでいく

using System;
public class LockReloadAssembliesScope : IDisposable
{
  public bool IsDisposed { get; private set; }
  public LockReloadAssembliesScope()
  {
    EditorApplication.LockReloadAssemblies();
    IsDisposed = false;
  }
  public void Dispose()
  {
    if (!IsDisposed)
    {
      EditorApplication.UnlockReloadAssemblies();
      IsDisposed = true;
    }
  }
}

public class AssetEditingScope : IDisposable
{
  public bool IsDisposed { get; private set; }
  public AssetEditingScope()
  {
    AssetDatabase.StartAssetEditing();
    IsDisposed = false;
  }
  public void Dispose()
  {
    if (!IsDisposed)
    {
      AssetDatabase.StopAssetEditing();
      IsDisposed = true;
    }
  }
}

IL2CPPに優しく、かつタイプセーフにDictionary<TKey,TValue>を使う

前提

UnityのIL2CPPを使ったビルドにおいて、Dictionary<TKey, TValue>を使うとすごい勢いでバイナリサイズが大きくなっていく。

iOSにおいてはUniversalBinaryで出力を余儀なくされることが多いのと、実行バイナリがストア上では暗号化される問題もあり、ひとつの組み合わせで100KB前後の水準で、最終的なダウンロードサイズが増大する。

"Generics使わなきゃいいじゃん"みたいなのは思考停止の原人の所業なので、きちんと型を大切にしながらIL2CPPによる生成コードサイズを削るにはどうすればいいのかを考えるのが、現代に生きる文明人に求められる仕事になる。

追い込まれた時こそ、先に踏み込んで行く必要がある。

分析

まずは何故Dictionaryがこれほどまでに膨らむかを考える。Dictionaryは比較的高級なコンテナクラスなので、内部実装として多くのものを抱え込んでいる。

corefx/Dictionary.cs at master · dotnet/corefx · GitHub

さもありなんという感じである。(Unityの実装はこれではないが、参考までに。。。)

これを全部のTKey, TValueの組み合わせでジェネリクスインスタンス化されてしまったらなすすべがないが、IL2CPPは型パラメータが参照型な場合に限っては生成したコードを共用してくれる仕組みが入っている。つまり、できる限りコードが共用されるような使い方をすれば、Dictionaryを使ってもバイナリサイズへのインパクトを少なくすることができる。

対策

値型を使う限り、どこかでコードサイズが膨らむのは避けられない。ただ、それはDictionaryみたいな高級なクラスで行われるべきではない、という理論で対策していく。 要するに任意の値型を参照型として扱えるようにして、Dictionary<TKey, TValue>に関しては実行コードが極力共有されるようにすればいい。 そうとわかれば割と簡単で、以下のような実装をしてみればいい。

public class Boxing<TValue>
{
    public TValue Value { get; set; }
    public Boxing(TValue value) { this.Value = value; }
    public override int GetHashCode() { return Value.GetHashCode(); }
    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Boxing<TValue>))
        {
            return false;
        }
        return Value.Equals(((Boxing<TValue>)obj).Value);
    }
    public static implicit operator TValue(Boxing<TValue> boxing)
    {
        return boxing.Value;
    }
    public static implicit operator Boxing<TValue>(TValue value)
    {
        return new Boxing<TValue>(value);
    }
}

要するに、手動Boxingをできるようにする。 この方法が良いのは、相互に暗黙的キャスト演算子を定義しておくことで、変更自体は最小の量にして、リスクを下げることができること、Dictionaryとしての機能自体は今まで通りほぼフルスペックで使えること。 一応記しておくと、適用するには

Dictionary<SomeStruct, SomeClass> dictionary;

みたいな定義をまるっと

Dictionary<Boxing<SomeStruct>, SomeClass> dictionary;

に置き換えていけばいい。大体はいけるが、たまに暗黙的キャストで解決できない場合とか、もろもろあるので、適宜やっていく。 凄くダーティーハックな感じはするが、実際大規模プロダクトだとこれだけでMB単位でのコード削減が見込めるので、費用対効果はすごく良い。

トレードオフ

楽ではあるが、都度都度Boxingが走ることになるので、負荷的にはやはり厳しいものはある。 1フレームで多数呼び出すような場合なら、そのままにするなり、せめてenumをintにキャストして扱うなどの対処ですましておくのが良い。 また、Dictionary以外で、たとえばインターフェース制約がかかってる型パラメータに対して適用する場合は、当然それ用にBoxingクラスを実装する必要が出てくる。

そもそもなんで共用できるのか?むしろなぜ共用できない場合があるのか?

なんでかといえば、要するにそのジェネリクスインスタンスのスタックサイズに差が生まれるからだ。 値型はスタックに配置するときに直接配置されるため、それぞれの型ごとにスタック上のサイズが決まる。一方で、参照型はスタック上ではほぼポインタとしてしか扱われないので、どんな型だろうと占有するスタックサイズは同一になる。 なので、値型/参照型の組み合わせによって、インスタンス化後に生成される実行コードそのものが別のものにならなければならないことになってくる。

CLRでの実行時は、この差はジェネリクスインスタンス化時にJITコンパイルすることによって解決されているので、ビルドサイズの問題には結び付かない。しかし、IL2CPP(または、AOT)の場合には、コンパイル時にこれらをすべてインスタンス化しておかなければならないため、この問題が表面化する。

結論

こんなことしないで済むのが一番なんで、早めにビルドサイズには気を使って、やっていきましょう。

コマンドラインから使える理想のC#コードフォーマッタがほしいという気持ち

この記事は Unity 2 Advent Calendar 2015 9日目の記事です

概要

はじめましての方ははじめまして。ヒカリエのあたりでリアクティブUnityおじさんをしているtrapezoidです。Haruto Otakeとも言います。よろしくお願いいたします。

この記事は、OSXでUnityを使っているWeb上がりのゲーム基盤開発エンジニアが、Unityの(C#の)コードフォーマットを巡る悲哀をNRefactoryの力で解決したという、リアクティブプログラミングもUnityもあまり関係のない話をします。

Unityにおけるコードフォーマッタ事情

現状、"機能としては"どのプラットフォーム上のIDEも実用上十分な性能のコードフォーマッタを持っている。

ただ、Visual Studioは基本的にはなんの問題もないんだが、MonoDevelopで深刻なのが、Unityがコードフォーマット設定を上書きするという挙動のせいで、プロジェクト切り替え時などに唐突にコードフォーマット設定がぶっ飛ぶ。

気付かないで作業し続けて巨大なdiffになってるのにPull Request送った時に気付く、とかよくある。僕は基本的に基盤開発が仕事なので、一日百万回ぐらいこれが発生して発狂しそうになる。

OmniSharpならこんな問題はないけれど、OmniSharpでの開発を強制するのはさすがに自由無さ過ぎてつらい。

Unity5.3で実はこの挙動自体が治ったりしてるんだが、さすがに入れれるのはもう少し先になるし、コミットフックも仕込みたい。 それに、上記の挙動のせいで既存コードには異なるコードフォーマットが混在してしまっているので、一度まとめて何とかする必要がどのみちあった。

既存のコマンドラインベースのC#コードフォーマッタ

そうなったときにやはりコマンドラインベースでコードフォーマッタがほしい、みたいな話になるけれど、MonoDevelop/OmniSharpのコードフォーマッタはそのままコマンドラインでたたける形になってない(調べた限り)。そこで他の選択肢を探すのだが、

  • MonoDevelop/OmniSharp主体の開発体制で使っている既存のフォーマット定義をなるべくそのまま利用したい、IDE上でフォーマットしたときと結果が一致するようにしたい
    • コミットフックでかかるとはいえ、書いてる途中に把握のためにコードフォーマットかけることも多いし、その状況で一々差分吐かれると非常に面倒。
  • クロスプラットフォームで動いてほしい

みたいなことを考え出すと、既存の選択肢だとそれぞれ色々と問題があった。

CodeFormatter

dotnet/codeformatter · GitHub

RoslynベースのMicrosoft製コードフォーマッタ。コマンドライン、今の設定を移行させるのが激しくめんどくさい。(チームのコーディング規約側を変えてやろうかと思った...) あと、Microsoft Build Tools 2015が必須なせいか、そのままだとMonoで動かなかった。上記の理由で選ばなかったので追っていない。

Artistic Style

Artistic Style - Index

古より存在するコードフォーマッタ。Cと名の付く言語とJavaなら全部フォーマットできますよという懐の深さを持っているが、設定はMonoDevelop/OmniSharpほど細かくはできない。 UniRxとかLINQとか使ってると、めっちゃ見辛いインデントにしてきて完全につらい気持ちになった。 基本UniRXとLINQ絡まないコード書くことのほうが少ないので、これは無し。

ないので作る

そもそもMonoDevelop/OmniSharpがどうやってるのやという話で、まああいつらのことだしどうせNRefactoryにあるんやろ、と考えて見てみたら、実際あった。

NRefactory/ICSharpCode.NRefactory.CSharp/Formatter at master · icsharpcode/NRefactory · GitHub

このへん。使い方としてはCSharpFormatterにCSharpFormattingOptionsとTextEditorOptionsを食わせて、あとはソースコードをstringでぶちこむだけ。NRefactoryはすごい。

CSharpFormattingOptionsとTextEditorOptionsは一々指定するのはない話なので、両方まとめてXmlSerializerで読み書きするとコードフォーマット定義ファイルの出来上がりです。あとはCLIアプリとしての体裁を整えるだけ。

そのうち公開したいけど、本当に設定とコードのテキストぶち込むだけなので、諸々含めてもせいぜい2桁行に収まる規模にしかならんのでやめました。 とりあえず各位気持ちで書いて気持ちでやっていってください。

以上。

次はmasakam1さんです。

Unity 5.3のCustomYieldInstructionの話

前提

Unityでは古来よりフレームを跨ぐ処理にCoroutineと呼ばれる仕組みが用いられている。 実態としてはC#に備わっているyield構文による、値の列挙を目的としたコルーチンの生成機能を利用している形になる。

仕組みとしては、上記構文を利用して生成したIEnumeratorをStartCoroutineを用いてUnityのシステム側に渡すと、毎フレーム1回、UnityがそのIEnumeratorのMoveNextを呼んでくれる。

yield returnした値はCurrentに入るわけだが、それがYieldInstructionのサブクラスであった場合のみ、そのYieldInstructionが表す非同期処理が終わるまで次のMoveNextの呼び出しを行わないようにしてくれる。

StartCoroutineはIEnumeratorを引数に取り、Coroutineを返す。CoroutineはYieldInstructionのサブクラスなので、Coroutineをyield returnすればCoroutineの中でCoroutineを待てるようになっている。

ただし、YieldInstructionは内部実装に利用されているクラスで、C#スクリプト層から独自のYieldInstructionを実装するようなことは、今まではできなかった。(型の定義のみで何も定義されていないという、割とC#の常識破りなクラスになっている...)

CustomYieldInstruction

Unity 5.3では、独自のYieldInstructionを実装する手段として、CustomYieldInstructionというクラスが追加された。

blogs.unity3d.com

実装方法自体は上記記事にあるように、CustomYieldInstructionを継承してkeepWaitingをoverrideするだけでいい。

本題

実は今回のCustomYieldInstructionの追加で、ドキュメントにさりげなく記載されている、わりかし重大な変更がある。(公式Blogに記事出すなら、このことにも触れてほしかった。。。)

実は、IEnumerator自体もCoroutineとしてみなして実行するようになっているのだ。

docs.unity3d.com

つまり、今までは

IEnumerator IncludeNestedCoroutine()
{
    yield return StartCoroutine(SomeCoroutine());
    Debug.Log("4th Frame");
}

IEnumerator SomeCoroutine()
{
    Debug.Log("1st Frame");
    yield return null;
    Debug.Log("2nd Frame");
    yield return null;
    Debug.Log("3rd Frame");
    yield return null;
}

と描いていた処理が

IEnumerator IncludeNestedCoroutine()
{
    yield return SomeCoroutine();
    Debug.Log("4th Frame");
}

IEnumerator SomeCoroutine()
{
    //省略
}

このように、StartCoroutineなしで実現できるようになった。

今まででもIEnumeratorを実装して、StartCoroutineを挟みさえすれば(冗長だが)独自YieldInstructionらしきものは作れはしていたのだが、 うっかりCurrentとして自身を返す実装をしてしまっていたりすると、StackOverflowExceptionを延々と吐くようになる。

試しにアップデートしたらいきなりStackOverflowException吐くようになってマジで面食らったので、一応共有。

対処は簡単ではあるんだけど、もうちょっと大々的に告知して欲しかったなあ。。。

MDR-1ABT買った

trapezoid.hatenablog.com

この記事で紹介したMDR-1RBTMK2の正当後継機、MDR-1ABTが先日発売されたので、発売日に早速購入してきた。 これでこのシリーズは3世代全部買ってる事になる。

装着感の改良

1RBTMK2よりもイヤーパッドが厚くなり、より立体感のある縫製になった。人間の人体、耳の周りは平坦ではないと思うので、それに合わせていい感じに分厚くなるように縫製されている。 あとはヘッドバンドに対してハウジングの角度が少しつく(ハウジングとスイーベル機構つなげる軸が、スイーベルの軸に対して平行でないように)ようになった為、より耳にハマるようになった。

操作性の改良

耳からはずして肩掛けして持ち歩いていると、ふとしたときにボタンが誤って押されるという自体が偶にあったのだが、1ABTから音量/曲送り/曲戻し/再生/停止は左のハウジングに搭載された静電センサで行うようになった。 タンクトップにこれだけみたいな感じの黒人スタイルだと誤動作起こしそうだけど、数日使った感じでは勝手に再生されたりするみたいなことは無いのでいい感じ。後述するがインターフェースがスッキリしたのにもだいぶ寄与していると思う

接続性の改良

microUSB充電端子のキャップが薄くなり、またカバーが柔らかい素材から硬い素材+ラッチで奥で止めてあるみたいな感じ?の可動性の高い作りになった。今まではカバー持ったままケーブル刺さないとないとすぐに戻ってしまってつらかったので、地味に良い改良。 ステレオミニジャックに関してはカバーがなくなり、右ハウジング側に分離された。防水じゃないんだしカバーあってもな・・・て思ってたし、音ゲーマーは頻繁にこれの抜き差ししたいという需要あるので、良い改良。 存在意義のわからなかったNFC有効/無効スイッチも消滅した。 前述した操作系の静電センサ化とあわせてボタン類の数は激減したので、見た目かなりスマートな感じになったという印象。

やっとカラーリングが増えて、シルバーも発売された。 迷わずシルバーにしました

LDAC端末を持ってないのでなんとも。Z3 Compactでも使えるようにしてくれないかな...

空中マウス買った

Amazon.co.jp: T2エアマウス 空中で使えるワイヤレスマウス 2.4GHz Fly Air Mouse: パソコン・周辺機器

空中マウス買った。 サンワサプライのOEM元っぽいやつが約2000円とかだったので、ゴミならゴミでいいかという気持ちだったのだけど意外と使えた。

所感

意外と小さい。 そしてボタンがやたら多い。

スクロールがPageUp/PageDownキーと矢印キーの扱いだったりアサインされてる場所が微妙だったりでこのへんは若干つらい。

肝心の空中マウス部分は挙動見る感じ3軸ジャイロなので、角速度つける感じの動きをする必要がある。 精度欲しければ肘固定するイメージで動かして、速度欲しい場合は手首を回す感じで動かすと良かった。 手に持って静止している状態でポインタも静止するぐらいの感度になっているようで、まあ若干思いどうりにならないこともあるけど慣れればいけるかなっていう使い心地だった。

あと、重力も見ているので雑に斜めになったまま握っても上下逆にしてもきちんと動く。安物なのでこの辺心配だったけど問題は無いようだ。

用途

主な目的はHMZ-T3装着時の没入感を上げることだった。

Amazon.co.jp: ソニー ヘッドマウントディスプレイ “Personal 3D Viewer" HMZ-T3: 家電・カメラ

HMZ-T3装着しながら試してみたが、目的の動画選んで再生するとかなら全然いける。

これでベッドに寝ながらHMZ-T3装着し続けるという運用が可能になった。

f:id:Trapezoid:20150214112546j:plain

今年の目標はVITATVとSAOロストソングを買ってHMZ-T3でリンクスタートする事です。

今年もよろしくお願いします。

Amazon.co.jp: PlayStation Vita TV Value Pack (VTE-1000AA01): ゲーム

Amazon.co.jp: ソードアート・オンライン ―ロスト・ソング― 初回限定生産版 (初回限定特典 ゲーム内で使用出来るアイテムが解放されるプロダクトコード 同梱): ゲーム

boxenを捨てた話

boxenを導入した話 - diary

本件ですが、brew caskとシェルスクリプトで何の問題もないので捨てました。

ご了承下さい