Gamesmithing: Clock UI in Unity

Josh_3_larger.png

In this “Gamesmithing” series of articles, I'll be talking about the various aspects of creating video games. It will touch on everything involved in the game-making process, from art to code to music composition.

In this initial piece we’ll be building  a clock UI in Unity. It'll be an old-school analog clock, not some highfalutin digital display. We'll go through creating the artwork, creating the GameObjects in Unity, and writing the controlling script afterwards. I'm releasing the code and the assets to be reused in your own projects.

A few things to note: this tutorial assumes that you have some familiarity with Paint.NET and Unity. You can probably sub in your favorite image editing program and and game engine so long as the image program allows layers and the game engine follows the entity-component system framework. Second, Unity was meant to be used by teams of programmers, artists, and designers. It's not a programmer-centric tool and Unity’s preferred idiom doesn’t always involve code. Searching online for "Clocks in Unity" showed a bunch of clocks that use the animation system. As for myself, I prefer reasoning about code than plumbing the depths of the animation system. Because of that, the clock will be implemented in code.

Alright, so let's get down to business. First, let's look online for some watch images. Look for something that catches your eye. Feel free to look around, mashing together aesthetics from different watches you like. If it works, great, if not, well, that's the creative process.

Watch search.png

Wait, what was that? Enhance!

watch closeup.png

Enhance!

Huh, I guess NCIS lied to me about how computers work.

More seriously, here's a better closeup.

watch better closeup.png

Looking at this from an artist's perspective, there's a few things to note when we use this as a reference.

Sample Watch w comments.png

In Paint.NET, we create a couple of layers. First, we make the outside circle for the watch's edge. On top, place the watch face layer and another layer for the hour numbers. Next, add some highlights and shadows to the outside edge layer.  After you’re satisfied with the clock, make a minute and an hour hand for the watch. Each of these is simply two triangles of different lengths, back-to-back. Save these files under my project’s /assets/art directory as .pngs. (File organization in Unity is probably worth a short post on its own. I keep a separate folder under /assets for all major file categories, like artwork.)

Graphics.png

Artwork? Check. I imported these into Unity as separate image files. You can import it as a spritesheet and slice things up if you want, but separate files fits my mental model a little better. It helps to bend the engine to your mental model when possible. Programming is hard enough without additional complexities.

Inside your Unity project, create an Image object under your screen's canvas. Right click the canvas or main scene object, go to the UI menu option, and select 'Image' from the submenu. If you don't have a canvas in your scene, Unity will helpfully create one for you when you add an Image object. Go to the /assets/art directory (or wherever you saved the art files) and drag the clock.png file onto the 'Source Image' attribute of the Image component. Change the dimensions of the Image to match the artwork (180 by 175 for my own files), then move the clock face to wherever it should go in the UI.

A quick aside regarding Sprites vs. Images:  Sprites are in "world" coordinates, while Images stay in the "screen" coordinates. An easy way to think about this is with Super Mario Brothers - the original. In that game, Mario moves to the right to advance. A block that starts off on the right edge of the screen moves to the left edge as Mario moves forward. In world coordinates, the block never changes. It's always at, say, (512, 20). In screen coordinates, it moves every time the camera changes position. In Unity, Sprites are rendered in world coordinates by default and Images are likewise in screen coordinates. Images are more useful for UI elements, like number of coins displayed in Mario. These are simply in screen coordinates and they’ll stay in the same spot when the camera moves. If you wanted a clock that your character can find in the game world, you could implement this as a Sprite instead. For our UI, though, it’s easiest to use an Image.

Now that we’ve got the clock face positioned appropriately, right-click on the Clock GameObject in the hierarchy and select the UI->Image option to create a child Image. Click and drag the MinuteHand file onto the Source Image attribute of the Image component of the new GameObject. Notice that the object is mostly centered on the clock face already. This is because Unity Images and Sprites default to a center position for any new object. The 'pivot' point of these hands is also in the middle of the image. When you change the rotation of the image, it will rotate around its pivot point. A center pivot is a sensible default, but not the behavior we want from the clock hands. The pivot point should be at the intersection of the two triangles composing the clock hand, not the middle of the image.

