From 866e5e7fd8d18a1a1e6a83028c244824ed6e8189 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 27 Apr 2024 00:10:28 -0700 Subject: [PATCH] Deterministic JavaScript intervals + Code Cleanup * Remove several unused functions in doodad.Drawing (velocity, acceleration, grounded, etc.) - uix.Actor is where these are actually managed. * In the JavaScript API, setTimeout() and setInterval() will translate the milliseconds from wallclock time into a fixed number of game ticks to match the target frame rate for better deterministic timing. --- pkg/balance/numbers.go | 6 ++++++ pkg/doodads/drawing.go | 42 ++--------------------------------------- pkg/doodle.go | 7 ++----- pkg/scripting/timers.go | 20 +++++++++++++------- 4 files changed, 23 insertions(+), 52 deletions(-) diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index df31a17..e8ca43e 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -15,6 +15,12 @@ const ( FormatZipfile // v2: zip archive with external chunks ) +// Target frame rate for the game (also ticks per second for logic). +const ( + TargetFPS = 60 + TargetClockRate = 1000 / 60 // wallclock milliseconds per tick +) + // Numbers. var ( // Window dimensions. diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index f57a282..5cd6082 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -41,52 +41,14 @@ func (d *Drawing) Position() render.Point { return d.point } -// Velocity returns the Drawing's velocity. -func (d *Drawing) Velocity() render.Point { - return d.velocity -} - -// SetVelocity to set the speed. -func (d *Drawing) SetVelocity(v render.Point) { - d.velocity = v -} - -// Acceleration returns the Drawing's acceleration. -func (d *Drawing) Acceleration() int { - return d.accel -} - -// SetAcceleration to set the acceleration. -func (d *Drawing) SetAcceleration(v int) { - d.accel = v -} - // Size returns the Drawing's size. func (d *Drawing) Size() render.Rect { return d.size } -// Grounded returns whether the Drawing is standing on solid ground. -func (d *Drawing) Grounded() bool { - return d.grounded -} - -// SetGrounded sets the grounded state. -func (d *Drawing) SetGrounded(v bool) { - d.grounded = v -} - -// MoveBy a relative value. -func (d *Drawing) MoveBy(by render.Point) { - d.point.Add(by) -} - // MoveTo an absolute world value. +// +// NOTE: used only by unit test. func (d *Drawing) MoveTo(to render.Point) { d.point = to } - -// Draw the drawing. -func (d *Drawing) Draw(e render.Engine) { - -} diff --git a/pkg/doodle.go b/pkg/doodle.go index dd30904..720bf7c 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -29,9 +29,6 @@ import ( ) const ( - // TargetFPS is the frame rate to cap the game to. - TargetFPS = 1000 / 60 // 60 FPS - // Millisecond64 is a time.Millisecond casted to float64. Millisecond64 = float64(time.Millisecond) ) @@ -230,8 +227,8 @@ func (d *Doodle) Run() error { if !fpsDoNotCap { elapsed := time.Now().Sub(start) tmp := elapsed / time.Millisecond - if TargetFPS-int(tmp) > 0 { // make sure it won't roll under - delay = uint32(TargetFPS - int(tmp)) + if balance.TargetClockRate-int(tmp) > 0 { // make sure it won't roll under + delay = uint32(balance.TargetClockRate - int(tmp)) } d.Engine.Delay(delay) } diff --git a/pkg/scripting/timers.go b/pkg/scripting/timers.go index 135e1b6..f01ab3e 100644 --- a/pkg/scripting/timers.go +++ b/pkg/scripting/timers.go @@ -3,6 +3,8 @@ package scripting import ( "time" + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" + "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" "github.com/dop251/goja" ) @@ -10,9 +12,9 @@ import ( type Timer struct { id int callback goja.Value - interval time.Duration // milliseconds delay for timeout - next time.Time // scheduled time for next invocation - repeat bool // for setInterval + ticks uint64 // interval (milliseconds) converted into game ticks + nextTick uint64 // next tick to trigger the callback + repeat bool // for setInterval } /* @@ -47,12 +49,16 @@ AddTimer loads timeouts and intervals into the VM's memory and returns the ID. func (vm *VM) AddTimer(callback goja.Value, interval int, repeat bool) int { // Get the next timer ID. The first timer has ID 1. vm.timerLastID++ - id := vm.timerLastID + + var ( + id = vm.timerLastID + ticks = float64(interval) * (float64(balance.TargetFPS) / 1000) + ) t := &Timer{ id: id, callback: callback, - interval: time.Duration(interval), + ticks: uint64(ticks), repeat: repeat, } t.Schedule() @@ -71,7 +77,7 @@ func (vm *VM) TickTimer(now time.Time) { var clear []int for id, timer := range vm.timers { - if now.After(timer.next) { + if shmem.Tick > timer.nextTick { if function, ok := goja.AssertFunction(timer.callback); ok { function(goja.Undefined()) } @@ -104,5 +110,5 @@ func (vm *VM) ClearTimer(id int) { // Schedule the callback to be run in the future. func (t *Timer) Schedule() { - t.next = time.Now().Add(t.interval * time.Millisecond) + t.nextTick = shmem.Tick + t.ticks }