Using ScriptableObjects at runtime vs editor time

Hello everyone,

I’ve been trying to find a good way to architecture my ability system. I came across some nice posts on the forum, but I still have questions.

I can simplify my main problem with an example from a blog post I’ve read: https://unity3d.com/how-to/architect-with-scriptable-objects#cool-things
Let’s focus on the PlayerHP situation. I see very well the pros of such a system because it allows to build a loosely coupled system. My problem is that if I want to create a EnemyHP, then spawn multiple enemies, they all share the same object, thus the same HP. The same goes with my abilities (that can be casted by one player or another, or enemies).

Based on the posts I read on the forum, I’ve used “Instantiate” so they are separate instances at runtime. But in that case, I can’t make links between the EnemyHP and other systems in the editor anymore. The solution is to inject the object’s reference everywhere I need it, but I lose the benefits of loosely coupled elements.

Is the advice in the blog post invalid in that case? Or am I missing something (probably obvious)?

Cheers,
Fleb.

PS: sorry for the eventual mistakes, I’m not a native english speaker.

The PlayerHP thing seems to only work for a simple game. It has the problem of needing Enemy1HP, Enemy2HP as you have realised…

The only way I’ve seen to do this would be to make the ‘FloatVariable’ a MonoBehaviour on a child GameObject. Basically, a GameObject/MonoBehaviour for each piece of data. That way it can be dragged around in the editor and also be a unique instance, but obviously it’s not an asset like a SO, and would have to exist in the scene.

A more complex system could be where every FloatVariable/StringVariable has some kind of ID, so your GUI scripts can find everything it needs in a way that is decoupled in code.

Thank you for your answer, that confirms what I suspected. I’m going to keep messing around until I find what suits me best.

Cheers,
Fleb.

Yeah, that specific scriptable object data structure is really only useful for very tiny projects. I would advise not using that pattern at all, for things so specific as health and stuff.

If you only need one instance of something, or something is global, then scriptable objects are great. They are also great for setting up templates.

As an example of how i use them for my ability system, I have a character class scriptable object that i create and then drag and drop abilities into. Because it exists on a SO, I don’t have to go into any scenes and edit the scripts.

Think of it like the Animator. You just edit the file, and then drop it onto a scene component that then clones it, and operates on it from there.

If your abilities were monobehaviors, and you have multiple game objects with those abilities, then your gonna be in copy and paste hell.

Thank you for the replies.

I’ve settled for something that I think will suit my project for the time being.

An AbilityManager that I can pass to any Game Object that cast abilities, such as my player, other players, or various enemies.
I then drag & drop abilities, which are Scriptable Objects instances, to the AbilityManager component. I clone them at runtime to prevent the shared instance thing.

Here is a simplified version of my code (no hotkey, skillbar UI management, …) if it can help others:

public class AbilityManager : MonoBehaviour
{
    public Transform abilitySource; // "launcher" of abilities

    public Ability[] abilities;


    void Awake()
    {
        Ability ability;
        for(int i = 0; i < abilities.Length; i++) {
            // clone the object because it's a scriptable objects and we want a unique instance of it.
            abilities[i] = Instantiate(abilities[i]);
            abilities[i].Initialize(abilitySource);
        }
    }

    void Update()
    {
        foreach(Ability ability in abilities) {
            ability.MakeUpdate();

            if(!ability.isOnCooldown()) {
               // my custom logic that then Launch the ability based on a hotkey. This is for the player.
               // .....
               Launch(ability);
            }
        }
    }

    // that way an IA component on an enemy can cast an ability too (there is more to that function aswell)
    public void Launch(Ability ability) {
           ability.Launch();
    }
}

And then the Ability base class that I override for my various abilities. It only has a cooldown logic for the sake of simplicity:

public abstract class Ability : ScriptableObject
{
    public string displayName = "New Ability";
    public float cooldownDuration = 2f;
    private float cooldownTimeLeft;

    protected Transform spawnPoint;

    public void Initialize(Transform source) {
        spawnPoint = source;
    }

    public void Launch() {
        if(!isOnCooldown()) {
            Trigger();
            cooldownTimeLeft = cooldownDuration;
        }
    }

    // Handle the actual code of the ability such as firing a projectile or creating a wall.
    protected abstract void Trigger();

    public virtual void MakeUpdate() {

        if (isOnCooldown()) {
            cooldownTimeLeft = cooldownTimeLeft - Time.deltaTime;
        }
    }

    public bool isOnCooldown() {
        return cooldownTimeLeft > 0f;
    }

    public float getCooldownTimeLeft() {
        return cooldownTimeLeft;
    }
}

And an example of a projectile ability:

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

[CreateAssetMenu(menuName = "Abilities/Projectile")]
public class ProjectileAbility : Ability
{
    public float range;
    public float speed;
    public Rigidbody projectilePrefab;
   
    protected override void Trigger() {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100))
        {        
            SpawnProjectile(spawnPoint.position, hit.point, range, speed);
        }
    }

    private void SpawnProjectile(Vector3 origin, Vector3 target, float range, float speed)
    {
        Rigidbody newProjectile = (Rigidbody)Instantiate(projectilePrefab, origin, Quaternion.identity);
        ProjectileLifeTime projectileLifeTime = newProjectile.GetComponent<ProjectileLifeTime>();
        projectileLifeTime.range = range;
        projectileLifeTime.speed = speed;
        newProjectile.velocity = (target - spawnPoint.position).normalized * speed;

        Collider newProjectileCollider = newProjectile.GetComponent<Collider>();
        Physics.IgnoreCollision(newProjectile.GetComponent<Collider>(), spawnPoint.GetComponentInParent<Collider>());

        GameObject[] projectiles = GameObject.FindGameObjectsWithTag("Projectile");
        foreach (GameObject projectile in projectiles)
        {
            Physics.IgnoreCollision(newProjectileCollider, projectile.GetComponentInParent<Collider>());
        }
    }
}

That way I can build multiple abilities and share them easily.

You will note that the targetting is based on mouse position, thus for enemy it doesn’t work well, but that’s my next thing to fix :wink:

Cheers,
Fleb.