ScriptableObject overhead?

I’d like to use a ScriptableObject to store global game state data. This data will change frequently and be updating many many variables throughout the life of my game. I did some research on SOs and found a blurb that says since SOs are serialized, there’s some overhead involved because when you change the data, it has to be serialized again. I only saw this mentioned once out of the many articles I read about SOs.

Does anyone have any experience with this? I’d like to crowd-source some info on it.

Benchmark it!

Pitfalls to avoid when benchmarking:

  • Likely there’s a different between editor performance and standalone.
  • Standalone has no automated serialization of ScriptableObjects (SOs).
  • Verify whether there’s an impact only between frames or whether every value change has an impact.
  • In Editor also pay attention to the overhead of having the corresponding SO actually open in the inspector which means the UI will refresh (that can become really sognificant on game objects too!).
1 Like

In your case what’s the advantage of using an SO for a world state? why not a plain C# object?

1 Like

The SO is just a container to hold data. For this purpose you could just as well use a C# singleton or static class to hold that data, and serialize it to json as needed.

There is no runtime serialization for SOs so whatever was said in that regard was referring to serializing large amounts of data in the editor within an SO. Common issue being a List containing tens of thousands of items and when you select the SO or object in the scene, bamm, Inspector is super slow because it renders the GUI for 10k items.

2 Likes

Thanks for the replies, guys.

I am currently using a static class to hold live game state data. It’s part of a global static class I’m using to hold project wide class definitions and extension methods. It works well. A bit bloated, but it works.

Since this is a learning/hope to release project, from time to time I’m re-evaluating how I’m doing things. There’d be no advantage to switch to a SO, heck, I’d have to refactor some code to accommodate it. But statics are cringe right? I try to inform myself on best practices and whatnot. :sweat_smile: Being a self taught hobbyist coder, I want to try to adhere to as many ‘professional ways’ as possible.

Not at all. Static variables and static classes are awesome, otherwise they wouldn’t exist.

Like anything they serve a purpose and they have limitations.

The key limitation that might apply to your case would be that they cannot be serialized without hand-writing code for each field.

You can also make a normal C# class, then store a reference to that instance in a static variable that all your code uses.

This lets you serialize / deserialize it, but also get at it everywhere by just using the static reference.

Mechanisms like this are often found in “Game Managers.”

2 Likes

The major thing to watch out for with statics is you often need to reset their state in order to disable Domain Reload. If you’re not disabling Domain Reload, do a couple hours work (if that) to turn it off because as your project grows it can reduce iteration time a lot. Whenever you declare a new static you should consider whether it needs to be reset.

2 Likes

I want to continue on Kurt’s post in regards to when you said this.

You wanna know why statics are claimed to be cringe within the community? It’s because they’re inherently global, and globals are claimed to be cringe within the programmer community.

Well… you said in your original post:

You’re inherently doing the “cringe” thing.

And as Kurt said, it’s not cringe. Globals aren’t evil like a lot of people try to claim out there.

What’s evil is using globals when you don’t understand the implications of the fact that it is a global. The “never use globals” is more a mantra espoused to avoid juniors/novices from stepping in fatal traps.

Lets say you have an enemy and you want to display the enemies health, but your enemies health display is a different class from the enemy itself. Well… how do I get the enemies health!?

In steps the novice programmer and they say “OH, I’ll just make the Health int static (global) and then my display script just accesses that static field!”

They do it… and then they test it and it works! Win!!!

Then they spawn 5 enemies and all of their enemies now have the same health and when you kill 1 they all have their health go to 0 (they don’t all necessarily die though depending on the logic). That’s a bug and the only resolution is to stop using the global.

These types of scenarios crop up a LOT. I have been in the industry for a very long and been the role of the senior or lead developer in a lot of them. And this type of scenario or its equivalent pop up constantly.

And some other seniors and leads their quick reaction to deal with the juniors messing up like this is to just slap a “NEVER USE STATICS/GLOBALS” ban.

But sometimes you need a global!

The entire time this happened wasn’t because the static/global was bad. It’s that the novice programmer didn’t realize the implications of using the static/global.

As long as you understand the implications of it, and you know that from a maintenance perspective that this thing should be and always will be a global/static… yeah, make it a global/static! It gets the job done don’t it?

(if we go into an enterprise realm where a product might be maintained for decades this conversation can get way more nuanced… but you’re not in that setting. Most games aren’t like this.)

As for ScriptableObject. The main overhead of a ScriptableObject is that:

  1. in the editor it is serialized, this does not impact runtime though. And this serialized nature of it is often the desired result of using a ScriptableObject. SO allows you to create data containers for your own custom asset types. This “overhead” is no different than the overhead of serialized fields on your components.

  2. just like monobehaviours/components all your ScriptableObject class types are made up of 2 parts. The C# object and the C++ object. When you create an instance of a SO, just like when you AddComponent a MonoBehaviour script, these 2 objects exist in 2 different parts of memory. The amount of overhead though is literally in the bytes of ram. It means a tiny bit more ram is used, and the caling of ‘new’ takes a little longer because it allocates 2 objects rather than 1.

