リフレクションについてまとめた

最近 "Reflection" について調べていたので、それについて自分なりにまとめようと思います。

▼学習を兼ねて作ったもの

sonoichi-blog.hatenablog.com

※この記事はUnityゆるふわサマーアドベントカレンダー 2018 12日目の記事です。(C#なのでギリUnityということで...)

Reflectionとは

クラスを他のプログラムから利用できるようにするため、 プログラムやライブラリ中にはクラス名やメンバー名、それらのアクセスレベル等の情報が格納されています。 これらの情報はメタデータと呼ばれ、 プログラムの実行時にメタデータを取り出すための機能をリフレクション(reflection)と呼びます。

(プログラムが自分自身の情報を調べることができる機能なので、reflection(鏡映、反射)と呼ぶわけです。)

実行時型情報 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

 です。

アクセシビリティレベルに関係なくメンバをあれこれできるのは便利だけど危険、加えて処理も遅いようなので、デバッグ用途で使うことの多い機能だと思います。

なぜリフレクションは遅いのか | POSTD

MemberInfo

名前や宣言されたクラスなど、メンバとしての情報を持っています。

各 Info のクラスの基底クラスであり、MemberTypeによって変数なのか、関数なのかといったことが判別できます。

gist.github.com

MethodInfo

関数に関する様々な情報を持ちます。

関数の実行もできます。

gist.github.com

methodInfo.IsSpecialName は関数がコンストラクタ( .ctor )や自動実装プロパティ( プロパティの get/set メソッド( get_xxx, set_xxx )等 )であるときに true になります。 

ParametorInfo.HasDefaultValue は .Net 4.5以降で使用可能です。

virtual, abstruct はそれぞれ MethodInfo.IsVirtual, MethodInfo.IsAbstract で判定できますが、overrideされたかのフラグは取得できません。判定したいときは

var isOverride = methodInfo.GetBaseDefinition().DeclaringType != methodInfo.DeclaringType;

のようにすればできます。

FieldInfo

変数に関する様々な情報を持ちます。

値の変更もできます。

gist.github.com

 IsDefined(), GetCustomAttribute(), GetCustomAttributes() はMethodInfoでも使用可能です。

PropertyInfo 

プロパティに関する様々な情報を持ちます。

gist.github.com

GetAccesser(), GetGetMethod(), GetSetMethod() の第一引数( bool nonPublic )は publicでないアクセサを取得するかどうかです。

PropertyのアクセサビリティレベルはPropertyInfoからは判定できないので、アクセサから求めます。

GetProperty() のオーバーロードに Type を渡すものがあり、なんだろうと思ったのですが、インデクサを取得するためのものでした。

上記の例では次のようなインデクサを定義しています。

[System.Runtime.CompilerServices.IndexerName("Indexer")]
public int this[int x, int y]
{
    get { return _array[y * _width + x]; }
    set { _array[y * _width + x] = value; }
}
private int _array;
private int _width;

IndexerNameAttributeで名前を指定しない場合、名前は"Item"になります。

インデックサ this[] をリフレクションで取得する

ConstructorInfo

コンストラクタに関する様々な情報を持ちます。 

ConstructorInfoを利用してインスタンスの作成ができます。

gist.github.com

BindingFlags

各 Info 取得時に検索するメンバを指定するためのものです。

ここでは、主に扱われそうなものを紹介します。

  • Public : publicなメンバ
  • NonPublic : publicでないメンバ
  • Static : 静的なメンバ
  • Instance : 静的でないメンバ
  • DeclaredOnly : 基底クラスから継承したメンバを含まない

BindingFlags は enum ですが、それぞれが各ビットのフラグを表しており、ビット演算のようにして扱うことができます。

// publicかつ非staticな関数を全取得

var methods = typeof(TestClass).GetMethods(BindingFlags.Public | BindingFlags.Instance);

 最後に

リフレクションはどんなメンバにもアクセスでき、外部から関数の実行や値の変更ができる便利な機能です。

実行時に設定ファイル(ScriptableObject)の値を変更したり、クラスが保持する値を全て出力したり、PlantUML用のコードを出力したりと、色々なことができそうな気がします。

間違った認識や、紹介した以外で便利な機能などありましたら教えていただけると嬉しいです。

型の構造を出力する拡張メソッドを作った

指定した型の内容を全出力する拡張メソッドを作りました。

f:id:sonoichi-60:20180812090950p:plain

ソースコード

gist.github.com

導入方法

  1. 以下のリンク先へ進み、右上の "Download ZIP" からダウンロード

    型の構造を出力する拡張メソッド · GitHub

  2. 解答したファイルをAssets内に移動

使い方

System.Typeの拡張メソッド、OutputStructure()を呼ぶことで、型の構造情報を持った文字列を得られます。

Debug.Log(typeof(Time).OutputStructure());
Debug.Log(typeof(int).OutputStructure());

第一引数の declearOnly は親クラスのメンバを含むかどうかのフラグ、

第二引数の ignoreSpecialName は特殊な名前のメンバ(自動実装プロパティによって追加されるメンバ等)を表示するかどうかのフラグです

public static string OutputStructure(this Type type, bool declaredOnly = false, bool ignoreSpecialName = true)

補足

基底クラスの型の全取得は以下の処理を利用しました

【C#】指定した型の基底クラスの情報をすべて取得する拡張メソッド - コガネブログ

 

Timelineエディタを使いやすくする

 

Timelineエディタを少し使いやすくする方法の紹介です

 

Trackのアイコンを変更する

Assets/Gizmos下に ファイル名をPlayableTrackのクラス名にしたアイコン画像を配置する
https://forum.unity.com/threads/icon-of-custom-timeline-track.509565/

f:id:sonoichi-60:20180801010258p:plain

Gismos/TrackAssets/(トラック名) とやりたかったが認識されず...
Gizmos直下のみ?

 

Track作成メニューの階層化

TrackAssetのクラスが所属しているnamespaceに応じて自動的に階層化されます

f:id:sonoichi-60:20180801010418p:plain
Test.Hoge.Test3Track とした場合、
Test > Hoge > Test3Track ではなく、
Test.Hoge > Test3Track となるみたいです

 

Trackの色を変える

TrackAsset継承クラスに、以下のような属性を付与するだけです
引数は前から順に 0~1 の r, g, b です

[TrackColor(0.22f, 0.35f, 0.9f)]

f:id:sonoichi-60:20180801010337p:plain

Trackの左端とClip下端の色が指定した色になります
CameraやUI、Materialなどで分類し、色を付けるとわかりやすいと思います

 

ClipのInspecter表示変更

以下、enumに応じてInspenterに表示する値を変えるサンプルです

gist.github.com

f:id:sonoichi-60:20180801010357p:plain

TimelineでTweenを再生できるトラックを作った

TimelineでTweenが再生できたらいいなと思ったので作りました。

 

 

導入方法

  1. AssetStoreよりDOTweenをインポート
  2. 下のリンクよりダウンロード

    github.com

  3. パッケージをインポート

  4. TimelineウィンドウのTrack作成メニューにTweenPlayablesが追加されています

f:id:sonoichi-60:20180730091642p:plain

 

使い方

クリップごとに任意で開始値・終了値・イージングタイプを指定し、開始点から終了点にかけてアニメーションを行います。

クリップのブレンドも可能です。

パッケージに入っているTween用のBehaviour, Mixer, Clip, Trackの基底クラスを利用すれば、楽にTweenのカスタムトラックが作成できると思うので、よかったら使ってみてください。

LinQでも使える2次元配列

2次元配列でLinQを使ったら1次元配列扱いになって不便だったので、LinQでも(2次元座標が)使える2次元配列のクラスを作りました。

 

 

実装 

 

使い方

LinQで2次元配列の要素の2次元座標を取得できます。

以下は使用例です。

// 幅10, 高さ10の二次元配列を作成
var width = 10;
var height = 10;
var ary = new Array2D<int>( width, height );

// 各要素に0~2のランダムな値を代入
for( int y = 0; y < height; y++ ) {
    for( int x = 0; x < width; x++ ) {
        ary.Set( x, y, Random.Range( 0, 3 ) );
    }
}

// "xが0"の要素に3を代入

ary.Set( elem => elem.x == 0, 3 );

// "xが5,またはyが3" の要素を全取得
var ary2 = ary.Where( elem => elem.x == 5 || elem.y == 3 );

// "valueが1" の要素を全取得
var ary3 = ary.Where( elem => elem.value == 1 );

// "xが5,またはyが3" かつ "valueが1"の全要素に
foreach( var elem in ary2.Union(ary3) ) {
    // なんかする
}

 

参考記事

qiita.com

 

SceneManageWindow作成で気づいたこと&参考にした資料

初めてのUnityエディタ拡張の上で気づいたこと&SceneManageWindow作成の上で参考にした資料をまとめておきます。

 

 

気づいたこと

TextFieldの文字列が消えない

"TextFieldに文字を入力し、Buttonを押したら入力文字列をリセット"

っていうよく使いそうな処理をしたら奇妙な現象が起こりました。

f:id:sonoichi-60:20171113184807g:plain

TextFieldから文字が消えない…

上のウィンドウでは以下のような処理をしてます。

_text = EditorGUILayout.TextField( _text, EditorStyles.textField );
if( GUILayout.Button( "Log" ) ) {
    Debug.Log( _text );
    _text = "";
}

問題なのは、「TextFieldで入力された文字列は(内部でstringの値が変更されても)フォーカスされている間保持される」という点です。

つまり、適当なタイミングでTextFieldのフォーカスを外せばいいということでした。

以下のような処理でフォーカスを外すことができます。

GUI.FocusControll("");

GUIのフォーカスを外す - けいごのなんとか

 

 画面上のマウス座標の取得

パスを表示するポップアップを作る際、右クリックメニューのようにクリックした点にポップアップを出したかったので、画面上のマウス座標を取得する必要がありました。

まず、Unityでのゲーム制作でよく使われるInput.mousePositionですが、これはエディタ実行時にはGameViewを基準としたマウス座標を返し、非実行時には(0,0,0)を返すので使えず。

調べると、Event.current.mousePositionという、いかにもな値があったのですが、これは処理を記述したエディターウィンドウのGUI上のマウス座標みたいです。

Unity - スクリプトリファレンス: Event

画面上のマウス座標は以下のように取得しました。

var mouseGUIPos = Event.current.mousePosition;
var mouseScreenPos = EditorGUIUtility.GUIToScreenPoint( mouseGUIPos );

 

他、参考にした資料 

▼エディタ拡張入門

はじめに - エディター拡張入門

【エディタ拡張徹底解説】初級編①:ウィンドウを自作してみよう【Unity】 – ケットシーウェア

▼シーンの作成・削除・複製・名前変更

第26章 AssetDatabase - エディター拡張入門

Unity - マニュアル: AssetDatabase

▼開始シーンの設定

エディタ上で再生を開始するSceneを固定する【Unity】【エディタ拡張】 - (:3[kanのメモ帳]

 ▼マルチシーンを開く

楽にシーンを開く拡張機能を作ってみた(マルチシーン対応)【Unity】【エディタ拡張】 - (:3[kanのメモ帳]

▼ScenesInBuildの並び替え可能なリスト

第14章 ReorderbleList - エディター拡張入門

▼データ保存関連

第3章 データの保存 - エディター拡張入門

JsonUtility をつかって Unity で JSON を取り扱う方法 - Qiita

▼Path関連処理(ディレクトリ名の取得、使用不可文字の取得など)

Path クラス (System.IO)

▼シーン一覧の折りたたみ

Unity のエディタ拡張で FoldOut をかっこよくするのをやってみた - 凹みTips

▼仕切り線

エディタ拡張で仕切り線を描く - Qiita

▼ビルトインのGUIStyleを見る

Show Built In Resources - Unify Community Wiki

 

最後に

前回の記事のPV数が思ってたより伸びてて、いそいそしてます。

これからもぼちぼち更新していくのでよろしくお願いします。

シーンをいろいろできる拡張機能を作った

プロジェクト内のシーンファイルを自動取得・一覧表示し、いろいろできる拡張機能を作ったので紹介します。

f:id:sonoichi-60:20171110043536g:plain

 

 

SceneManageWindowの紹介

シーンの一覧表示・ロード 

プロジェクト内の全てのシーンを自動で取得し、一覧表示します。

一覧には親ディレクトリごとに分けて表示されます。

シーン名右側の"Load"ボタンを押すと、そのシーンをロードすることができます。

f:id:sonoichi-60:20171110043536g:plain

開始シーンの設定 

"Start"トグルをonにすると、シーン名欄が桃色で表示され、開始シーンに設定されます。

開始シーンが設定されている場合、実行時にそのシーンから開始します。*1

on状態の"Start"トグルをもう一度押すと、未設定にすることができます。

f:id:sonoichi-60:20171110050356g:plain

シーンの作成・複製

"Directory"ボタンを押し、表示されるパスから作成先を選択できます。
シーン名を入力し、"Create Scene"ボタンを押すと、指定先にデフォルトシーンを作成します。

また、シーン名左側の四角をクリックすることで対象のシーンを選択できます。

1つのシーンを選択した状態でシーン名を入力し、"Deplicate Scene"ボタンを押すと、指定先に選択シーンを複製します。

f:id:sonoichi-60:20171110051705g:plain

同様に、1つのシーンを選択した状態で"Remove Scene"ボタンを押すとシーンの削除、シーン名を入力して"Rename Scene"ボタンを押すとシーン名の変更ができます。

f:id:sonoichi-60:20171110052358g:plain

シーングループの作成・編集

複数のシーンを選択し、グループ名を入力後、"Create SceneGroup"ボタンを押すと、シーングループを作成します。

これにより、シーンをグループ化する事ができます。

シーングループのシーンの追加・削除も可能です。

f:id:sonoichi-60:20171110102726g:plain

マルチシーンの作成・編集

複数のシーンを選択し、マルチシーンの名前を入力後、"Create MultiScene"ボタンを押すと、最初に選択したシーンをメインシーンとしてマルチシーンを作成します。

マルチシーン名右側の"Load"ボタンを押すと、最初に選択したシーンをOpenSceneMode.Singleでロードした後、その他のシーンをOpenSceneMode.Additiveでロードします。

 マルチシーンのシーンの追加・削除も可能です。

f:id:sonoichi-60:20171110103610g:plain

ビルドに含むシーンの簡易設定・編集

シーン名を右側の"NonBuild"と書かれたトグルを押すと"Build"となり、ビルド対象のシーンに設定できます。

もう一度押すとビルド対象から外れます。

また、"Scenes In Build"のリストはBuild Settingのものと同様に、並び替える事でシーンのIndexを変更する事ができます。

f:id:sonoichi-60:20171110105500g:plain

 

導入方法

1.ここからパッケージをダウンロード

2.パッケージをインポート

3.Window -> SceneManageWindow から開く

以上です。

 

補足・注意点

・作成時のUnityのバージョンは2017.2.0f3です。

2017では実行開始時のシーンを設定するUnityEditor.SceneManagement.EditorSceneManager.playModeStartSceneというものがあったので使ってみました。

そのため、2017より前のバージョンではStartトグルは表示されません。

▼公式リファレンス

Unity - Scripting API: SceneManagement.EditorSceneManager.playModeStartScene

 

・保持する必要がある情報はjsonに変換し、EditorUserSettingを使って保存しています。

このEditorUserSettingで保存したデータはLibraryディレクトリ内にあるので、バージョン管理でLibraryディレクトリを含んでいないが設定を共有したい場合、データの保存方法を見直す必要があるかと思います。

▼EditorUserSettingについて

プロジェクト内でデータを保存【Unity】【エディタ拡張】 - (:3[kanのメモ帳]

 

・シーンの作成先はすでにシーンファイルが存在するディレクトリの候補から選択するようにしています。

Assets下の全ディレクトリを候補とすると、大規模なプロジェクトだと多すぎて使いづらいし、小規模ならそこまでしなくてもいいかと思ったためです。

シーンファイルの存在しないディレクトリは候補に表示されないため、Projectビューで直接シーンを作成してください。

(中途半端感が否めないので、何かいい案があれば教えてください…)

▼一応、Assets下の全ディレクトリのパスを取得する方法

string[] allDirectories = System.IO.Directory.GetDirectories( UnityEngine.Application.dataPath, "*", System.IO.SearchOption.AllDirectories );

 

最後に

今回はシーン関連の効率化のための拡張機能を作りました。

大規模なプロジェクトほど、役に立つかと思います。

「それいる?」みたいな機能が多いかもですが、個人的には気に入ってます。

また、初めてのエディタ拡張だったのもあり、つまづいた点が幾つかありました。

それについては、また別の記事にまとめる予定です。

 

 20171113追記:書きました

sonoichi-blog.hatenablog.com

20171117追記:プロジェクト内にSceneAssetが存在しない際に表示が崩れるバグを修正しました

*1:Unity2017のみの機能です。