Unity: An Entity-Component Game Engine
Entity-Component Pattern: Allows a single entity to span multiple domains without coupling the domains to each other.
GameObject
: The single entity that spans multiple domains (Transform, Physics, Rendering, ...).
Component
: Keeps the domains isolated, where the code for each is placed in its own Component
class.
In Unity, GameObject
is simply a container of Components
.
- Use C#
- Strongly-typed, lexical analysis = debugging easy
- Lots of documentation, libraries, community supported
- Stay Organized
- Naming conventions
- Descriptive names, standard capitilization
- Unity handles spaces in names
- Logical folder structure
- Easy to locate assets
- Naming conventions
- Zero-tolerance for:
- Warnings & Errors
- Runtime Memory Allocaton
- Use the Profiler often
- Keep code constantly optimized
- Public Static Classes\Methods
- Public Instance Methods
- Events/Messages
Most common patterns:
- Singletons
- Object Pools
Game Managers, Static Classes, Global Variables, & You
Singletons are basically an object enforced to have a single instance only and always. You can access it anywhere, any time, without needing to instantiate it. That's why it's so closely related to
static
. For comparison,static
is basically the same thing, except it's not an instance. We don't need to instantiate it, and we can't, because it's automagically allocated. And that can and does bring problems.
The Singleton design pattern is a very specific type of single instance, specifically one that is:
- Accessible via a global, static instance field
- Created either on program initialization or upon first access (lazy instantiation)
- No public constructor (cannot instantiate directly)
- Never explicitly freed (implicitly freed on program termination)
This pattern introduces several potential long-term problems:
- Inability to use abstract or interface classes
- Inability to subclass
- High coupling across the application (difficult to modify)
- Difficult to test (can't fake/mock in unit tests)
One object to control them all
Game Manager Basic Requirements:
- Be easily extendible
- Have only one instance allowed
- Persist across all the scenes (shouldn’t be destroyed when a new scene is loaded)
- Provide global variable access to other classes
So why be Singleton? A non-instantiable static class could do the job, no?!
- You can’t extend MonoBehaviour with a static class, hence it can't be a Script Component.
- You can’t implement an interface with a static class.
- You can’t pass around a static class as a parameter.
Minimal Quick Hack:
public class GameManager : MonoBehaviour {
public static GameManager instance = null; // Static instance of GameManager which allows it to be accessed by any other script.
void Awake() { // Awake is always called before any Start functions.
if (instance == null) // Check if instance already exists.
instance = this; // If not, set instance to this.
else if (instance != this) // If instance already exists and it's not this:
Destroy(this.gameObject); // Then destroy this. This enforces our singleton pattern, meaning there can only ever be one instance of a GameManager.
DontDestroyOnLoad(this.gameObject); // Sets this to not be destroyed when loading new scenes.
}
}
- Singleton: Game Programming Patterns Book
- Singleton example on Unify wiki
- Toolbox example
- GameManager example
- Good read about Singleton issues
Execution Time Sharing (not multi-threading, concurrency, or parallelism)
Coroutine
inherits fromYieldInstruction
- A function that can suspend its execution (yield) until the given
YieldInstruction
finishes.- Maintains local parameter references when called.
- No return values or error handling (but can be overcome, if necessary)
- Can be used as a way to spread an effect over a period time. It is also a useful optimization:
- Replaces state machines elegantly
- Prevents execution blocking
When a task does not need to be needlessly repeated quite so frequently, you can put it in a coroutine to get an update regularly but not in every single frame. Similarly, calling an expensive function every frame in
Update()
will introduce significant slowdown, since it would block execution. To overcome this use a coroutine to call it, say, only every tenth of a second instead of every frame update.
For example:
void Start() {
StartCoroutine(SomeCoroutine);
}
void Update() {
// ExpensiveFunction(); // muh framerates :(
}
IEnumerator SomeCoroutine() {
while(true) {
ExpensiveFunction();
yield return new WaitForSeconds(.1f);
}
}
-
A common pattern effectively handled by coroutines:
- Operations that take more than 1 frame...
- Where we don't want to block execution...
- And want to know when finished running.
- Examples:
- Cutscenes, Animation
- AI Sequences/State Machines
- Expensive Operations
-
Coroutines also admit a slick, readable game loop:
void Start() {
StartCoroutine (GameLoop()); // Let's play!
}
// This is called from Start() and will run each phase of the game one after another.
IEnumerator GameLoop() {
yield return StartCoroutine (LevelStart()); // Start the level: Initialize, do some fun GUI stuff, ..., yield WaitForSeconds if setup too fast.
yield return StartCoroutine (LevelPlay()); // Let the user(s) play the level until a win or game over condition is met, then return back here.
yield return StartCoroutine (LevelEnd()); // Find out if some user(s) "won" the level or not. Also, do some cleanup.
if (WinCondition) { // Check if game level progression conditions were met.
Application.LoadLevel(++level); // or Application.LoadLevel(Application.loadedLevel) if using same scene
} else {
StartCoroutine (GameLoop()); // Let the user retry the level by restarting this (non-yielding) coroutine again.
}
}
// The Coroutines
IEnumerator LevelStart() { Debug.Log("Start"); yield return new WaitForSeconds(1f); }
IEnumerator LevelPlay () { while(alive) yield return null; }
IEnumerator LevelEnd () { Debug.Log("End.."); yield return new WaitForSeconds(1f); }
Use StartCoroutine methods to start a coroutine.
public Coroutine StartCoroutine(IEnumerator method); // Typical usage. Pass the name of the method in code.
public Coroutine StartCoroutine(string methodName, object value = null); // Higher runtime overhead to start the coroutine this way; can pass only one parameter.
Use StopCoroutine methods to stop a coroutine.
public void StopCoroutine(IEnumerator method); // Stops the coroutine stored in method running on this behaviour.
public void StopCoroutine(string methodName); // Stops the first coroutine named methodName.
public void StopAllCoroutines(); // Stops all coroutines running on this behaviour.
Note: If you call multiple coroutines with the same name, even a single StopCoroutine with that name will destroy them all!
Normal coroutine updates are run after the Update()
function returns.
Different uses of Coroutines by return type:
yield // The coroutine will continue after all Update functions have been called on the next frame.
yield WaitForSeconds // Continue after a specified time delay, after all Update functions have been called for the frame
yield WaitForFixedUpdate // Continue after all FixedUpdate has been called on all scripts
yield WaitForEndOfFrame // Continue after all FixedUpdate has been called on all scripts
yield WWW // Continue after a WWW download has completed.
yield StartCoroutine // Chains the coroutine, and will wait for the MyFunc coroutine to complete first.
In actual C# code:
yield return null;
yield return new WaitForSeconds(t);
yield new WWW(url);
yield return new WaitForFixedUpdate();
yield StartCoroutine(routine)
- Passing method name as code:
IEnumerator instance = null; // Need a reference to a specific coroutine instance to stop it.
instance = SomeCoroutine(a, b, c);
StartCoroutine(instance); // Start coroutine
// or instance = StartCoroutine(SomeCoroutine (a, b, c)); (Coroutine continue failure?)
StopCoroutine(instance); // Stop this specific coroutine instance.
- Passing method name as string:
IEnumerator Start() {
StartCoroutine("DoSomething", 2.0F);
yield return new WaitForSeconds(1);
StopCoroutine("DoSomething");
}
IEnumerator DoSomething(float someParameter) {
while (true) {
print("DoSomething Loop");
yield return null;
}
}
Events are closely related/similar to the Observer software design pattern.
Observer: gameprogrammingpatterns chapter
- Simple, workable solution for Decoupling classes:
- It helps us loosen the coupling between two pieces of code.
- It lets a subject indirectly communicate with some observer without being statically bound to it.
- It lets one piece of code announce that something interesting happened without actually caring who receives the notification.
Problems:
- Object heavy
- Have to implement an entire interface just to receive a notification.
- Can’t have a single class that uses different notification methods for different subjects.
Modern approach is for an “Observer” to be only a reference to a method or function, like C# delegates:
Delegates (C# Programming Guide)
- A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type. You can invoke (or call) the method through the delegate instance.
- Delegates are used to pass methods as arguments to other methods.
- The following example shows a delegate declaration:
public delegate int PerformCalculation(int x, int y);
-
Events enable a class or object to notify other classes or objects when something of interest occurs.
-
The class that sends (or raises) the event is called the publisher and the classes that receive (or handle) the event are called subscribers.
-
The publisher determines when an event is raised; the subscribers determine what action is taken in response to the event.
-
Event handlers are nothing more than methods that are invoked through delegates. You create a custom method, and a class can call your method when a certain event occurs.
-
C# has "events" baked into the language.
- The
event
keyword is used to declare an event in a publisher class.public event SampleEventHandler SampleEvent;
- The
-
the "observer" you register is a "delegate" (delegates are a reference to a method).
public delegate void SampleEventHandler(object sender, SampleEventArgs e);
Problems:
- Setting a delegate ALLOCATES memory.
- Using a single delegate and constantly setting it will cause a GC call to reclaim memory.
- Better to pre-set an array of delegates in the
Awake
function to overcome this.
-
- used in Unity UI system.
- Serialized in editor.
-
Examples:
- example
- TODO
- Unity Manual
- Scripting API
- Unity3D Custom Google Search
- Execution Order of Event Functions & Script Lifecycle Flowchart
- Scripting API
- Scripting Tutorials: Concise Videos & Code Snippets
- Good Coding Practices
- Script Serialization
- Mathf
- Random|Class
- Vector Cookbook
- Vector3|Vector2
- Physics|Raycast|Hit
- Physics2D|Raycast|Hit2D
- Rigidbody|2D
- Collision
- Input|Class