How do you keep modularity and flexibilty with Unity scripts? Or: How would one use interfaces?

… Yes, this is indeed a complete sentence, now let me delete my stuff, thanks.

I would say yes, interfaces are the answer to much of your issues.

Your only problem is being able to have serializable fields on which you reference objects as the interface in the inspector.

That’s fine… just type the field ‘UnityEngine.Object’, uncover a conversion property for it, and create a PropertyDrawer that restricts that field to the type you so desire.

Something like so:

Create an Attribute for the property drawer (goes in standard script/code directory):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
public class TypeRestrictionAttribute : PropertyAttribute
{

    public readonly System.Type RestrictionType;

    public TypeRestrictionAttribute(System.Type tp)
    {
        this.RestrictionType = tp;
    }
  
}

The PropertyDrawer (goes into an Editor folder as an Editor script):
note - this is just a basic implementation, you can add more bells and whistles

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;

[CustomPropertyDrawer(typeof(TypeRestrictionAttribute))]
public class TypeRestrictionPropertyDrawer : PropertyDrawer
{

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginChangeCheck();
        EditorGUI.PropertyField(position, property, label);

        if(EditorGUI.EndChangeCheck() && property.propertyType == SerializedPropertyType.ObjectReference &&
           !object.ReferenceEquals(property.objectReferenceValue, null) &&
           this.attribute is TypeRestrictionAttribute)
        {
            var attrib = this.attribute as TypeRestrictionAttribute;
            var tp = property.objectReferenceValue.GetType();
            if(!attrib.RestrictionType.IsAssignableFrom(tp))
            {
                var go = TryGetGameObjectFromSource(property.objectReferenceValue);
                if(go != null)
                {
                    property.objectReferenceValue = go.GetComponent(attrib.RestrictionType);
                }
                else
                {
                    property.objectReferenceValue = null;
                }
            }
        }
    }

    private static GameObject TryGetGameObjectFromSource(UnityEngine.Object obj)
    {
        if (obj is GameObject) return obj as GameObject;
        if (obj is Component) return (obj as Component).gameObject;
        return null;
    }

}

An interface to restrict to:
note - I personally like to have a base interface type that implies the interface is intended to be a component

public interface IComponent
{
    GameObject gameObject {get;}
    Transform transform {get;}
}
public interface ITorqueable : IComponent
{

    void ApplyTorque(float torque);

}

And finally usage:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class zTest01 : MonoBehaviour {

    [SerializeField]
    [TypeRestriction(typeof(ITorqueable))]
    private UnityEngine.Object _target; //you could type this as Component, MonoBehaviour, whatever... some generic base type of what you expect

    public ITorqueable Target
    {
        get { return _target as ITorqueable; }
        set { _target = value as UnityEngine.Object; }
    }


    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            this.Target.ApplyTorque(50f);
        }
    }

}
1 Like

When I say you can add more bells and whistles. For example I have a far more robust version of this in my spacepuppy framework:
https://github.com/lordofduct/spacepuppy-unity-framework-3.0/blob/master/SpacepuppyUnityFrameworkEditor/Components/ComponentTypeRestrictionPropertyDrawer.cs

That uses the SelectableComponentPropertyDrawer:
https://github.com/lordofduct/spacepuppy-unity-framework-3.0/blob/master/SpacepuppyUnityFrameworkEditor/Components/SelectableComponentPropertyDrawer.cs

This does several things, but the main feature is that it allows a drop down to list off all components on a gameobject that satisfy the constraint. This way you can select one of many:

1 Like

If you haven’t fully grokked UnityEvent yet, then you are missing out on the major decoupling mechanism in the Unity framework.

Please check out my blog post/video, as well as this thread from 2011.

Before UnityEvent, I felt as you did, that the Unity framework led to way too much tight coupling, unless you went to great lengths to avoid it. But UnityEvent fixes that, in my opinion. I now have a robust library of little components that I frequently reuse, none of which know about any of the others, and I snap these together in the inspector like Lego. Also, whenever I’m writing new/custom code (a player controller or whatever), when something interesting happens that other code might want to react to (e.g. picked up a coin), I quickly declare and fire an event — and now other components can react to that, and again neither side needs to know about the other.

And yes, I also use interfaces a lot. :slight_smile: But definitely check out this stuff; it will go a long way towards saving your sanity.

1 Like

I used to use events a lot, but they kinda had the same issue as everything else. UnityEvents however… I know about them but I was under the impression that they generate absurd amounts of garbage?! Though I may be mistaken. I’ll definitely check that out as well, thanks!

