Build a Game Engine

Dates and Times - UTimeSystem

Prerequisites

Article

If you've ever worked with dates and times in any programming language, I'm sorry. You are probably familiar with how challenging date and time formats can be across applications, operating systems, languages, databases, and file formats. Is it a date or time offset since 1900? 1970? As we'll see, Windows has a function that is since 1/1/1601. Windows and OS X both have proprietary date and time functions, while Linux follows the POSIX and C++ standard libraries. Then you get into the precision and maximum size of integers, and finally the differences between time zones and daylight savings times. It's all very complicated.

In addition, we need a simple way to use this information for different purposes: to get a double or float value of number of seconds since the last frame update, to add a date/time stamp to our log files, and to sync our networking between client and server.

We're going to keep things as simple as possible and try to abstract some of the complication away. We're going to create a system that can provide us with three types of output: a full date and timestamp, broken down into years, months, days, minutes, hours, etc.; a timestamp that gives us a high resolution for our frame counter and game loop; and a "tick" counter, which will increment at a given rate per second - the current tick is what we'll synchronize with our server and other players.

Let's take a look at the TimeSystem:

#define UENGINE_TICKS_PER_SECOND    30

class UTimeSystem : public USystem {
public:
    UTimeSystem();
    ~UTimeSystem() = default;

    /**
     * Initialize
     */
    void Initialize();

    /**
     * Get current timestamp (UTC)
     */
    static void Timestamp(struct timespec* ts);

    /**
     * Get current datetime (UTC)
     */
    static void DateTimeUTC(struct tm* out);

    /**
     * Get current datetime (local time)
     */
    static void DateTimeLocal(struct tm* out);

    /**
     * Get difference between two timespecs
     */
    static timespec Diff(timespec* start, timespec* end);

    /**
     * Get current tick
     */
    uint32_t Tick();

    /**
     * Override current system tick (0 = no override)
     */
    void Tick(uint32_t tick) { m_overrideTick = tick; }

    /**
     * System sleep
     */
    void Sleep(int milliseconds);

protected:
    // System startup time
    timespec m_startupTime;

    // Override tick
    uint32_t m_overrideTick;
};

First, a definition: we will define the "tick rate" of our engine to be 30 ticks per second. When we discuss networking and physics, we'll use this tick rate to create a fixed timestep - that is, the results of action A or movement B on two different machines will produce the same output for the same tick.

Let's look at the constructor and Initialize functions:

UTimeSystem::UTimeSystem() {
    memset(&m_startupTime, 0, sizeof(timespec));
    m_overrideTick = 0;
}

void UTimeSystem::Initialize() {
    Timestamp(&m_startupTime);
}

Our constructor sets everything to zero, and our Initialize function calls our own Timestamp function to set the startup time of our system.

Let's look at the easy methods first, that is DateTimeLocal and DateTimeUTC:

void UTimeSystem::DateTimeUTC(struct tm* out) {
    time_t t = time(NULL);
    out = gmtime(&t);
}

void UTimeSystem::DateTimeLocal(struct tm* out) {
    time_t t = time(NULL);
    out = localtime(&t);
}

Both of these functions will return a tm struct that contains a breakdown of the years, months, days, hours, minutes, and seconds since the Unix Epoch (1/1/1970). Fortunately, these are part of the C++ standard library and therefore the same across systems.

Next we'll get into the meat of it and look at Timestamp. A quick Google search will reveal many ways to get sub-second accuracies of timestamps across operating systems. Unfortunately, as of the time of writing this, there is no standard. Well, there is one attempt at a standard: timespec_get is defined on Windows and Linux, but not OS X. We'll use that as a base and add some platform specific code for OS X:

void UTimeSystem::Timestamp(struct timespec* ts) {
#ifdef __MACH__ // OS X does not have timespec_get, use clock_get_time
    clock_serv_t cclock;
    mach_timespec_t mts;
    host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock);
    clock_get_time(cclock, &mts);
    mach_port_deallocate(mach_task_self(), cclock);
    ts->tv_sec = mts.tv_sec;
    ts->tv_nsec = mts.tv_nsec;
#else
    timespec_get(ts, TIME_UTC);
#endif
}

