WASM Event Queue

* Refactor the event system in the WASM render engine to serialize the
  async JavaScript events into a channel, so that queued events are read
  off serially in the main loop similar to SDL. This fixes keyboard
  input issues, altho if you type really fast some input keys get lost.
physics
Noah 2019-06-26 20:33:24 -07:00
parent af67b20d9b
commit c5c85330de
7 changed files with 175 additions and 35 deletions

View File

@ -19,6 +19,11 @@ type Engine struct {
// Private fields.
events *events.State
running bool
// Event channel. WASM subscribes to events asynchronously using the
// JavaScript APIs, whereas SDL2 polls the event queue which orders them
// all up for processing. This channel will order and queue the events.
queue chan Event
}
// New creates the Canvas Engine.
@ -31,6 +36,7 @@ func New(canvasID string) (*Engine, error) {
events: events.New(),
width: canvas.ClientW(),
height: canvas.ClientH(),
queue: make(chan Event, 1024),
}
return engine, nil
@ -41,9 +47,10 @@ func (e *Engine) WindowSize() (w, h int) {
return e.canvas.ClientW(), e.canvas.ClientH()
}
// GetTicks returns the current tick count.
// GetTicks returns the number of milliseconds since the engine started.
func (e *Engine) GetTicks() uint32 {
return e.ticks
ms := time.Since(e.startTime) * time.Millisecond
return uint32(ms)
}
// TO BE IMPLEMENTED...

View File

@ -7,10 +7,35 @@ import (
"git.kirsle.net/apps/doodle/pkg/log"
)
// EventClass to categorize JavaScript events.
type EventClass int
// EventClass values.
const (
MouseEvent EventClass = iota
KeyEvent
ResizeEvent
)
// Event object queues up asynchronous JavaScript events to be processed linearly.
type Event struct {
Name string // mouseup, keydown, etc.
Class EventClass
// Mouse events.
X int
Y int
LeftClick bool
RightClick bool
// Key events.
KeyName string
State bool
Repeat bool
}
// AddEventListeners sets up bindings to collect events from the browser.
func (e *Engine) AddEventListeners() {
s := e.events
// Mouse movement.
e.canvas.Value.Call(
"addEventListener",
@ -21,8 +46,12 @@ func (e *Engine) AddEventListeners() {
y = args[0].Get("pageY").Int()
)
s.CursorX.Push(int32(x))
s.CursorY.Push(int32(y))
e.queue <- Event{
Name: "mousemove",
Class: MouseEvent,
X: x,
Y: y,
}
return nil
}),
)
@ -42,9 +71,6 @@ func (e *Engine) AddEventListeners() {
log.Info("Clicked at %d,%d", x, y)
s.CursorX.Push(int32(x))
s.CursorY.Push(int32(y))
// Is a mouse button pressed down?
checkDown := func(number int) bool {
if which == number {
@ -53,8 +79,14 @@ func (e *Engine) AddEventListeners() {
return false
}
s.Button1.Push(checkDown(1))
s.Button2.Push(checkDown(3))
e.queue <- Event{
Name: ev,
Class: MouseEvent,
X: x,
Y: y,
LeftClick: checkDown(1),
RightClick: checkDown(3),
}
return false
}),
)
@ -71,32 +103,110 @@ func (e *Engine) AddEventListeners() {
)
// Keyboard keys
// js.Global().Get("document").Call(
// "addEventListener",
// "keydown",
// js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// log.Info("key: %+v", args)
// var (
// event = args[0]
// charCode = event.Get("charCode")
// key = event.Get("key").String()
// )
//
// switch key {
// case "Enter":
// s.EnterKey.Push(true)
// // default:
// // s.KeyName.Push(key)
// }
//
// log.Info("keypress: code=%s key=%s", charCode, key)
//
// return nil
// }),
// )
for _, ev := range []string{"keydown", "keyup"} {
ev := ev
js.Global().Get("document").Call(
"addEventListener",
ev,
js.FuncOf(func(this js.Value, args []js.Value) interface{} {
var (
event = args[0]
key = event.Get("key").String()
repeat = event.Get("repeat").Bool()
pressed = ev == "keydown"
)
if key == "F3" {
args[0].Call("preventDefault")
}
e.queue <- Event{
Name: ev,
Class: KeyEvent,
KeyName: key,
Repeat: repeat,
State: pressed,
}
return nil
}),
)
}
}
// PollEvent returns the next event in the queue, or null.
func (e *Engine) PollEvent() *Event {
select {
case event := <-e.queue:
return &event
default:
return nil
}
return nil
}
// Poll for events.
func (e *Engine) Poll() (*events.State, error) {
s := e.events
if e.events.EnterKey.Now {
log.Info("saw enter key here, good")
}
if e.events.KeyName.Now == "h" {
log.Info("saw letter h here, good")
}
for event := e.PollEvent(); event != nil; event = e.PollEvent() {
switch event.Class {
case MouseEvent:
s.CursorX.Push(int32(event.X))
s.CursorY.Push(int32(event.Y))
s.Button1.Push(event.LeftClick)
s.Button2.Push(event.RightClick)
case KeyEvent:
switch event.KeyName {
case "Enter":
if event.Repeat {
continue
}
if event.State {
s.EnterKey.Push(true)
}
case "F3":
if event.State {
s.KeyName.Push("F3")
}
case "ArrowUp":
s.Up.Push(event.State)
case "ArrowLeft":
s.Left.Push(event.State)
case "ArrowRight":
s.Right.Push(event.State)
case "ArrowDown":
s.Down.Push(event.State)
case "Shift":
s.ShiftActive.Push(event.State)
continue
case "Alt":
case "Control":
continue
case "Backspace":
if event.State {
s.KeyName.Push(`\b`)
}
default:
log.Info("default handler, push key %s", event.KeyName)
if event.State {
s.KeyName.Push(event.KeyName)
} else {
s.KeyName.Push("")
}
}
log.Info("event end, stored key=%s", s.KeyName.Now)
}
}
return e.events, nil
}

