Skip to content

Articles ·

Age crops and NPCs on game days in Godot

Pick the right Calendar Time ageing model: discrete date ticks for crops and birthdays, continuous clock seconds for buffs and spoilage, per system.

This recipe ages things on the right clock. Crops and birthdays want calendar days ("takes 3 game-days"); buffs and spoilage want game seconds ("lasts 5 game-minutes"). Calendar Time offers both models — pick per system, not per project — and your game derives its own facts from the clock without ever overriding it.

1. Age crops one stage per game day

Crops want discrete calendar-date ageing. Listen to clock.signal_bus.date_changed and advance growth by the days that passed. One advance can cross several days at once — a sleep-to-morning, or a big timescale step — and event.change is that count, so read it instead of assuming one stage per signal:

func _ready() -> void:
    clock.signal_bus.date_changed.connect(_on_date_changed)

var growth_stage: int = 0
const MAX_STAGE: int = 3

func _on_date_changed(event: DateChangeEvent) -> void:
    # `change` is the signed day span of this advance, not always 1.
    growth_stage = mini(MAX_STAGE, growth_stage + maxi(event.change, 0))
    _update_sprite(growth_stage)

NPC birthdays work the same way: one tick per game-year per character. For a full farm loop built on this tick — soil plots, seasons, save-safe harvests — see the farming system recipe.

2. Age buffs in continuous game seconds

Effects that should scale with clock speed and pause with the clock — potion expiry, cooldowns, food spoilage — belong on the continuous model. AgeService is a per-clock resource that the clock owns and drives from game_seconds_advanced. AgeComponent is the scene node that reads the state it advances, so a timed effect is a component plus your own duration:

var settings := AgeSettings.new()
settings.update_timing = AgeSettings.UpdateTime.GAME_TIME_ELAPSED
settings.interval_unit = AgeSettings.TIME_UNIT.SECOND
settings.intervals_per_age = 1.0 # one age unit == one game second

clock.age_service = AgeService.new(settings, clock)

# The component reads the state the service advances; the preset only seeds
# its starting age, so a fresh buff starts at 0.
var buff := AgeComponent.new(AgePreset.new(), clock)
add_child(buff)

# Later: game-seconds since the buff started, against your duration.
var elapsed := buff.age_state.get_total_elapsed()
var remaining := maxf(0.0, 300.0 - elapsed) # expires at 5 game-minutes

There is no duration field on the component: age_state.current and get_total_elapsed() are elapsed age, and the expiry threshold is game policy you keep next to the effect.

Choosing wrong here is the classic bug: date ticks make buffs survive fast-forwarding, while second-counting makes crops skip stages. The model picker with both APIs is Ageing: Two Time Models.

3. Derive game facts; never override the clock

When systems need "is it night" for decisions — shopkeepers closing, guards changing patrols, encounters rolling — derive a game-owned fact from the clock's authoritative time-of-day. The clock publishes the active TimeOfDay resource on time_of_day_changed; its display_name comes from your calendar (Dawn, Daytime, Dusk, Night in the shipped templates), and what each system counts as night stays yours:

func _ready() -> void:
    clock.signal_bus.time_of_day_changed.connect(_on_time_of_day_changed)

# Shopkeeper policy: dusk counts as closed. Guard policy may differ —
# same clock, different facts, no drift between systems.
func _on_time_of_day_changed(tod: TimeOfDay, _last: TimeOfDay) -> void:
    is_closed = tod != null and tod.display_name in [&"Dusk", &"Night"]

Read the authoritative stage first and keep any fallback strictly weaker, so the clock always wins once it can speak. The full authority pattern is in Game Facts Driven by Clock Time.