Large Project Organisation

Checked with version: 2018.1

-

Difficulty: Beginner

Unity offers numerous ways to organize your files or how to approach making your game. This freedom allows for fast prototyping but comes at the price of potentially making the project slower to open, to navigate or develop when it becomes bigger. This guide will give pointers and hints on how to mitigate that and tips to take into account when starting your project.

Before starting

Before even starting working on your Unity project, there are a couple of steps to think about.

Source control

Using source control is very important for any project. It provides safeguards to minimise mistakes and simplifies working with multiple people. Unity has a source control service called Collaborate that is seamlessly integrated in the Editor.

If you are using an external source control such as Git, Svn, Mercurial or Perforce you will need to configure a couple of settings:

  • In your source control of choice, exclude the folders Library, Temp, obj as they are not needed to open the project on another computer and would just clutter the repository and use storage needlessly

  • In the Unity Editor, go to Edit > Project Settings > Editor and set:

    • Version Control to Visible Meta Files: This will place meta files next to your assets, in which parameters and import settings are stored. This allows you to share import settings and references by including these files in version control. Otherwise, these settings are stored in the Library folder and cannot be shared between members of the team.

    • Asset Serialization to Force Text: make all unity files (ScriptableObjects, Scenes, Prefabs and many others) use a text representation instead of a binary one. This allows source control to just store modifications to these files when they change, rather than a whole new copy of the file, and allows you to manually merge any conflicts (Note : on very big projects, the additional time needed to import text asset versus binary asset could create a quite noticeable difference. Most projects will not reach that size threshold where the benefits of text over binary are overcome by time of import, so setting to text in the beginning is still recommended)

  • Note for Perforce Users: In the Perforce Workspace settings, use "revert unchanged files". This will prevent unnecessary commits of files that have not changed.

Scales and standards

Decide on standards for the project so everyone working on the project uses the same ones.

  • Units : decide what one unit in Unity is, otherwise models or sprites will not be created using a unified scale. Introducing different scaling for different objects can lead to all sort of problems.

    • For most 3D games that use Rigidbody components, use one unit = one meter, as the physics system requires that for proper simulation

    • For 2D games that use Rigidbody components, define a "base sprite size" that will be equal to one meter and one unit. For example 32px = 1 meter and 1 unit. Set all Sprites Pixel Per Unit to that value, so your character sprite that is 64px tall, with a PPU of 32 will be two units therefore two meters tall for the physics system.

  • Naming: your assets should all follow a naming convention. It can be appending suffixes (An example for textures would be N for normal map or _AO for Ambient Occlusion map) or prefixes (Such as LowRes or HighRes_ on variations) . It will allow to filter through search and also can help writing custom importer setting value automatically.

Project Structure

Folders

Use the Project View in the Unity Editor to organize your assets. Unity keeps a meta file (if enabled, see Source control) next to each asset file that stores the asset’s import settings. That means you should avoid moving files through your OS file explorer, always move them from inside the Unity editor as it will take care of moving everything properly. If you need to move them outside the editor, do not forget to move the meta file with them, otherwise you will lose references and import settings.

To keep the project easy to navigate, avoid placing files in the root Assets folder. Use subfolders. How you organize those subfolders is generally decided by your projects but the two main ways to do it are:

  • A folder for each type of asset and subfolders in them per objects, zones (For example Assets/Materials, Assets/Prefabs, with subfolders Assets/Material/Level1 or Assets/Prefabs/Enemies)

  • A folder per objects or zones (Such as Assets/Level1/Enemies/Archer, Assets/Shared/UI, Assets/Forest/Trees) with all assets related to those in the folders (Assets/Forest/Trees/BigTree.fbx, Assets/Forest/Trees/Tree.mat, Assets/Forest/Trees/Tree_Bark.jpg).

Note that the second way will help if you plan on using Asset Bundles as you can then create bundles per content themes just by adding the parent folder to a specific bundle and easily splitting your related assets through different bundles.

Prototype folders

Keep everything that is related to a prototype or placeholder in a separate folder hierarchy, and avoid cross referencing between those folders and folders which contain shippable assets/code. For example, your Character prefab in the "normal" file hierarchy should not reference a test material in your Prototype folders. Create a prototype character prefab that would reside in the Prototype folders and never use that prefab in a non prototype scenes (you could even write editor scripts that check references used by objects in the prototype folder to be sure that this rules is enforced. )

Code snippet

