EECS 494 – Game Design and Implementation – Unity Guides

Unity Gameplay Architecture Tips

  1. The Inspector, and making GameObjects / Resources available to scripts.

    One of Unity's defining features is its Inspector (the big pane on the right side of this image).

    Unity Inspector

    While the game is being played, certain properties of every object can be edited during runtime (!). This is useful for finding, for example, just the right speed for player movement, or Just the right amount of energy / mana. All of this without needing to restart the game or recompile. Very powerful.

    In the above screen, you can see that the player_controller component makes the following fields available in the inspector...

    • Sprinting
    • Energy
    • Gravity
    • many others, etc

    To make fields like these accessible, a component (also known as a script) must simple create public variables, like so...

    public class player_controller : MonoBehaviour {
    	public bool Sprinting = false;
    	public float Energy = 1.0f;
    	public float Vector2 inputs = Vector2.zero;
    	// etc
    
    	void Start() { }
    	void Update() { }	
    }

    Note how the type of the public variable determines how it shows up in the inspector. bools are checkboxes, while floats are values, while Vector2s are pairs of values.

    But what if we want to interact with particular GameObjects already in the scene?

    Use cases for this might be a missile GameObject which chases the player. The Missile component needs a reference to the player so it can know where it is and follow it! This is very simple. Simply create a public GameObject variable...

    public class Missile : MonoBehaviour {
    	public GameObject player_object_ref;
    
    	void Update() {
    		// move towards player_object_ref.transform.position
    	}
    }

    ...Then, place this component on your missile GameObject. You'll see this player_object_ref field in the inspector under the Missile component for the missile GameObject. It will be null, however! So click on your player GameObject in the scene hierarchy, and drag it into the field! It will no longer be null, and your Missile component will now have a reference to the player object!

    This is a simple way to make resources / objects of all kind easily available to the code in your components. You can make other GameObjects available, you can make audio clips available (so you can play them – just drag from your project pane into the inspector), text files, etc.

  2. Making Data Accessible Throughout Your Codebase

    It will be convenient to make certain pieces of data available throughout your codebase. For example, consider a high-score counter that must be accessible and writable from multiple places in your codebase (maybe a high-score display class needs to read it, maybe a coin-class needs to write it, etc).

    You can accomplish this very quickly via a static global. Consider the following class...

    public class GameSessionData {
    	public static int high_score = 0;
    	public static int lives = 3;
    	public static int game_title = "My First 494 Game!"
    }

    These data members persist across scene transitions, and are accessible throughout your codebase like so...

    public class SomeTestingComponent : MonoBehaviour {
    	void Start() {
    		print(GameSessionData.high_score);
    		GameSessionData.game_title = "SUPER First 494 Game!";
    	}
    }

    Global, easily accessible data is typically discouraged, in that it turns a codebase into spaghetti if over-indulged in. It is fast, however, and it can be made safer via...

  3. The Singleton Pattern

    Static data, as shown above, can be useful for maintaining game-global data. However, it is rarely useful for maintaining game-global systems. This is because static data cannot receive Update(), Start(), Awake() function calls like a GameObject can, unless the static data is part of a GameObject class, like so...

    public class GameSessionData : MonoBehaviour {
    	public static int high_score = 0;
    	public static int lives = 3;
    	public static int game_title = "My First 494 Game!"
    	void Start() { }
    	void Update() { print("current_lives: " + lives.toString()); }
    }

    Because the static data is now part of a class derived from MonoBehavior, it may now be packaged with the automatically-called Start() and Update() functions. With the addition of these functions, an isolated, self-sustaining gameplay system may be constructed.

    But there's a problem!

    For these MonoBehavior functions (Start(), Update(), etc) to be executed, this component must be attached to a GameObject in the scene. What happens if that GameObject gets destroyed? The system stops, dead in its tracks! If the scene transitions, this object is guaranteed to be destroyed! More than that, we need to ensure that only one of these objects exists in the game (We have no need for more than one GameSessionData manager!).

    We need this object to survive scene transitions – to persist across scenes.

    We can accomplish this in the following way, via the Singleton pattern.

    public class YAudioManager : MonoBehaviour {
      private static YAudioManager instance = null;
      
    	void Awake () {
    		if (instance != null && instance != this) {
    			Debug.LogError ("Destroying Object: Instance already exists.");
    			Destroy (gameObject);
    			return;
    		}
    		else
    			instance = this;
    
    		DontDestroyOnLoad (gameObject);
    
    		// ... Other Initialization Code
            }
    }

    Here, we use a private static data member to decide whether an object of this type has ever been created before. If there is currently an object of this type (the reference is stored in the instance member), then we know we must destroy ourselves and return, otherwise the singleton pattern is violated (because there would then be two YAudioManagers).

    If the instance member is null, we know that we are the first object of this type to be instantiated, and we claim the instance as ours by storing a reference to ourself in it. This causes any future instantiations of YAudioManager to know we exist, and they will destroy themselves, preserving the Singleton pattern.

    The final DontDestroyOnLoad(gameObject) tells Unity that this GameObject shouldn't be destroyed during a scene transition, which means this object will survive until the end of the game! (assuming none of our other code deliberately calls Destroy() on it).

    With this setup, we have singleton gameplay systems which never get destroyed during the life of the game, are capable of updating and initializing themselves (via Update() and Start()), and provide nice centralized APIs and data that will empower our highly-distributed set of components.

    Moral of the story: It's nice to have both powerful centralized managers, and loose, simple, distributed components that use them (There's a strong analogy to be made here with the US gov and its balance of Powerful centralized agencies and the freedom of local govs / individual people).

