synopsis from article index: Between Unity's APIs and features built-in to c#, there are an overwhelming number of possible ways to organizing a project. In this article, we'll discuss a few of the options for organizing inter-object communication, and some of the reasoning and drawbacks behind using each model.
Unity Essentials Weekly
Organizing Object References
Article Table of Contents
- Object Oriented Crash Course
- Organizing Object References
- Singleton Patterns
- Object References through Inspector
- GameObject.Find(), et al
- Static Properties and Methods
- Unity Messaging
- Downloads for this Article
- Other Articles
The Hidden Agenda for this Article
The goal of this article is to introduce different message passing and method calling mechanisms available in Unity and c#, to provide as a foundation for future articles discussing best practices for designing and organizing your components in your Unity projects. This article presents materials from a variety of skill levels, so don't be too concerned if you don't understand everything from this article after the first read, we will be using this for reference on more in-depth topics down the road.
These future articles will discuss topics ranging from camera selection systems through incorporating components and resources in .Net .dll files for the most solid integrations possible.
The documentation on Unity3d.com also contains a list of ways to communicate between different objects on their Accessing Other Game Objects page in the script reference.
Object Oriented Crash Course
Since Unity developers are immigrating from many different walks of technological life, lets have a quick refresher on some object-oriented programming concepts. If you are not familiar with these concepts, you will want to learn this before continuing with Unity scripting.
OOP Principles
Some concepts that you should be familiar with from the above link to save yourself a lot of redesign time if you will be designing objects:
- SRP - The Single Responsibility Principle - A class should have one, and only one, reason to change.
- OCP - The Open Closed Principle - You should be able to extend a classes behavior, without modifying it.
- LSP - The Liskov Substitution Principle - Derived classes must be substitutable for their base classes.
- DIP - The Dependency Inversion Principle - Depend on abstractions, not on concretions.
- ISP - The Interface Segregation Principle - Make fine grained interfaces that are client specific.
You can get away with developing for Unity without these concepts, but a goal of this blog is to help you program for Unity well. One common practice for the uninitiated OOP developer is to use classes like packages, mimicking procedural style programming. I once saw this best described as a paradigm named "Procedural Object Oriented Programming", or shortened to an acronym, "POOP".
Static Properties and Methods
Here's a list of some of the differences between static and instance members, and how they are implemented in c#.
| OOP Term | .Net Keyword | Details |
|---|---|---|
| Class Methods | static | Method call within class without instance. No access to "this" reference. Accessed through call to ClassName.Method(). |
| Instance Methods | - | Method call to an instance of an object. Data is obtained through "this" reference. Accessed through call to instance.Method(), where instance is obtained through instance = new ClassName(); | static | Property shared by all instances of an object, i.e. this is only one copy of this value for the class. Accessed through ClassName.propertyName. |
| Instance Properties | - | Property for one instance of an object. Access through call to instance.propertyName, where instance is obtained through instance = new ClassName(); |
Below is an example class containing methods and properties of both the class and instance varieties.
1: class InstanceVsClass
2: {
3: // I am an instance variable, with a unique value for each
4: // instance of this object. I can only be accessed after
5: // a call to "new InstanceVsClass()"
6: string instanceVariable = "instance";
7:
8: // I am a class variable. I am available without an object
9: // instance, and am shared with all instances.
10: static string classVariable = "class";
11:
12: // I have access to both instance and class variables
13: string InstanceMethod()
14: {
15: return this.instanceVariable + ":"
16: + InstanceVsClass.classVariable;
17: }
18:
19: // I don't have access to instance members because I don't
20: // have a "this" reference
21: static string classMethod()
22: {
23: return InstanceVsClass.classVariable;
24: }
25: }
Organizing Object References
There are several ways to link up components to communicate with each other, including:
- Object references through inspector
- References loaded with GameObject.Find() and similar methods
- Singleton patterns
- Static properties and methods
- Unity messaging
Singleton Patterns
Singleton patterns may be a term that many have seen but aren't completely sure on the definition. Singletons are a shared instance of an object, usually saved to a static property, that is used instead of a series of static methods. The main reason for using a singleton is to allow the object to be instantiated in other ways down the road if needed.
1:
2: /// <summary>
3: /// Example singleton object in c#.
4: /// </summary>
5: public class SingletonExample
6: {
7: /// <summary>
8: /// Static class constructor is called first time the class is
9: /// accessed. We are creating our Singleton here.
10: /// </summary>
11: static SingletonExample()
12: {
13: singleton = new SingletonExample();
14: }
15:
16: /// <summary>
17: /// Reference to the singleton instance
18: /// </summary>
19: public static SingletonExample singleton;
20:
21: /// <summary>
22: /// Instance value
23: /// </summary>
24: public string myValue = "test";
25: }
26:
27: /// <summary>
28: /// Object using singleton example.
29: /// </summary>
30: public class Tester
31: {
32: /// <summary>
33: /// Prints the myValue property from the singleton to the console.
34: /// </summary>
35: public static void Test()
36: {
37: System.Console.WriteLine(SingletonExample.singleton.myValue);
38: }
39: }
There is a performance hit to using singletons over using static methods and properties, very similar to the performance hit for using c++ instead of c. Each call has an extra reference lookup before calling the method, since accessing the singleton is through a static property.
The above example is not well-suited for Unity, because typically you will want to use a singleton pattern on a MonoBehaviour, which cannot be instantiated directly (i.e. you can't call new MonoBehaviour). In order to use singletons with MonoBehaviour objects, you need to assign the singleton during the Awake() or Start() events, Awake() being preferred since its called earlier.
Below is an example usage of a class that GSD Software uses for the singleton pattern for Unity development, which is included as a download for this article.
1:
2: using UnityEngine;
3:
4: /// <summary>
5: /// Example script using the Singleton<T> generic class. This object
6: /// rotates a camera around an object.
7: /// </summary>
8: public class MyScript : MonoBehaviour
9: {
10: #region Properties
11:
12: /// <summary>
13: /// Camera being moved
14: /// </summary>
15: public Camera targetCamera;
16:
17: /// <summary>
18: /// Object camera is moving around
19: /// </summary>
20: public Transform target;
21:
22: /// <summary>
23: /// Number of seconds to take to revolve around the target object.
24: /// </summary>
25: public float duration = 5f;
26:
27: /// <summary>
28: /// Number of units away from object to place camera.
29: /// </summary>
30: public float distance = 10f;
31:
32: #endregion
33:
34: #region Events
35:
36: /// <summary>
37: /// We're assigning the singleton during awake, since its the
38: /// earliest access we have to the MonoBehaviour object instance.
39: /// Note that the .instance property is write-once, meaning that
40: /// once it has an instance additional calls will not overwrite
41: /// the original reference.
42: /// </summary>
43: void Awake()
44: {
45: Singleton<MyScript>.instance = this;
46: }
47:
The Awake() Unity event is chosen for assigning the singleton because this is the earliest point that we can guarantee the this pointer to be valid. As long as no access to this singleton is called before Start(), and this component is enabled, we can guarantee assignment of the singleton.
As mentioned in the code comments, the singleton instance is only written the first time, and all other redundant write calls are ignored. This means if for some reason multiple components with the same singleton exist in your Unity project (I would consider this a design mistake), the first component to call Awake() will be the instance used by the singleton.
48: /// <summary>
49: /// Use the active camera by default, and the game object attached to
50: /// this component to look at.
51: /// </summary>
52: void Start()
53: {
54: this.targetCamera = Camera.main;
55: this.target = this.transform;
56: }
57:
58: /// <summary>
59: /// Updates object position. I'm using trig functions here, but
60: /// you could use a quaternion to achieve the same effect.
61: /// </summary>
62: void Update()
63: {
64: this.theta += Time.deltaTime / this.duration;
65:
66: Vector3 offset = new Vector3();
67: offset.x = Mathf.Cos(this.theta) * this.distance;
68: offset.y = Mathf.Sin(this.theta) * this.distance;
69: offset.z = 0;
70:
71: Vector3 v = this.target.position + offset;
72:
73: this.targetCamera.transform.position = v;
74: this.targetCamera.transform.LookAt(this.target);
75: }
76:
77: #endregion
78:
79: #region Private
80:
81: /// <summary>
82: /// Current angle around target.
83: /// </summary>
84: private float theta = 0f;
85:
86: #endregion
87: }
88:
89: /// <summary>
90: /// This class uses the singleton to access the distance property.
91: /// </summary>
92: public class MyOtherClass : MonoBehaviour
93: {
94: /// <summary>
95: /// Draws two buttons to move the camera closer and further away
96: /// from the target object.
97: /// </summary>
98: void OnGUI()
99: {
100: GUILayout.BeginVertical();
101: if (GUILayout.Button("Closer"))
102: {
103: Singleton<MyScript>.instance.distance--;
104: }
105: if (GUILayout.Button("Further"))
106: {
107: Singleton<MyScript>.instance.distance++;
108: }
109: GUILayout.EndVertical();
110: }
111: }
112:
As mentioned before, this example uses a class from my company's internal library of Unity tools. The code listing for this class is displayed below, and is also available for download at the end of this page. Note that in order to test this code within Unity, you would need to split the above code into two files, since Unity requires any attached components to be contained in files with the same name as the component's class.
1:
2: using UnityEngine;
3:
4: /// <summary>
5: /// Simple implementation of a singleton instance for any script.
6: /// </summary>
7: public class Singleton<T> where T : MonoBehaviour
8: {
9: /// <summary>
10: /// Write-once, read-many object reference. Set this value at least
11: /// once and all objects will have access to it as a singleton.
12: /// </summary>
13: public static T instance
14: {
15: get { return _instance; }
16: set { if (_instance == null) _instance = value; }
17: }
18:
19: /// <summary>
20: /// Returns true iff a singleton has been assigned for this class.
21: /// </summary>
22: public static bool assigned
23: {
24: get { return instance != null; }
25: }
26:
27: /// <summary>
28: /// current object reference
29: /// </summary>
30: private static T _instance = null;
31: }
32:
33: /// <summary>
34: /// Avoids unity compiler warning in Unity 2.5 and earlier.
35: /// </summary>
36: public class Singleton {}
You will notice that this singleton object is not a subclass of MonoBehaviour. Code developed within your Unity assets folder do not all have to be components, placing normal c# classes anywhere in your assets folder will allow the components from your project to access these classes as desired.
Assigning Objects Through the Inspector
Using the inspector to connect your objects together is probably the most obvious way to connect your objects together. Selecting the object with your script as a component lets you visually inspect the references to other objects, as well as clicking on the references for it to highlight the referenced object.
The biggest drawback to this method is that you have to always manually assign the object references each type you add another script the the scene. The actual reference itself is a standard c# reference, so there is no performance loss to this mechanism, just a potential maintenance nightmare. I have personally seen more than one person writeoff Unity in a message board as unmaintainable due to this issue.
1:
2: using UnityEngine;
3:
4: /// <summary>
5: /// Example of accessing different objects through references setup
6: /// in the game object inspector.
7: /// </summary>
8: public class AssignExample : MonoBehaviour
9: {
10: #region Properties
11:
12: /// <summary>
13: /// Link to a camera setup in the inspector.
14: /// </summary>
15: public Camera targetCamera;
16:
17: /// <summary>
18: /// Link to a specific component type.
19: /// </summary>
20: public OtherComponent otherTarget;
21:
Notice how the reference for AssignExample.otherTarget uses the specific class "OtherComponent"? This makes it easier to setup the references for the component, as only game objects containing components of type AssignExample will be available from the drop down list in the inspector.
22: /// <summary>
23: /// How often to call other component.
24: /// </summary>
25: public float callFrequency = 1f;
26:
27: #endregion
28:
29: #region Events
30:
31: /// <summary>
32: /// Checks that all references have been assigned, outputting log
33: /// errors if any are missing and deactivates.
34: /// </summary>
35: void Start()
36: {
37: if (this.targetCamera == null)
38: {
39: Debug.LogError("targetCamera is not set");
40: this.enabled = false;
41: }
42: if (this.otherTarget == null)
43: {
44: Debug.LogError("otherTarget is not set");
45: this.enabled = false;
46: }
47: }
48:
49: /// <summary>
50: /// Points the camera at the other component and calls a method
51: /// on OtherComponent with the desired frequency.
52: /// </summary>
53: void Update()
54: {
55: this.targetCamera.transform.LookAt(this.otherTarget.transform);
56: if (Time.time - this.lastCall > this.callFrequency)
57: {
58: this.otherTarget.SetLastCalled(Time.time);
59: this.lastCall = Time.time;
60: }
61: }
62:
63: #endregion
64:
65: #region Private
66:
67: /// <summary>
68: /// Last time OtherComponent was called.
69: /// </summary>
70: private float lastCall = 0f;
71:
72: #endregion
73: }
74:
Once the references are assigned, calls are made directly at full performance. This is definitely a high-performance option, but is difficult to manage for complicated projects.
75: /// <summary>
76: /// Class that AssignExample will talk to. This code will need to be
77: /// places in a separate .cs file before testing in Unity. (Already
78: /// done for you in the downloads area.)
79: /// </summary>
80: public class OtherComponent : MonoBehaviour
81: {
82: #region Methods
83:
84: /// <summary>
85: /// Changes the value to display for this component.
86: /// </summary>
87: public void SetLastCalled(float f)
88: {
89: this.lastCalled = f;
90: }
91:
92: #endregion
93:
94: #region Events
95:
96: /// <summary>
97: /// Draw the last value passed in the upper-right corner.
98: /// </summary>
99: void OnGUI()
100: {
101: GUILayout.BeginVertical();
102: GUILayout.BeginHorizontal();
103: GUILayout.FlexibleSpace();
104: GUILayout.Label("Value=" + this.lastCalled);
105: GUILayout.EndHorizontal();
106: GUILayout.FlexibleSpace();
107: GUILayout.EndVertical();
108: }
109:
110: #endregion
111:
112: #region Private
113:
114: /// <summary>
115: /// Last time value passed from AssignExample.
116: /// </summary>
117: private float lastCalled = 0f;
118:
119: #endregion
120: }
You could also point otherTarget to a base class when several kinds of objects are desired as candidates for the reference. This includes Behaviour and Transform classes, but this results in the drop down list being populated by all components in the current scene, which is usually an unmanageable situation.
Reference Loading with GameObject.Find()
Rather than connecting objects manually, you can use methods from the Unity API to connect up by name and/or type of script. This results in a performance hit when loading the references, but isn't usually a problem as long as the results are cached.
You would never want to lookup the reference to a component from any frequently call c#. Once the reference is loaded, method calls are performed directly, which is the fastest way possible.
1:
2: using UnityEngine;
3:
4: /// <summary>
5: /// Example of interacting between objects using a name lookup, if
6: /// a reference hasn't already been assigned in the inspector.
7: /// </summary>
8: public class HybridLookupExample : MonoBehaviour
9: {
10: #region Properties
11:
12: /// <summary>
13: /// Object to point camera at.
14: /// </summary>
15: public Transform targetView;
16:
17: /// <summary>
18: /// Camera to point at object.
19: /// </summary>
20: public Camera targetCamera;w
21:
22: #endregion
23:
24: #region Events
25:
26: /// <summary>
27: /// Load references by name if not already assigned.
28: /// </summary>
29: void Start()
30: {
31: if (this.targetView == null)
32: {
33: GameObject g = GameObject.Find("target");
34: if (g != null)
35: {
36: this.targetView = g.transform;
37: }
38: }
39: if (this.targetCamera == null)
40: {
41: GameObject g = GameObject.Find("camera");
42: if (g != null)
43: {
44: this.targetCamera = g.camera;
45: }
46: }
47: }
48:
49: /// <summary>
50: /// Keep camera pointed at object.
51: /// </summary>
52: void Update()
53: {
54: // early exit for missing references
55: if (this.targetView == null) return;
56: if (this.targetCamera == null) return;
57:
58: // point camera
59: this.targetCamera.transform.LookAt(this.targetView);
60: }
61:
62: #endregion
63: }
Unity has the following methods to search for objects by different data types:
- GameObject.Find - Returns the first found active object with a given name.
- GameObject.FindWithTag - Returns the first active object with a given tag.
- GameObject.FindGameObjectsWithTag - Returns all active game objects with a given tag.
- Transform.Find - Searches child objects for a game object with a given name.
- Object.FindObjectsOfType - Returns all game objects with a component attached of the given type.
- Object.FindObjectOfType - Returns the first game object with a component attached of the given type.
Using Static Properties and Methods
Calling class (i.e. "static") methods and properties in c# is the same speed as instance properties and methods, which are the fastest ways to communicate between objects. The biggest disadvantage of using static methods rather than a singleton is that the code will likely need to be rewritten if ever there is a need for a second instance of a particular object with different settings.
1:
2: /// <summary>
3: /// Exposes static methods for other objects to communicate with.
4: /// </summary>
5: public class StaticLink : MonoBehaviour
6: {
7: /// <summary>
8: /// Draws the current update count.
9: /// </summary>
10: void OnGUI()
11: {
12: GUILayout.Label("UpdateCount=" + updateCount);
13: }
14:
15: /// <summary>
16: /// Called by other objects to increate updateCount.
17: /// </summary>
18: public static void IncrementUpdateCount()
19: {
20: updateCount++;
21: }
22:
23: /// <summary>
24: /// Number of times IncrementUpdateCount() has been called.
25: /// </summary>
26: private static int updateCount = 0;
27: }
28:
29: /// <summary>
30: /// Communicates with StaticLink through its static methods.
31: /// </summary>
32: public class OtherClass : MonoBehaviour
33: {
34: void Update()
35: {
36: StaticLink.IncrementUpdateCount();
37: }
38: }
Using Unity Messaging
Unity has a built-in system for passing messages between components on the same game object without references or strong typing. By using Component.SendMessage(), Unity attempts to call the specified method on all components attached to the same game object the component calling SendMessage().
This mechanism allows for argument passing, and by default prints an error if no component receives the message. In conjunction with GetComponent() or FindComponent(), messages can be passed between any two objects in the scene, albeit less efficiently than through more direct mechanisms.
1: /// <summary>
2: /// Same example as StaticLink, but using SendMessage() to
3: /// communicate instead. This requires both components to
4: /// be attached to the same object.
5: /// </summary>
6: public class MessageLink : MonoBehaviour
7: {
8: /// <summary>
9: /// Draws the current update count.
10: /// </summary>
11: void OnGUI()
12: {
13: GUILayout.Label("UpdateCount=" + updateCount);
14: }
15:
16: /// <summary>
17: /// Called via SendMessage() to increate updateCount.
18: /// </summary>
19: public void IncrementUpdateCount()
20: {
21: updateCount++;
22: }
23:
24: /// <summary>
25: /// Number of times IncrementUpdateCount() has been called.
26: /// </summary>
27: private static int updateCount = 0;
28: }
29:
30: /// <summary>
31: /// Communicates with MessageLink through SendMessage()
32: /// </summary>
33: public class OtherClass : MonoBehaviour
34: {
35: void Update()
36: {
37: SendMessage("IncrementUpdateCount");
38: }
39: }
The first class from the code sample above will call the method on the second class, if both are attached to the same game object. Behind the scenes, [I haven't decompiled Unity to confirm this, but I suspect that] .net reflection will be used to issue the method call to the other object.
For relatively rare calls, SendMessage() is unlikely to be the biggest bottleneck in your application, since you are likely doing millions of polygon operations every second.
Rick Strahl, one of the most prolific .Net bloggers and Microsoft MVP, has an article about the relative performance of using reflection for method calls in this article on his blog.
The bottom line is it's about 3.5x to 4x slower than a direct method call. If you're spending 0.001% of your time doing this, that's not a problem. If you start spending over 1% of your time doing this, you might want to consider an alternative messaging mechanism.
Downloads for this Article
- References.unitypackage - Package contains scenes using each of the scripts from this article
- Singleton.cs - full source code for the generic class in this example
- InstanceVsClass.cs - full source code for the generic class in this example
- SingletonExample.cs - full source code for the generic class in this example
- Tester.cs - full source code for the generic class in this example
- MyScript.cs - full source code for the generic class in this example
- MyOtherClass.cs - full source code for the generic class in this example
- AssignExample.cs - full source code for the generic class in this example
- OtherComponent.cs - full source code for the generic class in this example
- HybridLookupExample.cs - full source code for the generic class in this example
- StaticLink.cs - full source code for the generic class in this example
- StaticLinkOtherClass.cs - full source code for the generic class in this example
- SendMessage.cs - full source code for the generic class in this example
- SendMessageOtherClass.cs - full source code for the generic class in this example
Other Articles
The Unity Essentials Weekly blog is brought to you by GSD Software under our Unity Essentials brand name.
This product is presented to you free of charge, in the hope that spreading knowledge about Unity will lead to more development using Unity, which will lead to more projects for our company involving Unity, because we think it's the best thing since slice bread as far as 3D platforms go.
GSD Software is a Tempe, Arizona based company, specializing in web development, desktop software development, and virtual worlds.