Things I Learned Prototyping a Game in Unity

In August 2019 I started learning Unity by creating a small tactical turn-based combat game. Think an incredibly simplified XCOM. I would love to make an actual game that mimics XCOM’s combat but this project was only ever meant to be a prototype to teach me the basics of Unity and it accomplished that quite nicely. I got pretty far with the project and now that I’ve started a new one I’ve decided to write down the lessons I learned.

This is the most complete version of what I made. Editor view on top, actual Game view on the bottom.

Movement and firing cost action points, the gun fires three bullets that are slightly randomised, and being hit reduces the target’s health.

Learnings

How to set up an isometric camera view

This is trivial when you know how but when you’re new to Unity you don’t even know things like “the camera is just another object in the scene”.

Set your camera’s rotation to X: 30 Y: 45 Z: 0 and set the Projection to Orthographic.

Creating Unity window panels and fixing the problems that causes

I wanted to be able to define my level as a JSON file rather than placing everything manually in the scene. I got this working but then wanted an easy way of loading different levels into the scene so I followed a tutorial for creating a UI panel that I could type a level ID in to and click a button to load that level.

This worked perfectly until I tried to build the game next where it failed to build the class that drove my custom editor window.

1
2
3
Assets\src\CustomEditorWindow.cs(6,37): error CS0246: The type or namespace name 'EditorWindow' could not be found (are you missing a using directive or an assembly reference?)
Assets\src\CustomEditorWindow.cs(11,6): error CS0246: The type or namespace name 'MenuItemAttribute' could not be found (are you missing a using directive or an assembly reference?)
Assets\src\CustomEditorWindow.cs(11,6): error CS0246: The type or namespace name 'MenuItem' could not be found (are you missing a using directive or an assembly reference?)

It was trying to build the code in the context of the game and not in the context of the Unity editor. The solution I found doesn’t feel like it’s the way you’re meant to solve this problem, but it worked so happy days. Wrap the entire class in an #if to check whether the UNITY_EDITOR context exists:

1
2
3
4
5
6
7
8
9
10
11
12
 #if (UNITY_EDITOR)

using UnityEditor;
using UnityEngine;

namespace src {
public class CustomEditorWindow : EditorWindow {
...
}
}

#endif

Creating a UI

This is just “learning about the Unity UI tools”. Nothing special to report here. You can create a scalable UI and attach data to your code.

Unit health bars and billboarding

I wanted the units in the game to have healthbars and other information, so I added a UI Canvas above them and of course they turned with the units. Making those elements always face the camera was one of the things I’d just assumed that Unity would have a button for but after some searching I found that I had to implement that myself, by which I mean I copied some code from the internet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;

namespace src.UI {
public class Billboard : MonoBehaviour {
public Camera mainCamera;

private void LateUpdate() {
var mainCameraRotation = mainCamera.transform.rotation;
transform.LookAt(
transform.position + mainCameraRotation * Vector3.forward,
mainCameraRotation * Vector3.up
);
}
}
}

I can’t remember where I copied this from so I can’t cite my source, but this worked perfectly. Add that as a script to your UI elements and away you go.

Pathfinding and Nav Meshes

A unit moving around a large gameobject

This project taught me about Nav Meshes and how to have objects move around in Unity. Each grid cell is a 1x1 game object and you can create a navmesh that spans multiple objects so I just selected all of them and baked a navmesh and it all worked. This is something else that you can look up and easily find tutorials for already.

The navmesh visualised

The fun first problem was that the default Agent Radius was too big, so my units could not move between walls that were only a tile apart from each other. Lowering the Agent Radius to < 0.5 (I went with 0.4) meant that there was a sliver of walkable space between close walls. Making it too small though seemed to result in agents walking too close to walls and bumping along them. This seemed strange considering that agents can neatly avoid each other while moving but either that’s just how it is or I have more to learn.

Units moving individually and between walls

