Author Topic: May 9th - Your Own Reflection  (Read 780 times)

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 270
  • -Receive: 1132
  • Posts: 21,846
  • Toronto, Canada
    • View Profile
May 9th - Your Own Reflection
« on: May 09, 2016, 12:00:41 AM »
A few months ago I was working on the ship builder functionality of the upcoming game I'm working on and around the same time I was playing around with reflection -- the ability to automatically find functions on all classes with specific attributes on them to be precise. I needed this particular feature for TNet 3: I wanted to eliminate the need to have to register RCCs (custom object instantiation functions). I added that feature without any difficulty: simply get all assemblies, run through each class and then functions of that class, then simply keep a list of ones that have a specific attribute. Implementing it got me thinking though... what if I was to expand on this idea a bit? Why not use the same approach to add certain game functionality? Wouldn't it be cool if I could right-click an object in game, and have the game code automatically get all flagged custom functionality on that object and display it somehow? Or better yet, make it interactable?

Picture this: a modder adds a new part to the game. For example some kind of sensor. Upon right-clicking on this part, a window can be brought up that shows that part's properties: a toggle for whether the part is active, a slider for its condition, a label showing how much power it's currently consuming, etc. There aren't that many types of data that can be shown. There's the toggle, slider, label... Other types may include a button (for a function instead of a property), or maybe an input field for an editable property. So how can this be done? Well, quite easily, as it turns out.

First, there needs to be custom attribute that can be used to flag functionality that should be displayed via UI components. I called it simply "GameOption":
  1. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
  2. public class GameOption : Attribute
  3. {
  4.         public MonoBehaviour target;
  5.         public FieldOrProperty property;
  6.        
  7.         public virtual object value { get { return Get(target); } set { Set(target, value); } }
  8.  
  9.         public object Get (object target)
  10.         {
  11.                 if (target != null && property != null) return property.GetValue(target);
  12.                 return null;
  13.         }
  14.  
  15.         public T Get<T> () { return Get<T>(target); }
  16.  
  17.         public T Get<T> (object target)
  18.         {
  19.                 if (target != null && property != null) return property.GetValue<T>(target);
  20.                 return default(T);
  21.         }
  22.  
  23.         public virtual void Set (object target, object val)
  24.         {
  25.                 if (isReadOnly || target == null) return;
  26.                 if (property != null) property.SetValue(target, val);
  27.         }
  28. }
Next, there needs to be a function that can be used to retrieve all game options on the desired type:
  1. // Caching the result is always a good idea!
  2. static Dictionary<Type, List<GameOption>> mOptions = new Dictionary<Type, List<GameOption>>();
  3.  
  4. static public List<GameOption> GetOptions (this Type type)
  5. {
  6.         List<GameOption> list = null;
  7.  
  8.         if (!mOptions.TryGetValue(type, out list))
  9.         {
  10.                 list = new List<GameOption>();
  11.                 mOptions[type] = list;
  12.  
  13.                 var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
  14.                 var fields = type.GetFields(flags);
  15.  
  16.                 for (int b = 0, bmax = fields.Length; b < bmax; ++b)
  17.                 {
  18.                         var field = fields[b];
  19.                        
  20.                         if (field.IsDefined(typeof(GameOption), true))
  21.                         {
  22.                                 GameOption opt = (GameOption)field.GetCustomAttributes(typeof(GameOption), true)[0];
  23.                                 opt.property = FieldOrProperty.Create(type, field);
  24.                                 list.Add(opt);
  25.                         }
  26.                 }
  27.  
  28.                 var props = type.GetProperties(flags);
  29.  
  30.                 for (int b = 0, bmax = props.Length; b < bmax; ++b)
  31.                 {
  32.                         var prop = props[b];
  33.                         if (!prop.CanRead) continue;
  34.                        
  35.                         if (prop.IsDefined(typeof(GameOption), true))
  36.                         {
  37.                                 GameOption opt = (GameOption)prop.GetCustomAttributes(typeof(GameOption), true)[0];
  38.                                 opt.property = FieldOrProperty.Create(type, prop);
  39.                                 list.Add(opt);
  40.                         }
  41.                 }
  42.         }
  43.         return list;
  44. }
Of course it's even more handy to have this on the Game Object:
  1. static public List<GameOption> GetOptions (this GameObject go)
  2. {
  3.         return go.GetOptions<GameOption>();
  4. }
  5.  
  6. static public List<T> GetOptions<T> (this GameObject go) where T : GameOption
  7. {
  8.         List<T> options = new List<T>();
  9.         MonoBehaviour[] mbs = go.GetComponents<MonoBehaviour>();
  10.  
  11.         for (int i = 0, imax = mbs.Length; i < imax; ++i)
  12.         {
  13.                 MonoBehaviour mb = mbs[i];
  14.                 List<GameOption> list = mb.GetType().GetOptions();
  15.  
  16.                 for (int b = 0; b < list.size; ++b)
  17.                 {
  18.                         GameOption opt = list[b] as T;
  19.  
  20.                         if (opt != null)
  21.                         {
  22.                                 opt = opt.Clone();
  23.                                 opt.target = mb;
  24.                                 options.Add(opt);
  25.                         }
  26.                 }
  27.         }
  28.         return options;
  29. }
So now I can have a property like this in a custom class:
  1. public class CustomClass : MonoBehaviour
  2. {
  3.     [GameOption]
  4.     public float someValue { get; set; }
  5. }
...and I can do this:
  1. var options = gameObject.GetOptions<GameOption>();
  2. foreach (var opt in options)
  3. {
  4.     opt.value = 123.45f;
  5.     Debug.Log(opt.value);
  6. }
Better still, I can inherit a custom attribute from GameOption and have custom code handle both the getter and the setter. I could filter exactly what kind of custom attribute is retrieved using the gameObject.GetOptions<DesiredAttributeType>() call. With the way of retrieving custom properties set, all that's left is to draw them automatically after some action.

That is actually quite trivial using NGUI. I simply registered a generic UICamera.onClick delegate, and inside it I collect the options using gameObject.GetOptions then display them using an appropriate prefab. For example
  1. if (opt.value is float) // draw it as a slider
I also register an event listener to the appropriate UI element itself (in the case above -- a slider), so that when the value changes, I simply set the opt.value to the new one. So there -- the mod content maker no longer needs to worry about creating custom UI elements at all. All he needs to do is mark desired fields or properties as [GameOption], and they will show up via right-click. Simple!

Of course I then went on to make it more advanced than that -- adding an optional sorting index and category values (so that the order of properties that show up can be controlled via the index, and filtered using the category). I also added support for buttons -- that is, I simply expanded the attribute to include methods:
  1. AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method
...and added a MethodInfo to go with the FieldOrProperty attribute as well as an Invoke() function to trigger it. I also added support for Range(min, max) property for sliders, popup lists for multiple selection drop-down lists... I can go on, but there is no need to complicate the explanation further. Point is -- this approach is highly customizable and very powerful:

C# reflection is fun!
« Last Edit: May 09, 2016, 12:58:50 AM by ArenMook »