Sample Rotation problems.png

The x and y values of the pivot point are normalized from 0 (left / bottom of the image) to 1 (right / top). To calculate the proper pivot point, take a look at the image file for the clock hands. My file for the minute hand is 5 pixels wide and 83 pixels long, and the "center" of the two intersecting triangles is between the 5th and 6th pixel from the bottom. Dividing 5.5 by 83 gets us 0.06627. That's the y value of our pivot point. Since our image is symmetrical from left-to-right, we can keep the x value of the pivot point as 0.5.

After the pivot point is adjusted, make whatever slight adjustments you need to make sure that the minute hand is centered on the clock face. I had to adjust its position by (-1, 1) to get it good and centered. Check its rotation at a z-rotation (the only rotation that matters for 2D games) of 0, 90, 180, and 270 degrees. If it is centered, it should point to 12 o'clock, 9 o'clock, 6 o'clock, and 3 o'clock respectively. If not, adjust as needed.

Once the minute hand is rotating correctly, repeat the process to add an hour hand as a child Image to the Clock.

Now that our GameObjects are ready, create a new script to add to the clock face GameObject.  Opening up the file, you can see that our class inherits from MonoObject and has a Start() and Update() function - the standard Unity GameObject code file.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Clock : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

First thing we'll do is add a couple of public variables for our child Images, the clock hands. After we add these, go back to the Unity editor and drag the child objects to their appropriate variables in the script component of the Clock. Next, add a few other variables and constants.

public class Clock : MonoBehaviour
{
    public Image MinuteHand;
    public Image HourHand;

    public float SecondsPerGameDay = 96;
    private const float HOURS_PER_DAY = 24;
    private const float HOURS_PER_HOUR_HAND_REVOLUTION = HOURS_PER_DAY / 2;      // This clock can't distinguish between a.m. and p.m.
    private const MINUTES_PER_HOUR = 60;

    private float _timer;
    private float _hours;
    private float _minutes;

