Library for making endless game-loops, it`s heart of any game engine.
Game loop is endless loop:
for {
cycle()
}
Each cycle contains 4 different steps:
- Tick (fixed update logic). DeltaTime deterministic and always equal of
1s / targetTPS
. Cycle may have more than one tick, in case of lagging - Frame (draw function). Frame used for rendering game state. Count
frame
per1s
is calledFPS
- Tasks task is any additional logic. Tasks will be executed, only when current cycle has free budget. Good example of Task is golang garbage collector
- Throttle not used time in current cycle. This is just sleep call
Every update is deterministic, and deltaTime always fixed and same - independent of hardware.
You can make any game state updates in Tick
function
and draw game state in Frame
function
Executor will run your gameLoop
and automatic collect
and calculate all frame stats, also it will be throttle
processing, when CPU is more powerful than we need in targetTPS
package main
import "github.com/fe3dback/glx-frames/frame"
func main() {
executor := frame.NewExecutor(frame.WithTargetTPS(60))
err := executor.Execute(ctx, update, draw)
// ..
}
func update(st frame.TickStats) error {
// player.velocity += player.acceleration * st.DeltaTime
// player.position += player.velocity * st.DeltaTime
time.Sleep(time.Millisecond * 10)
return nil
}
func draw() error {
// something like:
// renderer.Render(gameWorld)
return nil
}
Optionally stats collector can be used in Executor
Stats collector is just a function func(s Stats)
.
This function will be executed right after each cycle end
frame.NewExecutor(
frame.WithTargetTPS(60),
frame.WithStatsCollector(func(stats frame.Stats) {
// do something with stats
})
)
Stats object contain information about cycle execution and timings
type Stats struct {
// CycleID is number of game loop cycles since game start (this will auto inc to +1 every loop)
CycleID uint64
// target ticks per second (state fixed/physics update per second).
// This is how many state updates game will have per second
TargetTPS int
// maximum calculated FPS that can be theoretically achieved in current CPU
PossibleFPS int
// Rate is always 1s / TargetTPS
Rate time.Duration
Game Timings // game loop timings
Cycle Timings // current cycle timings
Tick Timings // current tick timings
Frame Timings // current frame timings
Tasks Timings // current cycle running tasks timings
ThrottleTime time.Duration // when CPU is more powerful when we need for processing at TargetTPS rate, frame will sleep ThrottleTime in end of current cycle
CurrentTPS int // real counted ticks per second (ticks is fixed/physics update)
CurrentFPS int // real counted frames per second
}
type Timings struct {
Start time.Time
Duration time.Duration
}
You can provide some minor tasks, that will be executed only when cycle have free CPU time.
For example at targetTPS=60
, cycle capacity is 16.6ms
When update/draw logic took 10ms
, we have free 6.6ms
This 6.6ms
will be used for tasks processing
Super useful and required in most cases is garbage collection task, that will process golang GC only when we have free time.
Of course on low-end CPU's, when our TPS
always less that targetTPS
,
it will be executed anyway, at least once in 10 seconds
frame.NewExecutor(
frame.WithTargetTPS(60),
// add unlimited number of tasks
frame.WithTask(
frame.NewTask(
func() {
// try to run every frame
// but not more often that once in 1s
// but at least once in 10 second guaranteed
runtime.GC()
runtime.Gosched()
},
frame.WithRunAtLeastOnceIn(time.Second * 10),
frame.WithRunAtMostOnceIn(time.Second),
frame.WithPriority(TaskPriorityLow),
),
),
)
executor.Execute( .. )
Also it already defined in lib, you can use default task:
frame.NewDefaultTaskGarbageCollect()
You can add any number of tasks, and choose priority from LOW
to HIGH
,
also Executor
will take into account other task properties like LastRunTime
, AvgExecutionTime
and other in priority calculation.
See code in frame/executor_test
- TestTime = 1s
- TicksPerSecond = 24
- TickLatency = 25ms
- FrameLatency = 10ms
Tasks:
- high priority 5ms task (run at least once in second, but try to every 500ms)
- low priority GC (at least once in second, but try to every 100ms)
Color mapping:
- green = tick (update)
- yellow = frame (draw)
- blue = tasks
- white = throttle (sleep)
| -- STATS -- | -- Frame -- |
| elapsed | frame | TPS | FPS | capacity | update | frame | tasks | throttle |
| 0042ms | 001 | 24/24 | 24 | 41ms | 25ms | 10ms | 06ms | 00ms |
| 0084ms | 002 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0125ms | 003 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0166ms | 004 | 24/24 | 24 | 41ms | 25ms | 10ms | 01ms | 04ms |
| 0209ms | 005 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0251ms | 006 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0292ms | 007 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 04ms |
| 0334ms | 008 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0375ms | 009 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0416ms | 010 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0459ms | 011 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 06ms |
| 0500ms | 012 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0542ms | 013 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0584ms | 014 | 24/24 | 24 | 41ms | 25ms | 10ms | 05ms | 00ms |
| 0626ms | 015 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0667ms | 016 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 04ms |
| 0709ms | 017 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0750ms | 018 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0792ms | 019 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0833ms | 020 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0875ms | 021 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 0916ms | 022 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 04ms |
| 0958ms | 023 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
| 1000ms | 024 | 24/24 | 24 | 41ms | 25ms | 10ms | 00ms | 05ms |
--- PASS: TestExecutor_Execute (1.00s)