Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Doing a deep dive into a trivial problem that most game coders and scripters will bump into at some time or another: giving HUD widgets animations or tweens without creating state problems.
Going to do a deep dive into a trivial seeming problem.
I've got another example of the Curtain Problem in the field.
Specifically, I've been using Unity's OnGUI for the UI of my game, and - if you're first and foremost a programmer - OnGUI is really nice. Every frame you're responsible for drawing the GUI yourself. You don't cache state. If one frame you don't want to render a label - for example, maybe the timer isn't running, so you don't need to show the timer right now - you don't disable it and re-enable it later, you just don't render it that frame. Next frame you decide again.
It supports a document-view model of whatever data your UI is supposed to display effortlessly. Of course, we're game makers, and we're not happy with just putting our data on the screen. It's got to be Juicy. It's got to tween in, and bounce, and get bigger and smaller, etc. Since I'm a coder at heart, no problem - I smoothstep or sine wave or whatever those widgets around with nice stateless code that takes as inputs what time it is and what time this widget was supposed to appear in the first place, and my code will usually be bug-free.
I imagine people who are graphic designers at heart and only want to touch scripts as a last resort aren't so pleased.
A while ago I decided to switch to NGUI because, to support Oculus VR, I want to be able to render the HUD to a texture and then display that floating in front of the camera. (I could have used Unity's previous UI system but decided it was time to bite the bullet and start using everybody's favorite Unity UI add-on. Everybody can't be wrong!)
NGUI is as stateful as Flash. You create an assload of widgets and keep their state around forever, turning them on and off and playing animations or tweens on them as necessary. Don't get me started about the time I tried to make this cool UI screen for a game (that never shipped) and tried to get away from Flash's stateful-ness. That was a giant rabbit hole. So this time, with NGUI, I'm not trying to fight to fight the statefulness much.
And it's curtains for me. Here's one example situation: when a player is building up a combo, and their last timeout expires, they cash in that combo and add it to their total score. To make it clear to the player, the widget with their combo score in it tweens up to the real score.
I could have the widget reposition itself every frame in code. But remembering that time I tried to go against the grain of Flash, I decide to do it the NGUI way: let's try to move it by playing an animation.
Unity animations, once played, leave the target in their final resting state. Can't argue with that, that's probably what you want the majority of the time. But not this time; we want that widget back so we can build up the next combo. (Side note: astute readers will wonder - what if you start a new combo before the widget makes it to the top? Technically, yes, that's a bug, a bug that a stateless system would automatically be protected against, because on seeing it had a recent combo it would handle it correctly, but the bug will be rare enough and hopefully hard enough to notice that I'm not going to deal with it just now.)
It actually takes me a bit of time to figure out how to reset an animation! That's a Unity thing, and the answer seems to be here.
And then we hit our state problem. The widget shoots up, resets, and continues to display for a couple of frames before disappearing. There's a mismatch between the "should I show the widget" code and the animation.
Sigh. It's not exactly a game-breaker but it looks terrible. So I'll have to fix this, but I wonder if I'm doomed to problems like this every time. The problem seems to lie with me trying to use both stateless and stateful code together.
So I throw myself futher into statefulness. I create a separate widget for the animation...call it the 'traveller'...once it travels to the total score widget, we'll let the total score widget update. Which means the data in the total score widget is no longer just a stateless 'view' of your score 'document' but ... its own thing. It can now be Wrong.
One approach I've seen to something just like this, in another game, was to consider the value in the score widget the official score. It gets away from duplication of data. When the travelling HUD element with the +15 or whatever mades it to the score, that game added it in, and that was the new official value. The way that game made the view itself an inextricable part of the game felt wrong to me.
And if I used a similar approach with Energy Hook it could easily fail - that traveller widget currently just resets if a new combo is built up. If I did it the same way as that other game, those points would never make it into the final score.
So, here's what I end up with. The GUI, even though it has its own internal state, is still very much just a view of the document, like so. Cashing in a combo doesn't trigger a state change - the GUI watches until your total score increases, and then plays a purely cosmetic animation. I could delete the GUI script and the game would continue to play and keep score under the hood without a care. Another way of looking at this design, without even considering state, is to consider the code dependencies. The UI state depends on the Game state but not vice-versa.
public WidgetAnimateAndVanish comboScoreTraveller;
int lastFrameScore;
public void FixedUpdate()
{
if( avatar.totalScore > lastFrameScore )
{
comboScoreTraveller.Play(
(avatar.totalScore - lastFrameScore).ToString() );
}
lastFrameScore = avatar.totalScore;
}
protected override string LabelText()
{
return ( avatar.trickList.Count == 1 )?
string.Format ( "{0:####}", avatar.TrickSum() ):
( avatar.trickList.Count > 1 )?
string.Format ( "{1} x {0:####}",
avatar.TrickSum(),
avatar.trickList.Count ):
null;
}
The traveller (which theoretically could be used as an animation bridge between any two widgets) - it plays an animation and notifies the target when it's done:
public GameObject notifyOnCompletion;
public void Play( string labeltext )
{
foreach( Transform child in transform )
{
child.gameObject.SetActive(true);
}
GetComponentInChildren().text = labeltext;
GetComponent().animation.Play();
}
// Use this for initialization
void ResetAnimation ()
{
GetComponent().animation["ComboScoreAnimation"].time = 0.0f;
GetComponent().animation.Sample();
GetComponent().animation.Stop();
foreach( Transform child in transform )
{
child.gameObject.SetActive(false);
}
notifyOnCompletion.SendMessage("Notify");
}
And the total score widget, which has a hook to update when notified. And like I said, it doesn't simply add in the score floating in the traveller, it goes to the source.
int totalScoreCopy;
public void Notify()
{
//totalScoreCopy += traveller.score; // would be bug prone
totalScoreCopy = avatar.totalScore;
}
protected override string LabelText()
{
return totalScoreCopy > 0 ?
string.Format ( "{0:####}", totalScoreCopy ) :
null;
}
It works; looks good; and has been in there for a couple of weeks now and I haven't noticed any real problems.
What's the take-home here? This is a common problem with game UI's in general. This model, where it's document-view but the view doesn't completely update until after it has played its pretty animations, the communication is one-way, and all the hateful stateful event-driven crap is in the cosmetic side, seems pretty solid...
Now, I'll have no problem remembering to keep that communication one-way. But if I was working closely with other programmers on this, I'd like some way to make sure that the UI remained cosmetic. I'm not sure how I'd do that. Even if I set up namespaces it's a knee-jerk reaction for programmers to create circular dependencies at the slightest provocation - including me.
This may seem like a trivial thing to focus on, but we spend so much of our time dealing with these trivial things that should be simple. And often the least experienced coders and scripters do the UI of our games, creating systems that are a patchwork of band-aids that break when you breathe on them funny. So it's worth thinking about deeply.
(If you found this article interesting, please help me out on Greenlight.)
Read more about:
Featured BlogsYou May Also Like