View File

@ -13,7 +13,7 @@ var (
DebugWindowEvents = false
DebugMouseEvents = false
DebugClickEvents = false
DebugKeyEvents = false
DebugKeyEvents = true
)
// Poll for events.

View File

@ -0,0 +1,6 @@
package balance
var (
// Disable chunk texture caching (SLOW!)
DisableChunkTextureCache = false
)

View File

@ -105,9 +105,18 @@ func (d *Doodle) Run() error {
start := time.Now() // Record how long this frame took.
d.ticks++
if d.ticks%100 == 0 {
log.Info("...tick...%d", d.ticks)
}
// Poll for events.
ev, err := d.Engine.Poll()
if ev.EnterKey.Now {
log.Info("MainLoop sees enter key now")
}
if ev.KeyName.Now != "" {
log.Info("MainLoop sees key %s", ev.KeyName.Now)
}
if err != nil {
log.Error("event poll error: %s", err)
d.running = false

View File

@ -74,6 +74,8 @@ func NewChunk() *Chunk {
// Texture will return a cached texture for the rendering engine for this
// chunk's pixel data. If the cache is dirty it will be rebuilt in this func.
//
// Texture cache can be disabled with balance.DisableChunkTextureCache=true.
func (c *Chunk) Texture(e render.Engine) render.Texturer {
if c.texture == nil || c.dirty {
// Generate the normal bitmap and one with a color mask if applicable.

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/render/canvas"
doodle "git.kirsle.net/apps/doodle/pkg"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/log"
)
@ -16,6 +17,9 @@ func main() {
fmt.Printf("Hello world\n")
// testRawCanvas()
// Enable workarounds.
balance.DisableChunkTextureCache = true
// HTML5 Canvas engine.
engine, _ := canvas.New("canvas")
engine.AddEventListeners()
@ -23,6 +27,8 @@ func main() {
game := doodle.New(true, engine)
game.SetupEngine()
doodle.DebugOverlay = true
// Manually inform Doodle of the canvas size since it can't control
// the size on its own.
w, h := engine.WindowSize()