A few months ago, I started to put together a new training series on Unity, to touch on things I couldn’t include in my former courses.
To cover topics like data structure, custom editors, game events and player rewards, and being a fan of rhythm games like Guitar Hero and Tap Tap Revenge, I decided to remake a similar game with Unity.
The gameplay is fairly simple ; a list of expected inputs scrolls down the screen, and as they reach the end line, you’re meant to press the right button to score.
To put all this together, I used a pseudo Model-View-Controller design pattern, with a Track data model you author in UnityEditor, a GamePlay Controller component to handle game play mechanics, and a TrackView component to display the track to be played.
The latter uses UI Image components to represent different inputs, and stacks them up with a Vertical Layout Group.
Now, to give the player some feedback when you miss, play the right or wrong input, I compared several strategies, to simply change the Image colour before implementing a more elaborate visual feedback.
The idea is very simple, the TrackView references all input displays (music notes) in the track. It receives events from the GamePlay Controller when a note is missed or played. The event carries the index of the note, so the TrackView knows which note to update. All the details are part of the first chapter of the training series, touching on the Model-View-Controller design pattern.
Now, to change the Image colour property, I’ve experimented several ways, some with more flexibility, others with better performances, and it comes out that it’s not an all or nothing kind of deal. So I figured I’d share the results of my little experiment here.
Profiling wasn't meant to be part of my 2nd chapter, touching on the SOLID principles and Object Oriented Design & Programming with Unity. As I was implementing different designs, using C# events, interfaces and generics, mostly to demonstrate their benefits and downsides, I figured that if developers often disagree on such or such implementation, they ultimately come up with the performances argument. As I wanted to help people make their own opinion, I decided to add custom profiling to this chapter, and did well to do so, as it brought light on what was nothing but beliefs so far.
Below come details on the seven (no less) strategies I come up with, their benefits and downsides, and some benchmark results.
#1 : to the point.
The TrackView references Image components and changes their colour property directly.
images[index].color = newColor;
Result : not very flexible, the colour changes instantly, and even though performances are not bad, they’re not the best.
#2 : using Animator.
The TrackView references Animator components, and triggers parameters. Animator ultimately changes the Image colour property.
Result : a lot more flexible, the colour can blend over time, other properties can be animated, and performances are better than accessing the colour property directly, as Animations in Unity are well optimised under the hood. Though, some other things cannot be triggered with Animator, such as the playback of an AudioSource.
#3 : SendMessage.
The TrackView references simple Transform components and uses Transform.SendMessage() to broadcast a message that any component eventually catches.
A custom component catches the message and triggers Animator parameters.
Result : very flexible, the Animator handles the colour change, and any other component can also handle the message to do anything else. Though, it isn’t “safe” and performances take a little hit.
#4 : Custom Component.
The TrackView references Custom components, that trigger Animator parameters and possibly other things.
Result : somewhat flexible, performances are not bad, but it goes against the Single Responsibility principle and forces us to put all mechanics in one component.
#5 : Interfaces.
The TrackView references Custom components, of which the sole responsibility is to trigger methods of other components implementing a custom interface.
A custom component implements the interfaces and triggers Animator parameters. Other components may implement the interfaces to do other things.
Result : as flexible as SendMessage, with much better performances.
#6 : Unity Events.
The TrackView references Custom components, implementing Unity Events. Unity Events are setup to trigger Animator parameters, and could possibly do other things.
Result : very flexible as it allows everyone to add custom actions to events. Performances are quite impacted, as it uses the string implementation of Animator.SetTrigger(). Custom Event types could be added to handle the hashId implementation with better performances, but it would make the events less generic, thus a little misleading.
#7 : Interfaces & Unity Events.
Using the Interfaces strategy from above, another component implementing the interfaces references Unity Events.
Result : very flexible as it allows everyone to add custom actions to events, with optimised performances for Animator triggering using the specific component.
The charts below detail the different strategies, along with benchmark.
When it comes to optimise an event intensive gameplay, benchmarking the design and architecture is good practice to choose the design that offers the best performances while giving the level of flexibility we need to easily implement all the features.
Unity's built-in Profiler allows us to monitor specific portions of our code (using Custom Profiling Samplers and Recorders) to compare different strategies.
How does Profiling work out for you? Do you use it mostly to inspect memory? Rendering? Garbage Collection?
Do you use solely the Profiler window, or take full advantage of Custom Samplers and Recorders?
Please comment and share your own experience with profiling.