I would say definitely check out UnityEvent. It’s super useful and any garbage isn’t massive… I mean sure some might exist (depending certain things… like parameters and what not), but it’s minimal if not there for many use cases. (C# events have way more garbage)

As to some of your questions in your previous post:

I would definitely suggest using your own version. My framework mostly exists as an example.

An adhoc version wouldn’t be hard at all. In the PropertyDrawer I wrote in my previous post, when the objectReferenceValue is not null, and it’s a GameObject source, you’d just loop over all the components on the GameObject and find the ones that implement the RestrictionType, and create a popup with the names of those scripts using EditorGUI.Popup:

Interfaces are intended to be contracts.

You’re basically saying “the interface ensures that an object implementing the interface has these specific members with these signatures”.

The IComponent is the basic shape of a component. It has a gameObject and transform on it.

Note that when you have something typed as the interface in question… the members NOT defined by the interface are not directly available since the compiler isn’t sure they exist.

If we created a simple interface like so:

public interface ISomething
{
    void Foo();
}

And we had an instance of it, we couldn’t access the transform or gameObject:

var obj = GetComponent<ISomething>();
obj.transform.position = Vector3.zero; //NOPE

This would fail. ‘transform’ is not a known member of ISomething.

So… by defining IComponent, and making ITorqueable inherit from it. We’re saying that “this interface isn’t just torqueable, but it’s intended to be a component as well”. This way we can access the transform and gameObject of it with out doing unnecessary casting.

It also conveys to the user of the interface that they shouldn’t be implementing something like a ScriptableObject as a ITorqueable… since a ScriptableObject is not a component.

And here’s a simple implementation of the drop down:

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;

[CustomPropertyDrawer(typeof(TypeRestrictionAttribute))]
public class TypeRestrictionPropertyDrawer : PropertyDrawer
{

    private List<Component> _components = new List<Component>();
    private List<GUIContent> _names = new List<GUIContent>();
    private GUIContent _noneEntry = new GUIContent("None...");

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (property.propertyType != SerializedPropertyType.ObjectReference || !(this.attribute is TypeRestrictionAttribute))
        {
            //if we don't satisfy type requirements, draw default editor
            EditorGUI.PropertyField(position, property, label);
            return;
        }

        var go = TryGetGameObjectFromSource(property.objectReferenceValue);
        if (!object.ReferenceEquals(go, null))
        {
            //if a GameObject source is present, draw the drop down

            var attrib = this.attribute as TypeRestrictionAttribute;
            _components.Clear();
            _names.Clear();

            go.GetComponents(attrib.RestrictionType, _components);
            int j = 0; //we use an indexer to make the component type names unique... if 2 names are equal, Popup treats at as a single entry
            _names.AddRange(from c in _components select new GUIContent(string.Format("({0}) {1}", ++j, c.GetType().Name)));
            _names.Add(_noneEntry); //a none entry so that you can remove the selection

            int i = _components.IndexOf(property.objectReferenceValue as Component);
            EditorGUI.BeginChangeCheck();
            i = EditorGUI.Popup(position, label, i, _names.ToArray());
            if(EditorGUI.EndChangeCheck())
            {
                if (i >= 0 && i < _components.Count)
                    property.objectReferenceValue = _components[i];
                else
                    property.objectReferenceValue = null;
            }

            _components.Clear();
            _names.Clear();
        }
        else
        {
            //if a GameObject source was not present, just draw the default object field

            EditorGUI.BeginChangeCheck();
            EditorGUI.PropertyField(position, property, label);

            if (EditorGUI.EndChangeCheck() && !object.ReferenceEquals(property.objectReferenceValue, null))
            {
                var attrib = this.attribute as TypeRestrictionAttribute;
                var tp = property.objectReferenceValue.GetType();
                if (!attrib.RestrictionType.IsAssignableFrom(tp))
                {
                    go = TryGetGameObjectFromSource(property.objectReferenceValue);
                    if (go != null)
                    {
                        property.objectReferenceValue = go.GetComponent(attrib.RestrictionType);
                    }
                    else
                    {
                        property.objectReferenceValue = null;
                    }
                }
            }
        }

    }

    private static GameObject TryGetGameObjectFromSource(UnityEngine.Object obj)
    {
        if (obj is GameObject) return obj as GameObject;
        if (obj is Component) return (obj as Component).gameObject;
        return null;
    }

}
1 Like

You seem very focused on a perceived ‘superior’ nature… but I can’t quite put my finger on what you consider ‘superior’.

It seems to be just what you like is the superior choice.

Just another possibility to consider (it’s non-coder friendly if you do it right):