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
)
// 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.

View File

@ -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) {
}

View File

@ -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)
}

View File

@ -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
}