Lets Make Unity Great Again
A tale of despair and finding one's path again
by
Cratesmith
(aka Kieran Lord)
Disclaimer
- The talk name was submitted when the idea of Trump running for president seemed funny rather than terrifying
- This talk is going to be very code heavy. But functioning source for everything covered here is available on github (https://github.com/Cratesmith/GcapTalk2016)
- This talk is about the working around what I find to be the most frustrating issues in Unity. It's going to be negative. So just to be clear despite what's coming up:
- I still use unity.
- It's awesome.
- It's still the engine I recommend for most projects
About me
- Founder of Cratesmith Games Assembly. A VR games company in Brisbane
- Been in the Australian games industry 10+ years
- Used unity about 6 or 7 of those
- Done games on: iOS, Android, ps2, ps3, 360, xbox, pc, custom tech, an arcade cabinet, gear VR, cardboard, oculus DK2.
- I hate two things
- Spaces as tabs (seriously)
- Bad workflows
So what's wrong with unity now?
Lets talk about the problems
Unity is a small team, small game engine that has (mostly) grown into one supporting larger projects.
This leads to issues with larger projects and larger team sizes that need workarounds.
Making unity work for non programmers
I take a lot of inspiration from old doom/quake editors. The game should be able to drop objects in and have them "just work".
Unity is a tool for building content. Artists & designers should be able to focus on that rather than trying to second guess if their scene is set up right.
Most unity games require one or more system objects in every scene.
A home for project wide settings
Unity's systems have project wide assets that are easily accessible in code. These mostly store project settings
Unity doesn't provide an easy way to set up your own settings in a similar way.
This may have been an early design decision. But on larger projects having ways to store "global config" is pretty crucial.
Models in prefabs
- Unity tutorials encourage developers to put models into prefabs alongside scripts.
- While this is a simple way to get things working, any time the model's structure changes. The prefab needs to be manually rebuilt.
- The alternative (building models on Start) means models only appear when the game is playing. Which makes life very difficult for level designers.
Actual unity tutorial sreencap
Behaviour singletons
- Behaviour singletons are the standard practice in unity but their initialization and execution order often create hard to identify problems.
- A common approach to manage this is to load a "system" scene. However this often creates two execution cases for the whole game. System loaded first (in builds), or level loaded first (in editor)
We've all made a project that looked like this.
Why these problems?
- There are MANY more issues with unity
- However these are the problems I've had to solve when making larger projects
- A small time investment to solve these issues pays off for multiple projects.
Unity will fix all that soon won't they?
probably... not
- Running out of potential new customers is forcing them to need a new approach to their business
- This forces them to focus on features that attract new markets (AAA features) or generate revenue (subscription services)
- Unity has an unprecedented number of developers using it.
... and WE'RE MOSTLY TO BLAME
Solving the kinds of problems this talk is about wont generate more money for Unity (at this time)
Competition might drive this to change in future, but at present that seems a ways off.
So for the time being... we need to find our own workarounds to this.
void Update()
{
gameObject.ಠ_ಠ();
}
So lets talk
Workarounds
Global settings
public partial class ScoreSettings : ResourceSingleton<ScoreSettings>
{
#if UNITY_EDITOR
[UnityEditor.MenuItem("Edit/Game Settings/Score Settings")]
public static void SelectSettings()
{
UnityEditor.Selection.activeObject = instance;
}
#endif
}
We also want our settings assets to behave and appear just like the unity ones.
- They need to appear in the menu (which is the easy part)
- There needs to be the only instance of that asset in the project, and it must always exist.
Global settings
public partial class ScoringSettings : ResourceSingleton<ScoringSettings>
{
[SerializeField] CoinPickup.Settings m_coinPickup;
public static CoinPickup.Settings coinPickup { get { return instance.m_coinPickup; } }
}
public class CoinPickup : MonoBehaviour
{
[System.Serializable]
public class Settings
{
int numPoints;
}
void OnTriggerEnter(Collider other)
{
var player = other.GetComponent<ActorPlayer>();
if(player!=null)
{
player.GivePoints(ScoringSettings.coinPickup.numPoints);
}
}
}
Using mario coins as an example. Each coin prefab really doesn't need to store the number of points. It's project wide.
So we want to add our coin related settings to a partial class that stores these kinds of things.
Resource Singleton
public abstract class ResourceSingleton<T>
: ScriptableObject where T:ScriptableObject
{
static T s_instance;
protected static T instance
{
get
{
LoadAsset();
if(!s_instance)
{
throw new System.ArgumentNullException(
"Couldn't load asset for ResourceSingleton "+typeof(T).Name);
}
return s_instance;
}
}
static void LoadAsset()
{
if(s_instance!=null) return;
if(Application.isPlaying)
{
s_instance = Resources.Load(typeof(T).Name) as T;
}
else
{
#if UNITY_EDITOR
var temp = ScriptableObject.CreateInstance<T>();
var monoscript = MonoScript.FromScriptableObject(temp);
ScriptableObject.DestroyImmediate(temp);
var scriptPath = AssetDatabase.GetAssetPath(monoscript);
var assetDir = Path.GetDirectoryName(scriptPath)+"/Resources/";
var assetPath = Path.GetFileNameWithoutExtension(scriptPath)+".asset";
s_instance = AssetDatabase.LoadAssetAtPath<T>(assetDir+assetPath);
#endif
}
}
}
- Static access singleton representing a single asset that lives in a specific Resources folder
- Keeping instance property protected is my preference. You can make it public.
- At asking for the instance will call Resources.Load (or AssetDatabase if not playing)
rS Builder
- Searches project for all ResourceSingleton classes on load/import/build/play
- If no asset exists for a ResourceSingleton it makes one.
- If the asset is in a different folder it moves it.
- Never deletes or overwrites RS assets so settings are safe
Settings overrides
So now we can store default settings. But quite often we have a case where prefabs will need to override those settings.
We create a ScriptableObject asset type that also stores our settings class
On our behaviour we now add a parameter that accepts that asset type. If it's set we get our values from there. Otherwise they come from the defaults.
SETTINGS OVERRIDES
[CreateAssetMenu(menuName="Settings/ActorPlayer")]
public class ActorPlayerSettings : ScriptableObject
{
public ActorPlayer.Settings settings;
}
public partial class GlobalDefaults : ResourceSingleton<GlobalDefaults>
{
[FormerlySerializedAs("m_playerSettings")]
[SerializeField] ActorPlayer.Settings m_defaultPlayerSettings;
public static ActorPlayer.Settings defaultPlayerSettings {
get {
return instance.m_defaultPlayerSettings;
}
}
}
[RequireComponent(typeof(PreviewModel))]
public class ActorPlayer : BaseMonoBehaviour, IPreviewModelSource
{
#region settings
[System.Serializable]
public class Settings
{
public GameObject playerModelPrefab;
public float moveSpeed = 1f;
}
[SerializeField] ActorPlayerSettings m_settingsOverride;
public Settings settings {
get {
return m_settingsOverride!=null
? m_settingsOverride.settings
:GlobalDefaults.defaultPlayerSettings;
}
}
#endregion
}
- Unity requires the settings asset to be in it's own file (must match typename)
- Everything else can be kept in the partial class pattern.
- Property for settings always returns a valid object (because RS's always exist)
- Can cache settings on Awake for more performance.
Why use Settings overrides
There's a few benefits to them that aren't immediately apparent:
- Share settings with other prefabs. (Even sharing overrides!)
- Complex MonoBehaviours can operate with default settings
- Settings are saved when play mode stops.
Pop up Settings Drawer
- Settings overrides have a problem in unity. They don't let you edit that data in place
- Simple fix: create a propertydrawer for your settings assets that spawns a popout window that draws the editor for your settings asset.
- Alternatively use a 3rd party inspector window. Advanced Inspector is the one I most often hear about. (though haven't tested myself)
POP UP SETTINGS DRAWER
[CreateAssetMenu(menuName="Settings/ActorPlayer")]
public class ActorPlayerSettings : ScriptableObject
{
public ActorPlayer.Settings settings;
}
#if UNITY_EDITOR
[UnityEditor.CustomPropertyDrawer(typeof(ActorPlayerSettings))]
public class SettingsDrawer : PopupEditorDrawer {}
#endif
Setting up the propertydrawer is very simple but has to be done for each asset type.
Wont go into source for propertydrawer in the talk but it's provided in the downloadable examples.
Demo!
No more Models in prefabs
- Don't add models into your scripted/configured prefabs. Instead instantiate them on start.
-
Use pooling with preallocation to ensure that this doesn't cause allocation / performance spikes
- Unfortunately now our objects don't appear until the game plays... lets fix that
Preview Models
- You can instantiate a copy of the model in editor and destroy it whenever it's being saved to a prefab/play pressed/saved to scene
- Objects with HideFlags.DontSave still get written to prefabs. Don't use that.
- Destroy the model instance using ISerializationCallbackReceiver.OnBeforeSerialize() to prevent it appearing in prefabs/scenes.
- Destroy the model in UnityEditor.Callbacks.PostProcessScene in the scene when pressing play/building
Preview models
- I've included my implementation in the downloadable source.
- RequireComponent the PreviewModel class and implement IPreviewModelSource
- Can go through the impelmentation after the talk if people are interested
Models in prefabs
If you're building your own implementation.
- Making the models not selectable/visible in heirachy and adding a gizmo icon instead is a good idea.
- Test your model preview code thoroughly before adding it to the project at large. Bugs in Callbacks and ISerializationCallbackReceiver are very prone to crashing unity
- If your game has simple meshes for most things Gizmos.DrawMesh can be used to build a simpler implementation than full editor-only prefab instancing
DEMO
A replacement for behaviour singletons
Singletons are usually used to represent a manager that should either be global or belong to a particular scene.
They're used because static access makes them easy to use. Not because a "single instance only" rule fits the task at hand.
Lets make a replacement
Manager
using UnityEngine;
using System.Collections;
public abstract class Manager : ScriptableObject
{
// called by container
public virtual void OnAwake() { }
public virtual void OnStart() { }
public virtual void OnUpdate() { }
public virtual void OnFixedUpdate() { }
public virtual void OnLateUpdate() { }
// OnDestroy called by unity
public virtual void OnDestroy() { }
public ManagerContainer container { get; set; }
public bool enabled { get; set; }
}
We'll design our managers to be ScriptableObjects. This allows us to save their configuration in assets
Manager
using UnityEngine;
using System.Collections;
[CreateAssetMenu(fileName="PlayerManager.asset", menuName="Managers/Player Manager")]
public class PlayerManager : Manager
{
public ActorPlayer playerPrefab;
public ActorPlayer currentPlayer {get;set;}
}
Here's an example manager.
So far we can create assets that store settings. But we don't have a way to use it at runtime yet.
We need something to manage the managers. And ManagerManager is a terrible name.
Manager COntainer
using UnityEngine;
using System.Collections;
public class ManagerContainer : MonoBehaviour
{
[SerializeField] Manager[] m_managerPrefabs = new Manager[0];
List<Manager> m_managerInstances = new List<Manager>;
protected void Awake() {
for(int i=0;i<m_managerPrefabs.Length; ++i)
{
if(m_managerPrefabs[i]==null) continue;
m_managerInstances.Add(Instantiate(m_managerPrefabs[i]));
}
for(int i=0;i<m_managerInstances.Count; ++i)
{
var manager = m_managerInstances[i];
manager.enabled = true;
manager.OnAwake();
}
}
protected void OnDestroy() {
for(int i=0;i<m_managerInstances.Count; ++i) {
Destroy(m_managerInstances[i]);
}
}
protected void Update() {
for(int i=0;i<m_managerInstances.Count; ++i) {
m_managerInstances[i].Update();
}
}
protected void FixedUpdate() {
for(int i=0;i<m_managerInstances.Count; ++i) {
m_managerInstances[i].FixedUpdate();
}
}
protected void LateUpdate() {
for(int i=0;i<m_managerInstances.Count; ++i) {
m_managerInstances[i].LateUpdate();
}
}
}
^ it's the best name I could think of
But we still need a way to access them.
Handles the full lifecycle for our managers
A basic Managercontainer for example
GET MANAGER
- We need access to our managers without using a static accessor.
- MonoBehaviour type to add a local GetManager method.
public class ActorPlayer : BaseMonoBehaviour
{
ActorPlayerManager m_playerManager;
void Awake()
{
m_playerManager = GetManager<PlayerManager>();
}
}
- We're using a derived MonoBehaviour base class to add a local GetManager method.
public abstract class BaseMonoBehaviour : MonoBehaviour
{
public T GetManager<T>() where T:Manager
{
var scene = gameObject.scene;
return ManagerContainer.GetManager<T>(scene);
}
}
A monobehaviour caching a manager
We'll explain the scene parameter later.
Dependencies
[CreateAssetMenu(fileName="UnlocksManager.asset", menuName="Managers/Unlocks Manager")]
public class UnlocksManager : Manager
{
ActorPlayerManager m_playerManager;
public override void OnAwake()
{
m_playerManager = GetManager<PlayerManager>();
}
}
Calling GetManager on a Manager will fail with a "Dependency required" error.
Unless you specify that the manager depends on it using the IManagerDependency interface.
[CreateAssetMenu(fileName="UnlocksManager.asset", menuName="Managers/Unlocks Manager")]
public class UnlocksManager : Manager, IManagerDependency<PlayerManager>
{
ActorPlayerManager m_playerManager;
public override void OnAwake()
{
m_playerManager = GetManager<PlayerManager>();
}
}
Dependencies and sorting
- Dependencies solve a lot of our problems
- We can sort our manager prefabs before instantiating them.
- This gives us guaranteed correct initialisation/update order maintaining a hand sorted list of managers or priority numbers
- Cyclic dependencies for update/init order generate warnings at runtime. Not something you find out on build night.
SORTING
- Dependency sorting is quite easy.
- Do a depth first search of all the dependencies of all your manager prefabs
- Once you've visited all dependencies for an item, add it to the "sorted" list.
- This also lets you check for cyclic dependencies.
auto-construction
- Missing managers will automatically be instantiated
- They might be requested via GetManager<T>
- Or when another manager is being constructed they're listed as a dependency.
- ManagerContainers are Autoconstructed too!
Multi-scene managers
- Sometimes you need a Manager that's treated as a DontDestroyOnLoad object.
- The best solution to this I have found is to have a ManagerContainer for each scene and a non-scene "global" ManagerContainer.
- When calling GetManager, the scene MC is checked for it first (remember the scene parameter?), if the manager wasn't found the global MC is checked.
- The Global MC is executed first, then all scene MCs.
- This simplifies game systems but makes the manager implementation much more complex.
DEMO
More demo time
Questions?
Lets Make Unity Great Again
By cratesmith
Lets Make Unity Great Again
- 4,299