概要
Twitter上で以下のようなポストがRTされてて気になったので検証してみた結果をまとめる。
色んな手段が考えられるが、とりあえず以下の点を重視。privateなメンバをEditor拡張スクリプトから触らせるためにpublicにせざるを得ない件、度々思い出しては調べて、でも結局いい方法なくて…というのを繰り返してる。C++のfriend的な実装がC#でもできれば……と思ってるけど、そもそも考え方が間違ってるんだろうか…?
— イカ飯屋gonz (@gonz149) 2015, 8月 17
- 定義側の記述を簡単実現できる
- 呼び出し側に特殊な操作が不要
基本
「C#だしCondition属性使えないかなー」とか思ったけど以下の制約で断念。
Setterのみならまだ何とかなりそうだが、できればGetter/Setterそろえて実装できる方法が便利。
- 基本的にメソッドのみでプロパティに適用できない
- メソッドの返り値はvoidに限定され、out引数も使えない
参考 : https://msdn.microsoft.com/ja-jp/library/Aa664622(v=VS.71).aspx
ということで#if UNITY_EDITOR ~ #endif
でプロパティを括ってみる。
参考 : http://docs.unity3d.com/ja/current/Manual/PlatformDependentCompilation.html
using UnityEngine;
using System.Collections;
public class HogeHoge : MonoBehaviour
{
[SerializeField]
int piyo;
#if UNITY_EDITOR
// エディタから参照するためのプロパティ
public int Piyo
{
get { return this.piyo; }
set { this.piyo = value; }
}
#endif
// Update is called once per frame
void Update ()
{
// 実行時の参照(本来は使って欲しくない参照)
// ビルド時にエラーになる
Debug.Log(this.Piyo);
}
}
using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(HogeHoge))]
public class HogeHogeEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Test"))
{
var instance = this.serializedObject.targetObject as HogeHoge;
// プロパティを通してprivateな変数にアクセスできる
instance.Piyo++;
Debug.Log(instance.Piyo);
}
}
}
これでビルドした時にはPiyoがHogeHogeより除去されるので、実機実行時にPiyoを参照するようなコードを書いていた場合はコンパイルエラーで止まる。
改良
しかし、UNITY_EDITOR
はエディタでのプレビュー実行時にも有効なため、
このままだとビルドするまでは不正な参照を発見できない。
Jenkinsなどで定期ビルドをしているような環境ならこれでも十分かもしれないが、
そんなのが無い環境ではちとめんどい。
なので呼び出し時に「呼び出し元がエディタ側のコードかどうか」を確認するコードも追加してみる。
まずEditor側のアセンブリ(≒DLL)にAssemblyIsEditorAssembly
付与する。
これはUnity側で用意している「Editor用アセンブリであること」を示す属性である。
(意味的に初期値で設定されていて欲しいが何故か設定されていない)
参考 : http://docs.unity3d.com/jp/current/ScriptReference/AssemblyIsEditorAssembly.html
なお、アセンブリに対する指定はアセンブリ内のコードの何れかに記述されていればよいので、どのファイルでも良い。
できればAssembly.csなど専用ファイルを作ってそこに記述するのがお行儀が良いが、今回はさぼってHogeHogeEditor.csに記述した。
using UnityEngine;
using UnityEditor;
using System.Collections;
// アセンブリに「エディタ拡張用のアセンブリであること」を示す属性を付与
// こちらのアセンブリに含まれる何れかのコードに1つ記述されていれば良い
[assembly: AssemblyIsEditorAssembly]
[CustomEditor(typeof(HogeHoge))]
public class HogeHogeEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Test"))
{
var instance = this.serializedObject.targetObject as HogeHoge;
// プロパティを通してprivateな変数にアクセスできる
instance.Piyo++;
Debug.Log(instance.Piyo);
}
}
}
次にアクセス制限をするために、呼び出し元のアセンブリ情報を確認する実装を追加。
using UnityEngine;
using System.Collections;
public class HogeHoge : MonoBehaviour
{
[SerializeField]
int piyo;
#if UNITY_EDITOR
// エディタから参照するためのプロパティ
public int Piyo
{
get
{
// 呼び出し元メソッド情報を取得
var method = new System.Diagnostics.StackFrame(1).GetMethod();
// 「AssemblyIsEditorAssembly属性が付与されているアセンブリ = エディタ用」なので、
// 呼び出し元メソッドを定義しているクラスが含まれるアセンブリを確認する
if (method.DeclaringType.Assembly.IsDefined(typeof(AssemblyIsEditorAssembly), false) == false)
{
Debug.Assert(false, "Invalid Access! From {0}::{1}", method.DeclaringType, method.Name);
}
return this.piyo;
}
set
{
// 必要ならばこちらにもgetと同様の処理を書く
// (多くの場合、set側にこそ記述をすべき)
this.piyo = value;
}
}
#endif
// Update is called once per frame
void Update ()
{
// 実行時の参照(本来は使って欲しくない参照)
// エディタでの実行時にもアサートを吐く
Debug.Log(this.Piyo);
}
}
これで、実行時にエディタ側以外のコードからPiyoが呼ばれるとAssertが発生するようになり、
不正利用の発見が容易になる。
なお、呼び出し元確認部分のコードを汎用メソッドとして用意すると使いまわす上で便利だが、
その場合はStackFrameを取得する際の引数skipFramesの値がずれるので注意すること。
StackFrameクラスのリファレンス : https://msdn.microsoft.com/ja-jp/library/ws7f30w6.aspx
蛇足的おまけ
EditorApplication.isPlaying
を使うことで似たような挙動をより簡単に実現できるが、
あくまで「実行中か」で判断するため、
「Editor上でプレビュー実行中にInspectorなどのEditor側のコードでアクセスした場合でも不正扱いになる」
という欠点がある。
この誤爆的挙動を良しとするならば、こっちを選択するのもありかもしれない。
using UnityEngine;
using System.Collections;
public class HogeHoge : MonoBehaviour
{
[SerializeField]
int piyo;
#if UNITY_EDITOR
// エディタから参照するためのプロパティ
public int Piyo
{
get
{
// 実行時の呼び出しか確認
// ただし、Editorでのプレビュー実行中にEditor側のコードでここにきてもisPlaying = trueになる模様
// よって誤爆の危険がある
if (UnityEditor.EditorApplication.isPlaying)
{
// 呼び出し元メソッド情報を取得
var method = new System.Diagnostics.StackFrame(1).GetMethod();
Debug.Assert(false, "Invalid Access! From {0}::{1}", method.DeclaringType, method.Name);
}
return this.piyo;
}
set
{
// 必要ならばこちらにもgetと同様の処理を書く
this.piyo = value;
}
}
#endif
// Update is called once per frame
void Update ()
{
// 実行時の参照(本来は使って欲しくない参照)
// エディタでの実行時にもアサートを吐く
Debug.Log(this.Piyo);
}
}