I recently ran across a question on Reddit about UI woes in Unity. A large percentage of the replies suggested that the OP was not alone in struggling with Unity UI which came as a bit of a surprise to me.
Ever since Unity introduced the RectTransform I’ve absolutely loved doing UI in Unity. So much so, that I’ve built entire (non-game) apps in Unity to avoid having to mock about with Android or iOS UI builders.
I don’t know if I’m just weird, or I’ve found a flow that just works, but I figured I’d share it, and let you be the judge 🙂
There’s a project on GitHub here:
Canvas & RectTransform
The essential components of UI in Unity are obviously the Canvas and the associated RectTransform that replaces the regular Transform on all children of the Canvas. I’m not going to go into any great detail explaining these, but I feel there’s a couple of things I should at least point out:
- Canvas scaling is important. Getting the scaling settings correct is key to making your UI automatically adapt to various screen sizes and device types. You need to understand the scaling modes, the purpose of matching to either width or height of your reference resolution as well as the meaning of references pixels per unit.
- The RectTransform allows for both relative and absolute offsetting, at the same time. Keep this in mind:
- Min/Max anchors defines the outer anchors of your rect relative to the parent. “Min X” defines the left edge and “Max X” defines the right edge, relative to the width of the parent. Same for Y. Setting both min and max to the same value defines an explicit anchor point.
- Left/Top/Right/Bottom are absolute unit offsets from the anchors and appear whenever min != max.
- X/Y are absolute unit positions of your pivot inside your parent anchor box and appear when min==max.
- Width/Height are absolute unit sizes of your rect and appear when min==max.
- Pivot is the relative location of (0,0) inside your rect.
The only standard components that I use are (and these cover the vast majority of what I need):
- Image / RawImage
In addition to these, I have a few helpers that I’ve built over the years, most notably:
- GridBuilder – container for programatically laying out UI with prefabs.
- ViewAnimator – general helper for showing and hiding panels at screen edges.
Code & File Structure
I generally try to keep code local to its place in the scene hierarchy. For example, if I have the following scene graph:
Also, let’s say I have custom scripts on A, B and E that deals with some UI logic (C and D are just standard components, let’s say an Image and a TextMeshPro node), then neither A, nor E will touch C and D, all access should be through B. Events from C and D goes to B, nowhere else. B effectively “encapsulates” C and D. There is no way to rigorously enforce this, it’s more of a frame of mind, but creating prefabs can help. I also generally try to avoid having siblings know about each other (I.e. B and E should talk to A, not to each other).
Speaking of prefabs, I used to keep prefabs separated from code in the project structure because we generally design behaviours to be used by many prefabs, but I found that for UI purposes the code is, first of all, usually very very simple and, secondly, often tightly bound to one specific prefab. For this reason I now always name and store prefabs and their main code together.
In the above example, if B was a prefab named ‘B’ it would have a ‘B’ script on its root node, and that script would be stored next to the B prefab in the project. I would always (and only) ever refer to this prefab via the B script – even if it had a bunch of other behaviours on it. From the rest of the projects perspective, it’s a ‘B’, nothing else. This makes it very easy to find things, and local changes can be done with minimal risk.
Avoid the temptation to re-use a simple script for two different prefabs just because they currently both have a label and fire a click event. If there’s a need for two different button prefabs, I can almost guarantee that you will eventually have a need for different code for them as well. There is usually so little code in these scripts that having several code-identical versions with different names is preferable over the headache of trying to keep track of what else might break if you change a scripts shared by 15 different prefabs.
GridBuilder is my go-to component for any UI that can’t be laid out statically and needs to be created from code. I know Unity has its own variation of this, and also a system for pooled objects, but both were introduced long after my own implementations had become an integral part of my UI flow, so I don’t really care.
Whether you should, is entirely up to you (obviously), but here is how it works:
To build a grid, assign the GridBuilder component to any RectTransform in your scene and then between calls to grid.BeginUpdate() and grid.EndUpdate(), you add cells using grid.AddCell(). Each cell is given by a prefab that must extend the GridCell base class. There’s a few more methods to add spacing or entire rows, but that’s about it.
Internally, the grid uses an object pool to minimise allocations and make sure minimal amount of work is done to update the grid. You can essentially build the entire grid in Update() every frame – as long as the structure of the grid remains constant, there is very little overhead.
The grid is filled from the top left corner and can be asked to automatically extend the width and/or height to match the added cells. The row height is determined by the tallest cell in the row.
Now, there’s a million features I could have added to this, such as merging of cells and different layout orientation (I did have a variation at one point that allowed right to left fill, but I never used it so eventually removed the code in a refactoring), but over the 10+ years I’ve used this, I really haven’t had the need, and there’s something to be said about keeping things simple.
Do note that the grid builder itself is a GridCell, so you *can* create compound cells if you must.
Because the grid cleans itself before being filled, and because it re-uses cells, I usually keep all the cell prefabs that I use in any given grid, in-place at design-time for easy editing. Just remember to refer to the actual project prefabs, and not the instances in the scene graph since they will be removed on launch.
You should generally define your grid cell prefabs with a pivot in the upper left corner, and a fixed width and height. Though, I’m sure there are use-cases where you might want to do something other than that.