Unity Performance Tips

  1. Avoid Premature Optimization

    Don't optimize until you need to. If you begin to experience lag, unresponsive controls, or periodic, annoying frame-hitches, begin optimizing with the following tips...

  2. The Unity Profiler

    Unity Profiler

    Accessible via Window -> Profiler, the Unity Profiler is a convenient and powerful tool for diagnosing performance problems. Take some time to familiarize yourself with it.

  3. Specify Your Desired Framerate Via Application.targetFrameRate (and Friends)

    The framerate you target (60fps or 30fps) will determine how much time your game has to process everything. Choosing 30fps will get you a slightly less smooth experience, but will make it easier to prevent any annoying hitches from occurring when frames take too long. Learn more about specifying your target framerate here.

  4. Gfx.WaitForPresent Suggests a GPU-Bounded Frame.

    If Gfx.WaitForPresent is consuming substantial amounts of time, your computer's GPU is struggling to keep up. You should attempt to reduce your shader and effect usage.

  5. Minimize the creation of new objects

    Unity's C# scripting environment is garbage collected, and this collection will consume large amounts of time during the frames in which it occurs. To prevent annoying hitches and laggy, choppy frames, minimize the creation of new objects. Note that the creation of new structs, such as Vector3s, does not contribute to garbage collection. Garbage generation may be tracked in the GC Alloc column of the profiler. Keep this value as small as possible.

  6. GetComponent<T>(), Find(), Etc Are Slow.

    If possible, be sure to use these functions in a script's Start() or Awake() function, rather than in its Update() or FixedUpdate() functions. Store a reference to the objects / components you need, so you don't have to invoke these expensive functions every frame.

  7. Avoid Creating Large Scenes.

    Large scenes with 100s – 1000s of objects will put a burden on your computer. If you're smart about your transitions, you should be able to break larger areas up into a bunch of highly-performant smaller areas. For keeping data / objects alive across scene transitions, try the singleton pattern.

Unity FAQ

  1. I'm lost / I have no idea what I'm doing.

    Check out 494 Quest – a small, entirely complete game. It's source code is included. Feel free to explore how it accomplishes things, from controls, to collisions, to scene transitions, to enemy AI, etc.

  2. For Controller Support

    Try some InControl tutorials.

  3. Unity has an update! Should I update?

    Unless it has something you need, or your current version of Unity is buggy / unstable, I wouldn't bother. Unity has a reputation for releasing broken versions, and once your project is converted to the new version, you can't go back. Of course, if you're using version control as required by the course, your short-term risk is limited.

  4. What tools should I use for content creation? Art, music, etc?

    See my resource list and Austin's resource list.

  5. Can I purchase assets from the Asset Store?

    You are authorized to purchase the following assets: InControl

    For anything else, check with us in advance or it will be considered a violation of the honor code. Please see Collaboration and Honor Code in the syllabus for further details.

  6. Can we collaborate with other teams?

    Not on the tutorials or on project 1. Following project 1, everyone will be working on different games, and collaboration will be encouraged within reason.

  7. Can we collaborate with others outside the class?

    Not on the tutorials or on project 1. Following project 1, feel free to enlist others for help with art assets, music, etc – things that don't heavily impact the game's design or programming. Post to DeviantArt – it's swarming with people who will make art for you for cheap. (Be sure to tell them you're students!)

    Be sure your resources are allowed and cite them properly or it will be considered a violation of the honor code. Again, see Collaboration and Honor Code in the syllabus for further details.

  8. For all other questions...

    Try Google or Stack Overflow. If they fail you, please come to office hours.