问题
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