Tips For Working With Unity #4: Coding and General Tips
filed in Code, Open Development, Unity on Sep.16, 2010
Editor’s note: This is part 4 in a 5 part series outlining our experiences with Unity & Unity iPhone. Be sure to check out Part 1, Part 2, and Part 3.
Unity is like a big, old playground. It’s easy to play with all of the toys and have fun, but you can very easily hurt yourself. Unity’s biggest advantage and biggest flaw is that it’s very easy to get up and running very quickly. This is a flaw because it’s almost too easy, to the point where it’s easy to just start building your game, without thinking about the best way to organize it.
So, without further ado, let’s get started:
Tip #1: Use Singletons and Statics wherever you can
Singletons are a very handy construct, where you declare that only one instance of your class is ever allowed, and you return it whenever somebody asks for it. The Unity code for it will look something like this:
private static SingletonComponent This = null;
public static SingletonComponent Instance
{
get
{
if (This == null)
{
This = FindObjectOfType(typeof(SingletonComponent)) as SingletonComponent;
}
return This;
}
}
In order for this code to work, you must add the code to a Monobehaviour (in this case, I’ve called it SingletonComponent, but you’ll want to change it to whatever the name of your component is), and add the component to a GameObject. The “get” for the static Instance member does a lazy-binding lookup for the first object it finds of the type, and returns that. You can now access the class via code like:
SingletonComponent.Instance.Foo();
In some cases, though, you don’t want or need your object to live in a GameObject as a component. In those cases, use a static class, like so:
public static class StaticClass
{
static StaticClass()
{
// Static constructor is called before the first access to the static class
}
}
No extra setup is required, and you can call any methods in this class using:
StaticClass.Foo();
The biggest difference between the two is that the Singleton is actually an instance, while the static class is not (and requires all of its member methods to be static as well). These are both handy constructs that we’ve used all over the place in both Ace Attack and the Remnant demo.
Tip #2: Use generic versions of functions and classes wherever you can
.NET 1.1 was a fine piece of work, but one of the biggest features added in .NET 2.0 was generics, which, similar to templates in C++, will automatically encode type data. Thus, your code that would ordinarily look like this:
MyComponent c = GetComponent(typeof(MyComponent)) as MyComponent;
Becomes:
MyComponent c = GetComponent<MyComponent>();
This applies to all of the Unity functions, as well as the collections objects, such as List<>, Dictionary<>, and everything else in the System.Collections.Generic namespace. You can also create your own generic classes and methods, which can save you lots of work.
Tip #3: Build and use a debug console
I spent an afternoon building a debug console using the Unity GUI system, with tab completion and command history. Each command calls a delegate in a static class. It was one of the handiest things I ever created, and so, to save you having to go through the same pain, I’ll include it here. It’s by no means a fully-featured console, but it does do quite a bit.
// --------------------------------------------------------------------------------
// Copyright (C)2010 BitFlip Games
// Written by Guy Somberg guy@bitflipgames.com
//
// I, the copyright holder of this work, hereby release it into the public domain.
// This applies worldwide. In case this is not legally possible, I grant any
// entity the right to use this work for any purpose, without any conditions,
// unless such conditions are required by law.
// --------------------------------------------------------------------------------
using UnityEngine;
using System;
using System.Collections.Generic;
public class DebugConsole
{
private string ConsoleText = "";
private bool displayConsole = false;
public bool DisplayConsole
{
get { return displayConsole; }
set
{
displayConsole = value;
if (!DisplayConsole)
{
ConsoleText = "";
PreviousCommandIndex = -1;
}
}
}
private List<string> PreviousCommands = new List<string>();
private int PreviousCommandIndex = -1;
private string AutoCompleteBase = "";
private List<string> AutoCompleteOptions = new List<string>();
private int AutoCompleteOptionsIndex = -1;
private ConsoleCommands.ConsoleCommand GetCommand(string CommandText)
{
foreach (ConsoleCommands.ConsoleCommand Command in ConsoleCommands.Commands)
{
if (Command.CommandText.Equals(CommandText, StringComparison.CurrentCultureIgnoreCase))
{
return Command;
}
}
return null;
}
private void ExecuteCommand(string CommandText)
{
CommandText = CommandText.Trim();
PreviousCommands.Add(CommandText);
string[] SplitCommandText = CommandText.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
ConsoleCommands.ConsoleCommand Command = GetCommand(SplitCommandText[0]);
if (Command != null)
{
Command.Callback(SplitCommandText);
}
}
private void AutoComplete()
{
string AutoCompleteText = AutoCompleteBase.Trim().ToLower();
if (AutoCompleteOptionsIndex < 0)
{
AutoCompleteOptions.Clear();
foreach (ConsoleCommands.ConsoleCommand Command in ConsoleCommands.Commands)
{
if (Command.CommandText.ToLower().StartsWith(AutoCompleteText))
{
AutoCompleteOptions.Add(Command.CommandText);
}
}
AutoCompleteOptions.Sort();
if (AutoCompleteOptions.Count > 0)
{
AutoCompleteOptionsIndex = 0;
PreviousCommandIndex = -1;
}
}
else
{
if (AutoCompleteOptions.Count > 0)
{
AutoCompleteOptionsIndex = (AutoCompleteOptionsIndex + 1) % AutoCompleteOptions.Count;
}
else
{
AutoCompleteOptionsIndex = -1;
}
}
if (AutoCompleteOptionsIndex >= 0)
{
ConsoleText = AutoCompleteOptions[AutoCompleteOptionsIndex];
}
}
private void ClearAutoComplete()
{
AutoCompleteBase = "";
AutoCompleteOptions.Clear();
AutoCompleteOptionsIndex = -1;
}
public void OnGUI()
{
if (DisplayConsole)
{
string BaseText = ConsoleText;
if (PreviousCommandIndex >= 0)
{
BaseText = PreviousCommands[PreviousCommandIndex];
}
Event CurrentEvent = Event.current;
if ((CurrentEvent.isKey) &&
(!CurrentEvent.control) &&
(!CurrentEvent.shift) &&
(!CurrentEvent.alt))
{
bool isKeyDown = (CurrentEvent.type == EventType.KeyDown);
if (isKeyDown)
{
if (CurrentEvent.keyCode == KeyCode.Return || CurrentEvent.keyCode == KeyCode.KeypadEnter)
{
ExecuteCommand(BaseText);
DisplayConsole = false;
return;
}
if (CurrentEvent.keyCode == KeyCode.UpArrow)
{
if (PreviousCommandIndex <= -1)
{
PreviousCommandIndex = PreviousCommands.Count - 1;
ClearAutoComplete();
}
else if(PreviousCommandIndex > 0)
{
PreviousCommandIndex--;
ClearAutoComplete();
}
return;
}
if (CurrentEvent.keyCode == KeyCode.DownArrow)
{
if (PreviousCommandIndex == PreviousCommands.Count - 1)
{
PreviousCommandIndex = -1;
ClearAutoComplete();
}
else if (PreviousCommandIndex >= 0)
{
PreviousCommandIndex++;
ClearAutoComplete();
}
return;
}
if (CurrentEvent.keyCode == KeyCode.Tab)
{
if (AutoCompleteBase.Length == 0)
{
AutoCompleteBase = BaseText;
}
AutoComplete();
return;
}
}
}
GUI.SetNextControlName("ConsoleTextBox");
Rect TextFieldRect = new Rect(0.0f, (float)Screen.height - 20.0f, (float)Screen.width, 20.0f);
string CommandText = GUI.TextField(TextFieldRect, BaseText);
if (PreviousCommandIndex == -1)
{
ConsoleText = CommandText;
}
if (CommandText != BaseText)
{
ConsoleText = CommandText;
PreviousCommandIndex = -1;
ClearAutoComplete();
}
GUI.FocusControl("ConsoleTextBox");
}
}
}
public static class ConsoleCommands
{
public delegate void Command(string[] Params);
public class ConsoleCommand
{
public ConsoleCommand(string CommandText, string HelpText, Command Callback)
{
this.CommandText = CommandText;
this.HelpText = HelpText;
this.Callback = Callback;
}
public string CommandText;
public string HelpText; // Currently not used
public Command Callback;
}
public static ConsoleCommand[] Commands = new ConsoleCommand[] {
// Fill this with your commands:
new ConsoleCommand("MyCommand", "This is the help text for MyCommand", MyCommandDelegate),
};
public static void MyCommandDelegate(string[] Params)
{
// Do stuff
}
}
public class DebugManager : MonoBehaviour
{
private DebugConsole DebugConsole = null;
public bool ConsoleEnabled
{
get { return (DebugConsole != null) ? DebugConsole.DisplayConsole : false; }
}
void Awake()
{
DebugConsole = new DebugConsole();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (DebugConsole.DisplayConsole)
{
DebugConsole.DisplayConsole = false;
}
}
else if (Input.GetKeyDown(KeyCode.Tab))
{
DebugConsole.DisplayConsole = true;
}
}
void OnGUI()
{
DebugConsole.OnGUI();
}
}
Tip #4: Unity supports threading, but isn’t thread-safe
You are welcome, in Unity, to create a thread using the System.Threading namespace. However, be very careful that you do NOT do anything with a GameObject or any components of GameObjects in a separate thread. Unity will spit out some incomprehensible error message, and may actually crash on you. Unity won’t manage your threads, though, so be sure to shut them down in OnApplicationQuit().
In order to do work on a separate thread, you will have to copy off the pertinent data, work on that, and then call a callback on the main thread once it’s complete. This will require some diligence on your part by using Critical Sections, Mutexes, or other threading constructs to lock the data as it gets copied across threads, but once it’s set up, it’s pretty easy to use.
Note that, while you cannot work on any GameObject-related stuff, you can definitely perform operations on Quaternions, Matrices, Vectors, Bounds, and other mathematical constructs.
Tip #5: Do NOT, under any circumstances, change any source code while Unity is playing your game
Unity WILL crash, or, at the very least, be VERY unhappy with you.
Tip #6: Use iTween. It’s very handy
iTween is a tweening library that is built for Unity. It has the ability to smoothly transition values over time, with various types of curves. While it does have a few issues and limitations, we used it very heavily on both our Remnant demo and in Ace Attack to create smooth animations and fades.
Tip #7: Unity is good practice for “printf debugging”
Because Unity does not support breakpoints or any sort of debugger, one of the things that I found myself getting a lot of practice in was so-called “printf-debugging”, where you put up a set of debug logs to tell you what’s going on, and then further and further refining the data that’s displayed, and the locations that it’s displayed, until you track down the problem.
Tip #8: The infinite loop is your bane
While the Unity editor is generally fairly stable (modulo the odd crash), the kiss of death for Unity is the infinite loop. If, through accident or malice, your code gets into an infinite loop, Unity will lock up and refuse to respond to any input. Your only recourse is to forcibly crash it with Task Manager, and hope that you didn’t lose too much work.
Tip #9: Use Only one monitor
Unity allows you to detach and dock its sub-windows in any configuration you please. However, a combination of some voodoo magic that Unity does, along with a latent bug in some driver software somewhere, will cause a very frequent blue screen crash if you have your Game screen and Editor screen on different monitors. Moving them both onto one monitor fixes the issue.
Tip #10: System.Serializable will make your structures editable
The Unity editor will automatically display and allow you to edit any public member variables that it knows how to create an editor for. Sometimes, you want to create a structure which is displayed in the editor, but if you just create the struct or class and create a public member variable of that type, Unity will not display it. The way to make Unity display it is to use the [System.Serializable] attribute thus:
[System.Serializable]
public class MyClass
{
public int Foo;
}
The preceding tips are general rules of thumb or quick tips that we found to be very helpful during our development. Take them with as big a grain of salt as you require. Next time, I’ll wrap up this series with a set of tips about integrating Unity Free with Unity iPhone.
November 1st, 2010 on 9:12 pm
[...] For Working With Unity #2: Asset Workflow – Tips For Working With Unity #3: Large Sub-Projects – Tips For Working With Unity #4: Coding and General Tips – Tips For Working With Unity #5: Unity [...]
December 27th, 2010 on 11:08 pm
[...] Coding and General Tips [...]