1
\$\begingroup\$

I'm running into a problem that I think will seriously affect maintainability down the road, so I'd like to figure it out now before it becomes a problem.

I've been starting to use private [SerializeField] rather than public fields to avoid other scripts manipulating internal data.Then, for scripts referencing the component's fields through properties in an interface.

So in a MonoBehaviour I have something like:

[SerializeField]
private float speed;

public float Speed => speed; //implements interface    

However, In my custom inspectors, I now can't use

serializedObject.FindProperty(nameof(MonoBehaviourScript.speed));

This is because it doesn't have access to the private member speed. I now have to reference it by string name:

serializedObject.FindProperty("speed");

This worries me since now I can't easily rename the speed field. Also, Making both speed and Speed public is scary since then speed can be manipulated from outside.

Is there a good way to protect myself from external modification, while keeping refactoring easy?

\$\endgroup\$
2
\$\begingroup\$

Use your interface

Handily enough, you already have your answer. Leave the speed field public, but never pass around or declare references to that class. Only ever use the interface it implements.

“But wait!” you say. “This doesn’t stop me from shooting myself in the foot!”

Well, no. It doesn’t. You could still sneak around this by declaring a reference to that class anyway. You could give the class a name that implies it should not be used, but that reeks of code smell. If you want to be very strict about this, you could declare your class to have internal protection (i.e. not public) and compile it as a separate assembly. Probably not necessary.

There are many other options that don’t work well within Unity regarding the inspector.

Another alternative would be to publicize the names of editable fields from within the class itself, still using nameof. Ugly, but possible, and retains encapsulation at the cost of simplicity, requiring essentially declaring every field twice, but hey, you can rename it just once!

public class MyMonoBehaviour : MonoBehaviour, IMyMonoBehaviour {
    [SerializeField]
    private float speed; //can rename to your heart’s content, as long as you use a FormerlySerializedAs attribute. 
    public float Speed => speed;

    #if UNITY_EDITOR
    public static class FieldNames {
      //actual name of this property is irrelevant. Refactoring it does not affect the field name inside the MonoBehaviour, and vice versa.
      public static string Speed => nameof(speed);
    }
    #endif
 }

And to use it:

serializedObject.FindProperty(MyMonoBehaviour.FieldNames.Speed);

Another alternative is to have the editor be a nested class:

#if UNITY_EDITOR
using UnityEditor;
#endif

public class MyMonoBehaviour : MonoBehaviour, IMyMonoBehaviour {
    [SerializeField]
    private float speed; //don’t forget the FormerlySerializedAs attribute if you rename this. 
    public float Speed => speed;

    #if UNITY_EDITOR
    [CustomEditor(typeof(MyMonoBehaviour))]
    public class MyMonoBehaviourEditor : Editor{
        public override void OnInspectorGUI() {
            serializedObject.FindProperty(speed); //or MyMonoBehaviour.speed if you want to be more explicit
        }
    }
    #endif
 }

If you are worried about having the editor in the same file as the implementation, they could be separated into distinct files using partial classes. Note that both files would still need to be in the same assembly (meaning neither file can be in an editor folder):

MyMonoBehaviour.cs:

 public partial class MyMonoBehaviour : MonoBehaviour, IMyMonoBehaviour {
    [SerializeField]
    private float speed;
    public float Speed => speed;
 }

MyMonoBehaviourEditor.cs:

#if UNITY_EDITOR
using UnityEditor;

public partial class MyMonoBehaviour : MonoBehaviour, IMyMonoBehaviour {
    [CustomEditor(typeof(MyMonoBehaviour))]
    public class MyMonoBehaviourEditor : Editor{
        public override void OnInspectorGUI() {
            serializedObject.FindProperty(speed);
        }
    }
}
#endif

Note: I have not tested the above myself. It may not be possible to have nested classes be editors, or it may be possible to have the MyMonoBehaviourEditor class be private and still function correctly. If you try this out, feel free leave a comment or update the answer.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ I am always a bit scared of the #if UNITY_EDITOR preprocessor directive. It can lead to bugs which only manifest in the build and not in the editor, which makes them very difficult to analyze. \$\endgroup\$ – Philipp Oct 31 '19 at 11:41
  • \$\begingroup\$ Yes, that can be problematic, if you’re not careful and aware of issues that may arise. One must be vigilant, I suppose. Perhaps it could be separated a bit better using partial classes... \$\endgroup\$ – Ed Marty Oct 31 '19 at 11:45
  • \$\begingroup\$ I suspect using the interface is the way to go, but when I type serializedObject.FindProperty(nameof(IMyMonobehaviour.Speed)); it returns null. The Custom editor is still of type MyMonobehaviour not IMyMonobehaviour. Can you make a custom editor of an interface? \$\endgroup\$ – Adam B Oct 31 '19 at 13:36
  • \$\begingroup\$ IMyMonoBehaviour.Speed references the property, not the field in the MyMonoBehaviour. You still need to keep the field public for that case and use that field reference in nameof \$\endgroup\$ – Ed Marty Oct 31 '19 at 13:44
  • 1
    \$\begingroup\$ While there are various ways to enforce it on the C# side of things, unfortunately doing so would make the class no longer appear as a Component in the Unity inspector that can be added to GameObjects the normal way. They could be added in a different way, using a different script to do so, but that would likely make your interface cumbersome and clunky. \$\endgroup\$ – Ed Marty Nov 1 '19 at 2:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.