How do you organize scriptable objects and connect that to UI?

So I create my health bar as a scriptable object with the following code:
(The health decreases automatically with time)

[CreateAssetMenu]
public class PlayerHealth : ScriptableObject
{
public float health;
public float deathRate;
/*private void Update()
{
health = health - (deathRate * Time.deltaTime);
}
*/
}

I then created the health scriptable object in my asset files, and set it to 100
![alt text][1]
And then I created UI slider to display my health on screen and created a script to manage UI:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    public PlayerHealth player;
    public Slider HealthBar;
    // Start is called before the first frame update
    public PlayerHunger hunger;
    public Slider HungerBar;

    public void Start()
    {
        player.health = 100f;
    }
    public void Update()
    {
        //HealthBar.value = HealthBar.maxValue;
        player.health = player.health - (player.deathRate * Time.deltaTime);
        HealthBar.value = player.health;
    }


}

When I exit game mode the slider resets back to 100 (full) however in the scriptable object it is stuck at 95. How do I make it reset each time I exit game mode?

f87195c49a19976b0e92261f9395d1bf8b88025e.jpeg

Scriptable Objects maintain their values even when starting and restarting playing the game in editor mode, which of course is different to how values inside monobehaviours are reset when exiting play mode. They’ll persist between opening and closing Unity too, though in builds they will reset when the game is closed.

If you want values inside scriptable objects to reset, you can use OnEnable() or OnDisable() which is called whenever the scriptable objects enters or leaves the scope of the runtime environment. If you want your play health SO to reset when ending playmode in the editor, I suggest using OnDisable().

1 Like

Thank you! So say if I want my health to decrease automatically and decrease as hunger and hygine drops below 0
I would need something like this:
if (hunger <= 0 || hygine <= 0)
{
health = health - (deathRate * Time.deltaTime);
}
where should I be placing the above code? In the scriptable object script or in the UI script?
And say I have 3 variables: health, hunger and hygine
Can I create one script that contains all three ? Or create 3 separate scriptable objects?

Right now I have something like this in my health script but the value is still not resetting in inspector. What am I doing wrong?

[CreateAssetMenu]
public class PlayerHealth : ScriptableObject
{
public float health;
public float deathRate;
public void OnDisable()
{
Debug.Log(“OnDisable”);
}
}

I have a bit overengineered solution for that, part of my personal framework I’m building while I’m working. I have cut out the portions you will need if you want to use it. Using it is simpler than reading it. :smile:

This goes into a Optional.cs file:

using System;
using UnityEngine;

    [Serializable]
    public class Optional<T>
    {
        [SerializeField]
        private T value;
        [SerializeField]
        private bool hasValue;

        private Optional() {}
     
        private Optional(T value) => Value = value;

        public T Value
        {
            get => value;
            set
            {
                this.value = value;
                HasValue = true;
            }
        }

        public bool HasValue
        {
            get => hasValue;
            set => hasValue = value;
        }

        public static Optional<T> Create(T value) => new Optional<T>(value);

        public static Optional<T> Empty() => new Optional<T>();
    }

This goes in an AbstractConstant.cs file:

using System.Collections.Generic;
using UnityEngine;

    public class AbstractConstant<T> : ScriptableObject
    {
        [SerializeField] protected Optional<T> value = Optional<T>.Empty();

        public T Value => value.Value;

        public static implicit operator T(AbstractConstant<T> reference) => reference.Value;
        public override string ToString() => value.ToString();
        public override int GetHashCode() => base.GetHashCode();
     
#region compare
        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (obj is T o1) return Compare(Value, o1);
            if(!(obj is AbstractConstant<T> abstractConstant)) return false;
            return abstractConstant == this || Compare(Value, abstractConstant.Value);
        }

        public static bool Compare(T a, T b) => EqualityComparer<T>.Default.Equals(a, b);
#endregion
    }

And this goes in an AbstractVariable.cs file:

using System;
using System.Collections.Generic;
using UnityEngine;

    public class AbstractVariable<T> : AbstractConstant<T>, ISerializationCallbackReceiver
    {
        public Action<T> ONChange;

        private readonly Optional<T> _runtimeValue = Optional<T>.Empty();

        public bool HasValue => _runtimeValue.HasValue;
        public new T Value
        {
            get => _runtimeValue.Value;
            set
            {
                if(Compare(_runtimeValue.Value, value)) return;
                _runtimeValue.Value = value;
                ONChange?.Invoke(value);
            }
        }

        public void SetWithoutOnChange(T newValue) => _runtimeValue.Value = newValue;

        public void OnAfterDeserialize() => Value = value.Value;
        public void OnBeforeSerialize() { }

        public override string ToString() => _runtimeValue.ToString();

#region compare
        public override int GetHashCode() => base.GetHashCode();
        public override bool Equals(object obj)
        {
            if(obj == null) return false;
            if(obj is T o1) return Compare(Value, o1);
            if(!(obj is AbstractVariable<T> variable)) return false;
            return variable == this || Compare(_runtimeValue.Value, variable.Value);
        }
        public new static bool Compare(T a, T b) => EqualityComparer<T>.Default.Equals(a, b);
#endregion
    }

