-
Notifications
You must be signed in to change notification settings - Fork 25
Implement FLECS-style timer and tick source system #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
csprance
wants to merge
2
commits into
main
Choose a base branch
from
claude/implement-timer-tick-source-system-011CUqrH1Heykr47TkiXYpLP
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| ## AccumulatedTickSource | ||
| ## | ||
| ## Time-based tick source that fires at intervals but returns the actual accumulated time. | ||
| ## | ||
| ## Similar to IntervalTickSource, but returns the actual accumulated delta instead of | ||
| ## the fixed interval. Useful when you need to account for time drift or variable frame rates. | ||
| ## | ||
| ## [b]Example:[/b] | ||
| ## [codeblock] | ||
| ## # In world setup | ||
| ## var physics_tick = AccumulatedTickSource.new() | ||
| ## physics_tick.interval = 0.02 # ~50 FPS | ||
| ## ECS.world.register_tick_source(physics_tick, "physics-tick") | ||
| ## | ||
| ## # In system | ||
| ## class_name PhysicsSystem extends System | ||
| ## | ||
| ## func tick() -> TickSource: | ||
| ## return ECS.world.get_tick_source("physics-tick") | ||
| ## | ||
| ## func process(entities: Array[Entity], components: Array, delta: float) -> void: | ||
| ## # delta will be the actual accumulated time (e.g., 0.021 if slightly behind) | ||
| ## apply_physics(entities, delta) | ||
| ## [/codeblock] | ||
| class_name AccumulatedTickSource | ||
| extends TickSource | ||
|
|
||
| ## The interval in seconds between ticks | ||
| @export var interval: float = 1.0 | ||
|
|
||
| ## Accumulated time since last tick | ||
| var accumulated_time: float = 0.0 | ||
|
|
||
| ## Total number of ticks that have occurred | ||
| var tick_count: int = 0 | ||
|
|
||
|
|
||
| ## Update the tick source with frame delta | ||
| ## Returns the accumulated time when it's time to tick, 0.0 otherwise | ||
| ## Carries forward extra time to prevent drift during lag spikes | ||
| func update(delta: float) -> float: | ||
| accumulated_time += delta | ||
|
|
||
| if accumulated_time >= interval: | ||
| tick_count += 1 | ||
| last_delta = accumulated_time # Return full accumulated time | ||
| accumulated_time -= interval # Carry forward extra time | ||
| else: | ||
| last_delta = 0.0 # No tick this frame | ||
|
|
||
| return last_delta | ||
|
|
||
|
|
||
| ## Reset tick source state | ||
| func reset() -> void: | ||
| super.reset() | ||
| accumulated_time = 0.0 | ||
| tick_count = 0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| ## IntervalTickSource | ||
| ## | ||
| ## Time-based tick source that fires at fixed intervals. | ||
| ## | ||
| ## Returns a fixed interval delta when the accumulated time exceeds the interval. | ||
| ## Useful for systems that need to run at specific time intervals (e.g., every 1 second). | ||
| ## | ||
| ## [b]Example:[/b] | ||
| ## [codeblock] | ||
| ## # In world setup | ||
| ## var spawner_tick = IntervalTickSource.new() | ||
| ## spawner_tick.interval = 1.0 # Tick every second | ||
| ## ECS.world.register_tick_source(spawner_tick, "spawner-tick") | ||
| ## | ||
| ## # In system | ||
| ## class_name SpawnerSystem extends System | ||
| ## | ||
| ## func tick() -> TickSource: | ||
| ## return ECS.world.get_tick_source("spawner-tick") | ||
| ## | ||
| ## func process(entities: Array[Entity], components: Array, delta: float) -> void: | ||
| ## # This runs every 1 second with delta = 1.0 | ||
| ## spawn_enemy() | ||
| ## [/codeblock] | ||
| class_name IntervalTickSource | ||
| extends TickSource | ||
|
|
||
| ## The interval in seconds between ticks | ||
| @export var interval: float = 1.0 | ||
|
|
||
| ## Accumulated time since last tick | ||
| var accumulated_time: float = 0.0 | ||
|
|
||
| ## Total number of ticks that have occurred | ||
| var tick_count: int = 0 | ||
|
|
||
|
|
||
| ## Update the tick source with frame delta | ||
| ## Returns the fixed interval when it's time to tick, 0.0 otherwise | ||
| ## Handles lag spikes by processing all pending intervals in a single frame | ||
| func update(delta: float) -> float: | ||
| accumulated_time += delta | ||
|
|
||
| # Process all pending intervals (handles lag spikes and pauses) | ||
| var ticked := false | ||
| while accumulated_time >= interval: | ||
| tick_count += 1 | ||
| accumulated_time -= interval | ||
| ticked = true | ||
|
|
||
| last_delta = interval if ticked else 0.0 | ||
| return last_delta | ||
|
|
||
|
|
||
| ## Reset tick source state | ||
| func reset() -> void: | ||
| super.reset() | ||
| accumulated_time = 0.0 | ||
| tick_count = 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| ## RateFilterTickSource | ||
| ## | ||
| ## Frame-based tick source that samples another tick source at a specific rate. | ||
| ## | ||
| ## Ticks every Nth time the source ticks, accumulating the delta values. | ||
| ## Useful for creating hierarchical timing systems and deterministic frame-based execution. | ||
| ## | ||
| ## [b]Example:[/b] | ||
| ## [codeblock] | ||
| ## # In world setup | ||
| ## ECS.world.create_interval_tick_source(1.0, "second") | ||
| ## ECS.world.create_rate_filter(60, "second", "minute") # Every 60 seconds | ||
| ## | ||
| ## # In system | ||
| ## class_name AutoSaveSystem extends System | ||
| ## | ||
| ## func tick() -> TickSource: | ||
| ## return ECS.world.get_tick_source("minute") | ||
| ## | ||
| ## func process(entities: Array[Entity], components: Array, delta: float) -> void: | ||
| ## # This runs every 60 seconds | ||
| ## # delta will be the accumulated time from 60 ticks (~60 seconds) | ||
| ## auto_save_game() | ||
| ## [/codeblock] | ||
| ## | ||
| ## [b]Note:[/b] The source tick source is NOT updated by this class - World updates | ||
| ## all tick sources in order, so we just read the source's last_delta from its update. | ||
| class_name RateFilterTickSource | ||
| extends TickSource | ||
|
|
||
| ## The number of source ticks to wait before ticking | ||
| @export var rate: int = 60 | ||
|
|
||
| ## The source tick source to sample (set by World.create_rate_filter) | ||
| @export var source: TickSource | ||
|
|
||
| ## Internal counter for source ticks | ||
| var tick_count: int = 0 | ||
|
|
||
| ## Accumulated delta from source ticks | ||
| var accumulated_delta: float = 0.0 | ||
|
|
||
|
|
||
| ## Update the tick source | ||
| ## Samples the source's last_delta and ticks every Nth source tick | ||
| ## Returns the accumulated delta when it's time to tick, 0.0 otherwise | ||
| func update(delta: float) -> float: | ||
| # NOTE: Source is NOT updated here - World updates all tick sources | ||
| # We just read the source's last_delta from its previous update | ||
|
|
||
| if source.last_delta > 0.0: # Source ticked this frame | ||
| tick_count += 1 | ||
| accumulated_delta += source.last_delta | ||
|
|
||
| if tick_count >= rate: | ||
| tick_count = 0 | ||
| last_delta = accumulated_delta # Return accumulated delta | ||
| accumulated_delta = 0.0 | ||
| else: | ||
| last_delta = 0.0 # Not time to tick yet | ||
| else: | ||
| last_delta = 0.0 # Source didn't tick | ||
|
|
||
| return last_delta | ||
|
|
||
|
|
||
| ## Reset tick source state | ||
| func reset() -> void: | ||
| super.reset() | ||
| tick_count = 0 | ||
| accumulated_delta = 0.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| ## TickSource | ||
| ## | ||
| ## Base class for custom tick sources that control system execution timing. | ||
| ## | ||
| ## Tick sources determine when systems run and what delta value they receive. | ||
| ## The base implementation passes through the frame delta (default behavior). | ||
| ## Override [method update] to create custom timing behaviors. | ||
| ## | ||
| ## [b]Example (Custom tick source):[/b] | ||
| ## [codeblock] | ||
| ## class_name RandomTickSource extends TickSource | ||
| ## | ||
| ## var probability: float = 0.5 | ||
| ## | ||
| ## func update(delta: float) -> float: | ||
| ## if randf() < probability: | ||
| ## last_delta = delta | ||
| ## else: | ||
| ## last_delta = 0.0 # Skip this frame | ||
| ## return last_delta | ||
| ## [/codeblock] | ||
| class_name TickSource | ||
| extends Resource | ||
|
|
||
| ## The delta value from the last update (0.0 = didn't tick this frame) | ||
| var last_delta: float = 0.0 | ||
|
|
||
|
|
||
| ## Called every frame by World.process() | ||
| ## Must set last_delta and return it | ||
| ## [param delta] The frame delta time | ||
| ## [return] The delta value to pass to systems (0.0 to skip this frame) | ||
| func update(delta: float) -> float: | ||
| last_delta = delta # Pass through - override in subclasses | ||
| return last_delta | ||
|
|
||
|
|
||
| ## Reset tick source state | ||
| ## Override this to reset custom state variables | ||
| func reset() -> void: | ||
| last_delta = 0.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.