Behind the Scenes: Optimizing Phoenix Point's UI
by Nedislav Stoychev
Have you ever wondered what problems a studio goes through before releasing (or during the development of) a game?
Hi, I’m Ned. I'm a GUI programmer here at Snapshot Games, and I wanted to share with you some of the UI optimization problems that we had to solve during the development of Phoenix Point.
Let’s start way back in time, when Phoenix Point Backer Build 2 (BB2) was near its release and we were on fire fixing issues, looking for performance spikes and feeling hyped about the build that you would soon play. If you’ve forgotten about BB2 or haven’t been following us, our community manager Kevin did an awesome playthrough back then:
During the development of BB2, we had a lot of fun playtesting the build, including how it performed on different PC specs. Soon, as more people at the studio began playing, an issue with low-spec PC performance began to occur.
When we looked into it, all the reports stated the same thing: while hovering on the ability buttons with the mouse in the ability bar, the game seemed to lag/get stuck/slow down/not respond, etc.
We had to investigate. After booting up Unity Profiler and several hovers, we had this:
It’s even noticeable in the screen capture. While hovering on the abilities with the mouse cursor, the game seems to struggle. If you watch closely the Queen animations, you can see the animation skipping.
As the profile is showing, PostLateUpdate.PlayerUpdateCanvases() is above everything else and takes up to 50% of the CPU time for this specific frame. Hm.
And now for a little theory and analysis!
In Unity, you can work on UI in several ways: IMGUI, Unity UI and the new UIElements. For Phoenix Point we exclusively use Unity UI.
In Unity UI, all of the UI elements (buttons, scrollers, UI images, etc.) are attached on a Canvas (and yes, you can have more than one root hierarchies if you want). Usually, you set a hierarchy (or hierarchies) of game objects and all of the UI assets and elements go under that specific Canvas. So here’s an interesting fact:
Every time you make a change to an object, including the following…
Change positions of UI objects
Change the scale of UI objects
Enable/disable game objects
Reorder the hierarchy with the game objects
…forces Unity to “dirty” the canvas that this object is part of, and triggers that canvas to be rebuilt. With that said, a really common problem is that some devs build their UI in one single canvas hierarchy.
Here’s an example of how Unity builds a single canvas. It’s from a minimalistic game I wrote for a game jam, made entirely in Canvas.
Knowing all of this, let’s go back to the issue and check our state of the hierarchy back then:
And from what we see in the game, our ability buttons have those animation states:
Every button changes its animation state based on the ability that it triggers:
Idle for AP/WP (action points/willpoints) ability
Highlighted for AP/WP ability
Pressed state for AP/WP ability
Disabled state
When in Idle state, the button only changes its color, which is fine. What we must notice is that the Highlighted and Pressed state animations actually change the scale of the object and enable/disable some of the game objects… And as we already know, these actions trigger full canvas rebuild!
Every time that you hovered on an ability button or activated an ability, the UI was forced to be rebuilt. You can imagine our shock back then when we realized this.
So here’s a protip: Separate your UI into multiple nested canvases. The perfect scenario would be for you to separate your UI into:
A canvas with all the static UI
A canvas with elements that will often be enabled/disabled/moved up and down in the hierarchy
A canvas that will contain animation, etc.
If this practice is applied, here’s an example of how Unity will build nested canvases:
If any change happens, only the appropriate canvas will be affected and forced to rebuild.
With those practices in mind, the solution is easy. We split the current UI into several canvases and this was the result:
With the proper canvas splitting, the buttons triggered rebuild on that specified part of the UI - the ability bar. This issue provoked a series of checks and tests to see where we had similar issues… and yes, there were more of those!
Let’s take a look at another problem, now in Backer Build 3 (BB3). If you’ve forgotten about BB3, here’s another awesome overview from Kevin:
This time, we’ll look at the geoscape. The “setup” is the same as the one above. We were near release, everyone started playing the build and an issue began to occur. A lot of internal reports were mentioning that once you discovered a large part of the globe, the game would lag/stutter/slow down, etc. While it was investigated by the Geoscape team, the problem was in fact UI related! So, what was the profiler saying?
This specific profiling was done with а fully revealed geoscape (around 400 sites). We can notice that two functions stand out:
PostLateUpdate.PlayerUpdateCanvases()
PreLateUpdate.DirectorUpdateAnimationEnd()
What was happening?
We already know the reason behind PlayerUpdateCanvases(). Something was triggering canvas updates. But what about PreLateUpdate.DirectorUpdateAnimationEnd?
PreLateUpdate.DirectorUpdateAnimationEnd is an event triggered by the Animator component that Unity provides. Unity Animator itself is a great tool that can handle a lot… but for UI it’s not the perfect solution.
Again, some explanation:
The Unity animator is composed of animation layers and states that use animation clips. Every animation clip consists of a sequence of operations, such as color changes, transition from one position to another, enabling/disabling objects, etc. The way the animator component works is that for every frame during which the animator is enabled, it sets the appropriate values to the included objects in the animation clip, even if the animation clip has finished. This means that if you are changing the position of an object with an animation, even if the animation has finished, the animator will keep setting again and again its last position from the animation clip.
With this issue in mind and the canvas rebuilding, you must be really careful about combining Unity animator with UI. Especially if you do your UI in a single canvas. You don’t want canvas rebuild for finished/unused animations.
Back to our geoscape issue. Checking the hierarchy of the geoscape sites, we found out that their world space UI was using a combination of canvas + animator to handle its states. Basically, the more you explored the geoscape, the more canvases + animators (nearly 400) would be running during your game. And as we see from the capture, this would ruin your endgame.
Having discovered that, we applied some proper component and gameobjects handling so only the animator you needed to run would be running, while the other would be disabled. That way the issue was fixed.
Soon after that, we reworked the geoscape sites, their visuals, and how they handle their world space UI. This resulted in the geoscape sites you see today in the game.
We keep these particular experiences in mind when we need to check again for performance issues and analysis.
As our team grows and improves, we try to do as many UI iterations as possible before we hit a certain release target. This process involves constant improvement on how we organize our UI assets, workflow and follow certain practices. These are topics that I want to share with you in the near future.