How to make a custom tick system / gameloop

I asked this question already in Unreal slackers, but I hope to get some more insights and maybe also provide a “cite” for people facing a similar problem.

I would implement it similar to this in simplified/plain c++:

void GameLoop() {
    // Initialize
    Date currentDate = Date(2025-01-01);
    TimeKeeper timeKeeper;
    bool isPaused = false;
    
    while (!shouldQuit) {
        auto tickStartTime = Clock::now();
        
        // Get current speed setting
        auto currentSpeed = timeKeeper.GetSpeedAsMs();
        
        // Update simulation if not paused
        // should prob be done as tasks, since we have also different "ticks",
        // like tick, daily (4 default ticks), weekly,monthly etc
        if (!isPaused) {
            currentDate.AdvanceTimeByOneTick();
            doWork(); // had async here before, but thats not entirly correct.
        }
        
        // Time management - regulate tick rate based on speed setting
        if (currentSpeed > 0) {  // Skip timing for max speed
            auto processingTime = Clock::now() - tickStartTime;
            
            // Sleep for remaining time to maintain consistent speed
            if (processingTime < currentSpeed) {
                std::this_thread::sleep_for(currentSpeed - processingTime);
            }
        }
    }
}

I had in the initial question doAsyncWork, that’s not right, the tick should not proceed until all work for that tick is done.

The Background is a simulation heavy game. For example something like Crusader Kings, Victoria 3 etc, where Demographic groups, economy etc are simulated. Time processes not entirely in realtime.
You have ticks as a time unit — then for example 4 ticks are a day, 3×7 a Week etc.

Some things need to be recalculated every tick, other stuff only on a weekly, monthly … basis.
The Game has different Speeds, let’s call them 1, …, 5 where 5 is the fastest and runs as fast as it can process and for example 3 is default and takes 1sec per tick. If the simulation part takes longer as the defined value, then the next one starts immediately after, if its faster — then we wait until the time is up.

The recommendations were:

  1. Actor component as time manager on game state or world subsystem and letting tick update the state. With a delegate to handle ingame game time for others.
  2. A World Subsystem as the time manager, with the Tick function updating the in-game time state based on real time and a time factor. It also broadcasts events (via delegates) when the day changes so other systems can respond to in-game time changes.

You’re aiming for a robust, custom tick system in Unreal Engine, akin to simulation-heavy games like Crusader Kings or Victoria 3, where time progresses in discrete ticks with varying speeds and periodic events. Let’s break down how to achieve this, addressing your concerns and providing a structured approach.

Core Concepts

  1. Discrete Ticks: Your game operates on a fixed tick rate, not necessarily real-time.
  2. Variable Speed: Players can adjust the tick rate, affecting the game’s perceived speed.
  3. Periodic Events: Certain actions occur at specific intervals (daily, weekly, monthly).
  4. Simulation Synchronization: Ticks must complete their work before the next tick begins.

Implementation Strategy

  1. World Subsystem (recommended):
  • World subsystems are ideal for game-wide logic that persists across levels.
  • Create a’’ to handle your custom tick logic.
  1. Tick Function:
  • Override the’’ function of your’'.
  • Use’’ to measure real-time elapsed.
  • Implement your time management logic to control the tick rate.
  1. In-Game Time:
  • Maintain a data structure (e.g., a’’ or a custom structure) to represent your in-game time.
  • Increment this time based on your tick logic.
  1. Variable Speed:
  • Introduce a ‘’ variable that determines the speed of your ticks.
  • Calculate the desired tick duration based on’'.
  1. Periodic Events:
  • Use counters or timestamps to track in-game time and trigger events at specific intervals.
  • Use delegates or event dispatchers to notify other systems of these events.
  1. Simulation Synchronization:
  • Ensure that your simulation logic completes before the next tick starts.
  • Avoid asynchronous operations within a single tick unless you have a robust synchronization mechanism.
  • If you must use multithreading, use a job system or a task graph and ensure that the next tick does not start until the job system has finished.

Code:

`C++// TimeManagerSubsystem.h
#pragma once

include “Subsystems/WorldSubsystem.h”
include “TimeManagerSubsystem.generated.h”

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDayChanged);

UCLASS()
class YOURPROJECT_API UTimeManagerSubsystem : public UWorldSubsystem
{
GENERATED_BODY()

public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Tick(float DeltaTime) override;

void SetTimeScale(float NewTimeScale);
float GetTimeScale() const;

FDateTime GetCurrentGameTime() const;

FOnDayChanged OnDayChanged;

private:
FDateTime CurrentGameTime;
float TimeScale = 1.0f; // Default speed
double LastTickRealTime = 0.0;
double TickDuration = 1.0; // Default tick duration (1 second)
int32 TicksPerDay = 4;
int32 CurrentTickInDay = 0;

void AdvanceGameTime();

};

// TimeManagerSubsystem.cpp
include “TimeManagerSubsystem.h”
include “Engine/World.h”
include “HAL/PlatformTime.h”

void UTimeManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
CurrentGameTime = FDateTime(2025, 1, 1);
LastTickRealTime = FPlatformTime::Seconds();
}

void UTimeManagerSubsystem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

double CurrentRealTime = FPlatformTime::Seconds();
double ElapsedRealTime = CurrentRealTime - LastTickRealTime;

if (ElapsedRealTime >= TickDuration)
{
    AdvanceGameTime();
    LastTickRealTime = CurrentRealTime;
}

}

void UTimeManagerSubsystem::SetTimeScale(float NewTimeScale)
{
TimeScale = NewTimeScale;
// Adjust tick duration based on time scale
TickDuration = 1.0 / TimeScale; // Example: 2x speed → 0.5 second tick
}

float UTimeManagerSubsystem::GetTimeScale() const
{
return TimeScale;
}

FDateTime UTimeManagerSubsystem::GetCurrentGameTime() const
{
return CurrentGameTime;
}

void UTimeManagerSubsystem::AdvanceGameTime()
{
// Perform simulation logic here
// Example: Increment in-game time by one tick
CurrentTickInDay++;

if(CurrentTickInDay >= TicksPerDay)
{
    CurrentGameTime = CurrentGameTime + FTimespan(1, 0, 0); // Add one day
    CurrentTickInDay = 0;
    OnDayChanged.Broadcast();
}

}`

Key Improvements

  • World Subsystem: Provides a clean and persistent location for your time management logic.
  • Real-Time Measurement: Uses’’ for accurate time tracking.
  • Variable ‘’: Allows flexible speed adjustments.
  • Periodic Event Handling: Demonstrates how to trigger events based on in-game time.
  • Clearer Structure: Breaks down the logic into manageable functions.
  • Simulation Syncronization: Emphasizes the importance of ensuring that the simulation is finished before the next tick.

Additional Considerations

  • Saving and loading: Ensure that your time state is properly saved and loaded.
  • UI Integration: Provide UI elements to display the current in-game time and allow players to adjust the time scale.
  • Optimization: Profile your simulation logic to identify performance bottlenecks.
  • Job System/Task Graph: For heavy multithreading, look into Unreal Engine’s job system or task graph.
  • Game Instance: if you need the time to persist between level changes, then the game instance may be a better place to store the time information.

By implementing these guidelines, you can create a robust and efficient custom tick system for your simulation-heavy game in Unreal Engine. I hope this will help you.

AI slop answer with an incomplete explanation and useless code. Should be against the rules to reply with AI.

2 Likes