Interactive
February 14, 2024

Recreating UI from The Last of Us in Unity (and Building a UI Framework)

Recreating UI from The Last of Us in Unity (and Building a UI Framework)

A while back, I shared some insights on building game UIs with React, diving into the smooth workflow it offers. Despite my preference for React, Unity's UI Toolkit recently caught my eye, presenting a potentially less frustrating approach to UI development in the game engine. With UXML and USS, UI Toolkit seemed like it could be a bridge between my web dev experience and game dev experience.

Yet, as I delved deeper, the reality set in: USS, while promising, still lacks several CSS features such as media queries, grid support, and keyframe animations. This realization was a bit of a bummer, but it also sparked a curiosity in me. I wanted to see how far I could push the toolkit by recreating existing UI — the Skin Customization menu from The Last of Us: Part 2 Remastered seemed like the perfect challenge.

Styling

What started as a seemingly straightforward task soon reminded me why web UI development felt cumbersome until ChakraUI and Tailwind came into my life. So, in a spark of inspiration (or perhaps desperation), I built ZoboUI, a utility class framework inspired by Tailwind. This wasn't just about making things easier for myself; it was about transforming the Unity styling experience from something that often felt clunky to something more intuitive and enjoyable.

However, Unity's default controls, like Button, Slider, and ScrollView, threw a wrench in the works. They’re pretty rigid, not letting you tweak their child elements without a battle of selectors and specificity issues. That led me down the path of crafting custom components, aiming for something more modular and flexible.

Custom Controls to the Rescue

Instead of wrestling with Unity's UI toolkit to style components like the RadioGroup, I took a different approach. I broke it down into parts, making each subcomponent its own entity. This way, styling became a breeze, applying classes directly without fussing over additional USS files.

This modular approach not only made styling simpler but also added a layer of functionality, such as automatically selecting an item in a RadioGroup upon clicking. Despite Unity's limitations with pseudo-states, I developed workarounds that maintained the element states and applied styles dynamically, enhancing the UI's interactivity and visual feedback.

I decided to build a custom control that’s responsible for applying styles conditionally and also works in the UI Builder. I extended this conditional style control for the radio group for example, so that I could style the RadioGroup.Item and other elements differently when the correlated RadioGroup.Item is selected.

It also has the benefit of being more declarative like React, and keeps all my styling information of style-related files like UXML and USS, allowing me to focus on functionality in C# files and MonoBehaviours, rather than juggling styles across multiple files.

Templates

Maintaining consistency and reusability in UI elements was crucial, especially when applying direct styles to components. UI Toolkit's support for templates was a lifesaver, enabling me to create and reuse chunks of UXML as components.

Converting an element to a template

With templates, I would style an element in context within the UI Builder, and once it's where I need it to be, I would convert it to a separate component. I did this for the Skin Card component, the Tab trigger buttons, and even the dividers used across the UI. As templates, if I need to edit them, I can make the changes in one place and they're reflected everywhere.

Bringing in Providers

However, there was still a gap when it came to adding more dynamic functionality. For example, managing animation in UI Toolkit alone proved a bit too difficult because USS doesn’t support keyframes, and VisualElements don’t support Coroutines or Update which are important when you want things to happen at a certain time. While you could use the Scheduler to keep things in UI Toolkit land for a lot of things, my code started to become a bit hard to read.

Since USS currently falls short in areas like keyframe animation, I leaned on a React-like solution: Provider components. These acted as superpowers for VisualElements, bridging the gap between MonoBehaviours and the UI Toolkit.

Adding the CoroutineProvider to the root of the hierarchy.

I created a CoroutineProvider component, adding it at the root of the UI document. In the Unity Scene, I fetch the CoroutineProvider from the root UI document in `OnEnable` and then assign a CoroutineManager to it. The `CoroutineManager` is a MonoBehaviour that handles all the Coroutine calls. When the UIDocument is initialized, any interested child element can query (or walk up the hierarchy) to access the CoroutineProvider which has StartManagedCoroutine and StopManagedCoroutine methods.

I found this approach to be a good middle ground, in that it helped me merge MonoBehaviour functionality with UI Toolkit, but still allows me to create custom controls and keep structure in UXML. I used this to create a FadeIn component to smoothly fade between tabs. I also used it to create the radial button spinner.

This hybrid approach facilitated smoother transitions and interactions, albeit with the caveat that these effects couldn't be previewed in the UI Builder, requiring play mode for proper visualization.

Final Thoughts

This project was more than just a technical exercise; it was a deep dive into the possibilities and limitations of UI development with UI Toolkit. The creation of ZoboUI and the exploration of custom controls and templates have not only enhanced my UI-dev experience but also provided a blueprint for managing larger projects. Despite the hurdles, my appreciation for Unity's UI toolkit has only grown, and I'm excited about future updates.