Some things I had to implement myself were:

  1. Detecting that an agent was on top of a given tile and not allowing the player to tell another agent to move to that tile.
  2. I wanted units to always move to the center of a tile so I had to get the coordinates of the middle of the tile and pass those to the move instruction instead of the raw mouse coordinates.
  3. I wanted to implement a movement range and was surprised that Unity’s function for giving you a route length stops calculating after a small number of segments. I assume that this is a cost-saving decision as that information isn’t typically useful for long distances and constantly calculating that for complex routes could be expensive. I had to write code for iterating through the entire path to calculate the total length myself. Another one of those “I have to do this myself?” moments.

The path is now displayed, and the path and tile colour represent whether or not the unit can move to the target

Collision Detection and not shooting through walls

I got far enough to add basic combat to the game. You could select a unit, target an enemy, and the selected unit would fire three slightly randomised physics bullets in that direction. I did this by attaching a small gameobject to the units face and emitting bullets from that object.

Walls can be Triggers or Physics Objects but not both (actually yes they can)

If you marked a wall as a Trigger then you could listen for collision events and use that to detect that a bullet had hit it and destroy the bullet game object. Perfect.

I wanted units to fall over when they died and roll around, so I reenabled physics on them when their health reach zero and instead of hitting walls and falling down, they fell through the walls and out of the floor of the world. What?

In order to make walls report that they had been hit I had set their Box Collider to be a Trigger, but apparently this isn’t compatible with being a physics object. As soon as I turned off the Is Trigger option my units collided with them as expected.

I first solved this by putting a slightly smaller box inside of the wall and making the outer box the physics object and the inner box the bullet trigger. This worked great but felt wrong.

I thought that that was the solution I’d gone with, but while double-checking that for this post I see that I actually got rid of the inner box and just put two Box Colliders on the walls, one with Is Trigger enabled and one with it disabled. Much cleaner.

Units could shoot through walls if they were too close

I found that I was able to shoot through walls if a unit was told to fire at something on the other side of a wall. The bullets would collide with walls that they hit afterwards so this was confusing, but I realised that the issue was that the bullets were being spawned inside the wall which I guess doesn’t count as a collision.

This was solved by adding a collision detector to the bullet emitter and toggling a isInWall flag if it detected that it was inside a wall. If it’s true, don’t allow the fire command. This seemed nicer than letting the player waste an action point.

User Input: Ray Casting vs Mouse Events

This was one of the biggest lessons I learned from this whole thing. While I was learning how to Unity I used raycasting to handle user mouse input. I ended up with a class with too much logic in it handling a mess of different things (this was entirely my fault and has nothing to do with Unity or raycasting) and I had just learned that game objects have mouse events that you can listen for, so I thought to myself “aha! I will rewrite all of this to use mouse events instead of raycasting!”. So I did that, and I regretted it.

Here’s an example of why: I thought it would be clever to have my floor tiles listen for clicks, and if a unit was selected, broadcast a Move event with the tile’s coordinates. I did this, selected a unit, clicked a tile, and every single unit moved to that tile.

The tile had broadcast the Move event and every Unit had received it and started moving to that tile. I had to add code to the units for them to check if they were actually the selected unit before moving.

While this worked, things were definitely easier when I was firing a ray and building everything around the concept of having exactly one unit selected and calling commands directly against that unit.

Mouse events absolutely have their place - this is how I implemented tiles changing colour as the mouse moves around, and knowing when to draw the unit path. But they did not seem like the correct solution for interacting with units and things like movement.

So I bailed on the whole thing

This put in a position where I could either continue knowing that I was doing things in a way that felt wrong, or I could rewrite everything for a second time. Both options felt tiring so I abandoned the whole thing. Rewriting it would have continued to be educational but I hated the thought of starting again so I didn’t. I had already learned some great lessons and was content to leave it there.

Things I didn’t get to learn

The furthest I got this project was having turns and action points working and being able to move your units and shoot the enemy units. The next thing to implement would have been a simple opponent AI. I wasn’t planning to make it any more complex than “move towards the nearest enemy and shoot them” but that would have been an interesting challenge both in terms of making the computer understand the world and from implementing code in such a way that I wasn’t duplicating code for the player controller and the computer controller.