Game Configuration via ScriptableObjects

Any game or application in general benefits from being configurable over hard-coding configuration values. Say I want to provide a value of the maximum health points of my main character. I could simply hard-code this:

private const float MaxHealth = 100f;

which leads to a recompilation of the Assembly the field if defined in every time I want to change the value. A better approach might be to serialize the field on a character behaviour:

public class CharacterBehaviour : MonoBehaviour {
	
    public float MaxHealth => _maxHealth;
    
    [SerializeField]
    private float _maxHealth = 100f;

}
Note how I restricted write access to the field by exposing it as a get-only property on the public-facing side while declaring the field itself private. Unity's inspector still honors [SerializeField] attributes on private fields as well. This way, I'll be able to change the value from within the inspector, but won't be able to overwrite the value once my game is running.

This is a valid approach that works great for dynamic attributes of behaviours like current health points, names, etc. Changing the value also doesn't incur a recompile anymore as it is serialized from within the inspector.

But what about global game configuration? If I want to configure every character to have a maximum health. Providing the value on the character behaviour as seen above does not only unnecessarily increase the size of my serialized component because ideally I'd only need to provide the value once, it also leaves room for error because if I want to someday change the maximum health of all my characters, I'll have to go through every character behaviour in my scene/assets and change it manually.

If I wanted to provide a different maximum health value for every character in my game, I'd be fine with putting the field on the character behaviour. Global configs that don't change for many behaviours don't belong there though.

I could of course simply create a config GameObject that provides these values and use it from my scripts via `GameObject.Find`. This would also mean that I'd have to include the config objects in every scene that needs to load it. Also, using GameObjects that provide no value to the active scene and simply act as containers is frowned upon because it incurs a performance overhead that is better reserved for essential objects.

ScriptableObjects to the rescue

In addition to `MonoBehaviour` Unity provides a second serialized base class called `ScriptableObject`. These are classes that can be instantiated similar to prefabs as assets within your project view, seperate from any active scene.

namespace CelestialStatic.Unity.Settings {

    public abstract class UnitySettings : ScriptableObject {

    }

}
Creating an abstract base class that all settings will derive from allows me to load all of them from Resources later on and add base functionality.
namespace CelestialStatic.Unity.Settings {
	[CreateAssetMenu(fileName = "CharacterSettings", menuName = EditorConstants.SettingsMenuBase + "/Character")]
    public class CharacterSettings : UnitySettings {

        public float MaxHealth => _maxHealth;
        
        [SerializeField]
        private float _maxHealth = 100f;

    }
}

It's always good to move your classes into namespaces, avoiding name conflicts between your own classes and third-party libraries and packages you import from the asset store.

The `CreateAssetMenu` allows me to add the character settings asset to the context menu when right-clicking inside the project view. I'm using a constant `EditorConstants.SettingsMenuBase` because I want all of my settings grouped under the same base string `Celestial Static Games/Settings`.

"Create" context menu

Loading configuration during runtime

Now that I can store my configuration files, I still need to provide an easy way to load them during runtime.

When working on a larger game or codebase, you inevitably come across the concept of Dependency Injection. Any class requiring a reference on e.g. a world manager or UI window can declare the dependency on itself and as long as the required instances have been registered with the injection container, they will be automatically injected upon creation of new instances of the class.

On all my recent projects, I've chosen to go with Zenject (now Extenject after some legal disputes) as my preferred framework. It's very easy to get set up and has a minimal performance overhead. If you haven't checked out the concept or any of the frameworks available so far, I'd highly recommend you doing so. Its worth the investment even for small projects.

To register all configuration files with my DI container, I have to load them from my Resources folder and register them with their corresponding types.

namespace CelestialStatic.Unity.Settings {

    public class SettingsInstaller : MonoInstaller {

        public override void InstallBindings() {
            BindSettings();
        }

        private void BindSettings() {
            foreach (var setting in Resources.LoadAll<UnitySettings>("Settings")) {
                Container.Bind(setting.GetType()).FromInstance(setting).AsSingle().NonLazy();
            }
        }

    }

}

Most of this class is boilerplate code specific to Zenject, but the code inside `BindSettings()` is applicable to any framework (or even no framework at all).

Thanks to the magic of dependency injection all I have to do to access my configuration classes during runtime is declaring them in a constructor or injectable method:

public class CharacterBehaviour : MonoBehaviour {
    
    [Inject]
    public void Initialize(CharacterSettings characterSettings) {
    	// Access settings here...
        var maxHealth = characterSettings.MaxHealth;
    }

}

For any new type of configuration all I have to do is create a new class containing the fields I require and configuring the asset from within Unity's inspector.

I hope you found this tip useful and can see the benefits of providing a clean architecture for your configuration files.

Comments