はじめに
UnityMonobehaviourのフィールドにつける属性SerializeFieldや、SRDebuggerやRPCで見かけるメソッドについている属性など、
様々な箇所で利用されていますが、実際に自分で実装して使ってみたことがないので使い道を探るべく検証したのでこちらの記事に残したいと思います。
リフレクションとは
リフレクションが使われている例として、Unity標準のコンポーネントであるButtonはインスペクター上でメソッド名を指定して、押下時にそのメソッドを呼び出すことができます。
PrefabのデータはYAML形式で書かれていて、対応箇所を調べてみると以下の指定になっています。
- m_Target: {fileID: 8920755020898028706}
m_TargetAssemblyTypeName: UnityEngine.GameObject, UnityEngine
m_MethodName: SetActive
メソッド名が文字列で表示されています。
UnityEngine.GameObjectのSetActiveというメソッドを呼ぶという指定になっていて、コード外からメソッドを呼び出せるようになっています。
これを実現させているのがC#のリフレクションという機能です。
属性とは
認識が少し違うかもしれませんが、クラス、プロパティやメソッドに属性や値を持たせるイメージです。
例えばSerializeFieldの場合、インスペクターに表示するという属性を持たせます。
インスペクターに表示する処理をする際に、リフレクションで全フィールドを取得してその中でSerializeFieldを持っているものだけを表示対象にしているはずです。
インスペクターで値の幅を指定することができるRange(0,1)などの場合、フィールドにminとmaxの値を属性として持たせます。
インスペクターに表示する処理をする際に、Rangeという属性を持っているフィールドはスライダー表示にし、さらにmin,maxを参照してスライダーの最小値と最大値を決めているはずです。
二つの例をとってみても、属性とリフレクションは切り離せない関係だと私は思います。
活用例〜スキルのIDとメソッドを紐付ける〜
では実際に、自分で属性を実装するにはどうすれば良いか活用例を上げて解説していきます。
RPGのスキルを発動する時の処理で考えてみます。
シンプルな実装方法
通常スキルにはIDを割り当てて、IDによって呼ぶメソッドを決定します。
if文やswitch文でIDによって分岐を書いたり、DictionaryのKeyをidにしてValueに対象のメソッドをあらかじめ入れておく方法がありますが、書き方としてはあまりスマートには見えません。
(スマートではないですが、シンプルな方法でパフォーマンスには優れています)
属性とリフレクションを利用したスマートな実装方法
スキルユーティリティ関数
スキルのメソッドが以下のように定義されていたとします。
using UnityEngine;
public static class AttributeSkillUtil {
public static void Fire(GameObject targetGameObject) {
targetGameObject.name = "Fire";
}
public static void Freeze(GameObject targetGameObject) {
targetGameObject.name = "Freeze";
}
public static void Thunder(GameObject targetGameObject) {
targetGameObject.name = "Thunder";
}
}
このままではIDとの紐付けがないのでどのメソッドを呼べば良いかわかりません。
ここで属性の登場です。
属性クラスの作成
SkillIdをメソッドに付与する属性を作ります。
[AttributeUsage(AttributeTargets.Method)]
public sealed class SkillIdAttribute : Attribute {
public readonly int id;
public SkillIdAttribute(int id) {
this.id = id;
}
}
クラスの中身自体はシンプルで、idとコンストラクタが定義されているだけです。
コンストラクタの引数が後ほど属性を指定するときの引数になります。
また、属性クラスにも属性が指定されていて、上記の例だとメソッドのみに付与できる属性という意味になります。
付けなくても動作しますが、誤って用途外のプロパティやクラスに属性がつけられてしまうのを防ぐためにも指定した方が良いです。
作成した属性を付与するとスキルユーティリティクラスは以下のようになります。
using UnityEngine;
public static class AttributeSkillUtil {
public delegate void SkillFunc(GameObject targetGameObject);
[SkillIdAttribute(1)]
public static void Fire(GameObject targetGameObject) {
targetGameObject.name = "Fire";
}
[SkillIdAttribute(2)]
public static void Freeze(GameObject targetGameObject) {
targetGameObject.name = "Freeze";
}
[SkillIdAttribute(3)]
public static void Thunder(GameObject targetGameObject) {
targetGameObject.name = "Thunder";
}
}
こちらでメソッドとSkillIdの紐付けができました。
以下のデリゲートはリフレクションで呼び出すときに利用します。
public delegate void SkillFunc(GameObject targetGameObject);
リフレクションでスキルの処理を呼び出す
まず結論として最終的なコードです。
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
public class AttributeSkillTest : MonoBehaviour {
private Dictionary<int, AttributeSkillUtil.SkillFunc> _skillCache = new Dictionary<int, AttributeSkillUtil.SkillFunc>();
public void Start() {
MethodInfo[] skillMethodInfoList = typeof(AttributeSkillUtil).GetMethods(BindingFlags.Public | BindingFlags.Static);
foreach (MethodInfo info in skillMethodInfoList) {
SkillIdAttribute skillIdAttribute = info.GetCustomAttribute<SkillIdAttribute>();
if (skillIdAttribute == null) {
continue;
}
AttributeSkillUtil.SkillFunc skillMethod = (AttributeSkillUtil.SkillFunc)Delegate.CreateDelegate(typeof(AttributeSkillUtil.SkillFunc), info);
_skillCache[skillIdAttribute.id] = skillMethod;
}
}
public void InvokeSkill(int skillId, GameObject targetGameObject) {
_skillCache[skillId](targetGameObject);
}
}
スキルユーティリティクラスの関数一覧を取得
MethodInfo[] skillMethodInfoList = typeof(AttributeSkillUtil).GetMethods(BindingFlags.Public | BindingFlags.Static);
GetMethodsの引数にはstaticでpublicな関数を指定してそれ以外は取得しないようにしています。
MethodInfoには戻り値や引数に関する情報も入っているので、特定の戻り値やパラメータで対象を絞り込むことができます。
MethodInfoから属性を取得
SkillIdAttribute skillIdAttribute = info.GetCustomAttribute<SkillIdAttribute>();
指定した属性がない場合は、nullになります。
メソッドをキャッシュに登録
AttributeSkillUtil.SkillFunc skillMethod = (AttributeSkillUtil.SkillFunc)Delegate.CreateDelegate(typeof(AttributeSkillUtil.SkillFunc), info);
_skillCache[skillIdAttribute.id] = skillMethod;
こちらで注目するポイントはデリゲートを使用している点です。
MethdoInfoからも直接メソッドを呼び出すことができますが、デリゲートを生成した方が高速です。
またMethodInfoから呼び出す場合、戻り値と引数がobject型で固定されてしまうためキャストが必要になるのと、値型の場合はボクシングが発生してしまいます。
生成するコストはかかってしまいますが、一度生成すれば直接関数を呼ぶのとほとんど変わらない速さで呼び出すことができます。
実際に呼び出す
InvokeSkill(1, gameObject);
最後に
属性を使えば書き方はスマートになりますが、パフォーマンスの面で見るとスマートではないケースがあります。
しかし、見やすい書き方にすることで書き間違いを防ぎ不具合を減らすメリットにもなるのでバランスが大切だと思います。
今回試した内容は、属性とリフレクションを理解するシンプルな例になっていると思うのでご参考になれば幸いです。