ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

SpriteAtlasをコード上で編集する

Unity2017から導入されたSpriteAtlasという仕組み。
これは、複数の画像を一つの大きな画像にまとめて、描画命令を少なくしてパフォーマンスに貢献するための仕組みです。

詳しくはこれらを読むとわかりやすい。かも。

tsubakit1.hateblo.jp

kan-kikuchi.hatenablog.com

SpriteAtlasは、どれをパッキングするかをフォルダで指定すればそれだけであとはよしなにやってくれるとても便利な仕組みです。
ビルド時にはAtlasは完成しちゃっているので、ゲーム実行中にこのAtlasファイルをいじることは残念ながらできないのですが、エディタ上では可能です。
既存の画像をちょっといじって、いじってできた方の画像をパッキングする、みたいなことがコードでできるわけです。

こういうときに使えるのが、SpriteAtlasExtensionsというクラス

docs.unity3d.com

UnityEditor.U2D名前空間にあります。(U2Dって何なんでしょうね)
このクラスの持っている関数を使ってやると、パッキングの設定や、何をパッキングするのかなどをコード上で編集できるわけです。

この辺をいじると、普段意識することなく利用しているUnityEngine.Object型や、フォルダの扱いなどを改めて見直すことになってなかなか新鮮です。

なお、.spriteatlasファイルをテキストファイルとして開いてみると、中身のYAML自体はめちゃくちゃ単純なことがわかります。
fileIDやGUIDの意味や取得方法をしっかり理解していれば、SpriteAtlasExtensionsが無くても地道にテキストファイルを編集すればなんとかなりそうです。

GUIDについてはこちらの記事で説明しています。

nigiri.hatenablog.com

簡単めな記事を頻度上げて投稿したいお気持ち

慣れないうちは技術ブログっぽいことを目指しても難しいなぁと思う今日この頃。

2,3本、実用できなくもないものを上げましたが、書くのに想像以上の時間がかかってしまっています。 コンスタントに更新してきたい気持ちがあるので、これからは簡単めな記事にしようかなと考えています。
こんなクラスがあるよーとか、わかりやすい記事見つけたよーとか、そんなの。

平日出社して休日ゲームして自分の趣味コード書いてってやってると時間がとにかく足りないんですが、自分の勉強したことをとりあえず残すのはやはり大事だと思うので、なんとかブログは続けていきたいですね。

UnityでGUIDをいじって楽する

Unityの適当なシーン(.unity)やプレハブをテキストで開いてみると、

m_Script: {fileID: 何らかの数字列, guid: 何らかの文字列, type: 3}

こんな行が見つかるはずです。 「テキストファイルで開く」ができない場合は Edit -> Project Setting -> Editor -> Asset SerializationのModeをForce Textにしましょう。

さて、このguid何かというと、アセットのIDです。 Projectの中のどのAssetを参照しているのかがこれで特定できるようになっています。 m_Script: の行に書いてあるので、何かスクリプトを参照しているようですね。 fileIDに関しては今回は説明しませんが、こちらのブログでわかりやすく説明されています。 渋谷ほととぎす通信 Unity Prefabの中身(YAML)を読んで参照関係を正しく理解する

www.shibuya24.info

.unityや.prefabは先ほど書いたようにどのアセットを参照するかをGUIDで持っていましたが、ということはアセット側は全部自分のGUIDを持っているということですね。これは.metaファイルを見ることで確認することができます。付け加えるならば、GUIDはmetaファイルが生成されたタイミングで生成されます。GUIDは

docs.unity3d.com

こちらを用いることで取得できます。 尚、ファイルパスを参考にしてGUIDは生成されているようですが、同じパスだからといって同じGUIDが生成されるわけではありません。
というか生成されるたびに変わります。
あくまでもこれはパスを指定したらそのmetaファイルに書いてあるGUIDを取得してくる便利関数です。

