DEV Community

Cover image for Unity Foundational Architecture: Project Scaffolding

Unity Foundational Architecture: Project Scaffolding

Table of Contents:

Introduction

Great architecture in Unity is entirely about drawing the right boundaries. Whether you are building a view-first UI framework or completely decoupling your core logic from your visual representation, the success of those systems is dictated by your initial project setup and that poor scaffolding will fight you evry step of the way. In this article we are going to look at how to properly setup and structure a brand new Unity project.

Scaffolding

In this article, we are going to setup some basic scaffolding that most Unity projects will need (one way or another). Scaffolding is about setting up the foundational environment of a project. The goal is to handle all the repetitive, baseline setup so you can immediately start writing actual feature code.

After creating a new Unity project, there's a few things that Unity devs do but we are going to focus on the shared setup process (we're skipping things that are only local like editor layouts, IDE setup, etc).

Git

Next we'll create our .gitignore and a .gitattributes files.

For the .gitignore we can use the following file which should be placed at the root of your project folder:

##################################################
# Unity
##################################################

# Unity-generated folders
.utmp/
/[Ll]ibrary/
/[Ll]ogs/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Uu]ser[Ss]ettings/

# Unity 6 : ignore recovery folder created upon restarting after a crash.
/[Aa]Assets/_Recovery/
/[Aa]Assets/_Recovery.meta

# ignore the asset store tools plugin
/[Aa]ssets/AssetStoreTools*

# Explicitly track Unity's package manifest and packages-lock json files
!**/[Pp]ackages/manifest.json
!**/[Pp]ackages/packages-lock.json

# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db

# Unity3D generated meta files
*.pidb.meta
*.pdb.meta

# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/

# Recordings can get excessive in size
/[Rr]ecordings/

# Gradle cache directory
.gradle/

# Unity3D generated file on crash reports
sysinfo.txt

# Builds artifacts
*.apk
*.aab
*.unitypackage
*.app

# Crashlytics generated file
crashlytics-build.properties

# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*

# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*

##################################################
# IDEs
##################################################

# Rider
##################################################
/[Aa]ssets/Plugins/Editor/JetBrains*
.idea/

# VS Code
##################################################
.vscode/

# Visual Studio
##################################################
.vs/

##################################################
# Custom
##################################################

# ignore any folder named 'gitignore' and its contents
gitignore/
gitignore.meta

# End of Custom ##################################
Enter fullscreen mode Exit fullscreen mode

Note the Custom section at the end. Here is where you can add custom ignore patterns. I added a pattern to ignore any folder called gitignore in the project as a nice to have. This is so that devs can experiment with things locally without worrying about needing to discard/stash those changes so that they don't show up in another PR.

Next we'll create the .gitattributes file which is used to define how Git should track, format, and merge specific file types. But what's more important is its relation to Git LFS. Unity projects inherently contain massive binary assets like textures (.png, .psd), 3D models (.fbx, .obj), and audio clips (.wav, .mp3). Normal Git cannot handle these well and they will bloat your repository quickly. The .gitattributes file acts as the explicit instruction manual for Git LFS. It intercepts designated file extensions and flags those files to be uploaded to an external storage server (take note of all the lines ending in lfs), replacing them in your standard repo with a tiny text pointer instead. Make sure that you enable Git LFS for your project. Anyways, here's the .gitattributes file which should be placed at the root of your project folder:

# Define macros (only works in top-level gitattributes files)
[attr]lfs               filter=lfs diff=lfs merge=lfs -text
[attr]unity-json        eol=lf linguist-language=json
[attr]unity-yaml        merge=unityyamlmerge eol=lf linguist-language=yaml

# Ensure that text files that any contributor introduces to the repository have their line endings normalized
* text=auto

# Unity source files
*.cginc                 text
*.compute               text linguist-language=hlsl
*.cs                    text diff=csharp
*.hlsl                  text linguist-language=hlsl
*.raytrace              text linguist-language=hlsl
*.shader                text

# Unity JSON files
*.asmdef                unity-json
*.asmref                unity-json
*.index                 unity-json
*.inputactions          unity-json
*.shadergraph           unity-json
*.shadersubgraph        unity-json

# Unity UI Toolkit files
*.tss                   text diff=css linguist-language=css
*.uss                   text diff=css linguist-language=css
*.uxml                  text linguist-language=xml linguist-detectable

