Spawn Manager using Scriptable objects and events

I am working on a top-down game where I need several different objects to spawn. Just following the basic tutorials from Unity one can get a working spawn manager, though my main goal is to learn Unity, C# and different approaches, so while stumbling upon the famous talk “Unite Austin 2017 - Game Architecture with Scriptable Objects” by Ryan Hipple I decided to re-write my previous code using the SO approach.
Why I created this post:

There are many great tutorials out there, and yet I was struggling to create what I wanted based on these. Searching the internet did not yield the answers I was looking for - thought that might of course be on me (wrong Google and/or AI queries, still on a learning curve, lack of experience, etc.).
That being said I got a working approach towards a spawn manager that is sufficient for my needs and yet (I think) quite versatile. Maybe my post might help some other beginner, but I am writing this mainly to get some feedback from the community - should anyone be willing/able to review my approach.
I am aware of the “right tool for the right job” approach and I am sure that the below could be done easier and without as much code, but please remember I am highly commited to learning and hope to create some modular code I could re-use in the future :slight_smile:

Important: I am using the ScriptableObject-Architecture asset.
Spawn manager data code (ScriptableObject), :

using ScriptableObjectArchitecture;
using UnityEngine;

[CreateAssetMenu(menuName = "Systems/Spawn Manager", fileName = "SpawnManager")]
public class SpawnManagerSO : ScriptableObject
{
    [Header("Spawn data")]
    [SerializeField] public FloatVariable horizontalSpawnRange; // Linked to game's XY boundries.
    [SerializeField] public FloatVariable verticalSpawnRange; // Linked to game's XY boundries.
    [SerializeField] public GameObjectCollection whatToSpawn;
    [SerializeField] public BoolVariable spawnTrigger;
    [SerializeField] public FloatVariable spawnRateMin;
    [SerializeField] public FloatVariable spawnRateMax;

    public void Spawn()
    {
        float horizontalSpawnYPosition = verticalSpawnRange + 10f; // Y-coordinate - to spawn the object out of Player's sight.
        Vector3 spawnPosition = new Vector3(Random.Range(-horizontalSpawnRange, horizontalSpawnRange), 0, horizontalSpawnYPosition); // Randomize X spawning position.
        int objectIndex = Random.Range(0, whatToSpawn.Count);
        Instantiate(whatToSpawn[objectIndex], spawnPosition, whatToSpawn[objectIndex].transform.rotation);        }
}

Now a MonoBehaviour SpawnManager to attach to a scene object/prefab, with a method that gets called on spawnTrigger change and a coroutine to handle the (randomized) spawn rate:

using System.Collections;
using UnityEngine;

public class SpawnManager : MonoBehaviour
{
    [Header("Spawn Manager SO")]
    [SerializeField] public SpawnManagerSO whatToSpawnData;

    private void Start()
    {
        // Initiate the first change/trigger to start "chain reaction".
        whatToSpawnData.spawnTrigger.Value = !whatToSpawnData.spawnTrigger.Value;
    }

    public void Spawn()
    {
        // Spawn is triggered every time when BoolVariable is changed.
        // Important: BoolVariable actual value does not matter - Spawn is triggered "on value change", not "on true".
        whatToSpawnData.Spawn();
        StartCoroutine(CanSpawnTimer());
    }

    IEnumerator CanSpawnTimer()
    {
        // This handles the "chain reation" that changes the Spawn() trigger after some delay.
        // 60f / rate => rate is "per minute".
        yield return new WaitForSeconds(60f / Random.Range(whatToSpawnData.spawnRateMin, whatToSpawnData.spawnRateMax));
        whatToSpawnData.spawnTrigger.Value = !whatToSpawnData.spawnTrigger.Value;
    }
}