ここまでGUIDの基礎知識を得たところで、ということは、.unityや.prefabの持っているアセット参照用guidを置換してしまえばアセットの参照を置換することにもなるのでは? と思うはず。
実際その通りです。 この画像やっぱりこっちの新しいのに置き換えたいなぁ……でも結構いろんなシーンで使ってて、どこで使われているか探すの大変だなぁ。
そういう際は.unityと.prefabの古い画像のguidを全検索し、新しい画像のguidに置換してやるだけでOK!
ただのテキストファイルの編集なので、なんでも好きなもの使って直接書き換えてやりましょう!
私は諸事情あってエディタ拡張で作らざるを得なかったのでC#で書きました。

ローカル関数とラムダ式

私は普段仕事でも趣味でもUnityを使っているので、基本的に用いる言語はC#です。

ゲームプログラミングで重要なのはやはり速度ですが、複数人で開発したり長期的に改修が続く場合は可読性も大切です。 Unityでは読みやすいC#でプログラミングして、それが最終的に速いC++に変換することで、可読性と速度をそれなりに担保している、と私は考えています。 ただ、その際に結構大胆な変換が行われていて、単にC#の文法を学ぶだけでは、実は速度的に不利な書き方をしている、ということがよくあります。

C#コンパイルされてIL(中間言語)になり、さらにIL2CPP(その名の通り、IL to C++ということ)でC++に変換されるわけですが、一つ目のILへの変換を簡単に見ることができるのがSharpLab(https://sharplab.io/)
ILへの変換でどのような変更が行われているのかを逆コンパイル結果として見せてくれます。
例えばC#7.0以降で使用できるローカル関数という機能があります。
これは、関数の中で関数を定義できるというもので、ラムダ式で対応していたであろうところをこれで置き換えられます。

public class C {
    public void M() {
        void localMethod() {    }
    }
}

こういう感じですね。
ラムダ式は必ずデリゲートが生成され、外の変数を用いる際にはクロージャとしてクラスインスタンスが生成されるのですが、ローカル関数の場合は不要であればデリゲートは生成されず、クロージャも可能な場合は構造体が使用されます。このため、ラムダ式をローカル関数で置き換えることでパフォーマンス改善が見込める場合があるわけです。

こういうことをどこで勉強するかというと結構難しいのですが、試しに上のコードをSharpLabに書いてみると、このような逆コンパイル結果が得られます。

public class C
{
    public void M()
    {
    }

    [CompilerGenerated]
    internal static void <M>g__localMethod|0_0()
    {
    }
}

ローカル関数として定義したはずのlocalMethodがクラス関数になっていますね。
さらになんかstaticにされています。 なんでだろう……

さて、ラムダ式と比較するためにローカル関数をActionとして渡してみましょう。
以下のコードをSharpLabに入力します。

public class C {
    public void M() {
        Hoge(localMethod);
        
        void localMethod() {
            Console.WriteLine("スマブラやりたい");
        }
    }
    
    public void Hoge(Action fuga) {
        fuga();
    }
}

するとこうなります。

public class C
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        internal void <M>g__localMethod|0_0()
        {
            Console.WriteLine("スマブラやりたい");
        }
    }

    public void M()
    {
        Hoge(<>c.<>9.<M>g__localMethod|0_0);
    }

    public void Hoge(Action fuga)
    {
        fuga();
    }
}

次はラムダ式で同様のスクリプトを書いてみます。

public class C {
    public void M() 
    {
        Hoge(() => Console.WriteLine("スマブラやりたい"));
    }
    
    public void Hoge(Action fuga)
    {
        fuga();
    }
}

これを入力すると

public class C
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static Action <>9__0_0;

        internal void <M>b__0_0()
        {
            Console.WriteLine("スマブラやりたい");
        }
    }

    public void M()
    {
        Hoge(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<M>b__0_0));
    }

    public void Hoge(Action fuga)
    {
        fuga();
    }
}

先ほどのローカル関数と少し違いがありますね。
public static Action <>9__0_0というデリゲートが生成されており、且つそれのnullチェックが入っています。
このように勝手にnewが追加されちゃうので、この場合はローカル関数を使いましょうとなるわけです。 SharpLab、便利ですね。本当はILに変換されたさらに後の最適化も大切ですが、この段階でも簡単に確認できるのはありがたいです。
最後に、スマブラやりたいという文字列をMの中で変数として定義し、ローカル関数、ラムダ式に渡してみたパターンを見てみましょう。 ローカル関数パターン

