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.
This commit is contained in:
Noah 2024-04-27 00:10:28 -07:00
parent c4456ac51b
commit 866e5e7fd8
4 changed files with 23 additions and 52 deletions

View File

@ -15,6 +15,12 @@ const (
FormatZipfile // v2: zip archive with external chunks 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. // Numbers.
var ( var (
// Window dimensions. // Window dimensions.

View File

@ -41,52 +41,14 @@ func (d *Drawing) Position() render.Point {
return d.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. // Size returns the Drawing's size.
func (d *Drawing) Size() render.Rect { func (d *Drawing) Size() render.Rect {
return d.size 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. // MoveTo an absolute world value.
//
// NOTE: used only by unit test.
func (d *Drawing) MoveTo(to render.Point) { func (d *Drawing) MoveTo(to render.Point) {
d.point = to d.point = to
} }
// Draw the drawing.
func (d *Drawing) Draw(e render.Engine) {
}

View File

@ -29,9 +29,6 @@ import (
) )
const ( 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 is a time.Millisecond casted to float64.
Millisecond64 = float64(time.Millisecond) Millisecond64 = float64(time.Millisecond)
) )
@ -230,8 +227,8 @@ func (d *Doodle) Run() error {
if !fpsDoNotCap { if !fpsDoNotCap {
elapsed := time.Now().Sub(start) elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond tmp := elapsed / time.Millisecond
if TargetFPS-int(tmp) > 0 { // make sure it won't roll under if balance.TargetClockRate-int(tmp) > 0 { // make sure it won't roll under
delay = uint32(TargetFPS - int(tmp)) delay = uint32(balance.TargetClockRate - int(tmp))
} }
d.Engine.Delay(delay) d.Engine.Delay(delay)
} }

View File

@ -3,6 +3,8 @@ package scripting
import ( import (
"time" "time"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"github.com/dop251/goja" "github.com/dop251/goja"
) )
@ -10,8 +12,8 @@ import (
type Timer struct { type Timer struct {
id int id int
callback goja.Value callback goja.Value
interval time.Duration // milliseconds delay for timeout ticks uint64 // interval (milliseconds) converted into game ticks
next time.Time // scheduled time for next invocation nextTick uint64 // next tick to trigger the callback
repeat bool // for setInterval 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 { func (vm *VM) AddTimer(callback goja.Value, interval int, repeat bool) int {
// Get the next timer ID. The first timer has ID 1. // Get the next timer ID. The first timer has ID 1.
vm.timerLastID++ vm.timerLastID++
id := vm.timerLastID
var (
id = vm.timerLastID
ticks = float64(interval) * (float64(balance.TargetFPS) / 1000)
)
t := &Timer{ t := &Timer{
id: id, id: id,
callback: callback, callback: callback,
interval: time.Duration(interval), ticks: uint64(ticks),
repeat: repeat, repeat: repeat,
} }
t.Schedule() t.Schedule()
@ -71,7 +77,7 @@ func (vm *VM) TickTimer(now time.Time) {
var clear []int var clear []int
for id, timer := range vm.timers { for id, timer := range vm.timers {
if now.After(timer.next) { if shmem.Tick > timer.nextTick {
if function, ok := goja.AssertFunction(timer.callback); ok { if function, ok := goja.AssertFunction(timer.callback); ok {
function(goja.Undefined()) function(goja.Undefined())
} }
@ -104,5 +110,5 @@ func (vm *VM) ClearTimer(id int) {
// Schedule the callback to be run in the future. // Schedule the callback to be run in the future.
func (t *Timer) Schedule() { func (t *Timer) Schedule() {
t.next = time.Now().Add(t.interval * time.Millisecond) t.nextTick = shmem.Tick + t.ticks
} }