If you saved these, you can use both the Constants and the Variables like these:
ColorVariable.cs

using UnityEngine;

[CreateAssetMenu]
public class ColorVariable : AbstractVariable<Color> {}

IntConstant.cs

using System;
using UnityEngine;

[CreateAssetMenu]
public class IntConstant : AbstractConstant<int> {}

Of course you can make (almost) any type to work with these, just create a class for them based on these. Hope I didn’t make a mistake while I cut it out from the other parts of my framework.

1 Like

Hey there. You do need to actually set the values to your desired number in the OnDisable() call. Just putting in an OnDisable() call alone won’t reset the values. For example:

[CreateAssetMenu(menuName = "Player/Health")]
public class PlayerHealth : ScriptableObject
{
    //value used to determine player's current health
    //the below values are only used when creating a new scriptable object in your project assets
    public float health = 100f;

    //player to determine player's maximum health
    public float maxHealth = 100f;

    //on disable is called when the ScriptableObject leaves the game's scope
    //(eg: ending play mode in the editor)
    private void OnDisable()
    {
        //sets the health value to your maximum health value
        health = maxHealth;
    }
}

This above code will reset the health in your scriptable object to your max health when you stop playing in the editor.

To answer your other question, while you can’t use Update (and just about every other call aside from a scant few) inside a scriptable object, you can write methods inside the scriptable object to be called by a monobehaviour script.

For example, you could have the following method inside your player health SO:

public void CheckHygiene()
    {
        //whatever code you want here to check player's hygiene to affect health
    }

And then you can call it inside your script that controls the player, or however you want to structure it:

public class PlayerController : MonoBehaviour
{
    //variable to store playerhealth SO
    public PlayerHealth playerHealth;

    //update inside your monobehaviour script like normal
    private void Update()
    {
        //usually always good to check if there is an object inside any object variables (gameobjects, transforms, scriptableobjects, etc)
        if(playerHealth)
        {
            //then call the method inside the SO
            playerHealth.CheckHygiene();
        }
    }
}

Naturally you may not want to be calling it all the time in update, but could use something along the lines of InvokeRepeating to stretch out the time the code is executed.

It’s a simple structure but one I use a lot as I regularly use ScriptableObjects as more than data containers. This way of referencing methods inside of Scriptable objects is a good way to produce modular functionality (like special abilities or item pickup effects).

1 Like

Thank you for the great input!
I decided to set health to non decreasing and hunger to decreasing with time-
I did the exact same thing as I did with the health bar and the hunger value is decreasing but does not show that on its UI slider- Am I missing something here?


7127729--851534--Screenshot 2021-05-10 135403.jpg
7127729--851537--Screenshot 2021-05-10 135227.jpg
7127729--851540--Screenshot 2021-05-10 135311.jpg

Use scriptable objects for data only, they aren’t supposed to contain any game functionality, they are readonly data containers which let you change settings without opening scenes/prefabs which might be useful in big projects:

public class PlayerSettings : ScriptableObjects //good example
{
    [SerializeField] private float _maxHealth;

    public float MaxHealth => _maxHealth;
}

If you want your player to have health then use MonoBehaviour for that, they exist exactly for dynamic runtime data and game logic modyfing it.

Also, another bad thing in your code is using UI class as main logic container.
Let’s say you have PlayerHealth component on your Player and HealthBar on your Canvas. In that case PlayerHealth should be resposible for game logic, for changing health, receiving damage etc, and HealthBar should be responsible for visualisation ONLY, it should never change any values unless it’s interactable slider/button.
PlayerHealth don’t care about how it will be visulised, do HealthBar exists or no, or maybe you have two HealthBars, or one Health script for player/enemy, and want HealthBar for player one, or want to save health into database, many question and one answer → separate game logic with visualisation.

This is simply not true. Unity itself is promoting SOs as all kind of things. Like pluggable AI solution and whatnot.

There is no reason not to use SOs as many ways as possible. It’s just different from components, they need different considerations.

Thanks for the input. I agree using the UI script to change health and hunger values is not ideal. But how would I do it in the health scriptable object scripts since it can’t use Update () function. Is there an alternative to that for scriptable objects? should I use On Enable() ??

