Integrating WorldTime in a C# Godot Game
How to wire the WorldTime plugin into a game project using the addon + templates pattern.
Installation
1. Symlink the addon
# Linux / macOS
ln -s /path/to/plugins/WorldTime/demos/addons/world_time \
/path/to/game/Godot/addons/world_time
# Windows (Admin CMD)
mklink /j game\Godot\addons\world_time plugins\WorldTime\demos\addons\world_time
This makes [GlobalClass] types (WorldTimeCalendar, TimeSignalBus, etc.) available in the Godot inspector.
2. Copy templates (optional)
cp -r plugins/WorldTime/demos/templates/world_time/ \
game/Godot/templates/world_time/
Templates are copy-and-customize — modify them for your game's UI layout.
3. Compile override
If your game .csproj has <Compile Remove="addons\**\*.cs" />, add:
<Compile Include="addons\world_time\**\*.cs" />
Architecture
Game Bootstrap
└── WorldTimeBootstrap (RefCounted, owns everything)
├── TimeProvider — read-only time queries
├── TimeSignalBus — Godot signal bridge
└── WorldTimeEcsRuntime — ECS-backed time simulation
└── WorldTimeAdvanceSystem — advances clock each tick
Key types:
| Type | Role |
|---|---|
WorldTimeBootstrap |
Entry point — creates runtime, wires provider + signal bus |
TimeProvider |
Read-only: CurrentDate, TimeOfDay, IsPaused, TotalGameSeconds |
TimeSignalBus |
Bridges Core C# events → Godot [Signal]s |
WorldTimeEcsRuntime |
ECS world with time state + advance system |
WorldTimeCalendar |
[GlobalClass] Resource for inspector calendar config |
Quick Start (Code-Only)
The simplest integration — no scenes, no inspector wiring:
using Godot;
using MoonBark.CalendarTime.Godot;
using MoonBark.CalendarTime.Types;
public partial class GameRoot : Node
{
private WorldTimeBootstrap? _worldTime;
public override void _Ready()
{
var calendar = GameCalendar.Default12MonthCalendar(startYear: 1);
_worldTime = WorldTimeBootstrap.CreateStandalone(calendar);
// Listen for day changes
_worldTime.Provider.OnDateChanged += (newDate, oldDate) =>
GD.Print($"Day {oldDate.Day} → Day {newDate.Day}");
// Listen for time-of-day changes
_worldTime.SignalBus.OnTimeOfDayChanged += (newTime, prevTime) =>
GD.Print($"Time: {prevTime.Hours}:00 → {newTime.Hours}:00");
}
public override void _Process(double delta)
{
// Advance time each frame (unscaled real seconds)
_worldTime?.Advance((float)delta);
}
public override void _ExitTree()
{
(_worldTime as IDisposable)?.Dispose();
}
}
Quick Start (With Templates)
Use the provided template scenes for UI:
using Godot;
using MoonBark.CalendarTime.Godot;
using MoonBark.CalendarTime.Godot.UI;
using MoonBark.CalendarTime.Types;
public partial class GameRoot : Node
{
private WorldTimeBootstrap? _worldTime;
[Export] public CalendarDisplay? CalendarDisplay { get; set; }
[Export] public Label? TimeLabel { get; set; }
public override void _Ready()
{
var calendar = GameCalendar.Default12MonthCalendar(startYear: 1);
_worldTime = WorldTimeBootstrap.CreateStandalone(calendar);
// Wire template nodes
if (CalendarDisplay != null)
CalendarDisplay.Provider = _worldTime.Provider;
_worldTime.Provider.OnTimeAdvanced += OnTimeAdvanced;
}
private void OnTimeAdvanced(float deltaMinutes, float currentTime, uint dayNumber, uint tickCount)
{
if (TimeLabel == null) return;
var time = _worldTime!.Provider.TimeOfDay;
var h = (int)time;
var m = (int)((time - h) * 60);
TimeLabel.Text = $"{h:D2}:{m:D2}";
}
public override void _Process(double delta)
{
_worldTime?.Advance((float)delta);
}
public override void _ExitTree()
{
(_worldTime as IDisposable)?.Dispose();
}
}
Using Templates
Template .tscn files are in templates/world_time/ui/. Each wraps a C# script:
| Template | Script | Purpose |
|---|---|---|
date_time_display.tscn |
DateTimeDisplay.cs |
Shows formatted time + date |
calendar_display.tscn |
CalendarDisplay.cs |
Full calendar with month tabs |
time_control_panel.tscn |
TimeControlPanelNode.cs |
Pause / Play / Fast-Forward buttons |
time_of_day_light.tscn |
TimeOfDayLightNode.cs |
Binary day/night light |
Wiring templates in code
// After creating WorldTimeBootstrap:
CalendarDisplay.Provider = _worldTime.Provider;
DateTimeDisplay.Provider = _worldTime.Provider;
TimeControlPanel.PauseRequested += () => _worldTime.Pause();
TimeControlPanel.ResumeRequested += () => _worldTime.Resume();
TimeControlPanel.SpeedChanged += multiplier => _worldTime.SetTimeScale(multiplier);
Wiring templates in the inspector
Template nodes have [Export] properties. Assign TimeProvider in the inspector:
- Instance the template scene in your scene tree
- Select the node
- In Inspector, assign the
Providerfield
Note: TimeProvider is RefCounted (not Resource), so it cannot be assigned via inspector drag-and-drop. Use code wiring or the WorldTimeBootstrap pattern instead.
Calendar Configuration
Via code (recommended for games)
var calendar = new GameCalendar
{
DisplayName = "Farming Calendar",
DaysPerWeek = 7,
StartDayOfWeekIndex = 0
};
var year = new GameYear();
year.Months.Add(new GameMonth(28, "Early Spring"));
year.Months.Add(new GameMonth(28, "Late Spring"));
year.Months.Add(new GameMonth(28, "Early Summer"));
calendar.Years.Add(year);
calendar.TimeScale = new TimeScale(60, 60, 24, 720f); // 2 real min = 1 game day
var bootstrap = WorldTimeBootstrap.CreateStandalone(calendar);
Via inspector (using addon resources)
- Create a
WorldTimeCalendarresource in the inspector - Add
WorldTimeYearentries to theYearsarray - Add
WorldTimeMonthentries to each year'sMonthsarray - Convert to Core:
var coreCalendar = calendarResource.ToCore();
Pre-built calendars
// Standard 12-month, 365-day calendar
var calendar = GameCalendar.Default12MonthCalendar(startYear: 1);
// Custom time scale: 1 real second = 1 game hour
calendar.TimeScale = TimeScale.OneHourPerSecond();
// Custom time scale: 2 real minutes = 1 game day
calendar.TimeScale = TimeScale.TwoMinutesPerDay();
Time Events
C# events (on TimeProvider)
_provider.OnTimeOfDayChanged += (newTime, prevTime) => { };
_provider.OnDateChanged += (newDate, oldDate) => { };
_provider.OnTimeAdvanced += (deltaMinutes, currentTime, dayNumber, tickCount) => { };
Godot signals (on TimeSignalBus)
_signalBus.OnTimeAdvanced += evt => { };
_signalBus.OnDateChanged += evt => { };
_signalBus.OnTimeOfDayChanged += (newTime, prevTime) => { };
// Or connect via Godot signal system (GDScript compatible)
_signalBus.Connect(nameof(TimeSignalBus.TimeAdvancedSignal), Callable.From(...));
Pause and Speed Control
_worldTime.Pause();
_worldTime.Resume();
// Set speed: 1 real second = 60 game seconds (1 game minute)
_worldTime.SetTimeScale(60f);
// Set speed: 1 real second = 3600 game seconds (1 game hour)
_worldTime.SetTimeScale(3600f);
Save/Load
// Save
string json = _worldTime.SaveToJson();
FileAccess.WriteAllText("user://save.json", json);
// Load
string json = FileAccess.ReadAllText("user://save.json");
_worldTime.LoadFromJson(json);
The save includes AccumulatedRealMicroseconds (exact int64) + calendar state. ElapsedGameSeconds (float) is derived on load.
Fixed-Step Simulation (Production)
For deterministic simulation, use a fixed-step timer instead of _Process(delta):
public const float FixedStepSeconds = 1f / 60f;
private float _accumulator;
public override void _Process(double delta)
{
_accumulator += (float)delta;
while (_accumulator >= FixedStepSeconds)
{
_worldTime!.Advance(FixedStepSeconds);
_accumulator -= FixedStepSeconds;
}
}
This avoids Engine.TimeScale compounding and ensures deterministic tick counts.
Engine.TimeScale Warning
Do NOT use Engine.TimeScale to control WorldTime speed. Godot's _Process(delta) already multiplies delta by Engine.TimeScale, which compounds with GameSecondsPerRealSecond:
gameDelta = (realWall × Engine.TimeScale) × GameSecondsPerRealSecond
Use GameSecondsPerRealSecond or tick-count scaling instead. Keep Engine.TimeScale = 1.0.
Reference
- Godot Delta Contract —
Engine.TimeScaletraps, fixed-step pattern - Time Scaling Consumption — how to consume time correctly
- Saving & Loading — serialization patterns
- Time Signal Bus — why
TimeSignalBusis a Resource