The overhead is negligible at best. You shouldn’t be using SO for its performance… you should be using it if you need the ability to create a serializable editor time asset type for your own custom needs.

Think like how Unity has things like ‘PhysicsMaterial’, regular ‘Material’, and ‘Animation’. These are just data containers for configuring those things and an Animator or Renderer or Collider may use that data to simulate themselves.

Well… what if you write something that needs a similar data container?

5 Likes

Heck, even in an empty project it’s worthwhile to disable domain reload as enter playmode goes down from ±4 seconds to ±0.1 seconds. :slight_smile:

3 Likes

@lordofduct
I like that take. A bit different from my own experience but I’ve never had the chance to work in large projects with lots of people. But I can see why leads might take such mental shortcuts as an attempt help both the juniors and themselves.

In my experience it usually goes more like this - Someone has a new feature they need to implement and suddenly the design they’d previously been relying on becomes a hindrance to implementing it. As a result they find another solution that keeps the spirit of the original design but without the mechanics. They find it works and thus internalize and generalize this scenario.

For example: Say you needed to A/B test something where both versions run in the same process and both rely on globally shared data but that data needs to be different for each version. You instance the globals and decide to use a Service Locator Pattern to gain access to the global data that was previously just a simple static. It works great, everything goes according to plan, and it’s a complete success story. You then decide that the SLP was so brilliant that you should adopt it from now on for everything you can (because consistency is good, right?). And now you have two mantras: SLPs are great and should be used everywhere all the time. Static classes are the opposite of instanced SLPs and thus should be avoided at all costs. That’s how I usually have seen this sort of thing go from ‘useful tool in the belt’ to ‘absolute truth that must be adhered to always’.

1 Like

And quite often it’s static data (including a singleton that gives access to data via its static instance) that is the hindrance. But rather than doing the right thing of making this data non-static, something else is built around it, and that typically involves more static.

This can quickly lead to a lot of systems being entangled by their static data.

Analogy: imagine a rectangular room with smooth surfaces all around. Put in some colorful wool balls and start shaking the room. Every ball changes position but remains to itself. Then introduce static data in the form of nails in the walls. Shake again. You can imagine the entanglementness. More so the more nails exist.

4 Likes

I fully agree that a full-on ban on everything static is not the way to go.

I’ve been in a situation where a PR I opened got a lot of pushback from one senior programmer, even though it was a clear improvement over the old system (reducing memory usage in a context that was prone to causing crashes on devices with not a lot of memory), and demonstrated to be correct using unit tests - only because it used a static cache as an internal implementation detail. Evidently it was better to throw this code away completely and design something more complicated in a future sprint, rather than “take on the technical debt” of a perfectly encapsulated static cache being used :eyes:

However, I don’t think that the reason why static state is so widely considered dangerous is about correctness - at least in the immediate and direct sense - but mostly about managing complexity.

For the same reasons why one should prefer local variables over member variables, and private variables over public variables, one should also prefer instance variables over static variables: the more accessible that a variable is, the more places it is possible for that variable to be changed.

  • If you have a 20 line method, it’s easy to keep track of all the places that can alter the state of a local variable defined within it.
  • If you have a 1000 line class, it can already be a considerably more complex task to understand all the circumstances in which a member variable can get mutated. You’ll probably need to do quite a bit of scrolling, jumping from method to method, to get the full picture. But it should still be pretty manageable.
  • If you have a global property, with a dozen different classes directly mutating its state, and methods on those those dozen classes being called by 75 difference classes… now you can find yourself in a situation where your brain does not have enough capacity to fully comprehend all the ways that the property’s value can change. If things get this bad, it can be become a major pain point in the project, that can cause new bugs to crop up all the time. And these *spooky action at a distance-*type bugs can be very painful to debug as well.

That doesn’t mean that static state should never be used; it just means that it shouldn’t be the default, and that it should be used in moderation.

In addition to the use of static state being prone to increasing complexity, it also tends to result in code that can’t easily be tested and isn’t reusable or modular - something that I think further contributes towards it being considered such a code smell by so many.

3 Likes

Indeed but note that instead of that rather confusing construct Unity proposes to mimic the behavior of a domain reload, you can also just reset the value in an Awake method of a Monobehavior.
Just make sure that you never use the state of other objects, including statics inside of Awake methods. That is generally a good advice because Unity’s game objects are not considered ready to use anyways until Awake has finished.
Start() is safe game as it is executed after all Awake() calls.
In case of non-Monobheaviors you’ll have to find the best suited Monobehavior that utilizes your custom class.

1 Like