Level Up Your Code With Game Programming Pattern
Level Up Your Code With Game Programming Pattern
LEVELUPYOUR
CODEWITHGA
ME PROGRAM
M I N G PAT TERNS
2021 LTS EDITION
Contents
SOLID principles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Single-responsibility principle . . . . . . . . . . . . . . . . . . . . . . 8
Open-closed principle . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Abstract classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 A SOLID
understanding . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Design
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Patterns within
Unity . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Factory
pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
and cons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Object
pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
UnityEngine.Pool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Singleton pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53 Example: Simple
. . . . . . . . . . . . . . . . . . . . . . . . . . 59
Command pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
63 Pros and
cons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
67
State pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
machines . . . . . . . . . . . . . . . . . . . . . . . . 70 Example:
cons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
76
Observer
pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
cons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
. . . . . . . . . . . . . . . . . . . 93
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . 98
INTRODUCIN
G DESIGN PAT
TERNS
When working in Unity, you don’t have to reinvent the wheel. It’s likely
someone has already invented one for you.
If you’re still new to design patterns or need a quick refresher, the guide also
provides common scenarios where you can apply them in game development.
For those switching from another object-oriented language (Java, C++, etc.)
to C#, these samples will show you how to adapt patterns specifically to
Unity.
At the core of it, design patterns are just ideas. They won’t apply in all
situations. But they can help you build larger applications that scale when used
correctly. Integrate them into your project to improve code readability and make
your codebase cleaner. As you gain experience with patterns, you’ll recognize
when they can speed up your development process.
Then you can stop reinventing the wheel and, well, start working on something
new. Contributors
This guide was written by Wilmer Lin, a 3D and visual effects artist with over
15 years of industry experience in film and television, who now works as an
independent game developer and educator. Significant contributions were also
made by senior technical content marketing manager Thomas Krogh-Jacobsen
and senior Unity engineers Peter Andreasen and Scott Bilas.
This guide aims to present you with new ways of thinking about and
organizing your code. Several patterns for software design highlighted
in this guide are adapted to Unity development.
When in doubt, filter everything in this guide through the KISS principle:
“Keep it simple, stupid.” Only add complexity if necessary.
Then, the design pattern will serve its intended purpose: to help you
develop better software.
THESOLI
D
PRINCIPLES Before charging into the patterns themselves, let’s look at some design
principles that influence how they work.
— Single responsibility
— Open-closed
— Liskov substitution
— Interface segregation
— Dependency inversion
Let’s examine each concept and see how they help you make your code
more understandable, flexible, and maintainable.
Single-responsibility principle
A class should have one reason to change, just its single responsibility.
The first and most important SOLID principle is the single-responsibility principle
(SRP), which states that each module, class, or function is responsible for one
thing and encapsulates only that part of the logic.
Assemble your projects from many smaller ones instead of building monolithic
classes. Shorter classes and methods are easier to explain, understand, and
implement.
If you’ve worked in Unity for a while, you’re likely already familiar with
this concept. When you create a GameObject, it holds a variety of
smaller components. For example, it might come with:
Each component does one thing and does it well. You build an entire scene from
GameObjects. The interaction between their components is what makes a game
possible.
You’ll construct your scripted components in the same way. Design them so
each one can be clearly understood. Then have them work in concert to make
complex behavior.
[RequireComponent(typeof(PlayerAudio), typeof(PlayerInput),
typeof(PlayerMovement))]
public class Player : MonoBehaviour
{
[SerializeField] private PlayerAudio playerAudio;
[SerializeField] private PlayerInput playerInput;
[SerializeField] private PlayerMovement playerMovement;
— Readability: Short classes are easier to read. There is no hard and fast rule
but many developers set a limit of 200-300 lines. Determine for yourself
or as a team what constitutes “short.” When you exceed this threshold,
decide if you can refactor it into smaller parts.
— Extensibility: You can inherit from small classes more easily. Modify or
replace them without fear of breaking unintended features.
— Reusability: Design your classes to be small and modular so that you can
reuse them for other parts of your game.
When refactoring, consider how rearranging code will improve the quality of life
for yourself or other team members. Some extra effort at the beginning can save
you a lot of trouble later.
Many of the design patterns and principles presented in this guide help
you enforce simplicity. In doing so, they make your code more
scalable, flexible, and readable. However, they require some extra
work and planning. “Simple” does not equate to “easy.”
Though you can create the same functionality without the patterns
(and often more quickly), something fast and easy doesn’t necessarily
result in something simple. Making something simple means making it
focused. Design it to do one thing, and don’t overcomplicate it with
other tasks.
Open-closed principle
The open-closed principle (OCP) in SOLID design says that classes must be
open for extension but closed for modification. Structure your classes so that
you can create new behavior without modifying the original code.
A classic example of this is calculating the area of a shape. You can make a
class called AreaCalculator with methods to return the area of a
rectangle and circle.
For the sake of calculating area, a Rectangle class has a Width and
Height. A Circle only needs a Radius and the value of pi.
This works well enough, but if you want to add more shapes to your
AreaCalculator, you’ll need to create a new method for each new shape.
Suppose you want to pass it a pentagon or an octagon later? What if you need
20 more shapes? The AreaCalculator class would quickly balloon out of
control.
You could make a base class called Shape and create one method to
process the shapes. However, doing so would require multiple if
statements inside the logic to handle each type of shape. That won’t scale
well.
You want to open the program for extension (the ability to use new shapes)
without modifying the original code (the internals of the AreaCalculator).
Though it’s functional, the current AreaCalculator violates the open-closed
principle.
© 2022 Unity Technologies 12 of 99 | unity.com
The revised AreaCalculator class can now get the area of any shape
that properly implements the abstract Shape class. You can then extend
the AreaCalculator functionality without changing any of its original
source.
Every time you need a new polygon, simply define a new class that inherits from
Shape. Each subclassed shape then overrides the CalculateArea method to
return the correct area.
The Liskov substitution principle (LSP) states that derived classes must be
substitutable for their base class. Inheritance in object-oriented programming
allows you to add functionality through subclasses. However, this can lead to
unnecessary complexity if you’re not careful.
The Liskov substitution principle, the third pillar of SOLID, tells you how to
apply inheritance to make your subclasses more robust and flexible.
Imagine your game requires a class called Vehicle. This will be the base class
of a vehicle subclass that you will create for your application. For example,
you might need a car or truck.
Everywhere you can use the base class (Vehicle), you should be able to use a
subclass like Car or Truck without breaking the application.
Suppose you
are building a
turn-based
game where
you move the
vehicles
around a
board.
Rail movement
Road movement
An example game of cars versus trains
With this class, you expect to be able to pass any vehicle into the Navigator’s
Move method, and this will work fine with cars and trucks. What happens,
though, when you want to implement a class called Train?
The TurnLeft and TurnRight methods would not work in a Train class
since a train can’t leave its tracks. If you do pass a train into the Navigator’s
Move method, that would throw an unimplemented Exception (or do nothing)
when you get to those lines. You violate the Liskov substitution principle if you
cannot substitute a type for its subtype.
© 2022 Unity Technologies 17 of 99 | unity.com
Since a Train is a subtype of Vehicle, you would expect to use it any place
that accepts the Vehicle class. Doing otherwise might make your code
behave unpredictably.
— If you are removing features when subclassing, you are likely breaking
Liskov substitution: A NotImplementedException is a dead giveaway
that you’ve violated this principle. Leaving a method blank does so as
well. If the subclass does not behave like the base class, you’re not
following LSP – even if there’s no explicit error or exception.
— Keep abstractions simple: The more logic you put into the base class the
more likely you will break LSP. The base class should only express the
common functionality of the derived subclasses.
— A subclass needs to have the same public members as the base class:
Those members also need to have the same signatures and behavior
when calling them.
Follow the LSP principle more closely by creating a RoadVehicle type and
RailVehicle type. The Car and Train would then inherit from their respective
base classes.
Follow the Liskov substitution principle to limit how you use inheritance to keep
your codebase extendable and flexible.
The interface segregation principle (ISP) states that no client should be forced
to depend on methods it does not use.
In other words, avoid large interfaces. Follow the same idea as the single
responsibility principle, which tells you to keep classes and methods short.
This gives you maximum flexibility, keeping interfaces compact and
focused.
Imagine you’re making a strategy game with different player units. Each unit
has different stats like health and speed. You might want to make an interface
to guarantee that all of the units implement similar features:
}
© 2022 Unity Technologies 21 of 99 | unity.com
Let’s say you want to make a destructible prop like a breakable barrel or crate.
This prop will also need the concept of health despite not moving. A crate or barrel
also won’t have many of the abilities associated with other units in the game.
Split it into several smaller interfaces rather than make one interface that gives
the breakable prop too many methods. A class implementing them will then
only mix and match what it needs.
You can also add an IExplodable interface for the exploding barrel:
Again, this favors composition over inheritance, similar to the example with
Liskov substitution. The interface segregation principle helps decouple your
systems and makes them easier to modify and redeploy.
The dependency inversion principle (DIP) says that high-level modules should
not import anything directly from low-level modules. Both should depend on
abstractions.
Let’s unpack what that means. When one class has a relationship with another,
it has a dependency or coupling. Each dependency in software design carries
some risk.
If one class knows too much about how another class works, modifying the
first class can damage the second or vice versa. A high degree of coupling is
considered unclean code practice. An error in one part of the application can
snowball into many.
In the best scenario, aim for loose coupling and high cohesion.
You need to be able to modify and expand your game application. If it’s
fragile and resistant to modification, investigate how it’s currently structured.
The dependency inversion principle can help reduce this tight coupling between
classes. When building classes and systems in your application, some are
naturally “high-level” and some “low-level.” A high-level class depends on a
lower-level class to get something done. SOLID tells us to switch this up.
On a high level, you want the character to move to a specific location and for
something to happen. The Switch will be responsible for that.
On a low level is another class, Door, that contains the actual implementation
of how to open the door geometry. For simplification, a Debug.Log statement
is added to represent the logic of the opening and closing door.
Switch can invoke the Toggle method to open and close the door. It
works, but the problem is that a dependency is wired from the Door directly
into the Switch. What if the logic of the Switch needs to work on more
than just a Door for example, to activate a light or giant robot?
Once again abstractions come to the rescue. You can sandwich an interface
called ISwitchable in between your classes.
With
dependency
inversion
ISwitchable just needs a public property so you know whether it’s active,
plus a couple of methods to Activate and Deactivate it.
On the other hand, you’ll need to rework the Door to implement ISwitchable:
Like the rest of SOLID, the dependency inversion principle asks you to examine
how you normally set up relationships between your classes. Conveniently scale
your project with loose coupling.
Both are valid ways to achieve abstractions in C#. Which one you use
depends on your situational needs.
Abstract classes
The abstract keyword lets you define a base class, so you can
pass common functionality (methods, fields, constants, etc.) to
subclasses through inheritance.
Abstract class
fields, constants, static memebers
+delay
+activationTime +Activate()
inherits
"is a"relationship
“is a” relationship
Concrete
class
The advantage of abstract classes is they can have fields and constants
as well as static members. They can also apply more restricted access
modifiers, like protected and private. Unlike interfaces, abstract classes
let you implement logic that enables you to share core functionality
between your concrete classes.
Inheritance works well until you want to create a derived class that has
characteristics of two different base classes. In C#, you can’t inherit
from more than one base class.
If you had another abstract class for all Robots in your game, then
it’s harder to decide what to derive from. Do you use the Robot
or Switchable base class?
Interfaces
+MethodA()
+MethodB()
NPC “is a” robot NPC "is a" robot
inherit from at most
one abstract base class
Composition
NPC "has"
NPC “has”
an on/off switch
an on/off switch
implement multiple
Inheritance
interfaces as needed
Remember: A class can inherit from at most one abstract class, but it
can implement multiple interfaces.
A SOLID understanding
Getting to know the SOLID principles is a matter of daily practice. Think of them
as five basic rules to always keep in mind while coding. Here’s a handy recap:
— Single responsibility: Make sure classes only do one thing and have only
one reason to change.
The SOLID principles are guidelines to help you write cleaner code so that it’s
more efficient to maintain and extend. SOLID principles have dominated
software design for nearly two decades at the enterprise level because they’re
well-suited for large applications that must scale.
Determine for yourself how strictly you will apply the principles to your projects;
they’re not absolutes. There are nuances, and numerous ways to implement
each one that are not covered here. Remember: the thinking behind the principle
is more important than any specific syntax.
When unsure about how to use them, refer back to the KISS principle. Keep it
simple, and don’t try to force the principles into your scripts just for the sake of
doing it. Let them organically work themselves into place through necessity.
Design patterns let you repurpose well-known solutions for everyday software
problems. A pattern, however, isn’t an off-the-shelf library or framework. Nor
is it an algorithm, which is a specific set of steps to achieve a result.
Instead, think of a design pattern more like a blueprint. It’s a general plan that
leaves the actual construction up to you. Two programs can follow the same
pattern but have very different code.
When developers encounter the same problem in the wild, many of them
will inevitably come up with similar solutions. Once such a solution becomes
repeated enough, someone might “discover” a pattern and formally give it a
name.
Many of today’s software design patterns stem from the seminal work, Design
Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma,
Richard Helm, Ralph Johnson, and John Vlissides. This book describes 23
such patterns identified in a variety of day-to-day applications.
The original authors are often referred to as the “Gang of Four” (GoF), and
you’ll also hear the original patterns dubbed the GoF patterns. While the
examples cited are mostly in C++ (and Smalltalk), you can apply their ideas to
any object oriented language, such as C#.
While you can work as a game programmer without studying design patterns,
learning them will only help you become a better developer. After all, design
patterns are labeled as such because they’re common solutions to well-known
problems.
Software engineers rediscover them all the time in the normal course of
development. You may have already implemented some of these patterns
unwittingly.
Train yourself to look for them. Doing this can help you:
Of course, not all design patterns apply to every game application. Don’t go
looking for them with Maslow’s hammer; otherwise, you might only find
nails.
Like any other tool, a design pattern’s usefulness depends on context. Each
one provides a benefit in certain situations and also comes with its share of
drawbacks. Every decision in software development comes with
compromises.
Are you generating a lot of GameObjects on the fly? Does it impact your
performance? Can restructuring your code fix that?
Be aware of these design patterns and when the time is right, pull them from
your gamedev bag of tricks to solve the problem at hand.
Further reading
— Game loop: At the core of all games is an infinite loop that must function
independently of clock speed, since the hardware that powers a game
application can vary greatly. To account for computers of different
speeds, game developers often need to use a fixed timestep (with a set
frames per-second) and a variable timestep where the engine measures
how much time has passed since the previous frame.
— Prototype: Often you need to copy objects without affecting the original.
This creational pattern solves the problem of duplicating and cloning an
object to make other objects similar to itself. This way you avoid defining a
separate class to spawn every type of object in your game.
If you use composition to pick and choose components, you combine them
for complex behavior. Add Rigidbody and Collider components for physics.
Add a MeshFilter and MeshRenderer for 3D geometry. Each GameObject is
only as rich and unique as its collection of components.
Of course, Unity can’t do everything for you. Inevitably you’ll need other patterns
that aren’t built-in. Let’s explore a few of these in the next chapters.
© 2022 Unity Technologies 38 of 99 | unity.com
Sometimes it’s helpful to have a special object that creates other objects. Many
games spawn a variety of things over the course of gameplay, and you often
don’t know what you need at runtime until you actually need it.
However, if each product follows a common interface or base class, you can
take this a step further and make it contain more of its own construction logic,
hiding it away from the factory itself. Creating new objects thus becomes more
extensible.
You can also subclass the factory to make multiple factories dedicated to
specific products. Doing this helps generate enemies, obstacles, or anything
else at runtime.
Imagine you want to create a factory pattern to instantiate items for a game
level. You can use Prefabs to create GameObjects, but you might also want to
run some custom behavior when creating each instance.
Products need to follow a specific template for their methods, but they don’t
otherwise share any functionality. Hence, you define the IProduct
interface.
Using an interface to define shared properties and logic between your products
You can then define as many products as you need (ProductA, ProductB, etc.)
so long as they follow the IProduct interface.
return newProduct;
}
}
© 2022 Unity Technologies 42 of 99 | unity.com
Here, you’ve made the product classes MonoBehaviours that implement
IProduct take advantage of Prefabs in the factory.
Note how each product can have its own version of Initialize. The
example ProductA Prefab contains a ParticleSystem, which plays when the
ConcreteFactoryA instantiates a copy. The factory itself does not contain
any specific logic for triggering the particles; it only invokes the
Initialize method, which is common to all products.
You’ll benefit the most from the factory pattern when setting up many
products. Defining new product types in your application doesn’t change your
existing ones or require you to modify previous code.
Separating each product’s internal logic into its own class keeps the factory
code relatively short. Each factory only knows to invoke Initialize on
each product without being privy to the underlying details.
One product plays a sound, while another plays particles. Both use the same interface.
Improvements
The implementation of the factory can vary widely from what’s shown
here. Consider the following adjustments when building your own factory
pattern:
— Use a dictionary to search for products: You might want to store your
products as key-value pairs in a dictionary. Use a unique string identifier
(e.g., the Name or some ID) as the key and the type as a value. This can
make retrieving products and/or their corresponding factories more
convenient.
— Make the factory (or a factory manager) static: This makes it easier to use
but requires additional setup. Static classes won’t appear in the Inspector, so
you will need to make your collection of products static as well.
— Combine with the object pool pattern: Factories don’t necessarily need to
instantiate or create new objects. They can also retrieve existing ones in
the hierarchy. If you are instantiating many objects at once, (e.g.,
projectiles from a weapon), use the object pool pattern for more optimized
memory management.
The object pool pattern uses a set of initialized objects kept ready and waiting
in a deactivated “pool.” When you need an object, your application doesn’t
instantiate it. Instead you request the GameObject from the pool and enable it.
When done using it, you deactivate the object and return it to the pool instead
of destroying it.
Object pools can reduce stuttering that may result from garbage collection
spikes. GC spikes often accompany creating or destroying a large number of
objects due to the allocation of memory. You can pre-instantiate your object
pool at an opportune time, such as during a loading screen, when the user won’t
notice the stutter.
An object pool can help you shoot bullets without gameplay stutter.
The SetupPool method populates the object pool. Create a new stack of
PooledObjects and then instantiate copies of the objectToPool to fill it
with initPoolSize elements. Invoke SetupPool in Start to make sure
that it runs once during gameplay.
© 2022 Unity Technologies 47 of 99 | unity.com
You’ll also need methods to retrieve a pooled item (GetPooledObject) and
return one to the pool (ReturnToPool):
This way, you can appear to fire hundreds of bullets offscreen when in
reality, you simply disable and recycle them. Just make sure your pool size is
large enough to show the concurrently active objects.
If you need to exceed the pool size, the pool can instantiate extra
objects. However, most of the time it pulls from the existing inactive
objects.
If you’ve used Unity’s ParticleSystem, then you have firsthand experience with
an object pool. The ParticleSystem component contains a setting for the max
number of particles. This simply recycles available particles, preventing the
effect from exceeding a maximum number. The object pool works similarly,
but with any GameObject of your choosing.
Improvements
The example above is a simple one. When deploying an object pool for
actual projects, consider the following upgrades:
— Check for errors: Avoid releasing an object that is already in the pool.
Otherwise, it might result in an error at runtime.
How you use object pools will vary by application. This pattern commonly
appears when a gun or weapon needs to fire multiple projectiles like in a bullet
hell shooter.
Every time you instantiate a large number of objects, you run the risk of causing
a small pause from a garbage-collection spike. An object pool alleviates this
issue to keep your gameplay smooth.
If you’re using a version of Unity from 2021 and above, it includes a built-in
object pooling system, so there’s no need to create your own PooledObject
or ObjectPool classes like in the previous example.
UnityEngine.Pool
The object pool pattern is so ubiquitous that Unity 2021 now supports its own
UnityEngine.Pool API. This gives you a stack-based ObjectPool to track your
objects with the object pool pattern. Depending on your needs, you can also use
a CollectionPool (List, HashSet, Dictionary, etc.)
In the sample project (see the scene), you no longer need the custom pool
components. Instead, update the gun script with a using UnityEngine.Pool;
line at the top. This allows you to create a projectile pool with the built-in
ObjectPool:
// invoked when retrieving the next item from the object pool
private void OnGetFromPool(RevisedProjectile pooledObject) {
pooledObject.gameObject.SetActive(true);
}
}
}
— Destroying a pooled object (e.g., if you hit a maximum limit) You must then
Note how the built-in ObjectPool also includes options for a default pool size
and maximum pool size. Items exceeding the max pool size trigger an action to
self-destruct, keeping memory usage in check.
…
}
The UnityEngine.Pool API makes setting up object pools faster, now that you
don’t have to rebuild the pattern from scratch. That’s one less wheel to
reinvent.
SINGLET
O N PAT
TERN Singletons get a bad rap. If you’re new to Unity development, the singleton is
likely one of the first recognizable patterns that you will encounter in the
wild. It’s also one of the most maligned design patterns.
This is useful if you need to have exactly one object that coordinates actions
across the entire scene. For example, you might want exactly one game
manager in your scene to direct the main game loop. You also probably only
want one file manager writing to your filesystem at a time. Central, manager
level objects like these tend to be good candidates for the singleton pattern.
In Game Programming Patterns, it says that singletons do more harm than good
and lists it as an anti-pattern. This poor reputation is because the pattern’s ease
of use lends itself to abuse. Developers tend to apply singletons in inappropriate
situations, introducing unnecessary global states or dependencies.
Let’s examine how to build a singleton in Unity and weigh its strengths and
weaknesses. Then you can decide whether it’s worth incorporating into your
application.
using UnityEngine;
The public static Instance will hold the one instance of Singleton in the scene.
In the Awake method, check if it’s already set. If Instance is currently null,
then Instance gets set to this specific object. This must be the first singleton in
the scene.
If you attach the script to more than one GameObject in the hierarchy at
runtime, the logic in Awake will keep the first object and then discard the
rest.
— You need to set up the singleton in the hierarchy before using it.
Because the singleton often serves as an omnipresent manager script, you can
benefit from making it persistent using a DontDestroyOnLoad.
Further, you can use lazy instantiation to build the singleton automatically when
you first need it. You only need some logic to create a GameObject and then add
the appropriate Singleton component.
if (instance == null)
{
GameObject gameObj = new GameObject(); gameObj.name
= "Singleton";
instance = gameObj.AddComponent<Singleton>();
DontDestroyOnLoad(gameObj);
}
}
Using generics
if (instance == null)
{
GameObject gameObj = new GameObject();
gameObj.name = typeof(T).Name;
instance = gameObj.AddComponent<T>();
DontDestroyOnLoad(gameObj);
}
}
Singletons are unlike the other patterns in this guide in that they break with
SOLID principles in several respects. Many developers dislike them for a
variety of reasons:
— Singletons require global access: Because you use them as global
instances, they can hide many dependencies, making bugs much harder to
troubleshoot.
But many games are not enterprise-level applications. You don’t need to extend
them continuously the same way you might for business software.
In this way, you can make a manager object (e.g., game flow manager or audio
manager) that is always accessible from every other GameObject in your scene.
Also, if you’ve implemented the object pool, you can design your pooling system
as a singleton to make getting pooled objects easier.
If you decide to use singletons in your project, keep them to a minimum. Don’t
use them indiscriminately. Reserve the singletons for a handful of scripts that
can benefit from global access.
© 2022 Unity Technologies 60 of 99 | unity.com
COMMAN
D PAT TERN One of the original Gang of Four patterns, command is useful whenever you
want to track a specific series of actions. You’ve likely seen the command
pattern at work if you’ve played a game that uses undo/redo functionality or
keeps your input history in a list. Imagine a strategy game where the user can
plan several turns before actually executing them. That’s the command
pattern.
Storing these command objects in a collection like a queue or a stack allows you
to control the timing of their execution. This functions as a small buffer. You can
then potentially delay a series of actions for later playback or undo them.
To implement the command pattern, you need a general object that will contain
your action. This command object will hold what logic to perform and how to
undo it.
There are a number of ways to implement this, but here’s one version that uses
an interface:
In this case, every gameplay action will apply the ICommand interface (you
could also implement this with an abstract class).
Each command object will be responsible for its own Execute and Undo
methods. Thus, adding more commands to your game won’t affect any existing
ones.
You’ll need another class to execute and undo commands. Create a
CommandInvoker class. In addition to the ExecuteCommand and
UndoCommand methods, it has an undo stack to hold the sequence of
command objects.
Let’s imagine you want to move your player around a maze in your
application. You could create a PlayerMover responsible for shifting the
player’s position:
ICommand also needs an Undo method to restore the scene back to its
previous state. In this case, the Undo logic subtracts the movement vector,
essentially pushing the player in the opposite direction.
Once you create the command object and save its needed parameters, use the
CommandInvoker’s static ExecuteCommand and UndoCommand methods to
pass in your MoveCommand. This runs the MoveCommand’s Execute or Undo
and tracks the command object in the undo stack.
if (playerMover.IsValidMove(movement))
{
ICommand command = new MoveCommand(playerMover, movement);
CommandInvoker.ExecuteCommand(command);
}
}
Check out the sample project for implementation details for the InputManager
or set up your own input using the keyboard or gamepad. Your player can now
navigate the maze. Click the Undo button so you can backtrack to the
beginning square.
For example, think about a fighting game where a series of specific button
clicks triggers a combo move or attack. Storing player actions with the
command pattern makes setting up such combos much simpler.
On the flip side, the command pattern introduces more structure, just like the
other design patterns. You’ll have to decide where these extra classes and
interfaces provide enough benefit for deploying command objects in your
application.
© 2022 Unity Technologies 66 of 99 | unity.com
Improvements
Once you learn the basics, you can affect the timing of commands and play
them back in succession or reverse, depending on the context.
— Create more commands: The sample project only includes one type of
command object, the MoveCommand. You can create any number of
command objects that implement ICommand and track them using the
CommandInvoker.
operations. This way you can quickly cycle through the undo history or
redo those actions. Clear out the redo stack when the user invokes an
entirely new movement (you can find an implementation in the
accompanying sample project).
New
command
Older Undo
Newer
Redo
Current index
— Limit the size of the stacks: Undo and redo operations can quickly blow
up out of control. Limit the stacks to the last number of commands.
The CommandInvoker, like other external objects, doesn’t see the inner
workings of the command object, only invoking Execute or Undo. Give the
command object any data needed to work when calling the constructor.
STATE PAT TERN Imagine constructing a playable character. At one moment, the character may
be standing on the ground. Move the controller, and it appears to run or walk.
Press the jump button and the character leaps into midair. A few frames later,
it lands and reenters its idle, standing position.
Games are interactive, and they force us to track many systems that change
at runtime. If you draw a diagram that represents the different states of your
character, you might come up with something like this:
A simple state diagram
— Each state can trigger a transition to one other state based on conditions at
runtime.
— When a transition occurs, the output state becomes the new active state.
To describe a basic FSM in code, you might use a naive approach with an
enum and a switch statement.
This would work, but the PlayerController script can get messy quickly. Adding
more states and complexity requires us to revisit the PlayerController script’s
internals each time.
Fortunately, the state pattern can help you reorganize the logic. According to
the original Gang of Four, the state pattern solves two problems:
— An object should change its behavior when its internal state changes.
Transition from
previous state
Evaluate
each frame
Transition to
next state
Each concrete state in your game will implement the IState interface:
— Update: This logic runs every frame (sometimes called Execute or Tick).
You can further segment the Update method as MonoBehaviour does,
using a FixedUpdate for physics, LateUpdate, and so on.
— An Exit: Code here runs before leaving the state and transitioning to
a new state.
You’ll need to create a class for each state that implements IState. In the
sample project, a separate class has been set up for WalkState, IdleState,
and JumpState.
© 2022 Unity Technologies 73 of 99 | unity.com
Another class, the StateMachine, will then manage how control flow enters and
exits the states. With the three example states, the StateMachine could look
like this:
[Serializable]
public class StateMachine
{
public IState CurrentState { get; private set; }
To follow the pattern, the StateMachine references a public object for each
state under its management (in this case, walkState, jumpState, and
idleState). Because StateMachine doesn’t inherit from MonoBehaviour, use
a constructor to set up each instance:
— The Serializable attribute allows us to display the StateMachine (and its public
fields) in the Inspector. Another MonoBehaviour (e.g., a PlayerController or
EnemyController) can then use the StateMachine as a field.
— Each State object determines its own conditions for calling the
TransitionTo method to change the currently active state. You can
pass in any necessary dependencies (including the State Machine itself)
to each state when setting up the StateMachine instance.
Each state object will manage its own internal logic, and you can make as many
states as needed to describe your GameObject or component. Each one gets its
own class that implements IState. In keeping with the SOLID principles, adding
more states has minimal impact on any previously created states.
Review the sample project for the WalkState and JumpState implementation
as well. Rather than have one large class that switches behavior, each state
has its own update logic. This way, states can function independently from one
another.
The state pattern can help you adhere to the SOLID principles when setting up
internal logic for an object. Each state is relatively small and just tracks the
conditions for transitioning into another state. In keeping with the open-closed
principle, you can add more states without affecting existing ones and avoid
cumbersome switch or if statements.
On the other hand, if you only have a few states to track, the extra structure can
be overkill. This pattern might only make sense if you expect your states to grow
to a certain complexity.
Improvements
The capsule in the sample project changes color, and the UI updates with the
player’s internal state. In a real-world example, you could have much more
complex effects to accompany the state changes:
If you’ve used Unity’s Animator window, you’ll notice that its workflow
pairs well with the state pattern. Each animation clip occupies one state,
with only one state active at a time.
— Add a hierarchy: As you begin to describe more complex entities with the
state pattern, you might want to implement hierarchical state machines.
Inevitably some states will be similar; for example, if the player or game
actor is grounded, it can duck or jump whether in a WalkingState or
RunningState.
Low health
Player out
Recovered
Player sighted
Player out of
sight
Here’s the state pattern at work again in a completely different context. Every
state represents an action, such as attacking, fleeing, or patrolling. Only one
state is active at a time, with each state determining its transition to the next
one.
The observer pattern functions like a radio tower. The subject broadcasts to the observers.
The object that is broadcasting is called the subject. The other objects that are
listening are called the observers.
This pattern loosely decouples the subject, which doesn’t really know the
observers or care what they do once they receive the signal. While the
observers have a dependency on the subject, the observers themselves don’t
know about each other.
Events
The observer pattern is so widespread that it’s built into the C# language. You
can design your own subject-observer classes but it’s usually unnecessary.
Remember the point about reinventing the wheel? C# already implements the
pattern using events.