    private const float DEGREES_PER_CIRCLE = 360;


    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

Besides our Image variables, we have a public SecondsPerGameDay. This is something that can be set from the Unity editor, even during debugging. Useful. Right now, I have it set so that every day takes 96 seconds, or 4 seconds per hour. Feel free to adjust this as needed. After that, we have a few bog-standard time constants and a few float variables, _timer, _hours, and _minutes. The latter two will be derived from _timer.

Now that we've got the variables we need, let's write Update().

void Update()
{
    if (_normalTime)
    {
        _timer += Time.deltaTime;
    }
    else
    {
        _changeTimer -= Time.deltaTime;
        _timer += _changeRate * Time.deltaTime;
        if (_changeTimer <= 0)
        {
            _timer = _changeEndTime;
            _normalTime = true;
        }
    }

    _hours = _timer * HOURS_PER_DAY / SecondsPerGameDay;
    _minutes = _timer * HOURS_PER_DAY * MINUTES_PER_HOUR / SecondsPerGameDay;

    float minuteRotation = _minutes % MINUTES_PER_HOUR * -1 * DEGREES_PER_CIRCLE / MINUTES_PER_HOUR;
    float hourRotation = _hours % (HOURS_PER_HOUR_HAND_REVOLUTION) * -1 * DEGREES_PER_CIRCLE / (HOURS_PER_HOUR_HAND_REVOLUTION);

    MinuteHand.transform.rotation = Quaternion.AngleAxis(minuteRotation, Vector3.forward);
    HourHand.transform.rotation = Quaternion.AngleAxis(hourRotation, Vector3.forward);

    if (_timer >= SecondsPerGameDay)
    {
        _timer -= SecondsPerGameDay;
    }
}

First, we add the time difference between this frame and the last to _timer. (Time.deltaTime can be a variable rate in Unity, but if you hitch everything to that variable rate things work out.) Next, we derive _hours and _minutes component from _timer. Note that if 5 hours have passed, _minutes will be 300, not 0. Important for the next step, where we take the remainder of minutes or hours divided by 60 or 12 to figure out the clock hand rotations. For instance, if _minutes is 308, we don't care about the first 5 hours (300 minutes) when displaying the minute hand. We only care about the remaining 8. We get the ratio of the remainder_minutes / minutes_per_rotation, then we multiply by the degrees in a circle (because Unity works in degrees). We multiply this by -1, because z-rotations in Unity go counterclockwise due to... I dunno, the right-hand rule? Quaternions? String theory? It’s been a decade since my last math class.

Next, we'll apply the rotation to the MinuteHand and HourHand Images. (Damnit, quaternions actually are showing up. Just know that this is how you do a simple 2D rotation in Unity, because reasons.) Finally, we check if _timer is getting larger than it needs to be, and we reset it if so.

Testing this out, it works pretty well. We can add some more functionality onto it, though. Add the following code to the file, putting the declared variables at the top because we aren't monsters that vomit variable declarations throughout our code files.

private bool _normalTimeUpdate;
private float _changeTimer;
private float _changeRate;
private float _changeEndTime;

/// <summary>
/// Changes the Clock Time to a new time, instantly or over a duration.
/// </summary>
/// <param name="newTime">The new time, 24 hour clock, [0000, 2400). 0060 is an error, use 0100 instead.</param>
/// <param name="changeDuration">How many seconds to extend the change over. The default argument, 0, makes the change instantaneous</param>
public void SetTime(int newTime, float changeDuration = 0)
{
    if (newTime < 0 || 2400 <= newTime || newTime % 100 >= 60)
    {
        // We were given a bad time
        throw new System.ArgumentException(string.Format("newTime {0} is in an invalid format", newTime));
    }


    int minutes = newTime % 100;
    int hours = newTime / 100;
    float newTimer = (hours / HOURS_PER_DAY + minutes / (MINUTES_PER_HOUR * HOURS_PER_DAY)) * SecondsPerGameDay;


    if (changeDuration == 0)
    {
        _timer = newTimer;
        return;
    }

    _normalTimeUpdate = false;
    _changeTimer = changeDuration;
    _changeEndTime = newTimer;
    if (newTimer < _timer)
    {
        newTimer += SecondsPerGameDay;
    }
    _changeRate = (newTimer - _timer) / changeDuration;
}

This function can set the clock to a new time, and it can either do it instantaneously or perform the change over a number of seconds. The new variable _normalTimeUpdate is a flag to see if the clock should update normally or if the clock is being updated over time by this SetTime function. _changeTimer tracks the time spent as the SetTime update is applied. changeRate is how fast the clock hands should spin - faster if you advance the clock 12 hours in 2 seconds rather than 2 hours in two seconds. changeEndTime makes sure that floating point math doesn't screw us over with it's imprecision. When we've waited long enough, we'll just set the time to what it should be. Because _timer, the current time value, is a floating point, it's probably not the exact time we wanted to change to, but rather something within 0.00001 of what we asked for. It's a small difference that doesn't matter until it does, and you might find yourself spending two weeks hunting down a mysterious floating point bug. Learn from my mistakes: don't trust floating points.

So, in the new function, we validate the input. I'm a little sad that the newTime variable is so weakly-typed, but It'll Do For Game Development (tm). It calculates the new value that _timer should be set to. If it's an instant change (as it is by default), it assigns the value and returns. Otherwise, it sets all the variables we just talked about (accounting for when you advance from 11 pm to 1 am as well). To take advantage of all this new information, we'll have to adjust the Update() function as well.

void Update()
{
    if (_normalTimeUpdate)
    {
        _timer += Time.deltaTime;
    }
    else
    {
        _changeTimer -= Time.deltaTime;
        _timer += _changeRate * Time.deltaTime;
        if (_changeTimer <= 0)
        {
            _timer = _changeEndTime;
            _normalTimeUpdate = true;
        }
    }

    _hours = _timer * HOURS_PER_DAY / SecondsPerGameDay;
    _minutes = _timer * HOURS_PER_DAY * MINUTES_PER_HOUR / SecondsPerGameDay;

...

We've added a branch onto the Update function to not adjust _timer with Time.deltaTime if we've recently called SetTime(). If we did, we decrement _changeTimer, adjust _timer by _changeRate, and check if _changeTimer has run out. If it has, we set the time directly and go back to normal time updating.

There you have it: a working Unity clock. Happy coding.

The code in this article is released under the MIT license and the clock art assets under the Creative Commons Attribution license. If you need the specific terms / wording / version of the license, email me at joshua . galecki at gmail.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Clock : MonoBehaviour
{
    public Image MinuteHand;
    public Image HourHand;

    public float SecondsPerGameDay = 96;
    private const float HOURS_PER_DAY = 24;
    private const float HOURS_PER_HOUR_HAND_REVOLUTION = HOURS_PER_DAY / 2;      // This clock can't distinguish between a.m. and p.m.
    private const float MINUTES_PER_HOUR = 60;

    public float _timer;
    public float _hours;
    public float _minutes;

    private const float DEGREES_PER_CIRCLE = 360;

    private bool _normalTimeUpdate;
    private float _changeTimer;
    private float _changeRate;
    private float _changeEndTime;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (_normalTimeUpdate)
        {
            _timer += Time.deltaTime;
            GetComponent<Image>().color = new Color(1, 1, 1, 1f);
        }
        else
        {
            _changeTimer -= Time.deltaTime;
            _timer += _changeRate * Time.deltaTime;
            if (_changeTimer <= 0)
            {
                _timer = _changeEndTime;
                _normalTimeUpdate = true;
            }

            GetComponent<Image>().color = new Color(1, 1, 1, 0.85f);
        }

        _hours = _timer * HOURS_PER_DAY / SecondsPerGameDay;
        _minutes = _timer * HOURS_PER_DAY * MINUTES_PER_HOUR / SecondsPerGameDay;

        float minuteRotation = _minutes % MINUTES_PER_HOUR * -1 * DEGREES_PER_CIRCLE / MINUTES_PER_HOUR;
        float hourRotation = _hours % (HOURS_PER_HOUR_HAND_REVOLUTION) * -1 * DEGREES_PER_CIRCLE / (HOURS_PER_HOUR_HAND_REVOLUTION);

        MinuteHand.transform.rotation = Quaternion.AngleAxis(minuteRotation, Vector3.forward);
        HourHand.transform.rotation = Quaternion.AngleAxis(hourRotation, Vector3.forward);

        if (_timer >= SecondsPerGameDay)
        {
            _timer -= SecondsPerGameDay;
        }
    }

    /// <summary>
    /// Changes the Clock Time to a new time, instantly or over a duration.
    /// </summary>
    /// <param name="newTime">The new time, 24 hour clock, [0000, 2400). 0060 is an error, use 0100 instead.</param>
    /// <param name="changeDuration">How many seconds to extend the change over. The default value, 0, makes the change instantaneous</param>
    public void SetTime(int newTime, float changeDuration = 0)
    {
        if (newTime < 0 || newTime >= 2400 || newTime % 100 >= 60)
        {
            // We were given a bad time
            throw new System.ArgumentException(string.Format("newTime {0} is in an invalid format", newTime));
        }


        int minutes = newTime % 100;
        int hours = newTime / 100;
        float newTimer = (hours / HOURS_PER_DAY + minutes / (MINUTES_PER_HOUR * HOURS_PER_DAY)) * SecondsPerGameDay;


        if (changeDuration == 0)
        {
            _timer = newTimer;
            return;
        }

        _normalTimeUpdate = false;
        _changeTimer = changeDuration;
        _changeEndTime = newTimer;
        if (newTimer < _timer)
        {
            newTimer += SecondsPerGameDay;
        }
        _changeRate = (newTimer - _timer) / changeDuration;
    }
}
Clock.png
HourHand.png
MinuteHand.png