My Unite 2017 Talk - Game Architecture with Scriptable Objects


Unite 2017 wrapped up in Austin last week and I finally got to give a talk that was all about ScriptableObjects (and ride a jackalope). I have been exploiting the hell out of these things since I first discovered the under-documented feature back in 2011.

Session Details

Game Architecture with Scriptable Objects focuses on making modular, data-driven, editable game systems while avoiding (and trash talking) crutch patterns like singletons.

Using ScriptableObject based classes, you can easily edit and store references to data from prefabs in a scene. However, ScriptableObjects are not constrained to just static data. You can change data from one prefab and read it from another. This allows for the exchange of state between prefabs without the need for a rigid connection between them.

A similar pattern can be used to construct an event system. Scene objects can add themselves to a list on a ScriptableObject based asset to indicate that they are listening for a certain game event. Later, a different scene object can "raise" the event, looping through the list and notifying all listening objects. Again, this pattern removes rigid connections between different systems in the game.

Game Architecture with Scriptable Objects covers these topics and more, with a ton of simple code samples, practical demos, and bad programming jokes.

The video is on Unity's YouTube channel
The slides are on SlideShare
The sample project is on GitHub

Follow Up

One common question I got about the FloatVariable is how to avoid having the runtime data save when the game stops. The truth here is that the sample I showed in the session has been dramatically simplified from what I normally use so it does not handle this use case.  You can use OnEnable to copy your default value into a non-serialized current value. Then you only access through the CurrentValue property. An added benefit here is that a float property can be set by a UnityEvent while a float field cannot.

Another note with these is that you may want to add DontUnloadUnusedAsset to the HideFlags to make sure you do not lose state if nothing is referencing it.

The full implementation of this variable system that we use at Schell Games has a bunch more features, custom icons, and a generic base class making it easier make new variable types. Same thing goes for our event system. While I can not post the source for these things, I hope that the concept helps people start hacking apart ScriptableObjects more!