So now I have something like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu]

public class PlayerHunger : ScriptableObject
{
    public float hunger = 100f;
    public float maxHunger = 100f;
    public float hungerRate;

    private void OnEnable()
    {
        hunger = hunger - (hungerRate * Time.deltaTime);
    }

    private void OnDisable()
    {
        hunger = maxHunger;
    }
}

And in the UI script I just display it:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    public PlayerHealth playerHealth;
    public Slider HealthBar;
   
    public PlayerHunger playerHunger;
    public Slider HungerBar;

    public PlayerHygine playerHygine;
    public Slider HygineBar;

    public void Start()
    {
        playerHunger.hunger = 100f;
        playerHealth.health = 100f;
    }
    public void Update()
    {
        HungerBar.value = playerHunger.hunger;
       
        //playerHunger.hunger = playerHunger.hunger - (playerHunger.hungerRate * Time.deltaTime);

       

        HygineBar.value = playerHygine.hygine;
        //playerHygine.hygine = playerHygine.hygine - (playerHygine.hygineRate * Time.deltaTime);

        HealthBar.value = playerHealth.health;
       
    }

   


}

Nothings is happening now. Not the value nor the UI.

Yes, in the project I work they are containers for AI BehaviourTrees or they can contain a lot of other things, it’s very powerful, but it’s still data, you modify it with some visual editor with its own complex serialization process, and the whole thing can describe some complex AI behaviour, but it’s still data, just an XML/Json string in your assets folder. You changed it in the inspector and in runtime you don’t touch it, that’s usually what happens.
If there are examples when people use them in runtime, have what’s called business logic inside, modify variables and do stuff like that then I would like to hear about good ones.

This is a little bit more than a state machine representation, since the actions themselves are also pluggable, so they implemented in SOs.
https://learn.unity.com/tutorial/pluggable-ai-with-scriptable-objects#5c7f8528edbc2a002053b487

There are countless examples on Youtube and in various tutorials how to build a pluggable system with SOs. From handling inventory to plug and play achievement systems or “skill/ability”-systems (like attacks, defenses, etc in a tactical RPG).

Scriptable objects are just not meant for that. The right way to do that, and what I would do personally:

public class PlayerSettings : ScriptableObject
{
    public float maxHealth = 100f;
    public float maxHunder = 100f;
    public float hungerRate;
}

public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private PlayerSettings _settings;

    private float _amount;

    public event UnityAction<float, float> Changed;

    private void Start()
    {
        _amount = _settings.maxHealth;
    }
 
    public void ApplyDamage(float dmg)
    {
        _amount -= dmg;
        HealthChanged?.Invoke(_amount, _settings.maxHealth);
    }

public class HealthView : MonoBehaviour
{
    [SerializeField] private PlayerHealth _health;
    [SerializeField] private Slider _slider; 

