How to create a MonoBehaviour fabric for non-MonoBehaviour classes?

Viewed 90

I am developing a small nuclear reactor simulator game. I have a bunch of reactor component classes: HeatVent, HeatExchanger, UraniumCell etc. They are not deriving from MonoBehaviour since they don't have any Unity logic, but they do implement a shared interface IReactorComponent. What I want to do is to be able to create prefabs of such components (simple heat vent, advanced heat vent, doubled uranium cell etc.) The prefabs would have different sprites and something like that, but the main issue is to define what reactor component class the prefab is related to, because I can't just drag'n'drop a non-MonoBehaviour script on inspector. Also, I want to be able to set settings in the inspector (for example, HeatVent has CoolAmount and HeatCapacity properties, UraniumCell has FuelAmount, HeatProduce and PowerProduce properties).

I have read about factory method pattern and as I understood, I have to create a fabric class that derives from MonoBehaviour for each reactor component class like HeatVentBehaviour, HeatExchangerBehaviour etc. Yes, that completely solves my issue with prefabs but is there any way to not create an additional MonoBehaviour wrap for each class? If I had 15 IReactorComponent classes, I would need to create 15 fabrics which feels like not the greatest solution.

1 Answers

Sounds like what you are looking for is ScriptableObject!

Instances of those are assets so they don't live in a scene but in the Assets folder and basically behave a little bit like prefabs except: They already exist and do not need to be instantiated anymore.

Mostly they are used as just configurable data containers. They have an Inspector so you can easily fill them with your desired data and references to other assets (e.g. the related prefab in your case).

But in addition you can as well let them implement behavior like your interface and thereby change the behavior of your scene objects by using different implementations of a method from different ScriptableObjects!

For the factory you then only need to figure out for which method to use which ScriptableObject instance e.g. either by having different methods or by having a Dictionary where you fill in your SO references.


Just as an example how this might look like (make sure each MonoBehaviour and ScriptableObject has its individual script file with matching name)

SpawnManager.cs

public class SpawnManager : MonoBehaviour
{
    [SerializeField] private ReactorComponentBehaviour _behaviourPrefab;
    [SerializeField] private BaseReactorComponent[] _components;

    public bool TrySpawn<T>(out T component, out ReactorComponentBehaviour componentBehaviour) where T : IReactorComponent
    {
        component = default(T);
        componentBehaviour = default;

        var foundComponent = components.FirstOrDefault(c => c.GetType() == typeof(T));

        if(foundComponent == null) 
        {
            Debug.LogError($"No component found of type {T.GetType().Name}!");
            return false;
        }

        // Here Instantiate doesn't spawn anything into the scene but
        // rather creates a copy of the ScriptableObject asset
        // This is just to avoid that any changes in the fields during the game
        // would change the original ScriptableObject asset and thereby ALL related behavior instances 
        component = Instantiate ( (T) foundComponent);

        // This now indeed spawns the related MonoBehaviour + GameOver
        componentBehaviour = Instantiate (behaviourPrefab);
        componentBehaviour.Init(component); 

       return true;   
    }
}

BaseReactorComponent.cs

public abstract class BaseReactorComponent : ScriptableObject, IReactorComponent
{
    public abstract void WhateverIReactorComponentNeeds();

    // Common fields and methods e.g.
    public Sprite Icon;
}

HeatVent.cs

[CreateAssetMenu]
public class HeatVent : BaseReactorComponent
{
    public int CoolAmount;
    public int HeatCapacity;

    public override void WhateverIReactorComponentNeeds ()
    {
        // Do something
    }
}

UraniumCell.cs

[CreateAssetMenu]
public class UraniumCell : BaseReactorComponent
{
    public int FuelAmount;
    public int HeatProduce;
    public int PowerProduce;

    public override void WhateverIReactorComponentNeeds ()
    {
        // Do something
    }
}

And finally you need only one base prefab with the

ReactorComponentBehavior.cs

public class ReactorComponentBehavior : MonoBehaviour
{
    [SerializeField] private Image _image;

    private IReactorComponent _component;

    public void Init(IReactorComponent component)
    {
        _componemt = component;

        // Do other stuff like e.g. adjust visuals according to the component etc
        _image.sprite = component.Icon;
    }

    // And then use whatever this behavior should do with the assigned component
}

So in the end you would use that like e.g.

if(spawManagerReference.TrySpawn<HeatVent>(out var component, out var componentBehaviour)
{
    // Do something with the behavior e.g. set its position, parent etc
}
else
{
    Debug.LogError($"Failed to get a {nameof(HeatVent)}!");
}

If then at some point you still want different additional behaviours you could let them inherit from the common ReactorComponentBehavior and rather reference the prefabs inside the BaseReactorComponent itself .. then every component can bring its own prefab but still have a common core behaviour

Related