public class C {
    public void M() {
        string str = "スマブラやりたい";
        localMethod();
        
        void localMethod() {
            Console.WriteLine(str);
        }
    }
}

これを入力すると

public class C
{
    [StructLayout(LayoutKind.Auto)]
    [CompilerGenerated]
    private struct <>c__DisplayClass0_0
    {
        public string str;
    }

    public void M()
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);
        <>c__DisplayClass0_.str = "スマブラやりたい";
        <M>g__localMethod|0_0(ref <>c__DisplayClass0_);
    }

    public void Hoge(Action fuga)
    {
        fuga();
    }

    [CompilerGenerated]
    internal static void <M>g__localMethod|0_0(ref <>c__DisplayClass0_0 P_0)
    {
        Console.WriteLine(P_0.str);
    }
}

ラムダ式パターン

public class C {
    public void M() {
        string str = "スマブラやりたい";
        Hoge(() => Console.WriteLine(str));
    }
    
    public void Hoge(Action fuga) {
        fuga();
    }
}

これを入力すると

public class C
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public string str;

        internal void <M>b__0()
        {
            Console.WriteLine(str);
        }
    }

    public void M()
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        <>c__DisplayClass0_.str = "スマブラやりたい";
        Hoge(<>c__DisplayClass0_.<M>b__0);
    }

    public void Hoge(Action fuga)
    {
        fuga();
    }
}

ローカル関数およびラムダ式に変数を渡すために用意された<>c__DisplayClass0_0というクロージャが、ローカル関数のときはstructに、ラムダ式のときはclassになっていますね。 当然パフォーマンス的に構造体の方がヒープ領域を使用しない分良く、やはりローカル関数を選ぶべきケースだとわかります。
しかし、ローカル関数の方もラムダ式の方と同じく、Hoge(Action fuga)に渡して実行されるようにするとクロージャはclassになってしまいます……
ローカル関数は可能であればメンバ関数にクラスチェンジできる、ここが、ラムダ式との大きな違いであるわけですね。
ちなみにですが、この場合は引数としてstrを渡してあげればクロージャを用意する必要も無いです(それはそう)。

このように、どっちでもいいじゃんと思ってしまいそうなところでも実はほんのちょっと書き方を変えるだけでパフォーマンスが良くなったり悪くなったりするので、文法を学ぶ際は実行時にできるだけ近い状態を調べることが大切です。

こういう便利なものが個人でも使えるんだからありがたいですよね。

ブログ新設しましたので、挨拶

初めまして。荷桐といいます。 4年ほど前から別のブログでどうしようもない記事を書いたりしてたんですが、 年月を経て結構自分の立場とか環境も変わったり なんやかんやあって新しくしました。

軽く自己紹介と、どういうこと書こうと思ってるかとか書き残しておきますので、 もし興味をお持ちであればちょくちょく見に来ていただけると嬉しいです。

自己紹介

  • 生年月日: 平成生まれ
  • 職業: ゲームエンジニア(フロントエンド)1年目
  • 言語: C#(メイン), C++, Ruby, Java
  • 趣味: ゲーム(デジタル), コーディング
  • Twitter(@u_osusi)

もうちょっと詳しく

物心ついた時からゲーム三昧。
好みはオンラインゲームより1人プレイのコンシューマゲームにかなり寄っています。
好きなゲームはゼノ(ギアス, サーガ, ブレイド全部)、ワイルドアームズエースコンバット影牢、アトリエ他色々。
大学ではごくごく一般的な物理化学系の基礎研究をしていましたが、何故かゲームプログラミングを始めて就職。 プログラミングは独学となります。 独学の危険性は理解しているつもりなので、現在仕事を通して勉強中。

ブログで書くつもりのこと

技術勉強しているのを残すのも意義があるかなと思うので技術系記事。
仕事でメインに触っているのがUnityなのでUnityやC#の話がほとんどになると思います。

よろしくお願いします。