    private void OnEnable()
    {
        _health.Changed += OnHealthChanged;
    {

    private void OnDisable()
    {
        //always unsubscribe
        _health.Changed -= OnHealthChanged;
    }

    private void OnHealthChanged(float current, float max)
    {
        //slider is from 0 to 1 here
        _slider.value = current / max;
    }
}

public class PlayerHunger : MonoBehaviour
{
    //initialization and fields here

    private void Update()
    {
        hunger -= _settings.hungerRate * Time.deltaTime;
        Changed?.Invoke(hunger, settings.maxHunger);
    }
}

public class HungerView : MonoBehaviour
{
    //Same as health
}
1 Like

In example you sent, they are still kind of data. They don’t really act, there is StateController for that, you set everything up in the inspector and this thing just works during runtime.
I would even argue is this a good approach that States do things and modify controller. The behaviour thing passing itself to data class to let it modify thyself, it should be vice verse and I think any OO programmer would say you the same, Controller should grab actions/transitions from states and do whatever he wants, he is Controller, and in that case State is just pure data with actions/transitions. Transition is already pure data.
Actions and Decisions are kind of crutch. It’s scriptable object which should be created once, it’s stateless and contains one function. Custom inspector and bit of reflection will turn this into plain C# classes. In the end you will have StateController as behaviour, plain classes as logic with nice reflection inspector, and two data classes.
I’m not arguing with fact that you can put your whole game into scriptable object, and some tutorials may have info about how to do that, but do you really need it?

And I wouldn’t consider youtube a good place to learn anything about code design (even Unity to be honest), one of last things I saw is singleton in SO, I saw a lot of videos about managers and systems, but I rarely see people saying that singletons/managers/systems and all that stuff just not the best design decisions.
Even with Unity learn, it’s not the best code, written not by Unity devs or professional developers with 5-10 years in commercial development, so I would argue with how good things they show to the people.

Thank you so much! Will test it out. But the reason I want to make the values as scriptable objects is to keep the data between scenes. How would you do it with your method?
Could you explain the logic behind making maxHealth scriptable objects and not monobehaviors like the rest?
Correct me if I’m wrong- I would be attaching the player health and player hunger scripts separately to the player, and the HealthView scripts to the UI slider?

You’re forgetting a very important problem. There are a ton of non-programmers who are making games. Actually more non-programmers than programmers. This means programmers are expensive (in terms of resources). Whatever we can do to lower the workload on programmers is a good thing, even if your precious school-book OOP fancy design says otherwise. SOs just do that. They allow the non-programmers to create and configure pieces of software and plug them into each other without the programmer immediate help (programmer do the work upfront by creating the modules).

Game software is not enterprise software. There are no awards for “clean code” and “best OOP architecture of the year”. There is satisfaction though when the game designer can put together a custom AI themselves from the parts you throw at them and your game is running on acceptable framerate. And no one cares about your OOP design.

At the end of the day, one thing counts: can you make the game before deadline and can you make it work on the toaster with 60FPS. Do whatever you need to do to achieve that.
I even run my own player loop with one wired MonoBehaviour. I can run Update cycle in any ScriptableObject or plain C# object I want…

Disclaimer: I do programing for 35 years and I make enterprise software for a living for more than 24 and managing engineering teams for more than 10. So I know how enterprise software made. Games aren’t enterprise software.
Maybe big, continuous MMOs count. The maintainability there is similar to an enterprise software. But we aren’t building WoW here…

ps1: sorry OP that we hijacked your thread for philosophical debate

p2: Tekrel, sorry if it seems that I went 0 to 60 in 2 seconds, but it is a recurring theme on these forums that some fresh developer, who was thought that OOP design is the uber alles drops in here and tries to evangelize everyone to the same textbook BS they thought in the school.
In real life, software engineering is different, especially game development. In enterprise software, the maintainability is king due to the structure of those companies and the life expectancy of the software. In game development, it’s different. There are parts which are keepers (engine) and part that are usually thrown out after the game life came to an end (scripts). With some exceptions of course, sometimes engine code must go, sometimes some parts of scripts end up remaining. And we here, are writing scripts, not engine code in general. Maintainability usually not a primary concern. Performance and flexibility are usually more important, among other things.

1 Like

If you really want to use ScriptableObject for passing data between scenes then I would probably create specific one for that. One will be PlayerSettings which will contain maxHealth and maxHunger, second will be RuntimeSceneData which will contain currentHealth and currentHunger. Player will save his values into this SO in OnDestroy, and grab in Awake.
Another approach would be DontDestoryOnLoad(playerDataObject).
If you have many levels and you want you game to be persistent so you can exit, enter and continue from the place you were before, then you can use PlayerPrefs between scenes, you will both save data on your disk and track health/hunger from another scene.

Imagine you have GameDesigner in your team, or you just have big game, or big team, what ever, you don’t really want to open scene or prefab hierachy, search for component and modify any value, you want one place when all your settings, game balance etc. are set, scriptable objects are the right instrument for that, you will have one folder when all configuration is done.

Yes, your player will have PlayerHealth, PlayerHunger, and on your canvas you will have HealthBar and HungerBar.

Your only problem with the scriptable object is that the values are not resetting, right?

If this is the case, you just need to introduce a second set of variables and separate them. I posted my solution above, if you open up the code, you can see that in the “AbstractVariable” script, you can see a “_runtime” part. That is what you use while your game is running. There is the “value” part, what is showing up in the inspector. And there is the serialization callback, where you can take the inspector value and write into the runtime one. This happens when you start the game. So you will. always run your game with whatever value you put in the inspector and you can change it in runtime, the serialized value will remain for the next startup. If you take these three things from that file, you can make it work.

@
Update: it’s working. I just forgot to set max value of hygine to 100 in the slider.

Hi. I was able to get the values to reset using On Disable(). Now my main problem is the Hygine bar UI not updating in runtime. It’s scriptable object value is though. Strangely the hunger bar is updating as it should.

public void Update()
    {
        HungerBar.value = playerHunger.hunger;
      
        playerHunger.hunger = playerHunger.hunger - (playerHunger.hungerRate * Time.deltaTime);

      

        HygineBar.value = playerHygine.hygine;
        playerHygine.hygine = playerHygine.hygine - (playerHygine.hygineRate * Time.deltaTime);

        HealthBar.value = playerHealth.health;
      
    }