Options for setting mandatory fields on components created by code?

泄露秘密 提交于 2019-12-13 02:14:39

问题


When I create a GameObject like below I want my component to have/set mandatory fields.

GameObject go = new GameObject("MyGameObject");
MyComponent myComponent = go.addComponent(MyComponent);

Since I cannot use a constructor to set private fields I either need a setter for these fields or a public Init function that sets multiple mandatory properties. But this allows changing the values anywhere in the code and unlike a constructor with overloads it's not obvious that Init "needs to" or "can be" called after creation to set these parameters.

So are there other options that allow me to set mandatory fields when creating a Component and use "proper" encapsulation?


回答1:


Just as stated in the comments, Unity implemented 2 main methods for MonoBehaviour initialization logic: Start and Awake.

There are drawbacks of these 2 initialization methods:

  • They cannot accept any parameter
  • You're not sure of their order of execution

There's a standard order of execution and from this you can be sure that Start is executed after Awake. But if you have multiple MonoBehaviours of the same type then it's not easy to track which executes first.

If your issue is just the execution order and you have have different types then you could just update the script execution order.

Otherwise there are further approaches to avoid both drawbacks by using a Factory Method inside the monobehaviour.

Consider that in this case the order of execution will be:
Awake => OnEnable => Reset => Start => YOUR INIT METHOD

PRIVATE FACTORY METHOD

public class YourMono : MonoBehaviour
{
    //a factory method with the related gameobject and the init parameters
    public static YourMono AddWithInit(GameObject target, int yourInt, bool yourBool)
    {
        var yourMono = target.AddComponent<YourMono>();
        yourMono.InitMonoBehaviour(yourInt, yourBool);
        return yourMono;
    }

    private void InitMonoBehaviour(int yourInt, bool yourBool)
    {
        //init here
    }
}

This method offers the better encapsulation since we can be sure that InitMonoBehaviour wioll be called just once.

EXTENSION FACTORY

You can also create a factory from an extension. In this case you remove the factory method from the class, it might be useful to decouple factory logic from gameplay logic.

But in this case you will need to make InitMonoBehaviour internal and implement the extension in the same namespace.
As a result InitMonoBehaviour will be a bit more accessible (internally from the same namespace) so it has lower encapsulation.

    [Serializable]
    public class YourData
    {
    }

    namespace YourMonoNameSpace.MonoBehaviourFactory
    {
        public static class MonoFactory
        {
            public static YourMono AddYourMono(this GameObject targetObject, YourData initData)
            {
                var yourMono = targetObject.AddComponent<YourMono>();
                yourMono.InitMonoBehaviour(initData);
                return yourMono;
            }
        }
    }

    namespace YourMonoNameSpace
    {
        public class YourMono : MonoBehaviour
        {
            private YourData _componentData= null;
            internal void InitMonoBehaviour(YourData initData)
            {
                if(_componentData !=  null ) return;
                _componentData = initData;
            }
        }
    }

That said both of these options has also a drawback:

  • Someone could forget to launch them.

So, if you want this method to be "required" and not optional, I suggest to add a bool flag _isInitiated to make sure no one forgot to implement it.

INVERSION OF CONTROL - INSIDE AWAKE

Personally I would implement the logic with an Scriptable Object, or with a Singleton, and I would call it during awake to make sure the initialization is called all the times.

public class YourMonoAutoInit : MonoBehaviour
{
    public InitLogic _initializationLogic;

    //a factory method with the related gameobject and the init parameters
    public void Awake()
    {
        //make sure we not miss initialization logic
        Assert.IsNotNull(_initializationLogic);
        InitMonoBehaviour(_initializationLogic);

    }

    private void InitMonoBehaviour(InitLogic initializationLogic)
    {
        //init here using
        int request = initializationLogic.currentRequest;
        bool playerAlive = initializationLogic.playerIsAlive;
    }
}

public class InitLogic : ScriptableObject
{
    public int currentRequest = 1;
    public bool playerIsAlive = false;
}

//this is another monobehaviour that might access the player state and change it
public class KillPlayer : MonoBehaviour
{
    public InitLogic playerState;
    public void KillThisPlayer() => playerState.playerIsAlive = false;
}

With this last version you're achieving inversion of control.
So instead of SETTING DATA INTO THE MONOBEHAVIOUR:

//SETTING DATA FROM CLASS TO MONOBEHAVIOUR
public class OtherScript : MonoBehaviour
{
    private void CreateMonoBehaviour() => YourMono.AddWithInit(gameObject, 1, true);
}

You will GET DATA IN MONOBEHVARIOUS FROM CLASS.

//GETTING IN MONOBEHVARIOUS FROM CLASS
public class YourOtherMono : MonoBehaviour
{
    public YourData data;

    private void Awake()
    {
        (int yourInt, bool yourBool) = data.GetData();
        //do something with the data received
    }
}


来源:https://stackoverflow.com/questions/56218963/options-for-setting-mandatory-fields-on-components-created-by-code

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!