Looking at the bottom of this function, for Windows and Linux, we'll simply call timespec_get. However, for OS X we'll need to use a function called clock_get_time, which returns an OS X specific mach_timespec_t structure. We can copy the returned information over to our generic timespec struct.

Now with the ability to return a timespec across all platforms, we can also get the difference between two timespecs using the Diff function:

timespec UTimeSystem::Diff(timespec* start, timespec* end) {
    timespec t;

    if (end->tv_nsec - start->tv_nsec < 0) {
        t.tv_sec = end->tv_sec - start->tv_sec - 1;
        t.tv_nsec = end->tv_nsec - start->tv_nsec + 1000000000.0f;
    }
    else {
        t.tv_sec = end->tv_sec - start->tv_sec;
        t.tv_nsec = end->tv_nsec - start->tv_nsec;
    }
    return(t);
}

Because the timespecs have both a number of seconds and a number of nanoseconds (billionths of a second), we need to compare the nanoseconds first to see if we need to "wrap" around backwards. For example if we have our first timespec at 1 second and 600 nanoseconds and the second at 2 seconds and 100 nanoseconds, then we can't just do 100 - 600 and 2 - 1, treating each part separately. If the ending nanoseconds is less then the starting nanoseconds, we'll set seconds equal to end minus start minus one and the nanoseconds to end minus start plus one second (one billion nanoseconds). If the end nanoseconds are higher than the start, then the math is done on each part. Finally, a new timespec value is returned.

In our main game loop, we'll use these functions to get a floating point number of seconds difference between frames to send to our systems, like so:

timespec lastTimestamp, current, tdiff;
timeSystem->Timestamp(&lastTimestamp);

while(gameRunning == true) {
    // Get the current timestamp
    timeSystem->Timestamp(&current);

    // And the difference to the last timestamp
    tdiff = timeSystem->Diff(&lastTimestamp, &current);
    float delta = (float)tdiff.tv_sec + (tdiff.tv_nsec / 1000000000.0f);

    // Do main game loop here
    update_all_systems(delta);

    // Store current timestamp as last
    lastTimestamp = current;

    // Let other threads / apps play too
    timeSystem->Sleep(1);
}

Finally, we'll provide one last helper function to let our thread sleep - mostly to relinquish control to other applications so that we don't take up 100% CPU all of the time:

void UTimeSystem::Sleep(int milliseconds) {
    std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}

Ticks

The last thing we'll talk about here is the Tick functions. In order to do that, I need to take a brief detour into fixed timesteps and client server replication. If you've ever used Unity's FixedUpdate function, then you've already used a fixed timestep, even if you didn't understand what it was and why.

Most systems in our game use a variable timestep - they update every frame, regardless of how long it is between frames. Hopefully this is fairly consistent.. if you're getting 60 fps, then you're updating roughly once every 16 ms. That is not typically the case though, and the frame rate will vary from frame to frame, often depending on what else your CPU is doing at the time. Sometimes you're the only game in town and you're getting 120 fps, and sometimes Windows Update is running. What that really means is most frames are great, but some frames are really long or really short. When it comes to things like physics and collision systems, that's when things start to get weird. In addition, once we start talking multiplayer and dedicated servers, we will want all of our clients and servers to be synced at the same rate, even if some players are rendering the screen faster or slower than others.

A fixed timestep is one that runs at a fixed interval, regardless of the frame rate. For example, at 30 ticks per second, we are doing a "step" every 33 ms. In practice, this means that inside of our game loop, we will always call the update function of our physics or networking systems with a delta of 33 ms. Sometimes, we might have to wait a couple of frames until at least 33 ms have passed and sometimes, if Windows Updates are running, we may need to call the update function multiple times if it's been 66 ms or 99 ms since our last frame.

Here's how that looks in practice:

void UApplication::Update(float delta) {
    // Loop over all systems
    auto it = m_systems.begin();
    for (; it != m_systems.end(); it++) {
        // If tickRate == 0, then update every frame
        if ((*it)->tickRate == 0) {
            (*it)->system->Update(delta);
            continue;
        }

        // Otherwise, increment the time since last update
        (*it)->ticker += delta;

        // If we're over our tick rate, then tick
        while ((*it)->ticker > (*it)->tickRate) {
            // Pass the specified tick rate as the delta
            (*it)->system->Update((*it)->tickRate);

            // Decrement the counter by the tick rate, loop again if needed
            (*it)->ticker -= (*it)->tickRate;
        }
    }
}

