Common pitfalls in Unity
Updated 25 days ago
Mistakes to avoid when building your game
As a full-time freelancer, I have worked on all kinds of projects from all kinds of clients. Certain mistakes appear time and again in project after project. Here are ways you can save time, money, or headaches when building your game.
  1. Not taking advantage of Unity's component-based programming paradigm One of the most common mistakes I see in Unity games and assets is a failure to utilize the component-based development paradigm to its full potential. Unity is designed around the philosophy of reusable components. You are intended to create unique GameObjects by composing together multiple components. By re-using the components in different ways, you can easily create different kinds of GameObjects. Even if you aren't familiar with designing reusable components, you're familiar with using them. Let's look at Unity's Rigidbody and Collider components as an example. You probably use these components (or their 2D equivalents) all the time. You'll use them on all kinds of different objects. Players, enemies, projectiles, vehicles, debris, or anything else that uses physics may have a Rigidbody attached. Anything solid will have some kind of collider attached. These are excellent examples of highly reusable components that save huge amounts of time. Now let's look at how an inexperienced developer might fail to utilize reusable components. Say that you are creating your first enemy for a 2D side-scrolling action game. This is a soldier that walks along the ground and fires bullets at the player. It's very tempting to start throwing all of the functionality that enemy will need into a single script. You start the "EnemySoldier" script and add in health, movement AI, attack AI, some code to spawn bullets from a prefab, animations, sound effects, etc. At the end of it all, you've got a 1200-line script which does everything you need an enemy to do. Now you want to add a second enemy, a drone that flies and drops bombs. You can't just give it the "EnemySoldier" component, because that is specifically designed for a walking enemy. Here is where an inexperienced programmer will start copying chunks of code from the EnemySoldier script and pasting them into a new "EnemyDrone" script. Pretty soon, you have a mess of copy-pasted code. Any bugs from EnemySoldier may have been duplicated into EnemyDrone, and improvements or new functionality may have to be duplicated in both classes. What you should have done is break the enemy functionality down into multiple reusable components. For example, you could have created a Health script, a WalkingAI script, a FlyingAI script, a FireProjectiles script, etc. Then you could create new enemies easily by assigning them the appropriate components. This might sound like more work at first - because you're creating more scripts - but each script will be much shorter, and in the end you'll save a lot of time because you don't need to keep creating new scripts each time you create a new enemy.
  2. Going overboard with assets and plugins right from the start You're starting on a new game. You have a general idea of what you want to build, so you start browsing the Asset Store, scanning for packages that sound like they could work with your game. Pretty soon you've purchased 30 packages and added them all to your project - models, textures, sound effects, script packages, Editor extensions, and more. Then you start trying to assemble your game. All those packages are going to save you hundreds of hours of time, right? What could go wrong? There are actually many disadvantages to loading up on too many assets and plugins, all of which I commonly run into on freelance projects:
  3. All of those models, textures, and sound effects you've downloaded bloat your project and make it harder to find the files you want. Let's say you're trying to create a new material. You click the little circle next to the albedo tab, and Unity shows a picker dialog for you to select your texture. Now you have to skim through hundreds of textures from a half-dozen packages to find the one you're looking for. Or you want to find a fire particle effect prefab. In the project pane, you type "fire" into the search box - and get a list of hundreds of prefabs, textures, materials, scripts, etc. I've worked on projects where it takes longer to find the particle effect prefab I want than it would to just make a new one from scratch.
  4. Likewise, all those models, textures, and sound effects bloat your project and make it gigantic. I've worked on projects where the client has 5 - 10 gigabytes of asset packages in the project that they aren't using. If you're using a version control system (which you should be), these can slow down your repository and make clones take forever. You might even have to pay for additional storage space from your repository host.
  5. The more files you have in your project, the longer it takes to switch platforms in the Editor. I've worked on projects where it takes 30 minutes to switch platforms from iOS to Android because the project has hundreds of unused assets that still have to be converted each time.
  6. Script packages and plugins need to be updated. Often when I'm hired to finish or overhaul a Unity game, I find that the various asset packages and plugins used by the project are a year or two out of date and have compatibility issues with the newest version of Unity or the newest devices/operating systems. I then have to spend hours updating everything, which can lead to the next problem
  7. Script packages and plugins can have compatibility issues with each other. Nothing wastes time and gets on your nerves like trying to compile a game which utilizes four different Android plugins which each require different versions of the Android support libraries, causing Google's Version Resolver library to go haywire. Or maybe you have two script packages which have a file named "GameManager" that isn't in a namespace, triggering compiler errors. Or one plugin only works with Unity 2018.2 and another only works with Unity 2018.3. All of this isn't to say that you shouldn't use assets from the Unity Asset Store or other sources - just that you should only add them as needed, so you don't bloat your project and create unnecessary headaches for yourself or anyone you hire to work on your project.
  8. Using Resources.Load() You want to access an asset in your project from a script. Maybe you want to instantiate a prefab, so you do something like GameObject prefab = Resources.Load<GameObject>("Prefabs/EnemySolider"); Easy enough, and then you're done, right? Here's the problem: if you rename or move that asset later, the code breaks, but there's no way to know that until you're playing the game and it tries to load that asset. If this code isn't run often, it's easy to miss the issue. Another problem - everything you stick in the Resources folder gets loaded as soon as your game launches. This can make your game get stuck on the splash screen for a long time while assets are loaded into RAM, and increases your game's memory usage. In most cases, when you need to access an asset from a script, you should add a serialized field to the script and assign the asset to that field in the Inspector. For example, place this at the top of your script: [SerializeField] private GameObject enemySoldierPrefab; This adds an "Enemy soldier prefab" field in the inspector. You can select your prefab, creating a link to it in the Editor metadata. Now if you move or rename the prefab, your script won't lose the reference to it! In general, you'll want to avoid using Resources altogether. Even the Unity team recommends not using the Resources system. There are a few exceptions:
  9. For loading assets which are small in size (such as text or ScriptableObjects) and many in number, where it would be pointlessly tedious to select them all in the inspector. For example, if you have a match-3 game where your level layouts are stored in ScriptableObject assets, you could load a level like this: LevelData level = Resources.Load<LevelData>("Levels/Level" + levelIndex);
  10. When you need to load a large number of small assets into an array, e.g. Prize[] allPrizes = Resources.LoadAll<Prize>("Prizes/");
  11. When you need to dynamically select an asset from a large number of assets based on multiple variables. For example, GameObject tankPrefab = Resources.Load<GameObject>("Units/Tanks/" + selectedArmy + selectedSkin + "Tank" + selectedSize);
  12. Relying on GameObject.Find() and Transform.Find() One of the most common mistakes I see from new and inexperienced Unity developers is heavy reliance on Unity's Find() functions. Let's look at an example scenario, an Update() function that displays the player's health on a UI Text object: void Update() { int hp = GameObject.FindObject("Player").GetComponent<Player>().HP; //find Player object in the scene GameObject textObject = transform.Find("Text"); //find "Text" in one of our children Text text = textObject.GetComponent<Text>(); text.text = "HP: " + hp; } There are many problems with this code, but let's focus on the ones related to Find(): - It searches for the same objects, the Player and the Text, each frame. Searching is a relatively expensive operation, and thus this code is wasting CPU cycles. On mobile, this contributes to battery drain, and may even hurt the framerate if there are many objects in the scene to search through. - The code breaks if you rename the Player or Text objects in the scene and forget to update the code - The code can break if you have multiple objects named "Player" or multiple children named "Text" In many cases, the correct way to get a reference to something is through serialized inspector fields: [SerializeField] private Text healthText; [SerializeField] private Player player; void Start() { Assert.IsNotNull(healthText); Assert.IsNotNull(player); } void Update() { healthText.text = "HP: " + player.HP; } Instead of searching for the objects each frame, we simply plug the references into the inspector. This code is much cleaner. It does not need to search each frame, saving CPU cycles and improving performance. It does not break if we rename the Text or Player GameObjects in the Editor; in fact, it doesn't care at all what the objects are named or if other objects have the same names.
  13. Putting everything in the scene instead of using prefabs Here's a really common mistake I see in almost every amateur project: the original developer put all of the game's UI into the main scene. The scene hierarchy contains a single canvas with a huge number of children: - LoginScreen - MainMenuScreen - SettingsScreen - ShopScreen - InventoryScreen - GameplayScreen - FriendsScreen - TutorialScreen etc. The UI works by setting one screen active at a time and hiding the rest. Each of these screens might have dozens of children, including modal dialogs and other elements that are also hidden or shown as needed. So if this is a common approach, what's wrong with it?
  14. The scene hierarchy is now a mess. If you add a new team member or hire a contractor to fix up your game, they'll have a hard time navigating through all the children on your canvas. Searching the scene might bring up dozens of results from this canvas - imagine if you search the word "Text"!
  15. To edit one of the screens or its children, you have to enable it and hide all of the other screens. This gets tedious very quickly, and you're likely to accidentally leave objects that are supposed to be hidden after you edit them.
  16. All of that UI has to be loaded as soon as the scene is loaded. This increases load time, which leads to unhappy players!
  17. If you accidentally delete some part of the UI without realizing it, it will be very difficult to recover it later.
So what's the solution? Put each screen into its own prefab.