I've been using the Unity game engine nearly daily for around a dozen years (with five of those years as an early employee of Unity Technologies.) With all this time spent using Unity, I've seen a lot of projects in various states of chaos and have been responsible for a making a mess or two.
After a recent "best practices" consulting gig at a local game studio, I've been thinking about what advice I would include in my top three tips for anyone starting a new project in Unity.
The battle against entropy begins the moment you select File/New Project...and the choices you make now will directly impact the amount of joy or pain you experience every day while working on your game. Since I firmly believe that creating should be a joyful experience, my top piece of advice for anyone starting a new project is to spend time planning a sane project structure.
I have personal preferences when it comes to "the best way" to organize a project, but regardless of how you choose to organize things, I think all good structures are built around a principle like this:
Anyone looking at the project should be able to quickly understand how your project is structured; so that
Everyone using the project can determine precisely (without ambiguity) where any new asset should go; so that
Anyone with access to the project can quickly find any file.
All of this starts with you really thinking things through - you can't hope for other people to understand how things are organized and maintain a project if you don't understand it yourself.
Ok, that is pretty high-level/big picture. Let's consider some concrete advice:
Your structure isn't as "obvious" as you believe and you need to actively consider how it is communicated to new team members. Part of this is about carefully considering how you name your folders, but I like going one step further and having the communication right there in the Project Window in the form of notes on folders that say exactly what is inside (like having "Plugins - Extensions to Unity functionality. Special folder offering phase-one compilation" right there in the inspector when the Plugins folder is selected.)
How do I do this? In Unity, the type DefaultAsset is used for assets that do not have any specific type... like folders. Like all Assets, DefaultAssets have an AssetImporter and can have a custom Editor to get a custom inspector. I like to store a description of a folder in the AssetImporter's userData and use the custom Editor for DefaultAssets to display userData strings.
Again, use the userData in a folder's AssetImporter. But in addition to descriptive text, include the allowed file extensions. Technically, I recommend storing the GUID to a ScriptableObject that contains that list so that when you realize that "Code Only" folders need to store not only .cs files but also .asmdef files, you can update that globally.
With that in place, you can use AssetPostProcessors to ensure that an asset is allowed to be where it is on import and warn the user if they place a file someplace where it doesn't belong. You could even take this a step further and hook it up to your VCS to reject commits until the user gets that prefab out out the scripts folder.
Your project contains some stuff that isn't really part of the game: things like reference implementations and prototype environments. Invariably, whether through mis-clicking or misunderstanding, these things creep into production scenes or are referenced in ways that pull them along into your build.
I like to keep these things separated at the top level of the project in a "Prototypes" folder and to have a hard rule that says that nothing from outside this folder should ever reference anything inside this folder.
Fortunately, it is pretty easy to detect when this rule is violated-we can make a simple editor tool that walks through the SerializedObjects in our project and ensures that none of them have a SerializedProperty that is a PPtr to an asset in /Prototypes. As your project grows, this can start to take a little time to process everything, so this step is perhaps best done as part of your Continuous Integration setup.
With a sane project structure in place, it is time to turn our attention to how we structure our code.
People have written entire books that barely scratch the surface of this topic, and I'm not under the delusion that I'll make much of an impact with a few words. My hope here is only to offer some practical and Unity-specific advice and to mostly look at this in the context of the previous point, because unsurprisingly, the same principles that applied to our project as a whole also apply here - we want to think through a structure that removes ambiguity and makes things easy to find.
I usually start by making a broad distinction between four types of code at the highest level:
Runtime code that is not game specific (utilities and core systems)
Actual gameplay code
Code that extends the Unity Editor (tools, etc.)
Tests (unit and integration)
For sanity and maintainability, each of these things should be kept separate from the others and each should only be allowed to reference the things that came before it.
We communicate code structure on two levels: through project hierarchy and through API. This is the reason that I prefer parity between namespaces and folder structure in Unity.
Fortunately, most IDEs (I'll plug JetBrains Rider here because I strongly recommend it if you are working on a Mac) can auto-enforce this. By default, Rider assumes that the namespace each class matches its location in the project and lets you know with "Code Inspection: Namespace does not correspond to file location" if you aren't following the convention.
I mentioned that we don't want, for example, core systems code referencing gameplay code. We can enforce this by using Assembly Definition files to keep those high-level distinctions between kinds of code in our project. Assembly definition files define your own managed assemblies based upon your project's structure and since references can't be cyclic, we can be assured that nothing in our core systems references gameplay-specific code.
This comes with a few added bonuses:
You cut down on compile time since you won't have to recompile all scripts when you make changes to an isolated area.
You can enforce the "no references to Prototypes" and "no Prototypes in builds" rules by having a Prototypes assembly that nobody references (and marking it to be only included in the Editor).
You can make your tests be proper tests by marking their assembly as a Test Assembly in the Inspector. This adds references to unit.framework.dll and UnityEngine.TestRunner.dll in the Assembly Definition file so that you can use Unity Test Runner with them.
The little inconveniences and time wasters that you experience each day will build up and suck the joy out of development. My advice is to aggressively remove these things are they are encountered.
Keep your console clean and run your project with "Warnings as Errors" - add mcs.rsp and smcs.rsp to your Assets folder with -warnaserror+ in each to force you to actually deal with things. This might initially slow down your development as you adjust to things like unused variables preventing compilation, but you'll quickly adapt, and the peace of mind is worth it.
When iterating in the Editor, keep "Error Pause" toggled in the Console or, better yet, keep a debugger with break on error attached. Don't let runtime errors slide even a little bit.
Keep your code clean and use static code analysis as you go to help you get rid of things like redundant using directives or unused parameters.
Write tests. You have a new project and the opportunity to go for a lot of test coverage. This is the time to get into that habit.
Set up something for Continuous Integration (even when you start and if you are the only developer). A lot of the steps already mentioned in this post are ideal candidates for automating. Additionally, you can do things like look into your scenes to ensure that there aren't missing scripts referenced and to run any custom validation code-these things are best discovered and addressed quickly.
When writing this, I tried to prioritize simple things that make a big difference when done early in a project's life. There are, of course, a million other things where I would say "oh, that is super important!", but I don't think any of them would bump these three points right when you start a new project.
One last thing that I'd like to point out - all of this advice applies even if you are a solo developer working on a hobby project. Just read statements like " you can't hope for other people to understand how things are organized and maintain a project it if you don't understand it yourself" as "you can't hope for future you to understand how things are organized and maintain a project if present you doesn't understand it."
I hope you enjoyed these tips and found them useful. If you have any other advice to share with someone creating a new project, let me know!