UI Essentials
TOOLS USED: Unity, Visual Studio, Perforce, PureRef, Greenshot
This project is an experiment in developing UI widgets from scratch. I was restricted to using only code, and the primitive Unity UI components: Text and Image. My goal for this project was to practice my problem solving, programming, and communication skills. My focus was on building UI widgets using a variety of games as reference. I have a whole list of things I want to build but I am showcasing the first 4 here. Throughout this project, my mentor introduced me to many new ways of thinking, problem solving strategies, and architectural patterns. I learned a great workflow for breaking down and analyzing video for UI reference. I also learned about the MVC (model-view-controller) pattern, creating decision trees, and creating reusable code. At the end of each part I wrote a dev log about how I solved the problems I encountered, which helped solidify my understanding and write what I learned to memory.
ROLES AND RESPONSIBILITIES
Record video for reference material and conduct a frame by frame analysis to break down the processes happening in different pieces of UI. Form a design for what will be implemented. Implement the UI and program functionality.
The back end system and fake data packets were created by my mentor to speed up the process!
SEE SECTIONS BELOW FOR MORE DETAILS + TAKEAWAYS
Basic inventory system screen with multiple tabs and scrolling grid item layout.
A linear health bar with behaviour inspired by the health bar in Cyberpunk 2077.
Quest log system that supports multi-stage quests.
One-after-another style fading log.
Unity Implementation
Linear Health Bar
For my first challenge, I attempted to recreate the player’s linear health bar from one of my favourite games: Cyberpunk 2077.
Everything starts with a breakdown. I recorded video, used VirtualDub to conduct a frame-by-frame analysis and pasted screenshots into PureRef to create a reference board:
The challenge was creating more complex behaviour including:
bar animating different directions
bar animating at different speeds
an animation delay timer
different parameter settings depending on the direction of movement
To tackle these challenges, I implemented a MVC (model-view-controller) pattern to more efficiently keep track of the different widget elements and the communication between them.
Here’s a breakdown of how I used the pattern:
Let these boxes represent the scripts I needed ->
This HealthBarVisual is the VIEW. It’s in charge of translating the data into values that work with the UI and then push the result to the various RectTransforms that exist in the scene. This means that this script must be on a GameObject in the scene in order to reference the RectTransforms.
The HealthBarVisual receives requests from PlayerHealth and passes them along to the HealthTracker. Then, it reads back the data that HealthTracker has coordinated, translates it into UI-friendly values, and feeds it into the RectTransforms.
PlayerHealth.cs will never read any data from this script. This script is not aware of the Models.
This HealthTracker is the CONTROLLER. It receives requests from PlayerHealth via HealthBarVisual and is responsible for responding to in-game events. If the behavioural design of the health bar needs to be modified this is the place to do it. Welcome to designer land.
HealthTracker contains the logic for coordinating the behaviour of the various AnimatedFloat instances.
It then provides getter methods for the HealthBarVisual to read back.
The Actual Value represents the m_targetValue.
The Animated Value represents the m_currentValue.
AnimatedFloat instances are the MODELS. They are the backend that contains the data and calculations for animating.
AnimatedFloat follows the principle of “only do one thing and do it well”. It’s only job is to change a float from one value to another over some given time and store those values as private variables. The model doesn’t know anything about any of the other scripts in the system. It provides getter and setter methods that the HealthTracker will use to control and interface with the data.
The animation is simply controlled by a conditional bool check:
does the m_currentValue = m_targetValue. When this is false the animation runs until the m_currentValue = m_targetValue.
Fading Log and Quest Log
For these challenges I reused the AnimatedFloat class for animating the elements and continued to use an MVC pattern to implement. This time, the biggest challenge was getting the flow of logic correct for the intended behaviour. Here, a decision tree proved to be useful:
The decision tree was easily translated into code like so:
The quest log design:
The decision tree for the quest log:
The decision tree is translated to code like so:
With the Quest Log, things got a little more complex, so I divided the Update() method into 2 steps: initialization and update. Having an initialization step and update step is a common, and therefore recognizable programming pattern.
A look at the initialization step:
Another pattern that emerged throughout the project specifically in initialization methods was “Clear, Cache, Create”. It proved to be good practice to clear all former cached data here and then subsequently create and cache new data.
A look at the Update step:
Another small yet very important thing I learned regarding extracting code from an ‘if’ block into a helper method: If the ‘if’ statement was being used as a ‘guard’ (as it was here) then ensure that the guarding condition is brought with the code that is being extracted. This can be done using an assert at the beginning of the new helper method, as seen in the first line of both the InitializeActiveStage() and UpdateActiveStage() methods. Why? Defensive programming. These methods can now be called anywhere in this script. If asserts are not used in place of the conditional guard, then there is nothing protecting these methods from running without error if they were to be called on in the wrong place.