# Unity YAML files
*.anim                  unity-yaml
*.asset                 unity-yaml
*.brush                 unity-yaml
*.controller            unity-yaml
*.flare                 unity-yaml
*.fontsettings          unity-yaml
*.giparams              unity-yaml
*.guiskin               unity-yaml
*.lighting              unity-yaml
*.mask                  unity-yaml
*.mat                   unity-yaml
*.meta                  unity-yaml
*.mixer                 unity-yaml
*.overrideController    unity-yaml
*.playable              unity-yaml
*.prefab                unity-yaml
*.preset                unity-yaml
*.renderTexture         unity-yaml
*.scenetemplate         unity-yaml
*.shadervariants        unity-yaml
*.signal                unity-yaml
*.spriteatlas           unity-yaml
*.spriteatlasv2         unity-yaml
*.terrainlayer          unity-yaml
*.unity                 unity-yaml
*.physicMaterial        unity-yaml
*.physicsMaterial2D     unity-yaml

# Unity LFS
*.cubemap               lfs
*.unitypackage          lfs

## 3D models
*.3dm                   lfs
*.3ds                   lfs
*.blend                 lfs
*.blend1                lfs
*.c4d                   lfs
*.collada               lfs
*.dae                   lfs
*.dxf                   lfs
*.FBX                   lfs
*.fbx                   lfs
*.jas                   lfs
*.lws                   lfs
*.lxo                   lfs
*.ma                    lfs
*.max                   lfs
*.mb                    lfs
*.obj                   lfs
*.ply                   lfs
*.skp                   lfs
*.stl                   lfs
*.ztl                   lfs

## Audio
*.aif                   lfs
*.aiff                  lfs
*.it                    lfs
*.mod                   lfs
*.mp3                   lfs
*.ogg                   lfs
*.s3m                   lfs
*.wav                   lfs
*.xm                    lfs

## Video
*.asf                   lfs
*.avi                   lfs
*.flv                   lfs
*.mov                   lfs
*.mp4                   lfs
*.mpeg                  lfs
*.mpg                   lfs
*.ogv                   lfs
*.wmv                   lfs

## Images
*.bmp                   lfs
*.exr                   lfs
*.gif                   lfs
*.hdr                   lfs
*.iff                   lfs
*.jpeg                  lfs
*.jpg                   lfs
*.pict                  lfs
*.png                   lfs
*.psd                   lfs
*.tga                   lfs
*.tif                   lfs
*.tiff                  lfs
*.webp                  lfs

## Compressed Archive
*.7z                    lfs
*.bz2                   lfs
*.gz                    lfs
*.rar                   lfs
*.tar                   lfs
*.zip                   lfs

## Compiled Dynamic Library
*.dll                   lfs
*.pdb                   lfs
*.so                    lfs

## Fonts
*.otf                   lfs
*.ttf                   lfs

## Executable/Installer
*.apk                   lfs
*.exe                   lfs

## Documents
*.pdf                   lfs

## .a files (Static Libraries): These are archive files often used for compiled C++ static libraries, frequently used in Unity projects for iOS native plugins or specialized plug-in libraries.
*.a                     lfs

## Spine export file for Unity
*.skel.bytes            lfs
Enter fullscreen mode Exit fullscreen mode

Project Settings

You should configure any initial project settings in the Project Settings window which you can open by selecting Edit > Project Settings... in the top toolbar. While many of the settings here are project specific there are a few that you should adjust right away.

  • Player settings:
    • You should update the Company Name, Product Name, and Version fields so that they are not set to the default.
    • You should also configure the Splash Image. I usually just set it to this (see image below) and simply append to the list of logos when a logo for the game is ready: Splash Image Configuration
  • Editor settings:
    • You should set Asset Serialization > Mode to Force Text to force YAML instead of binary for asset files. This makes it easier to work with Git and Unity YAML-Merge.
    • You should also set Enter Play Mode Settings > When entering Play Mode to Reload Scene Only to enable fast enter play mode. This will be the default setting in Unity 6.5+ and moving forward as they are deprecating and removing the option to Reload Domain when entering Play Mode. This makes it so that Unity does not destroy the entire C# code domain state and does not reload any assemblies which means that static data, variables, events and memory are not reset when entering play mode in editor. The plus side is that this makes you write cleaner code and also makes entering play mode super-super quick!

Project Folder Structure

Let's start with folder structure. This is the folder hierarchy that I use for my projects:

[PROJECT_ROOT]/
├── Assets/
│   ├── _Project/
│   │   ├── _Modules/
│   │   │   └── Default/
│   │   │       ├── _Runtime/
│   │   │       └── Editor/
│   │   ├── _Scenes/
│   │   ├── Art/
│   │   ├── Audio/
│   │   │   ├── Music/
│   │   │   └── Sounds/
│   │   ├── Resources/
│   │   ├── Sandbox/
│   │   ├── Settings/
│   │   └── StreamingAssets/
│   │       └── Videos/
│   ├── TextMesh Pro/
│   └── ...
├── Packages/
├── .gitignore
├── .gitattributes
├── README.md
└── ...
Enter fullscreen mode Exit fullscreen mode