[MenuItem("Tools/Check Prototype References")]
static void CheckprototypeReferences()
{
    string prototypePath = EditorUtility.OpenFolderPanel("Choose folder", Application.dataPath, "");

    if (prototypePath != "")
    {
        var files = System.IO.Directory.GetFiles(prototypePath, "*.*", System.IO.SearchOption.AllDirectories);

        //make it relative to asset folder so we can compare easily later
        prototypePath = prototypePath.Replace(Application.dataPath, "Assets");

        foreach (var s in files)
        {
            string relativePath = s.Replace(Application.dataPath, "Assets");

            //ignore metafile
            if (!relativePath.EndsWith(".prefab") && !relativePath.EndsWith(".asset"))
                continue;

            var assets = AssetDatabase.LoadAllAssetsAtPath(relativePath);

            foreach (var a in assets)
            {
                SerializedObject obj = new SerializedObject(a);
                var prop = obj.GetIterator();

                while(prop.NextVisible(true))
                {
                    //monobehaviour have exposed Icon & script we do not want to test
                    if (prop.propertyType == SerializedPropertyType.ObjectReference 
                        && prop.displayName != "Icon" && prop.displayName != "Script")
                    {
                        var referencedAssetPath = AssetDatabase.GetAssetPath(prop.objectReferenceValue);

                        //ignore built-in resources, only look if we are linking stuff from the projects
                        if (!referencedAssetPath.Contains("Assets/"))
                            continue;

                        if(!referencedAssetPath.Contains(prototypePath))
                        {
                            Debug.Log("External referenced on " + relativePath + ": " + prop.displayName + " to " + referencedAssetPath, a);
                        }
                    }
                }
            }
        }
    }
}

Resources

Avoid using the Resources folder as much as possible. Resources is a special folder name in Unity where assets placed can be loaded dynamically using Resources.Load in code. Some games will require dynamically loaded assets, but try to minimize the assets you place in there as much as possible, as Unity cannot strip any unused assets in there (it cannot tell if you load it via code or not) so everything in that folder will be included in the final build.

As you load assets in code with the path & filename, it also makes refactoring (moving or renaming files) harder and more error prone, as it requires you to manually change the path or name in code.

Prefer:

  • To use a direct reference (public variable in Monobehaviour) to assign through the editor. This will also support moving or renaming of files.

  • If you really need or want to use Resources.Load, use the Resources.LoadAsync version of the function, which will not block execution (on some devices, Loading can take a long time). This function returns an AsyncOperation that you can either poll every update to check if it's finished, or use the completed event like this:

Code snippet

ResourceRequest request = Resources.LoadAsync("cube");

        request.completed += operation =>

        {

            ResourceRequest op = operation as ResourceRequest;

            Instantiate(op.asset);

        };

Assets

Avoid source assets in the Assets folder

Unity offers a way to import source files from a range of different software (Some examples are Photoshop, 3DS Studio Max or Blender) directly. If used for a quick test or prototyping, never commit those to your source control or keep them in the Assets folder past the prototyping or test phase because:

  • Those files are usually bigger than exported files. Bigger files lead to cluttering the source control history, and every time you modify one, the source control will store the new version, leading to an explosion of the size of your repository.

  • Those files rely on the software they belong to being installed on the machine opening the Unity project. Any collaborator without the right software installed or a build machine will not be able to open the project.

It is preferable to store those files either in a separate source control software (maybe more suited for binary files) or, if you cannot do that, store them outside of the the Assets folder, so Unity does not import them, and exports your assets to a Unity compatible format to place in the Assets folder and use in game.

Keep assets size small

Try to keep your asset size to the smallest possible (a bad example would be to export a 2048x2048 texture for a coffee mug on a table). You can reduce the size of a texture in Unity import settings but that will not reduce the import time of the texture, which makes the initial setup on fresh versions of the project way longer.

Use Compressed Format

Test locally with whatever format, but once decided, use compressed format for audio and image files. .png/.tga/.tiff for images (or even .dds if your image software supports exporting those), .ogg for audio. Unity will import (or simply use as is) way faster, cutting down the original import size of the project.

Assembly Definition Files

Assembly Definition Files (ADF) compile all scripts in a folder and its subfolders into a separate DLL. This reduces compilation time, by splitting it to only the dll to which the changed script file belongs. For example, if you have an ADF for all your Enemies scripts, changing the enemy scripts will not recompile all your helpers, player, game systems or UI scripts but rather just the scripts related to Enemies.

Define ownership

In teams of more than one, define ownership of zone/assets/features. Some assets like scenes or prefabs do not handle simultaneous changes by multiple people very well, creating conflict. Having a single person who can change (or give the right to change) a given assets helps to avoid that problem.

Prefabs

Use them. A lot. Prefabs keep a single point where you can modify settings for objects in your game. Just avoid nesting them (for now…) as changes on the original prefab will not be pushed to the one nesting it.

Scriptable Objects

Use them. A lot. Just like prefabs, they offer a single place where you can change their settings that may be shared across scenes or objects. Note that if your scriptable object references other scriptable objects or prefabs, and a GameObject in your scene references that scriptable object, when the scene GameObject is loaded, it will trigger a loading of the scriptable object and all references to the objects in a recursive fashion. This can lead to long loading times for scriptable object referencing a lot of other objects or content. But you can also use that to your advantage if your project prefers to load objects at scene load in one big load.