My Editor setup consists of the following:

  • ObjectSpawnManager, created from SpawnManagerSO (holding references to ObjectCollection, variables and trigger event);
  • FloatVariables for min/max spawn range (final spawn rate is randomized at runtime);
  • ObjectCollection to store prefabs of objects-to-spawn (each object currently has a simple Move script attached);
  • BoolVariable to use as trigger for the Spawn method;
  • BoolVariable event - to run the Spawn method on trigger.
    9778995--1402059--upload_2024-4-18_18-3-44.png

Objects (prefabs) in Scene have the following attached:

Adding a new spawn manager with other objects requires little work (at least I think so) - copy whole folder “Objects”, adjust ObjectSpawnManager to point to correct items, fill ObjectCollection with new prefabs and add create a SpawnManager (the Monobehaviour script) in the scene with the correct event listener.

Now here are my questions:

  • does this setup look like something viable and modular, or rather a hard-to-understand mess?
  • is it actually following the ScriptableObject design philosophy or is it “overusing” SO too much (for example compared to putting code in Update())?
  • is it a huge overkill (I am aware it is overkill to some extent) and unnecessary division into small chunks, or is it a step in the right direction on learning game programming?
  • if the system is “fine”, what could be tweaked?

Since I am on the early stages of my path I would be grateful for any feedback.

Admittedly, I haven’t used SOs as much as I should. Your setup is not difficult to understand.

  1. whatToSpawnData.spawnTrigger.Value = !whatToSpawnData.spawnTrigger.Value; is bit confusing.
    Maybe whatToSpawnData.spawnTrigger.Value = true; is better
  2. Personally, I would move the Spawn() function from the SO to the SpawnManager, since SOs are really just meant to hold data.

Ryan Hippie’s video is a huge trap. You do not need to and generally don’t want to make everything individual scriptable object variables.

His video is about values that need to be referenced from a number of different places. Such as his health example. Yes, having an individual scriptable object value for the player’s health makes sense as it needs to be read/written to from a number of different locations.

Do these values in your SpawnManagerSO have the same requirement? If they don’t, just make them regular values.

1 Like

Thanks for your feedback! Regarding pt 1.: The reason that I am actually just setting the spawnTrigger to its opposite value is that… I could not get it to work with (…) = true. The event is triggered every time there is a change in the variable and I actually only want to trigger an event every time a change happens “to true”. So I started to use the value as trigger - I treat every change as a trigger and I dont take into consideration the actual value (true/false) into account.
I just couldnt figure out how to do that - probably lack of experience. If you have any suggestions I would like to hear, since its actually driving me crazy that I could not figure it out :slight_smile:
EDIT: Figured it out… To no surprise of mine I have not been using events correctly - I mixed both together: BoolVariable and BoolVariableEvent, and I was raising event from BoolVariable - thus on every change… I should only be using BoolVariableEvent and raising it from code when necessary. Learning something every day :slight_smile:

I did, however, have a similar feeling to what spiney199 actually confirmed - I found myself changing almost everything to SO, and it is still hard to figure out where to stop :stuck_out_tongue: (I guess a solid game design would resolve this issue, and I am just making stuff up as I go along - adding functionality to the game mainly to learn new stuff).

Pt 2.: I was thinking about that but I saw that methods can also be stored as SO for repetitive tasks, but I think I will follow your suggestion - seems like it would be better for organizing my code. Right now I have some methods in SO’s, but others (like coroutines) I have in Monobehaviours and I already do wonder sometimes “where did I put it?”. Thanks!

Thanks for your response! Yes, I do understanc what you mean - I find myself trying to put everything into SO now and its hard to know when to stop. I guess getting more and more experience will help, but for now I will have to ask myself the question you posed.
When it comes to my actual SpawnManagerSO, I would like to have certain values available cross-scene, and others linked to one “global reference” - if I need to change/scale the value I would like to make one change and have it affect all values and scenes. Unelss there is a better way to do this - e.g. a “config” file for such global values? (havent been looking for it yet, will do it t his weekend)