The first block of code covers systems that update every frame (tickRate == 0). Looking at the second half of the function, each system has a tickRate, which is 1 second divided by the tick rate, and a "ticker", which is a counter since the last update. We increment the ticker by the delta time difference, and where it is passes our tick rate, we call that system's Update function, passing in the fixed timestep tick rate instead of the delta frame time. Then, we decrement our ticker by the tickRate and we may need to do this loop multiple times to get caught up.

If you want to read more about why this is important, check out the Gaffer on Games article on Fixing Your Timestep.

Let's take this back to our TimeSystem... later on, when we start to talk networking and physics, we'll want to know what "tick" number we're currently in. In addition, we'll use the tick to sync clients and servers and "replay" our simulations... if you receive a message from a client that they pushed the "go forward" key, and you know they have an average ping time of 60ms, then at 30 ticks per second, I know I need to "rewind" two ticks (66 ms) in order to accurately process any updates. Let's take a look at how we convert timestamps to ticks:

uint32_t UTimeSystem::Tick() {
    if (m_overrideTick) {
        return(m_overrideTick);
    }

    timespec t;
    Timestamp(&t);

    timespec diff = UTimeSystem::Diff(&m_startupTime, &t);
    double d = (double)diff.tv_sec + ((double)diff.tv_nsec / 1000000000);

    return(std::floor(d * UENGINE_TICKS_PER_SECOND));
}

First, we check for a tick override... this will be used on the server later to "rewind" our state and play it back again (now we're in tick 102, then 103, then 104, etc. until we're caught up). If we're in a "normal" state, we get our timestamp, calculate the difference since the system started up, and convert that to a number of seconds (stored in a double) since startup. Finally, we convert that to a tick count by multiplying it by the ticks per second and taking the floor / rounded down value of it. That means that while the client and the server don't have to sync their current tick state, it does mean that they'll need to tick at the same rate per second.

Timer

Last but not least, we'll write a convenience class called Timer. This will work like a stopwatch - you can start it, stop it, reset it, and get the elapsed time since the start. We'll use this in our game loop to calculate the delta between frames. Let's take a look:

class UTimer : public UObject {
public:
    UTimer();
    ~UTimer() = default;

    /**
     * Start timer
     */
    void Start();

    /**
     * End timer
     */
    void End();

    /**
     * Get total elapsed time since start (in seconds)
     */
    float Elapsed();

    /**
     * Reset the timer (start counting over again)
     */
    void Reset();

protected:
    // Start time of timer
    timespec m_startTime;

    // End time
    timespec m_endTime;

    // Whether the counter is running
    bool m_active;
};

We store the start and end times, as well as a boolean for whether we are currently running or not (used in the Elapsed function). The functions themselves are just as straightforward, leveraging our TimeSystem for all of the hard work:

void UTimer::Start() {
    GetUSystem<UTimeSystem>()->Timestamp(&m_startTime);
    m_active = true;
}

void UTimer::End() {
    GetUSystem<UTimeSystem>()->Timestamp(&m_endTime);
    m_active = false;
}

float UTimer::Elapsed() {
    timespec endTime;

    // If we have started and ended (or haven't started), use end time
    if (m_active == false) {
        endTime = m_endTime;
    }
    else {
        // Otherwise use current time
        GetUSystem<UTimeSystem>()->Timestamp(&endTime);
    }

    timespec elapsed = GetUSystem<UTimeSystem>()->Diff(&m_startTime, &endTime);
    float delta = (float)elapsed.tv_sec + (elapsed.tv_nsec / 1000000000.0f);
    return(delta);
}

void UTimer::Reset() {
    GetUSystem<UTimeSystem>()->Timestamp(&m_startTime);
}

Next Steps

We now have a fairly robust date and time system that works across platforms and gives us three ways to get information about our date and time: a timestamp that gives us nanosecond resolution; a standard date and time structure that we can use for logging or other instances where we need years, days, months, hours, minutes, and seconds; and a "tick", which gives us a fixed timestep that we can use in our networking, physics, and other systems later on to sync and create reproducible replays of game state.