The key take-aways from this folder structure is the following:

  • The Special Folders that Unity reserves that we have setup prematurely (Editor, Resources, StreamingAssets). Note that you can have multiple of some folders in your project and Unity will consider/group them all together when processing them.
  • The _ prefix for some of the folder names is just so that Unity orders it first inside its parent folder in the project explorer which uses alphabetical sorting for folders.
  • The _Project folder where we have already setup a neat standard moving forward for how we separate and organize our project's code modules (more on that later) from other assets like _Scenes (level design), Art, Settings, etc.
  • The _Scenes folder, where we keep the scene files as well as any files related to a given scene. Usually I create a folder with the same name as the scene and place stuff like baked navmesh and lighting files there (unity will do this automatically for you as well when you bake navmesh or lighting).
  • The Art folder. This folder is for artists only (no code). Many artists want to just focus on art and be able to have their own project structure for organization and this folder allows them to do that. Textures, images, 3D models, all go in here.
  • The Audio folder. Again, this is a folder where audio engineers can have their own file organization. Although I usually impose a Music and Sounds subfolder so that my code can easily scan a single folder when generating assets that connect the audio to in-game systems (so I'll leave this up to you).
  • The Sandbox folder. This is where devs can roughly test things and place files that might not ever be used in production if a feature they are prototyping is rejected. Essentially, this is where someone can prototype a feature. I usually create subfolders with the names of each dev in this folder so each dev has there own working directory.
  • The Settings folder. This folder is to group all the settings/configuration files for the project. Things like InputSystem (.inputactions file), Localization (locales, tables, and settings), URP Settings (URP Render Pipeline assets, etc),
  • The TextMesh Pro and Assets/... folders. These folders sit outside the _Project folder and are mainly for external asset packages like the ones you'd download off the asset store. It may be tempting to group them under a third party folder but I've seen some assets that assume they are placed at the root of the assets folder and if not placed there don't function properly as a result. While this is a very small minority of asset plugins, it's best to side with caution here. Besides, since we prefixed the project folder with _ these external assets will always end up sorted at the end of the project explorer window.

TextMesh Pro

In the editor toolbar menu at the top, select Window > TextMeshPro > Import TMP Essential Resources. This will import the TextMesh Pro essential resources. This will also trigger when you add your first TMP Text component in a UI. You might want to configure a default font at some point but I'll leave that up to you.

Scenes

There are at least 2-3 scenes that I create at the start of every project.

  • The Bootstrap scene: This scene is the entry point of the application in a build and should be set as index 0 in the build settings scene list. For more information about bootstrapping see my other article. If you prefer to use your own bootstrapping logic than you can skip creating this scene however I strongly advise reading the other article and setting up your bootstrapping logic using a scene.
  • The Splash scene (optional): This scene is only needed if you want to have something more flashy as a splash screen other than the built in logo fades with a plain background (think animations or 3D scene or particle effects). If you implement this scene don't forget to add it as index 1 (after the bootstrap scene) in the build settings scene list.
  • The Welcome scene: Every game has this, it's the first scene where the player has input agency and is usually reffered to as "the main menu". Here I usually just setup a UI view with the Game's title at the top and a basic vertical menu with some basic buttons which have some simple functionality. The 2 buttons I implement are Play (brings you to another scene) and Quit which calls Application.Quit(). This scene should be set as index 2 (after the splash scene) in the build settings scene list.

When creating scenes I also like to impose a structure to the scene hierarchy. To make it easier to keep the same initial hierarchy when creating scenes, I create a scene template via RightClick in Project Window > Create > Scene > Scene Template. This will create a scenetemplate asset that you can point to a scene to use as the template. So create a regular scene and place it adjacent to the scenetemplate asset and then simply drag the scene into the Template Scene field of the scenetemplate asset's inspector. Also make sure to check Pin in New Scene Dialog so that our template stands out.

My basic scene template is as follows:
Basic Scene Template

Everything in [] is at position (0,0,0) and has zero rotation and a scale of (1,1,1). The [Context] object and everything under it are mostly for contextual things like navmesh, players spawned at runtime, level specific managers, etc. The [Environment] object is for the level designer and is meant to contain all the 3D models and gameobjects that make up the 3D world as well as any lights, post processing volumes, etc.

To use this Scene Template that we created, we just have to press Ctrl+N in the Editor and we should see our scene template in the New Scene Dialog list.

Code Modules

Every project can, at some point, have code that can be grouped into its own module and even if you don't have any initially it's good to set this structure up now. The _Modules folder is where all your codebase will live. Each code module should be separated within its own folder and have assembly definitions to enforce separation of concerns and to ensure a clean dependency between modules and assemblies. I will not go over assembly definitions in this article but I will explain the one or two that every Unity dev uses whether they know it or not which is the "Default" hidden Assembly-CSharp and Assembly-CSharp-Editor assemblies.

The default Assembly-CSharp assembly (which you may have already seen in your IDE) is the default assembly that any code under the Assets/ lives in. Unity sets this assembly up by default and automatically makes it depend on every single other assembly in the project. This assembly is also a "Runtime" assembly which is a term I use to mean that the code in this assembly is compiled and packaged in our built game. Basically any code we add under _Modules/Default/Runtime/ will be part of this default Assembly-CSharp assembly, which is why the folders are structured this way.

You may have noticed that there is also an Editor folder under the _Modules/Default/ folder. This is because if we write Editor code we do not want this code to be compiled and packaged with our built game, in fact Unity will throw errors when building if this is the case. By adding Editor specific scripts under this folder, Unity will automatically add them into a default Assembly-CSharp-Editor assembly (which you also may have seen in your IDE before). This entire assembly is automatically excluded from all builds except for editor which means that the code here is not packaged with your game builds and Unity will not throw any errors. So you should use this folder for any Editor specific code.

You can make other modules like a "Core" module which contains interfaces and common logic if you want but you'll have to create your own assembly definitions for that and that is a whole other topic. If you do create another module, you should follow the same folder structure where you create the module folder "Core" and under it create the "_Runtime" and "Editor" folders which will house their respective assembly definition files.

README

Finally, you should create a README.md with instructions on how to clone the project and install it on your machine.

Here's a good starting point:

# PROJECT_NAME

- [PROJECT_NAME](#project_name)
  - [Installation](#installation)
    - [Clone Project](#clone-project-using-git)
  - [Configuration](#configuration)
    - [Setup Custom Mergetool (optional)](#setup-custom-mergetool-optional)
  - [Getting Started](#getting-started)

## Installation

### Clone Project Using Git

```
cd [CHOOSE_A_DIRECTORY_TO_STORE_THE_REPO]
git clone [INSERT_URL_TO_GIT_REPO_HERE]
```

Next, Continue to the `Git Configuration` section to configure git or just go straight to `Getting Started` section below.  

## Configuration

### Setup Custom Mergetool (optional)

UnityYAMLMerge.exe is a merge tool that aids with the merging of scene, prefab and asset files (and any other YAML files). It comes packaged with each installation of the Unity Editor, therefore if you upgrade the project version you should also update your custom merge tool to use the newer UnityYAMLMerge.exe for your version of Unity.  

You must tell Git to use UnityYAMLMerge.exe as a custom merge tool and also as a custom diff tool. It is located in the [UnityEditor installation folder](https://docs.unity3d.com/2021.3/Documentation/Manual/SmartMerge.html) under `./Editor/Data/Tools/UnityYAMLMerge.exe`.  
On Windows this is located at `C:/Program Files/Unity/Hub/Editor/{EDITOR_VERSION}/Editor/Data/Tools/UnityYAMLMerge.exe`.  

Once you have setup Git to use UnityYAMLMerge, you must then tell UnityYAMLMerge what diff tool to use as a fallback. To do this, you simply need to edit `mergespecfile.txt` located adjacent (within the same folder) to the UnityYAMLMerge.exe file.  
Follow the instructions in this txt file to setup a fallback merge/diff tool.  

For more info on UnityYAML merge (Smart Merge) and setup with git or git GUIs see : https://docs.unity3d.com/2021.3/Documentation/Manual/SmartMerge.html  

## Getting Started  
Enter fullscreen mode Exit fullscreen mode

Notice how I included a paragraph in the README about Unity YAML-Merge? That's because this is not something that you can do for the whole project. Each developer should be setting this up locally for every project that they are collaborating on. If you are working on a project and are arguing 90% of the time about who is currently modifying a scene or prefab file then you are using Unity wrong in my opinion. Unity YAML-Merge allows you to merge scene, prefab, and asset files without breaking or corrupting anything. There are some edge cases but from my experience they are far and in between and are generally not an issue in practical use when work is separated cleanly.

Push the Initial Commit

With all of the above done, you can stage all your changes and push the initial commit to your main branch.

Top comments (0)