Create Modular Game Architecture in Unity With Scriptableobjects
Create Modular Game Architecture in Unity With Scriptableobjects
C R E AT E M O D U L A R
GAME ARCHITECTURE
IN UNITY WITH
S C R I P TA B L E O B J E C T S
Contents
Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Serialization. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Comparison. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Destroying ScriptableObjects . . . . . . . . . . . . . . . . . . . . . . . 14
Data containers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Design patterns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Refactoring example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Custom Inspectors. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Architectural benefits. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
ScriptableObject variables. . . . . . . . . . . . . . . . . . . . . . . . . 27
Dual serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Enum-like categories. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Extending behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
ScriptableObjects methods. . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Modifying ScriptableObject data . . . . . . . . . . . . . . . . . . . . . . . 41
Pluggable behavior. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
AI with ScriptableObjects . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Pattern: Observer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Avoiding singletons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
ScriptableObject-based events . . . . . . . . . . . . . . . . . . . . . . . 45
System.Action or UnityAction. . . . . . . . . . . . . . . . . . . . . . . . . 49
Example: InputReader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Pattern: Command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
ScriptableObjects or C# classes?. . . . . . . . . . . . . . . . . . . . . . . 61
Generic version. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Conclusion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
More resources. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Documentation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
From Unite. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
As Unity components go, they don’t call attention to themselves. Instead, they
quietly work behind the scenes. It’s likely that you won’t notice them – that is,
until you need them.
You will often hear ScriptableObjects described as “data containers.” That label,
however, doesn’t quite do them justice. Applied correctly, ScriptableObjects can
help you speed up your Unity workflow, reduce memory usage, and simplify .
your code.
This guide assembles tips and tricks from professional developers for deploying
ScriptableObjects in production. These include examples of how to apply them to
specific design patterns and how to avoid common pitfalls.
Because you can work with them interactively in the Editor, you’ll discover
that ScriptableObjects are especially useful when you’re collaborating with
non-programmers, such as artists and designers. We hope that some of these
techniques can complement your existing workflow and streamline your project
setup.
Let’s explore the unsung hero of game architecture, the humble ScriptableObject.
A ScriptableObject does not have a Transform and exists outside of the Scene
Hierarchy. Instead, it lives at the project level as an asset, much like a material .
or 3D model.
[CreateAssetMenu(fileName="MyScriptableObject")]
public class MyScriptableObject: ScriptableObject
{
public int someVariable;
}
Navigate to Assets > Create > MyScriptableObject (or right-click in the Project
window), and you can instantiate a custom asset from your ScriptableObject class.
Creating a ScriptableObject
ScriptableObjects are especially useful for anything that doesn’t need to change
at runtime.
— Serialize/deserialize them
This last point highlights one of ScriptableObjects’s key features: the ability to
appear in the Inspector. This means that its fields are easy to read and modify in
the Editor.
Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, .
a database, or a file.
— Saving and loading: If you open a .unity scene file with a text editor .
and have set unity to “force text serialization,” the serializer is run .
with a YAML backend.
— The Inspector window: This interface doesn’t talk to the C# API to figure
out the values of whatever it’s inspecting. Instead, it asks the object to
serialize itself and displays the serialized data.
For more information about serialization in Unity, see this blog post or watch
“How Unity’s Serialization system works.”
Comparison
MonoBehaviour ScriptableObject
OnDisable When the ScriptableObject goes out of scope, this is called. This .
happens if you load a Scene without references to the ScriptableObject
asset or right before the ScriptableObject’s OnDestroy.
Note: This only destroys the native C++ part of the object. See
Lifecycle and Creation for more information.
Editor-only functions
Reset Reset invokes when you hit the Reset button in the Inspector
context menu.
Here’s a brief overview of a ScriptableObject’s event functions and life cycle. Compare
this with the order of execution of MonoBehaviour event functions, starting from the top.
Files
— Its Transform
MonoBehaviour
attaches to
GameObject
In contrast, Unity saves ScriptableObjects into their own asset files. These files
are smaller and more compartmentalized than MonoBehaviours.
If you choose to use Mode: Force Text in the Project Settings > Asset
Serialization window, you can open a ScriptableObject asset in a text editor. .
It might look something like this:
Under each object are its serialized properties, represented by key-value pairs.
For more information, read the blog post “Understanding Unity’s serialization
language, YAML.”
[CreateAssetMenu(fileName="MyScriptableObject"]
public class MyScriptableObject: ScriptableObject
{
public int someVar;
}
ScriptableObject.CreateInstance<MyScriptableObjectClass>();
Destroying ScriptableObjects
Once you have the knack of creating and destroying your own ScriptableObjects,
it’s time to explore some creative ways to use them in your game application.
— Inventories
— Audio collections
At runtime, you could store this data on MonoBehaviours, but doing so can be
inefficient. As you saw in the previous comparison, MonoBehaviours carry extra
overhead since they require a GameObject – and by default a Transform – to
act as a host. That means that you need to create a lot of unused data before
storing a single value.
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSource0bject: {fileID: 0}
m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
– component: {fileID: 7467558130563611466}
– component: {fileID: 2006805663113952451}
m_Layer: 0
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
––– !u!4 &7467558130563611466
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSource0bject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 338842853706088653) m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -1.1780801, y: -2.6573246, z: -0.01486098}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
––– !u!114 &2006805663113952451
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSource0bject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSource0bject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0} m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a2d85be56c1ae45d69604547c4544ba5,
m_Name: PlayerID m_EditorClassIdentifier:
The ScriptableObject slims down this memory footprint and drops the
GameObject and Transform. It also stores the data at the project level. That can
be helpful, especially if you need to access the same data from multiple scenes.
The extra data from a MonoBehaviour might not impact your application’s
performance at first, but as your game grows and has more objects, it will
become noticeable.
Persistent data that needs to be saved from one session and then loaded
into another is typically stored in a different file format (e.g., JSON, XML,
MessagePack, Protocol Buffers, and so on). See Dual Serialization below for
more details.
If each component has its own copy of those member variables, that means you
might be carrying around a lot of duplicate data.
Design patterns
Design patterns can help developers create more maintainable and flexible
code, which can be useful in the often-changing world of game development.
Read Level up your code with game programming patterns for more about
SOLID principles and design patterns.
You can store large quantities of shared data in this manner. Consider
ScriptableObjects for:
Refactoring example
Consider a MonoBehaviour that controls an NPC’s health. You might define its
class like this:
[Range(10, 100)]
public int healthThreshold;
Any data that does not need to change can move it into a ScriptableObject:
[CreateAssetMenu(fileName="NPCConfig")]
public class NPCConfigSO : ScriptableObject
{
[Range(10, 100)]
public int maxHealth;
[Range(10, 100)]
public int healthThreshold;
Use the CreateAssetMenu attribute to configure the menu action. You can
optionally specify the default fileName or menu item order.
Many of the code samples in this guide are simplified for illustrative purposes
and easier readability (e.g., public fields).
It’s recommended that you maintain and follow a code style guide as your
codebase grows. See Create a C# style guide for more information.
Custom Inspectors
When separating data into a ScriptableObject, you have data contained in two
places, the ScriptableObject asset and the MonoBehaviour referencing it.
— Derive a new class from Editor, and store this in a folder named “Editor.”
Apply the CustomEditor attribute with the NPCHealth type.
— Draw the inspectors from the base class and new custom Inspector.
using UnityEditor;
[CustomEditor(typeof(NPCHealth))]
public class NPCHealthEditor : Editor
{
private Editor editorInstance;
if (editorInstance == null)
editorInstance = Editor.
CreateEditor(npcHealth.config);
You can expand on this example with custom property drawers and editor
attributes. This can even make for a better user experience when working with
ScriptableObjects.
With ScriptableObjects, you can cleanly separate shared data and unshared data.
Anything unique to the GameObject instance remains inside the MonoBehaviour,
while the shared data appears in the ScriptableObject. Architecture with
ScriptableObjects, however, goes beyond just saving memory.
There are a few benefits to restructuring the code architecture:
— Editing shared data is faster and less error-prone: Changes to the shared
data now happen all at once. If you need to modify a setting for your
NPCs, for example, you could adjust it in just one location, and have it
update for every affected component in every scene. .
.
This reduces any potential errors from mass editing a large number of
individual GameObjects by hand. Offloading data into ScriptableObjects
can also help with version control and prevent merge conflicts when
teammates work on the same scene or Prefab.
You can make your shared data containers even more granular with a
ScriptableObject representing just one value. For instance, you could create a
ScriptableObject class called IntVariable that holds one public field called
value:
using UnityEngine;
Your designers can then reserve data for game logic without needing a software
developer each time they want to do it. However, this requires planning to be
successful. Decide with your designers how to divide authoring gameplay data.
With some extra Editor scripting, this can become a near-seamless experience.
Another possibility is making the fields in the Inspector toggle between using a
shared value from a ScriptableObject and a constant. This can allow the game
design team greater freedom to override the ScriptableObject data per instance.
Dual serialization
You can mix how to serialize data within Unity. This allows you to work with
ScriptableObjects in the Editor, but then store their data in another location,
such as a JSON or XML file. This allows you to take advantage of each format’s
strengths.
File formats like JSON and XML can be difficult to work with in the Editor but .
are easy to modify outside of Unity with any text editor.
In contrast, ScriptableObjects work well in the Editor and can be swapped with
a quick drag-and-drop operation. However, they aren’t easy to modify outside of
Unity or share within your community of players.
Mixing serialized formats could open up new possibilities for your game, such as
level editing or modding. At build time, a script can convert the other files into
ScriptableObjects, which is faster to load than plain text.
While you’ll want to keep some sensitive data safely tucked away on your
servers (e.g., virtual currency, account information), exposing part of your game
data to the community may enhance gameplay. If you open up a sandbox level
Imagine a ScriptableObject that defines your game level layout. It may simply
contain a number of Transforms that define placement of Prefabs, starting
configurations, and so on. Your game scripts will use this data to assemble .
each level.
[CreateAssetMenu(fileName ="LevelLayout")]
public class LevelLayout : ScriptableObject
{
public Vector3[] wallPositions = new Vector3[2];
public Vector3[] playerPositions = new Vector3[2];
public Vector3[] goalPositions = new Vector3[2];
public Vector3 ballPosition;
}
This defines how you set up the level. Your level management scripts can read
the data from the LevelLayout object, then instantiate your Prefabs in their
correct positions.
A custom script can use JsonUtility to export this same data to disk. This results
in a text file outside of the Editor that your users can modify with external tools.
using System.IO;
}
}
Your custom data replaces the contents of the ScriptableObject and allows you
to use this externally modded level like any other in your game. The application
is none the wiser.
Be sure to see this work for yourself in the sample project. If we load a modified .
JSON file, this customized level overrides the default level data on the
ScriptableObject.
2 0 2 0 LT S E D I T I O N
© 2023 Unity Technologies 30 of 75 | unity.com
Mix serialized formats for more flexibility
Instead of creating a new object and loading the JSON data into it, JsonUtility
loads the JSON data into an existing object. This updates the values stored .
in classes or objects without any allocations.
Here are common ways to protect anything that you don’t want modded:
— Encryption: Use encryption to protect data files from being easily read or
modified. This can make it more difficult for users to alter critical data.
— Digital signatures: You can use a fingerprint algorithm to verify that your
data files have not been tampered with.
Design patterns are general solutions that can help you build larger, scalable
applications. They can improve code readability and make your codebase
cleaner. Design patterns reduce refactoring and the time spent testing.
Enum-like categories
In your game application, suppose you make a number of assets from an empty
GameItemSO ScriptableObject, like so:
Using UnityEngine;
[CreateAssetMenu(fileName=”GameItem”)]
public class GameItemSO : ScriptableObject
{
Do two variables refer to the same ScriptableObject? Then they’re the same
item type. Otherwise, they’re not.
So, you could have a ScriptableObject that defines special damage effects (e.g.,
cold, heat, electrical, magic, and so on) or rock-paper-scissors designations
from your favorite zero-sum game.
Comparing ScriptableObjects
If your application requires an inventory system to equip gameplay items,
ScriptableObjects can represent item types or weapon slots. The fields in the
Inspector then function as a drag-and-drop interface for setting them up.
Inventory slots
categorized by
ScriptableObject
This artist-friendly UI allows your designers how to modify and extend gameplay
data without extra support from a software developer. Giving the design team
the means and responsibility of maintaining gameplay data allows everyone to
focus on what they do best.
Extending behavior
if (gameItem.IsWinner(otherGameItem))
{
Debug.Log(gameItem.name + " beats " +
otherGameItem.name);
}
}
}
Compare that with maintaining a traditional enum. If you have a long list of enum
values without explicit numbering, inserting or removing an enum can change
their order. This reordering can introduce subtle bugs or unintended behavior.
Suppose you wanted to make the item equippable in an RPG. You could append
an extra boolean field to the ScriptableObject to do that. Are certain characters
not allowed to hold certain items? Are some items magical or have special
abilities? ScriptableObject-based enums can do that.
Your gameplay data can thus evolve as you work with your designers. While
you’ll need to coordinate how to set up fields initially, later the designers can fill
out the details independently.
Because you can create methods on a ScriptableObject, they are useful for
containing logic or actions as much as they are for holding data. In this way, you
can use them as delegate objects.
A delegate is a type that defines a method signature. This allows you to pass
methods as arguments to other methods. Think of it like a variable that holds a
reference to a method, instead of a value.
An event, on the other hand, is essentially a special type of delegate that allows
classes to communicate with each other in a loosely coupled way. Events are
explored in more detail in the Pattern: Observer chapter.
For general information about the differences between delegates and events,
see Distinguishing Delegates and Events in C#.
The idea is that if you need to perform specific tasks, you encapsulate the
algorithms for doing those tasks into their own objects. The original Gang of Four
refers to this general design as the strategy pattern.
Suppose you want a pathfinding object that calculates a route through a maze. The
object itself wouldn’t actually contain any pathfinding logic. Instead, it just keeps a
reference to another object that does.
If you want to solve the maze with a specific path search technique (e.g., A*,
Dijkstra, etc.), implement the correct solution within this separate “strategy” object.
At runtime, you can then swap to a different algorithm by exchanging objects.
ScriptableObjects methods
For example, you can define several enemy units in a game with different
movement behavior. Let’s suppose some of them need to patrol, stand idle, or
flee from the player.
If the method needs data from the scene, the EnemyUnit object can pass in a
reference to itself as a parameter. Any other necessary dependencies in the
scene can be passed in as well.
Pluggable behavior
You can make this pattern more useful by defining the EnemyAI
ScriptableObject as an abstract class. This allows it to act as a template
for a variety of ScriptableObjects that are compatible with the EnemyUnit
MonoBehaviour, so the abstract ScriptableObject can stand in for more than .
one algorithm.
Thus, you could have concrete ScriptableObject classes for behaviors like
Patrol, Idle, or Flee that derive from the base EnemyAI. Even though they all
implement the same MoveUnit method, each can produce very different results.
In the Editor, each asset is interchangeable. You can just drag and drop the .
ScriptableObject of choice into the EnemyAI field. Any compatible
ScriptableObject is “pluggable” in this fashion.
AI with ScriptableObjects
using UnityEngine;
using Random = UnityEngine.Random;
using System;
[Serializable]
public struct RangedFloat
{
public float minValue;
public float maxValue;
}
[CreateAssetMenu(fileName ="AudioDelegate")]
public class SimpleAudioDelegateSO : AudioDelegateSO
{
public AudioClip[] clips;
public RangedFloat volume;
public RangedFloat pitch;
Any MonoBehaviour can then use a ScriptableObject instance derived from the
AudioDelegateSO class. You can also make variations of the AudioDelegate for
different audio effects.
Avoiding singletons
Many developers opt to use singletons – one global instance of a class that
survives scene loading. Singletons, however, introduce global states and make
unit testing difficult.
More on singletons
The subject of singletons in Unity game development is often cause for debate.
Singletons may be a suitable solution for smaller projects or prototyping. In large
applications, the cons of using singletons often outweigh their advantages.
Many developers consider the singleton to be an anti-pattern.
Singletons are easy to learn and understand but can introduce issues when
they’re used incorrectly. Most of the patterns described here will help you avoid
relying on singletons.
If you want easy access to shared data, consider a Runtime Set based on
ScriptableObjects (see below). If you need a way to send messages between
objects, try a ScriptableObject-based event channel. Restructuring your
architecture away from singletons may improve scalability and testability.
Read the e-book Level up your code with game programming patterns to learn
more about the pros and cons of singletons.
ScriptableObject-based events
As you’ve already seen, ScriptableObjects aren’t just for handling data. They
can contain methods, just like any other script. These methods can serve as a
means for objects to communicate.
This is why many developers use singletons in the first place: easy, persistent
access to certain resources. ScriptableObjects can often provide the same
benefits without introducing as many unnecessary dependencies.
The event keyword limits the delegate so that it can only be invoked from
within the ScriptableObject class (or derived class where it’s declared).
You can set up any number of event channels to determine various aspects
of gameplay. Because they exist at the project level, ScriptableObjects can
raise events that are globally accessible. This can connect otherwise unrelated
objects in the scene.
If you want a more general purpose delegate that is not tied to the Unity game
engine, use System.Action. If you want a delegate specifically designed for
UnityEvents, use UnityAction.
Here, you can make a VoidEventChannelSO that raises an event without passing
any parameters. This one contains a UnityAction named OnEventRaised.
Another object can invoke the public RaiseEvent method to trigger the event.
Regardless of which component you choose to listen for events, the event
channels provide a means of communicating between your objects at runtime.
Did the player complete a task or score a point? Is the game over? An event can
notify any GameObject in the scene that needs that information.
— Audio management: Many things in your game can trigger sounds. This
system can play AudioClips or adjust the AudioMixer in response to
application events.
— Save Data management: This handles saving and loading game data, as
well as settings to your file system.
These systems all specialize in different tasks, but they need to talk to one
another. Events can form the glue that keeps them connected.
Note that you can send different types of data with each event, using different
event payloads. For example, the ScriptableObject-based events include
IntEventChannelSO, a Vector2EventChannelSO, a VoidEventChannelsSO,
and so on. The event used will depend on the context.
— Cameras: These are used to control a player’s view of the game world
and can add dramatic or cinematic effects, such as shaking or cutting to a
different perspective.
— Quests: These are tasks or objectives that the player must complete in
order to progress through the game or receive a reward. Quests often
involve a variety of gameplay elements, such as fetching items, defeating
enemies, or solving puzzles.
A custom Editor or property drawer can create a "Raise Event" button in the
Inspector. This can help you manually invoke the event for debugging.
For example, here's a basic Editor script that creates a custom Inspector button
for the VoidEventChannelSO:
[CustomEditor(typeof(VoidEventChannelSO))]
public class VoidEventChannelSOEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Raise Event"))
{
eventChannel.RaiseEvent();
}
}
}
With a little more work, you can make buttons for event channels that carry data
as well. Reserve a field for a debug value in the event channel itself, then pass
this to the Editor script. You can find examples of how to implement this in the
SOAP or Unity Atoms projects.
As you continue using event channels for object decoupling, consider developing
debugging tools, such as keeping a record of all listeners for each event. The
event channel class can have methods to add and remove subscribed objects,
making it easier to identify which events are causing specific behaviors .
during runtime.
Example: InputReader
Objects that listen for user input need a specialized type of event channel.
Unity’s Input System uses InputActions to represent raw input data as logical
concepts (e.g., jump, walk, etc.).
Instead, the Input System takes the place of the subject or broadcaster:
Here, we set up Actions and ActionMaps in the Input System. Each InputAction
describes a separate axis of input and binds to the keyboard, gamepad, or
alternative input devices.
While this pattern may be overkill for a minimalist arcade game, we demonstrate
this on a simple project to make the structure easier to digest.
The benefits won’t be apparent until your project grows and you add many more
components. Decoupling the inputs from the GameObjects consuming them
gives added flexibility and reusability.
If you have to modify the InputActions during development, you only need to
maintain the InputReader itself. The listening objects are unaffected if the events
don’t need to change. Thus, maintaining the connection from input to observers
becomes less work – especially when you have a lot of observers.
You can choose to use static events to ease the burden of locating the
ScriptableObject on the listening objects.
InputReader.MoveP1Event += OnMoveP1;
InputReader.MoveP2Event += OnMoveP2;
When using static events, be extra diligent when managing subscriptions. Don’t
forget to unsubscribe in OnDisable:
InputReader.MoveP1Event -= OnMoveP1;
InputReader.MoveP2Event -= OnMoveP2;
Static events will always be reachable and won’t be garbage collected if they
have active subscribers. Any dangling subscribers will prevent their cleanup for
the duration of your application.
Static events, however, aren’t serializable. If you want to work interactively in the
Editor, choose non-static events and make sure you reference the appropriate
ScriptableObject in the Inspector.
Then, you store these command objects in a collection, like a queue or a stack,
which works as a small buffer. This gives you extra flexibility to control each
command object’s execution. Common applications include playing back a series
of actions with specific timing or making those actions undoable.
— In a turn-based strategy game, the player could select a unit and then
store its moves or actions in a queue or other collection. At the end of the
turn, the game could execute all of the commands in the player’s queue.
— In a puzzle game, the command pattern could allow the player to undo and
redo actions.
This simple structure lets you execute the commands in sequence. Imagine
a tutorial or cutscene that moves a GameObject through a prescribed set of
actions or animations. The command pattern is well suited for that.
Because each command is a separate object, it’s easy to reorder them. Just
decide how you want to maintain the CommandBuffer:
— If you’re using a list or array, track the current Command’s index, then
increment or decrement the index as you need to undo or redo commands.
See the MoveCommandSO and CommandManager in the sample project for one
example of undoable movement. Here, a rudimentary tutorial scene labels parts
of the game board to explain how to play.
Click the Next button to advance through the explanatory text. Likewise, click
the Back button to cycle in reverse through the instructions.
You can find out more about the command pattern in the e-book Level up
your code with game programming patterns. Also, see this community post,
“Command pattern with ScriptableObjects,” which demonstrates this pattern
with ScriptableObjects.
ScriptableObjects or C# classes?
When deciding on the right code architecture for your project, it’s important
to consider the skills and preferences of your team, as well as your game’s
performance requirements. While some designers may prefer to use the Unity
Editor interactively, others may prefer to work entirely in C#.
Take these factors into account when creating a codebase that’s easy for
everyone on your team to work with. Of course, no design pattern is a one-size-
fits-all solution, and it’s important to carefully evaluate the pros and cons of each
before implementing it.
Remember that the “right” code architecture is just the one that works best for
your team and your project.
using System.Collections.Generic;
using UnityEngine;
if (items.Contains(thingToRemove))
items.Remove(thingToRemove);
}
}
At runtime, any MonoBehaviour can reference the public Items property to obtain
the full list. Another script or component must be responsible for managing how
the GameObjects are added or removed from this list.
Generic version
You may want to use a Runtime Set with a specific type of MonoBehaviour.
For instance, this could allow you to maintain a list of enemy or pickup items
accessible to anything in your scene. In that case, you could create specific
Runtime Sets for each type (e.g., EnemyRuntimeSet, PickupRuntimeSet, etc.).
One way to streamline the creation of additional Runtime Sets is to use a generic
abstract class:
As an alternative to using events, each Foo component can add or remove itself
using its OnEnable or OnDisable methods. Then, if you set the FooRuntimeSet
field in the Inspector, the Foo component will appear in the Runtime Set
automatically. This is especially handy if you’re using the Foo component with
Prefabs.
Note
You can also fix this issue with a custom Editor script and Inspector. For a good
example of this, see SOAP (ScriptableObject Architectural Pattern) on the .
Asset Store.
The terms foo and bar are common placeholder names in computing. .
These terms were likely chosen because they are short, easy to remember, .
and sound distinctive.
While their exact origins are unclear, some people believe that the terms
originated from radar operators in World War II. The nonsense word “foo” .
also appeared as a catchphrase in a 1930’s comic strip.
Today, the use of foo and bar as dummy variable names is a widespread
convention in the programming community.
Here, you will have the chance to see how ScriptableObjects can be applied in a
real-world example and gain a better understanding of how they can be used to
improve the efficiency and organization of your projects.
— Delegate objects: A simple audio delegate shows how you can randomize
the ball’s collision sounds just by swapping ScriptableObjects. Objectives
are also ScriptableObjects that plug into the game management system
for determining win-lose conditions.
— Event channels: The observer pattern helps you set up game events for
UI, sounds, and scoring. Different GameObjects can subscribe to different
“event channels,” similar to tuning in to a specific radio broadcast.
— Dual serialization: Some game data is stored like the level layout in
ScriptableObjects for ease of use in the Editor but with the option to
save it as JSON files. Externally modded JSON data can then rebuild a
ScriptableObject, which works with the original setup script.
Of course, the game itself is not the main focus of this sample. A paddle-and-
ball arcade game can be built with far fewer lines of code. Instead, the idea is to
demonstrate how ScriptableObjects can help you create components that are
testable and scalable while still being designer-friendly.
Note that you can achieve many of the techniques in this guide using C#
classes instead of ScriptableObjects. However, the Unity Editor provides the
convenience of viewing and editing ScriptableObjects more easily. This can help
your artistic and design teams interface with your project.
Do your designers want to set up gameplay data without constant support from
the software team? If so, then maybe Scriptable Objects have a place in your
project.
Of course, not using a pattern can be just as valuable as using one. What may
seem like a natural fit for one application may not be for another. Evaluate the
advantages and disadvantages of a pattern before deploying it.
There are no hard-and-fast rules for how to structure your Unity project.
Balance the skill sets and personal preferences of your team with your .
code architecture.
Then, you can focus on the important thing: making your game an engaging
experience for your players.
These advanced best practice guides can help you discover more techniques
for Unity development:
— Create a C# style guide covers some of the best practices when setting up
your project.
You’ll find all of the latest and greatest Unity technical e-books and how-to
articles on the Unity best practices hub.
From Unite
— Ryan Hipple, a senior full stack game engineer at Meta, has a GitHub
project that illustrates much of his Unite Austin presentation. You can also
read his corresponding blog post.
Unity Professional Training offers both online and in-person training to give you
and your team additional skills and knowledge to work more productively, and
collaborate efficiently, in Unity.