Scenes Structure

Hierarchy depth and count

Keep the depth of the hierarchy to a minimum. As a general rule, you should prefer multiple small hierarchies at the root to one big hierarchy.

The reason for minimal hierarchy depth is that Unity transform system has to walk that hierarchy to notify all children when a transform changes.

The reason for multiples hierarchies is that changes are tracked per hierarchy. So if you have 100 hierarchies at the the root, and change one object in those, only one hierarchy will have the transform traversal. But if you have one big hierarchy at the root, any change to any object will trigger a hierarchy change on all your objects. Additionally, Unity will multi-thread transform change checks on root GameObject hierarchies. The more hierarchies you have, the more Unity can multi-thread and speed up multiple transforms change per frame.

For the same reason, you can place all static objects (static environment meshes, lights or sounds) in a single hierarchy if that simplifies your scene navigation/organization for you. Since those will never have their transform changed, the drawback mentioned above does not apply.

To keep the hierarchy depth in check and spread your hierarchies in root:

  • Do not nest pure gameplay objects with the graphics, physics & audio representation. If something does not need to move with your character, it should not be a child of it. For example, having a GameObject that handle the patrol points of a navmesh agent character is a child of that character. All pure gameplay GameObjects should be as close to the root as possible.

  • Similarly, when you create a GameObject dynamically, do not make them a child of the object that created them unless they need to be moved with that object

Reducing hierarchy depth is not only beneficial for the engine performance, but for the ease and comfort of the person working on the project. More shallow hierarchies are easier to understand and traverse :

  • Use Optimize GameObject in the Rig tab of your Animation import settings. This will prevent the "skeleton" GameObject of your rig being created, reducing the hierarchy. If you need to still expose some (you may use one as the target to attach equipment on the character) you can manually pick it to only expose this one.

Organization

Use an empty GameObject to organize your hierarchy. For example use a GameObject named "----Lights----" to place above your lights

Note that you should not make it the parent of your lights, but just place it above it in the hierarchy for reasons explained in Hierarchy depth and count above.

If you want for ease of working to parent all light under your Light "header" so you can collapse everything, you should write an editor script that unparents everything before build and/or entering playmode, otherwise performance could suffer on large scenes.

Dynamic Objects in Editor

If you have dynamic objects that are created and executed in the editor not in playmode (For example, a reflection camera that moves to match the scene view camera so Reflections work in the Scene View or a render target updated in the scene view assigned to a material) be sure to set the hideFlags of those objects to HideAndDontSave (or DontSave if you want to keep it visible in the hierarchy to edit it). Otherwise all changes in position or content will dirty the scene, asking to save it every time. This leads to a greater chance of creating conflict if someone saves and commits the scene to source control inadvertently as this will happen to everyone, all the time.

Game Design and Features

Create cheat function

To improve the speed of the development/test/improve cycle, add in "cheat" functions for various features as early as possible. An example could be if your project has currency, make a cheat to get huge amounts of it. If you need to gain items to open doors, make a cheat to grant said item instantly. Create a way for the player to be teleported at points of your level if levels are long. QA and debugging will become way easier. There are multiple ways to do this:

  • Static functions that implement the cheat with [MenuItem("Cheat/MyCheat")] will allow you to cheat when playing in the editor through a top menu entry.

  • Good old "Cheat code" based on key press in a particular order, or even simply using the F1/F2 function keys of your keyboard.

  • Code an in-game console that can be opened with a keypress and in which commands can be typed & interpreted to cheat.

Wrap all these in code with the define DEVELOPMENT_BUILD || UNITY_EDITOR so that all build made with the Development Build checkbox of the build window not ticked will remove those and you will not ship your game with all those cheats in. You can do that in two ways:

  • A conditional attribute for cheat functions

Code snippet

[Condition(DEVELOPMENT_BUILD || UNITY_EDITOR)] 
void MyCheatFunc() 
{
   //your cheat goes there
}

This allows the compiler to just ignore that function on non-development build, so calls to that function will just be ignored.

  • Or wrap code related to cheating inside functions between

Code snippet

#if DEVELOPMENT_BUILD || UNITY_EDITOR
//Code related to cheating
#endif

This ensures they will be stripped from the final build. Note, that if you wrap a function between those, the build will fail because all calls to that function will be calling a non-existent function for the compiler. It is preferable to use the Conditional attribute above for functions.

Use small feature test scenes

Create and keep small scenes that are only made to test a single feature separate. Some examples could be a scene full of enemies to test the combat, an obstacle course whitebox to test the locomotion, one with interactable objects. This allows you to develop, improve and verify features are not broken by new ones. It will also allow you to more simply isolate a problem testers may have found in the "full" game. For example, you have a character that sometimes fails to grab to a ledge? This can be much easier to reproduce in a small scene than having to replay the first 5 minutes of a level 20 times to reach the point where the problem occurs.