Comments

  1. This was a great presentation - thanks for sharing! Super keen on implementing this idea :)

    ReplyDelete
  2. One of my favorite talks from the conference. Thank you so much!

    ReplyDelete
  3. I love everything presented in your talk! Thanks for putting the effort into that!

    ReplyDelete
  4. I know you cannot post source for this, but as a non-specific question: How do you keep the editor view for this the most generic. Do you dynamically create code for property drawers and such? I'm currently working on something that also uses SOs for simple types. It's not the same thing and a very different use-case but it still has the principle that a SO sometimes wraps a single value and I need the most generic way of doing that. I currently derive a new sub-class for every atomic value that has a simple type. Do you do that with ValueReference or do you have a very generic version that somehow also has a generic property drawer that knows how to generate code to render it in the inspector?

    ReplyDelete
    Replies
    1. The event system that I made handles this by keeping the generated code simple. The generated class is nothing more than a definition of the class and fulfillment of a few generics.

      The inspector then works with SerializedProperty rather than directly with the fields. This means that the inspector does not need to know the actual type since PropertyField takes care of that.

      Delete
    2. One thing that seems a bit problematic also is the GameEvent. You give it a list of GameEventListeners that every monobehavior has to derive from that wants to listen to it. That seems very limiting. An interface would obviously better but Unity doesn't serialize on interfaces... so unless I use a monobehavior per event I want to listen to and then let that communicate with other monobehaviors that do the ACTUAL logic... I don't know this seems like one of those hacks that work around the big limitations of Unity's serialization system. Adding a component per event a gameobject needs to react to seems very very cluttering and not like good design at all to me.
      Is that how you do it or do you have a more clever solution in real projects?

      Delete
    3. In our system, there are three ways to register for an event: GameEventListener MonoBehaviour, interface, and delegate.

      The MB one is the most flexible since it does not require any code changes. It does use a MB per event responded to so we kept the editor simple with an optional "Advanced" dropdown. If not planned, this list can get large, but if you properly modularize your systems, each prefab will only be concerned with a small number of events since it does only one thing. Enemies are only concerned with combat and high level state related events so even if your game gets bigger, they should not observe every new system.

      The interface option allows us to do the response in code. This is for things that we know need to respond to certain events to have any functionality. It is less flexible but faster and cleaner on the editor. A MB implementing the interface takes the events it responds to in the editor and the response is done in code. There is no need to serialize the interface references since they register at runtime.

      The delegate option is similar to the interface one, but makes it easier for a class to respond to multiple events.

      Delete
  5. Hi that talk was amazing... i am used to use singleton but i hate when I have to drag all manager into the scene to test out a new scene. The process you discussed is amazing. But I can't understand how i start implementing those in my project in a bigginer friendly way. Please need your attention on this

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Thanks for showing us a great architectural concept with Scriptable Objects! This indeed is very helpful in creating small modular building blocks which are easily configurable. Just awesome!

      One thing I noticed from your example code, and what makes me wonder, is that you simply assign those SO variables to various instance objects (like the slider, health meter, pitch value in the mixer, etc.) directly inside update() methods of all their corresponding MonoBehaviours.

      I guess this is just to simplify the demonstration code and not something we really would want?

      Delete
    2. If your data is subject to change every frame you would do it in the Update loop, if it is not changing frequently, you could do it as an event model.

      I simplified a lot of these systems for demonstration purposes, but in the real-world games we have shipped with a system like this, that pattern occurs a lot. What is it about the implementation that is making you wonder?

      Delete
  7. Thank you for the excellent talk at Unite 2017. It was a great explanation. I have a question about the events. All the events in your demo were static events (no arguments). How do you handle dynamic events (with arguments)? Do you add more variables to the GameEvent SO to handle this, or do you create multiple SO's to handle them? For example, if want to call a custom event with a GameObject argument.

    ReplyDelete
    Replies
    1. In the much more elaborate system I built in the studio, we allow for argument passing with events. This means that you need a new event type definition for each passed type. for example, GameObjectEvent : GameEvent. You also need a listener type, GameObjectEventListener : GameEventListener and a UnityEvent type, GameObjectUnityEvent : UnityEvent.

      I have this code get auto generated to make it easy to add new types, but it is still simple to implement each one.

      Delete
    2. That answers my question. Thanks.

      Delete
    3. This comment has been removed by the author.

      Delete
    4. This comment has been removed by the author.

      Delete
    5. If I do that (making a GameObjectUnityEvent:UnityEvent) I will loose it on the inspector !

      Delete
    6. You said you auto generate the code to create new types. I have been having issues with some AOT platforms and using generics. Is that why you auto generate instead of using generics? What method do you use to generate the implementation?

      Delete
  8. Hi, thank you for your presentation and the talk! I have a question about resetting ScriptableObject variables when the game stops (I've asked the same question on YouTube).

    Suppose you have a variable IntVariable (which is an asset of your ScriptableObject). You can access and change its RuntimeValue integer field in playmode. However, when you reload your scene and get every GameObject and Component re-initialized, your IntVariable asset instance will still have the previous RuntimeValue, because it had no chance to reset it. Neither OnEnable/Awake nor ISerializationCallbackReciever methods are called on ScriptableObject asset instance when reloading the scene.

    What is the solution to resetting all these variables when the scene reloads? Having a gameobject with a component which looks up all variables and resets them? Of course, this component must be executed before all other scripts, so this looks like a hack...

    ReplyDelete
    Replies
    1. You are correct that the RuntimeValue does not naturally reset between scenes. In a lot of cases, this is a desired feature. It allows data to carry from one scene to another without needing DontDestroyOnLoad objects.

      In the case where you do want them to reset, define a function that sets RuntimeValue to InitialValue. You can write a component with an OnDestroy in each scene that resets a bunch of Variables. Alternatively you could use the SceneManager.sceneUnloaded callback.

      If you have more than a few variables to reset, you could make a ScriptableObject that indexes all of the variables that need to be reset and use OnDestroy or sceneUnloaded to have that object reset all of the variables it is pointing at. You could even make that object auto-populate itself by scanning the asset database for variables at build time or onvalidate.

      Delete
    2. Thank you! This is helpful for most of scenarios. However, resetting the required variables in OnDestroy/sceneUnloaded of some component does not solve the issue that these variables will not be reset when exiting the play mode in the editor. Do you have some suggestions for this case? Using playmodeStateChanged event?

      Delete
    3. There should be no need to reset on playmode stop if you operate on a RuntimeValue that is explicitly non-serialized.

      I would have Value or InitialValue be the serialized data that you change in the inspector. When the game starts (something like ISerializationCallbackReciever.OnAfterDeserialize), copy InitialValue into the nonserialized RuntimeValue.

      If you operate on RuntimeValue it should not get saved to disk.

      Delete
    4. Yes, you're right. Every time I enter the play mode, I get OnDisable and then OnEnable called on my variables. I can reinitialize runtime values there. Thank you!

      Delete
    5. What is the usage of copying InitialValue into RuntimeValue ?

      Delete
  9. One question that I keep coming back to is where you mention in your talk that "this can become quite cumbersome" and then you introduce FloatReference as an additional layer to FloatVariable ... but you omitted to explain WHY this had to be a separate class and what the benefits are.

    What are the reasons of using this "xxxxReference" class layer? Can't we just throw this all in the xxxxVariable classes? I think I'm missing a point but I just can't see it.

    ReplyDelete
    Replies
    1. Additional question: how do you prevent developers from using the xxxxVariable instances directly, or is that not a concern?

      Moreover, have you had issues with the variables being "public settable" by default and how do you combat that? For example, I can foresee weird bugs that end up being caused by, imagine that, the player's health changed from the GUI system? I'm thinking of preventing that by restricting write access to variables, at a minimum by adding a "writable" checkbox.

      Delete
    2. Dammit. I got it. :)

      Thinking about the "writable" bit brought it to my attention. If I had this "writable" flag in the variable itself, it would be either globally writable or globally read-only. I would HAVE to put that in the reference class, so individual "consumers" of the variable can have the writable status set individually for them.

      Though for instance your "constant" use case seems to make more sense to have in the variable class, ie this would then make the variable "globally read-only". Whereas if you need the reference to be constant, you could as well use a simple float field instead.

      The only neat thing about constant being a flag in the reference class seems to be able to switch between constant and non-constant value without losing the variable reference - perhaps this is meant for experimenting/debugging stuff? I can imagine something like setting player health to constant temporarily to make the player invincible during playtesting.

      Delete
    3. My full implementation actually does have a "read-only" option for variables. If a write is attempted, it can be ignored and optionally throw an exception.

      The goal of the serializeable reference class is to give designers the choice to use a shared scriptable object variable or specify a value in line. (I was using the term "constant" in the talk but I have since changed it to "literal" which is a better description)

      As for making things read-only from certain contexts, I would not go in that direction. We define certain variables as read-only to allow for a way to flag runtime constants. Preventing writes from specific places gets weird. Variables have a public set function that can be called from a UnityEvent. There isn't really a good way to identify the source at that point.

      Our workflow is kind of simple, if you do not want to edit the HP from the UI, do not call the setter from the UI. I do not want to take power away from designers just because it lets them break things. It is this kind of flexibility that lets them create new features.

      Setting the player HP to read-only at runtime to debug sounds like an awesome idea, I never thought of that!

      Delete
  10. Hi! Awesome talk and architecture. I'm new to Unity and these approaches make me very enthusiastic about it.

    But probably because I lack the experience I'm missing some important piece in the picture. In your example with FloatVariable for player hp it was created as an asset which then was referenced from different components to bind them to the same instance of a variable. But how should I go about making 'enemy' prefab containing game object for unit itself and game object for a life bar that use the same approach in a way I will still be able to drop as many of them to the scene and have them use personal hp instances (one per enemy)?

    ReplyDelete
    Replies
    1. This has been a pretty common question. First, unless you are building a game that focuses on one on one combat, I would not recommend making a scriptable object HP variable for each enemy. The variable examples I gave do not work well in a situation where you have a dynamic set of targets like with a lot of enemy setups. I have a much more in depth system that we are using in the studio now that allows us to instance HP variables per enemy, but it is not trivial.

      For applying HP meters to various enemies, you could consider a RuntimeSet from my examples. Each enemy could add and remove themselves from the set on spawn and death. Then you could have a system monitoring the set and assigning health UI from a pool. Having a higher level system for the health UI also lets you intelligently lay it out on screen so that it doesn't overlap.

      Delete
    2. Sounds good. Thank you very much!

      Delete
  11. I loved your talk and have started using some of the ideas in my own project, namely runtime sets. However, I'm using Asset Bundles in my project, and this is causing big problems, because it seems that the scriptable object assets get copied into every bundle where an asset references it. This results in assets that are using scriptable object in editor without problem suddenly using copies in builds, because they are not in the same asset bundle as other objects that reference the data.

    One way around the scriptable object data being duplicated is to put it all into a single data bundle and load it via the bundle manager for each asset that references it, but this then completely breaks the inspector and ease of use. Do you use Asset Bundles at Schell, and if so do you have any tricks to avoid the data being duplicated?

    ReplyDelete
    Replies
    1. Nevermind, I figured out that I just needed to put the ScriptableObject assets into another asset bundle to prevent each asset that references it getting its own copy. Now I just preload my data bundle on startup and everything works fine in builds.

      Thanks for the talk by the way.

      Delete
    2. I'm glad you found a solution! Sorry I couldn't point you in that direction sooner. There is some Unity documentation (https://docs.unity3d.com/Manual/AssetBundles-Dependencies.html) that covers this a bit.

      If a asset A is put in an asset bundle and it references another asset B, B is copied into the bundle as well. This can obviously lead to multiple copies of the same event.

      If you do not want things to be copied, they must be in their own bundle. Depending on the scope of the game, this could be something like one bundle with the core events and a bundle for the events used in each level.

      Delete
  12. Hey! I think I saw you mention in one of the comments that you have a system for using ScriptableObjects as instance variables. Could you elaborate on that?

    ReplyDelete
    Replies
    1. The system we have for this centers around a VariableInstancer component. An instancer defines a lit of variables that it will create its own local copy of (in memory). We then pass the instancer a variable SO like a key and ask it for its local copy.

      So an enemy prefab could have a VariableInstancer component that says it instances the HP variable. On Enable, this will create a copy of the HP variable in memory. If we ask the instancer for HP it returns the local copy. This is super useful to use in a behavior tree where we can have nodes that branch on variable values specific to the tree owner.

      Delete
    2. Is there any ways to visually tweak those instances in editor ? Or raise event for just that certain enemy ? (Like death event)

      Delete
  13. That sample project on github...How is it used? There is no scene. I thought this was going to be the project that was shown in the video.

    ReplyDelete
    Replies
    1. That is the project from the video and there are several scenes. Each demo folder has one or more scenes in it.

      Delete
    2. hmmm. That's weird. There are no demo folders in my project. I'm going to re-download to check.

      Delete
    3. Thanks. I am OK now.

      I had downloaded the wrong file: UnityGameArchitecture-master. Do you have any idea what UnityGameArchitecture-master is? (I'm not sure where I got it, or how I got it instead of Unite-2017-master.)

      Delete
  14. I was looking at your Inventory Example trying to figure out how best to do game state saving & serialization. I figured out the basic idea behind the saving/loading with serializers but I was wondering how and what called Save() and Load() on the Inventory assets.

    In a real (non-demo) project would you add everything that needed to be saved to a list manually? Or would you have a class that went through and collected everything up for you?

    Once you had all of your objects what events would you most likely call Save() and Load() on? Awake? OnEnabled? OnApplicationPause? Ect.

    Thank you for publishing all of this info about Scriptable Objects! It has really helped me create more modular architecture!

    ReplyDelete
    Replies
    1. I wouldn't look too much into the inventory system, I cut it from the presentation (and the repo) for a reason. I could not find a simplified implementation that would not distract from the point.

      That said, in the past I have done serialization by having a ScriptableObject that represents player state data that wants to be saved. This is reasonably inexpensive to keep up to date. From there you can call JsonUtility.ToJson to save it at checkpoints of any kind, but this really depends on the game. JsonUtility.FromJsonOverwrite can be used to load from disk back to your data in the SO.

      If you are on a slower platform and want a "continuous" save, you may want to go with a solution that can be multithreaded.

      Delete
  15. I see the use of "public static implicit operator float" in a few areas of the sample code. Is this to make sure your FloatReference is cast properly?

    Do you have plans on talks for 2018?

    I'd love to learn more about how to create clean modular code within Unity.(If you have anymore references please share)

    ReplyDelete
    Replies
    1. public static implicit operator float lets you use a FloatVariable as a float with less code since it will implicitly cast. This is super useful, but be careful when using it with a bool! An object in c# evaluated as a bool can be interpreted as a null check!

      I do plan on submitting for Unite 2018, but I have not decided the topic yet.

      Delete
    2. This got me!

      if (BoolVariable)
      {
      do something
      }

      ..was continuously running.. had to go through and change it to (BoolVariable.Value == true)

      Delete
  16. How would you go about passing certain instances with your Event System in the video?

    I'm familiar with Extending a UnityEvent. But not sure how you'd go about handling this.

    Lets say I had a List and List.
    Would you be able to Select a "NPC" and and "Item" from the lists and pass them to a listener when the Event is fired?

    ReplyDelete
    Replies
    1. The system that I built at Schell Games does support passing data through an event, but the super-simplified version I presented does not have that. The main goal of the talk was to introduce the concept, not to distribute a complex implementation.

      That said, making an event that passes data isn't too tricky. You would need to define a NPCGameEvent : ScriptableObject and a NPCGameEventListener : MonoBehaviour. NPCGameEvent has a list of registered NPCGameEventListeners (and add/remove listener calls to populate it).

      NPCGameEvent needs a .Raise(NPC npc) function that calls a .OnEventRaised(NPC npc) for each of the listeners. The OnEventRaised function can invoke a NPCUnityEvent if you want to hook up the response in the inspector.

      To get fancier, you can implement most of this with generics to make it simpler to define new event arguments.

      Delete
    2. Thanks for the reply, I'll work on creating generic versions of the classes like you mentioned.

      Delete
  17. How would you deal with events that need to be unique for each instance of the prefab? As an example DeathEvents for enemies where only the 1 instance of the enemy needs to respond to it.

    ReplyDelete
    Replies
    1. There are a few options:
      1. You could either have all enemies get the player death event and only have certain ones do anything in the response.
      2. You could have enemies register with the event only when they have a reason to respond. For instance, when an enemy is aggro on the player they listen for the death event, when they de-aggro, they stop listening.
      3. You could use a different system like c# events and have the enemy register with the player based on whatever conditions you are using to know if the enemy responds.

      Delete
    2. Thank you for answering. Would you use the same kind of options for communicating between different components on the same prefab? I know delegates are useful but I'm not sure if there is a better way to keep the systems decoupled. Not sure if I'm trying too hard to avoid direct references between scripts :P

      Delete
    3. I tend to not use my event system when communicating within a prefab. It is helpful to make sure things are modular and do not always need direct connections, but within a prefab, it is easy to see the connections.

      When you do want to keep things modular, you can use UnityEvents for some simple communication between components for a small performance fee (but it is really flexible).

      Another thing you can do is use interfaces. A component can scan for all other components that implement an interface and make calls on them.

      Delete
    4. Thank you so much for the advice. It's been really helpful for a relative newbie like me :)

      Delete
  18. This comment has been removed by the author.

    ReplyDelete
  19. Hi Ryan, loved the talk. I was just at Schell for the open house this past week :) I'm a hobbyist just learning about ScriptableObjects and I'm trying to design a modular interaction system for a game. I want to have buttons, switches, doors, machines, etc. A player should be able to see that something is "Interactable", and either press the button, flip the switch, open/close the door, operate the machine, etc. I don't want to have to write this interaction code into each thing - essentially I want the player to be able to say "Hey, A THING! Do THE THING!"

    But there will be multiple doors, multiple switches, multiple machines as well. So each either needs a custom event for each possible interaction, or I need to pass data with the event that describes the interaction and the source.

    I want the player to be able to look at the interactions available to him based on what he's interacting with and decide what to do. But I might want to have an enemy that interacts with stuff in similar, but different ways (breaking a machine instead of using it, for example), so the conditions and priorities should be configurable based on actor.

    I understand the system at Schell is much more complicated than the example, and you talk in some of the above comments about passing data with events, or having different event classes, but I'm having a hard time parsing which strategies you would recommend for different situations. I would love some advice on extending this event system for more advanced uses, like in an interaction system as described with multiple targets and actors.

    ReplyDelete
  20. I've watched your Unite talk, read this blog, and poked through the sample code. Thank you so much for this info. I have a couple questions now that I'm about halfway through implementing these concepts in my game:

    1. Do scriptable objects remove the need to save game data to a file on the users device? For example, the amount of coins a player has?

    2. Is there an easy way to see all the listeners of an event? I have to click around my project to find them all now. I could manually keep track in a separate application but that seems tedious.

    3. Do you have a state machine integrated with this type of event system? I have almost completely gotten rid of my "GameManager" class. One benefit of having that class was a clean state machine with state enter and exit methods. Now the state changes have been replaced with GameEvents. It's more decoupled, but feels a little fragile.

    Thanks again!!

    ReplyDelete
    Replies
    1. Replying to my own comment here...

      1. So while these assets are persisted between sessions, you will still want to save data somewhere else. If you push an update to your game, then everyone's scriptable object assets will be the same as what you pushed with.

      2. So far I just go into the game event listener class and print out the object that is listening. Not the best solution but it's better than nothing.

      3. I'm still not sure how to handle overall game state cleanly with so much modularity and with game events like these.

      Delete
  21. Never thought about using scriptable objects as anything other than a settings container. Great stuff!

    On the question of architecture, how do you apply your "everything's a prefab" philosophy to your overall state flow?

    Take a baseball game with a batting phase, running phase, and pitching phase. Would you have a top down approach, like a state machine, waiting for a batter to hit and sending instructions to interfaces on prefabs that implement them, or would you have a loose group of components, like a "hit detector" that simply sends a message when a bat has a ball, and lets every other game object do what it does, changing the phase naturally?

    Would your camera have various scripts and logic for each phase, or would your camera be dumb (zoom in/out) and have a camera controller prefab feeding it instructions based on the phase?

    ReplyDelete
  22. This comment has been removed by the author.

    ReplyDelete
  23. Can use this architecture with UNet ? (Both HLAPI and LLAPI)

    ReplyDelete
  24. Spectacular presentation! I work alone (literally: there's no one else here), so a good, clear, useful video like that one is invaluable. Kind of like having a colleague (who knows what he's doing).

    Regarding the listener lists: do you think using C#'s built-in event system be adequate?

    ReplyDelete

Post a Comment