From d1b9a0a9a14f0f2b0d36c786f66aacc9e71043c1 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 16 Dec 2022 18:34:14 +0900 Subject: [PATCH] internal/ui: freeze the input state for each frame After this change, the input APIs will return more consistent results for one frame. Closes #2496 --- gameforui.go | 4 +- genkeys.go | 1 + input.go | 126 ++++++++++++++---- internal/ui/context.go | 8 +- internal/ui/input.go | 69 ++++++++++ internal/ui/input_glfw.go | 194 +++++---------------------- internal/ui/input_js.go | 219 ++++++++----------------------- internal/ui/input_mobile.go | 96 ++++---------- internal/ui/input_nintendosdk.go | 74 ++--------- internal/ui/keys.go | 1 + internal/ui/ui.go | 12 -- internal/ui/ui_glfw.go | 25 ++-- internal/ui/ui_js.go | 36 +++-- internal/ui/ui_mobile.go | 39 +++--- internal/ui/ui_nintendosdk.go | 17 +-- mobile/ebitenmobileview/input.go | 8 +- 16 files changed, 362 insertions(+), 567 deletions(-) create mode 100644 internal/ui/input.go diff --git a/gameforui.go b/gameforui.go index 942680357..c6fdc6f5e 100644 --- a/gameforui.go +++ b/gameforui.go @@ -147,7 +147,9 @@ func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float6 return float64(sw), float64(sh) } -func (g *gameForUI) Update() error { +func (g *gameForUI) Update(inputState ui.InputState) error { + theInputState.set(inputState) + if err := g.game.Update(); err != nil { return err } diff --git a/genkeys.go b/genkeys.go index bcaa22afd..22f29faff 100644 --- a/genkeys.go +++ b/genkeys.go @@ -485,6 +485,7 @@ const ( KeyReserved1 KeyReserved2 KeyReserved3 + KeyMax = KeyReserved3 ) func (k Key) String() string { diff --git a/input.go b/input.go index b3fce3b16..bb25b95e6 100644 --- a/input.go +++ b/input.go @@ -15,6 +15,8 @@ package ebiten import ( + "sync" + "github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" "github.com/hajimehoshi/ebiten/v2/internal/ui" @@ -35,7 +37,7 @@ import ( // // Keyboards don't work on iOS yet (#1090). func AppendInputChars(runes []rune) []rune { - return ui.Get().Input().AppendInputChars(runes) + return theInputState.appendInputChars(runes) } // InputChars return "printable" runes read from the keyboard at the time update is called. @@ -64,29 +66,7 @@ func InputChars() []rune { // // Keyboards don't work on iOS yet (#1090). func IsKeyPressed(key Key) bool { - if !key.isValid() { - return false - } - - var keys []ui.Key - switch key { - case KeyAlt: - keys = []ui.Key{ui.KeyAltLeft, ui.KeyAltRight} - case KeyControl: - keys = []ui.Key{ui.KeyControlLeft, ui.KeyControlRight} - case KeyShift: - keys = []ui.Key{ui.KeyShiftLeft, ui.KeyShiftRight} - case KeyMeta: - keys = []ui.Key{ui.KeyMetaLeft, ui.KeyMetaRight} - default: - keys = []ui.Key{ui.Key(key)} - } - for _, k := range keys { - if ui.Get().Input().IsKeyPressed(k) { - return true - } - } - return false + return theInputState.isKeyPressed(key) } // CursorPosition returns a position of a mouse cursor relative to the game screen (window). The cursor position is @@ -98,7 +78,7 @@ func IsKeyPressed(key Key) bool { // // CursorPosition is concurrent-safe. func CursorPosition() (x, y int) { - return ui.Get().Input().CursorPosition() + return theInputState.cursorPosition() } // Wheel returns x and y offsets of the mouse wheel or touchpad scroll. @@ -106,7 +86,7 @@ func CursorPosition() (x, y int) { // // Wheel is concurrent-safe. func Wheel() (xoff, yoff float64) { - return ui.Get().Input().Wheel() + return theInputState.wheel() } // IsMouseButtonPressed returns a boolean indicating whether mouseButton is pressed. @@ -116,7 +96,7 @@ func Wheel() (xoff, yoff float64) { // // IsMouseButtonPressed is concurrent-safe. func IsMouseButtonPressed(mouseButton MouseButton) bool { - return ui.Get().Input().IsMouseButtonPressed(mouseButton) + return theInputState.isMouseButtonPressed(mouseButton) } // GamepadID represents a gamepad's identifier. @@ -377,7 +357,7 @@ type TouchID = ui.TouchID // // AppendTouchIDs is concurrent-safe. func AppendTouchIDs(touches []TouchID) []TouchID { - return ui.Get().Input().AppendTouchIDs(touches) + return theInputState.appendTouchIDs(touches) } // TouchIDs returns the current touch states. @@ -393,5 +373,93 @@ func TouchIDs() []TouchID { // // TouchPosition is cuncurrent-safe. func TouchPosition(id TouchID) (int, int) { - return ui.Get().Input().TouchPosition(id) + return theInputState.touchPosition(id) +} + +var theInputState inputState + +type inputState struct { + state ui.InputState + m sync.Mutex +} + +func (i *inputState) set(inputState ui.InputState) { + i.m.Lock() + defer i.m.Unlock() + i.state = inputState +} + +func (i *inputState) appendInputChars(runes []rune) []rune { + i.m.Lock() + defer i.m.Unlock() + return append(runes, i.state.Runes[:i.state.RunesCount]...) +} + +func (i *inputState) isKeyPressed(key Key) bool { + if !key.isValid() { + return false + } + + i.m.Lock() + defer i.m.Unlock() + + switch key { + case KeyAlt: + return i.state.KeyPressed[ui.KeyAltLeft] && i.state.KeyPressed[ui.KeyAltRight] + case KeyControl: + return i.state.KeyPressed[ui.KeyControlLeft] && i.state.KeyPressed[ui.KeyControlRight] + case KeyShift: + return i.state.KeyPressed[ui.KeyShiftLeft] && i.state.KeyPressed[ui.KeyShiftRight] + case KeyMeta: + return i.state.KeyPressed[ui.KeyMetaLeft] && i.state.KeyPressed[ui.KeyMetaRight] + default: + return i.state.KeyPressed[ui.Key(key)] + } +} + +func (i *inputState) cursorPosition() (int, int) { + i.m.Lock() + defer i.m.Unlock() + return i.state.CursorX, i.state.CursorY +} + +func (i *inputState) wheel() (float64, float64) { + i.m.Lock() + defer i.m.Unlock() + return i.state.WheelX, i.state.WheelY +} + +func (i *inputState) isMouseButtonPressed(mouseButton MouseButton) bool { + i.m.Lock() + defer i.m.Unlock() + return i.state.MouseButtonPressed[mouseButton] +} + +func (i *inputState) appendTouchIDs(touches []TouchID) []TouchID { + i.m.Lock() + defer i.m.Unlock() + + for _, t := range i.state.Touches { + if !t.Valid { + continue + } + touches = append(touches, t.ID) + } + return touches +} + +func (i *inputState) touchPosition(id TouchID) (int, int) { + i.m.Lock() + defer i.m.Unlock() + + for _, t := range i.state.Touches { + if !t.Valid { + continue + } + if id != t.ID { + continue + } + return t.X, t.Y + } + return 0, 0 } diff --git a/internal/ui/context.go b/internal/ui/context.go index e43366d74..ead3cbc13 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -35,7 +35,7 @@ type Game interface { NewOffscreenImage(width, height int) *Image NewScreenImage(width, height int) *Image Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64) - Update() error + Update(InputState) error DrawOffscreen() error DrawFinalScreen(scale, offsetX, offsetY float64) } @@ -89,7 +89,9 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update return err } - ui.beginFrame() + // Read the input state and use it for one frame to give a consistent result for one frame (#2496). + var inputState InputState + ui.beginFrame(&inputState) defer ui.endFrame() // The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589) @@ -126,7 +128,7 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update if err := hooks.RunBeforeUpdateHooks(); err != nil { return err } - if err := c.game.Update(); err != nil { + if err := c.game.Update(inputState); err != nil { return err } // Catch the error that happened at (*Image).At. diff --git a/internal/ui/input.go b/internal/ui/input.go new file mode 100644 index 000000000..ebe5fd9fd --- /dev/null +++ b/internal/ui/input.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ui + +import ( + "unicode" +) + +type MouseButton int + +const ( + MouseButton0 MouseButton = iota // The 'left' button + MouseButton1 // The 'right' button + MouseButton2 // The 'middle' button + MouseButton3 // The additional button (usually browser-back) + MouseButton4 // The additional button (usually browser-forward) + MouseButtonMax = MouseButton4 +) + +type TouchID int + +type Touch struct { + Valid bool + ID TouchID + X int + Y int +} + +type InputState struct { + KeyPressed [KeyMax + 1]bool + MouseButtonPressed [MouseButtonMax + 1]bool + CursorX int + CursorY int + WheelX float64 + WheelY float64 + Touches [16]Touch + Runes [16]rune + RunesCount int +} + +func (i *InputState) resetForFrame() { + i.WheelX = 0 + i.WheelY = 0 + i.RunesCount = 0 +} + +func (i *InputState) appendRune(r rune) { + if !unicode.IsPrint(r) { + return + } + if i.RunesCount >= len(i.Runes) { + return + } + + i.Runes[i.RunesCount] = r + i.RunesCount++ +} diff --git a/internal/ui/input_glfw.go b/internal/ui/input_glfw.go index bf119e0f0..411d98b96 100644 --- a/internal/ui/input_glfw.go +++ b/internal/ui/input_glfw.go @@ -18,136 +18,11 @@ package ui import ( "math" - "sync" - "unicode" "github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/glfw" ) -type Input struct { - keyPressed map[glfw.Key]bool - mouseButtonPressed map[glfw.MouseButton]bool - onceCallback sync.Once - scrollX float64 - scrollY float64 - cursorX int - cursorY int - touches map[TouchID]pos // TODO: Implement this (#417) - runeBuffer []rune - ui *userInterfaceImpl -} - -type pos struct { - X int - Y int -} - -func (i *Input) CursorPosition() (x, y int) { - if !i.ui.isRunning() { - return 0, 0 - } - - i.ui.m.RLock() - defer i.ui.m.RUnlock() - return i.cursorX, i.cursorY -} - -func (i *Input) AppendTouchIDs(touchIDs []TouchID) []TouchID { - if !i.ui.isRunning() { - return nil - } - - i.ui.m.RLock() - defer i.ui.m.RUnlock() - for id := range i.touches { - touchIDs = append(touchIDs, id) - } - return touchIDs -} - -func (i *Input) TouchPosition(id TouchID) (x, y int) { - if !i.ui.isRunning() { - return 0, 0 - } - - i.ui.m.RLock() - defer i.ui.m.RUnlock() - for tid, pos := range i.touches { - if id == tid { - return pos.X, pos.Y - } - } - return 0, 0 -} - -func (i *Input) IsKeyPressed(key Key) bool { - if !i.ui.isRunning() { - return false - } - - i.ui.m.Lock() - defer i.ui.m.Unlock() - - gk, ok := uiKeyToGLFWKey[key] - if !ok { - return false - } - return i.keyPressed[gk] -} - -func (i *Input) AppendInputChars(runes []rune) []rune { - if !i.ui.isRunning() { - return nil - } - - i.ui.m.RLock() - defer i.ui.m.RUnlock() - return append(runes, i.runeBuffer...) -} - -func (i *Input) resetForTick() { - if !i.ui.isRunning() { - return - } - - i.ui.m.Lock() - defer i.ui.m.Unlock() - i.runeBuffer = i.runeBuffer[:0] - i.scrollX, i.scrollY = 0, 0 -} - -func (i *Input) IsMouseButtonPressed(button MouseButton) bool { - if !i.ui.isRunning() { - return false - } - - i.ui.m.Lock() - defer i.ui.m.Unlock() - if i.mouseButtonPressed == nil { - i.mouseButtonPressed = map[glfw.MouseButton]bool{} - } - for gb, b := range glfwMouseButtonToMouseButton { - if b != button { - continue - } - if i.mouseButtonPressed[gb] { - return true - } - } - return false -} - -func (i *Input) Wheel() (xoff, yoff float64) { - if !i.ui.isRunning() { - return 0, 0 - } - - i.ui.m.RLock() - defer i.ui.m.RUnlock() - return i.scrollX, i.scrollY -} - var glfwMouseButtonToMouseButton = map[glfw.MouseButton]MouseButton{ glfw.MouseButtonLeft: MouseButton0, glfw.MouseButtonMiddle: MouseButton1, @@ -156,53 +31,44 @@ var glfwMouseButtonToMouseButton = map[glfw.MouseButton]MouseButton{ glfw.MouseButton4: MouseButton4, } -// update must be called from the main thread. -func (i *Input) update(window *glfw.Window, context *context) error { - i.ui.m.Lock() - defer i.ui.m.Unlock() +func (u *userInterfaceImpl) registerInputCallbacks() { + u.window.SetCharModsCallback(glfw.ToCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) { + // As this function is called from GLFW callbacks, the current thread is main. + u.m.Lock() + defer u.m.Unlock() + u.inputState.appendRune(char) + })) + u.window.SetScrollCallback(glfw.ToScrollCallback(func(w *glfw.Window, xoff float64, yoff float64) { + // As this function is called from GLFW callbacks, the current thread is main. + u.m.Lock() + defer u.m.Unlock() + u.inputState.WheelX += xoff + u.inputState.WheelY += yoff + })) +} - i.onceCallback.Do(func() { - window.SetCharModsCallback(glfw.ToCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) { - // As this function is called from GLFW callbacks, the current thread is main. - if !unicode.IsPrint(char) { - return - } +// updateInput must be called from the main thread. +func (u *userInterfaceImpl) updateInputState() error { + u.m.Lock() + defer u.m.Unlock() - i.ui.m.Lock() - defer i.ui.m.Unlock() - i.runeBuffer = append(i.runeBuffer, char) - })) - window.SetScrollCallback(glfw.ToScrollCallback(func(w *glfw.Window, xoff float64, yoff float64) { - // As this function is called from GLFW callbacks, the current thread is main. - i.ui.m.Lock() - defer i.ui.m.Unlock() - i.scrollX += xoff - i.scrollY += yoff - })) - }) - if i.keyPressed == nil { - i.keyPressed = map[glfw.Key]bool{} + for uk, gk := range uiKeyToGLFWKey { + u.inputState.KeyPressed[uk] = u.window.GetKey(gk) == glfw.Press } - for _, gk := range uiKeyToGLFWKey { - i.keyPressed[gk] = window.GetKey(gk) == glfw.Press + for gb, ub := range glfwMouseButtonToMouseButton { + u.inputState.MouseButtonPressed[ub] = u.window.GetMouseButton(gb) == glfw.Press } - if i.mouseButtonPressed == nil { - i.mouseButtonPressed = map[glfw.MouseButton]bool{} - } - for gb := range glfwMouseButtonToMouseButton { - i.mouseButtonPressed[gb] = window.GetMouseButton(gb) == glfw.Press - } - cx, cy := window.GetCursorPos() + cx, cy := u.window.GetCursorPos() // TODO: This is tricky. Rename the function? - m := i.ui.currentMonitor() - s := i.ui.deviceScaleFactor(m) - cx = i.ui.dipFromGLFWPixel(cx, m) - cy = i.ui.dipFromGLFWPixel(cy, m) - cx, cy = context.adjustPosition(cx, cy, s) + m := u.currentMonitor() + s := u.deviceScaleFactor(m) + cx = u.dipFromGLFWPixel(cx, m) + cy = u.dipFromGLFWPixel(cy, m) + cx, cy = u.context.adjustPosition(cx, cy, s) // AdjustPosition can return NaN at the initialization. if !math.IsNaN(cx) && !math.IsNaN(cy) { - i.cursorX, i.cursorY = int(cx), int(cy) + u.inputState.CursorX, u.inputState.CursorY = int(cx), int(cy) } if err := gamepad.Update(); err != nil { diff --git a/internal/ui/input_js.go b/internal/ui/input_js.go index 73c0d2a76..48fbacaee 100644 --- a/internal/ui/input_js.go +++ b/internal/ui/input_js.go @@ -31,89 +31,17 @@ var ( stringTouchmove = js.ValueOf("touchmove") ) -var jsKeys []js.Value - -func init() { - for _, k := range uiKeyToJSKey { - jsKeys = append(jsKeys, k) - } -} - -func jsKeyToID(key js.Value) int { +func jsKeyToID(key js.Value) Key { // js.Value cannot be used as a map key. // As the number of keys is around 100, just a dumb loop should work. - for i, k := range jsKeys { - if k.Equal(key) { - return i + for uiKey, jsKey := range uiKeyToJSKey { + if jsKey.Equal(key) { + return uiKey } } return -1 } -type pos struct { - X int - Y int -} - -type Input struct { - keyPressed map[int]bool - mouseButtonPressed map[int]bool - cursorX int - cursorY int - origCursorX int - origCursorY int - wheelX float64 - wheelY float64 - touches map[TouchID]pos - runeBuffer []rune - ui *userInterfaceImpl -} - -func (i *Input) CursorPosition() (x, y int) { - if i.ui.context == nil { - return 0, 0 - } - xf, yf := i.ui.context.adjustPosition(float64(i.cursorX), float64(i.cursorY), i.ui.DeviceScaleFactor()) - return int(xf), int(yf) -} - -func (i *Input) AppendTouchIDs(touchIDs []TouchID) []TouchID { - for id := range i.touches { - touchIDs = append(touchIDs, id) - } - return touchIDs -} - -func (i *Input) TouchPosition(id TouchID) (x, y int) { - d := i.ui.DeviceScaleFactor() - for tid, pos := range i.touches { - if id == tid { - x, y := i.ui.context.adjustPosition(float64(pos.X), float64(pos.Y), d) - return int(x), int(y) - } - } - return 0, 0 -} - -func (i *Input) AppendInputChars(runes []rune) []rune { - return append(runes, i.runeBuffer...) -} - -func (i *Input) resetForTick() { - i.runeBuffer = nil - i.wheelX = 0 - i.wheelY = 0 -} - -func (i *Input) IsKeyPressed(key Key) bool { - if i.keyPressed != nil { - if i.keyPressed[jsKeyToID(uiKeyToJSKey[key])] { - return true - } - } - return false -} - var codeToMouseButton = map[int]MouseButton{ 0: MouseButton0, // Left 1: MouseButton1, // Middle @@ -122,123 +50,92 @@ var codeToMouseButton = map[int]MouseButton{ 4: MouseButton4, } -func (i *Input) IsMouseButtonPressed(button MouseButton) bool { - if i.mouseButtonPressed == nil { - i.mouseButtonPressed = map[int]bool{} - } - for c, b := range codeToMouseButton { - if b != button { - continue - } - if i.mouseButtonPressed[c] { - return true - } - } - return false +func (u *userInterfaceImpl) keyDown(code js.Value) { + u.inputState.KeyPressed[jsKeyToID(code)] = true } -func (i *Input) Wheel() (xoff, yoff float64) { - return i.wheelX, i.wheelY +func (u *userInterfaceImpl) keyUp(code js.Value) { + u.inputState.KeyPressed[jsKeyToID(code)] = false } -func (i *Input) keyDown(code js.Value) { - if i.keyPressed == nil { - i.keyPressed = map[int]bool{} - } - i.keyPressed[jsKeyToID(code)] = true +func (u *userInterfaceImpl) mouseDown(code int) { + u.inputState.MouseButtonPressed[codeToMouseButton[code]] = true } -func (i *Input) keyUp(code js.Value) { - if i.keyPressed == nil { - i.keyPressed = map[int]bool{} - } - i.keyPressed[jsKeyToID(code)] = false +func (u *userInterfaceImpl) mouseUp(code int) { + u.inputState.MouseButtonPressed[codeToMouseButton[code]] = false } -func (i *Input) mouseDown(code int) { - if i.mouseButtonPressed == nil { - i.mouseButtonPressed = map[int]bool{} - } - i.mouseButtonPressed[code] = true -} - -func (i *Input) mouseUp(code int) { - if i.mouseButtonPressed == nil { - i.mouseButtonPressed = map[int]bool{} - } - i.mouseButtonPressed[code] = false -} - -func (i *Input) updateFromEvent(e js.Value) error { +func (u *userInterfaceImpl) updateInputFromEvent(e js.Value) error { // Avoid using js.Value.String() as String creates a Uint8Array via a TextEncoder and causes a heavy // overhead (#1437). switch t := e.Get("type"); { case t.Equal(stringKeydown): if str := e.Get("key").String(); isKeyString(str) { for _, r := range str { - if unicode.IsPrint(r) { - i.runeBuffer = append(i.runeBuffer, r) - } + u.inputState.appendRune(r) } } - i.keyDown(e.Get("code")) + u.keyDown(e.Get("code")) case t.Equal(stringKeyup): - i.keyUp(e.Get("code")) + u.keyUp(e.Get("code")) case t.Equal(stringMousedown): - button := e.Get("button").Int() - i.mouseDown(button) - i.setMouseCursorFromEvent(e) + u.mouseDown(e.Get("button").Int()) + u.setMouseCursorFromEvent(e) case t.Equal(stringMouseup): - button := e.Get("button").Int() - i.mouseUp(button) - i.setMouseCursorFromEvent(e) + u.mouseUp(e.Get("button").Int()) + u.setMouseCursorFromEvent(e) case t.Equal(stringMousemove): - i.setMouseCursorFromEvent(e) + u.setMouseCursorFromEvent(e) case t.Equal(stringWheel): // TODO: What if e.deltaMode is not DOM_DELTA_PIXEL? - i.wheelX = -e.Get("deltaX").Float() - i.wheelY = -e.Get("deltaY").Float() + u.inputState.WheelX = -e.Get("deltaX").Float() + u.inputState.WheelY = -e.Get("deltaY").Float() case t.Equal(stringTouchstart) || t.Equal(stringTouchend) || t.Equal(stringTouchmove): - i.updateTouchesFromEvent(e) + u.updateTouchesFromEvent(e) } - i.ui.forceUpdateOnMinimumFPSMode() + u.forceUpdateOnMinimumFPSMode() return nil } -func (i *Input) setMouseCursorFromEvent(e js.Value) { - if i.ui.cursorMode == CursorModeCaptured { - x, y := e.Get("clientX").Int(), e.Get("clientY").Int() - i.origCursorX, i.origCursorY = x, y - dx, dy := e.Get("movementX").Int(), e.Get("movementY").Int() - i.cursorX += dx - i.cursorY += dy +func (u *userInterfaceImpl) setMouseCursorFromEvent(e js.Value) { + if u.context == nil { return } - x, y := e.Get("clientX").Int(), e.Get("clientY").Int() - i.cursorX, i.cursorY = x, y - i.origCursorX, i.origCursorY = x, y -} - -func (i *Input) recoverCursorPosition() { - i.cursorX, i.cursorY = i.origCursorX, i.origCursorY -} - -func (in *Input) updateTouchesFromEvent(e js.Value) { - j := e.Get("targetTouches") - for k := range in.touches { - delete(in.touches, k) + if u.cursorMode == CursorModeCaptured { + x, y := e.Get("clientX").Int(), e.Get("clientY").Int() + u.origCursorX, u.origCursorY = x, y + dx, dy := u.context.adjustPosition(e.Get("movementX").Float(), e.Get("movementY").Float(), u.DeviceScaleFactor()) + u.inputState.CursorX += int(dx) + u.inputState.CursorY += int(dy) + return } - for i := 0; i < j.Length(); i++ { - jj := j.Call("item", i) - id := TouchID(jj.Get("identifier").Int()) - if in.touches == nil { - in.touches = map[TouchID]pos{} - } - in.touches[id] = pos{ - X: jj.Get("clientX").Int(), - Y: jj.Get("clientY").Int(), + + x, y := u.context.adjustPosition(e.Get("clientX").Float(), e.Get("clientY").Float(), u.DeviceScaleFactor()) + u.inputState.CursorX, u.inputState.CursorY = int(x), int(y) + u.origCursorX, u.origCursorY = int(x), int(y) +} + +func (u *userInterfaceImpl) recoverCursorPosition() { + u.inputState.CursorX, u.inputState.CursorY = u.origCursorX, u.origCursorY +} + +func (u *userInterfaceImpl) updateTouchesFromEvent(e js.Value) { + for i := range u.inputState.Touches { + u.inputState.Touches[i].Valid = false + } + + touches := e.Get("targetTouches") + for i := 0; i < touches.Length(); i++ { + t := touches.Call("item", i) + x, y := u.context.adjustPosition(t.Get("clientX").Float(), t.Get("clientY").Float(), u.DeviceScaleFactor()) + u.inputState.Touches[i] = Touch{ + Valid: true, + ID: TouchID(t.Get("identifier").Int()), + X: int(x), + Y: int(y), } } } diff --git a/internal/ui/input_mobile.go b/internal/ui/input_mobile.go index 5f987caef..cfcdd409c 100644 --- a/internal/ui/input_mobile.go +++ b/internal/ui/input_mobile.go @@ -16,82 +16,38 @@ package ui -type Input struct { - keys map[Key]struct{} - runes []rune - touches []Touch - ui *userInterfaceImpl +type TouchForInput struct { + ID TouchID + + // X is in device-independent pixels. + X float64 + + // Y is in device-independent pixels. + Y float64 } -func (i *Input) CursorPosition() (x, y int) { - return 0, 0 -} +func (u *userInterfaceImpl) updateInputState(keys map[Key]struct{}, runes []rune, touches []TouchForInput) { + u.m.Lock() + defer u.m.Unlock() -func (i *Input) AppendTouchIDs(touchIDs []TouchID) []TouchID { - i.ui.m.RLock() - defer i.ui.m.RUnlock() - - for _, t := range i.touches { - touchIDs = append(touchIDs, t.ID) + for k := range u.inputState.KeyPressed { + _, ok := keys[Key(k)] + u.inputState.KeyPressed[k] = ok } - return touchIDs -} -func (i *Input) TouchPosition(id TouchID) (x, y int) { - i.ui.m.RLock() - defer i.ui.m.RUnlock() + copy(u.inputState.Runes[:], runes) + u.inputState.RunesCount = len(runes) - for _, t := range i.touches { - if t.ID == id { - return i.ui.adjustPosition(t.X, t.Y) + for i := range u.inputState.Touches { + u.inputState.Touches[i].Valid = false + } + for i, t := range touches { + x, y := u.context.adjustPosition(t.X, t.Y, u.DeviceScaleFactor()) + u.inputState.Touches[i] = Touch{ + Valid: true, + ID: t.ID, + X: int(x), + Y: int(y), } } - return 0, 0 -} - -func (i *Input) AppendInputChars(runes []rune) []rune { - i.ui.m.Lock() - defer i.ui.m.Unlock() - return append(runes, i.runes...) -} - -func (i *Input) IsKeyPressed(key Key) bool { - i.ui.m.RLock() - defer i.ui.m.RUnlock() - - _, ok := i.keys[key] - return ok -} - -func (i *Input) Wheel() (xoff, yoff float64) { - return 0, 0 -} - -func (i *Input) IsMouseButtonPressed(key MouseButton) bool { - return false -} - -func (i *Input) update(keys map[Key]struct{}, runes []rune, touches []Touch) { - i.ui.m.Lock() - defer i.ui.m.Unlock() - - if i.keys == nil { - i.keys = map[Key]struct{}{} - } - for k := range i.keys { - delete(i.keys, k) - } - for k := range keys { - i.keys[k] = struct{}{} - } - - i.runes = i.runes[:0] - i.runes = append(i.runes, runes...) - - i.touches = i.touches[:0] - i.touches = append(i.touches, touches...) -} - -func (i *Input) resetForTick() { - i.runes = nil } diff --git a/internal/ui/input_nintendosdk.go b/internal/ui/input_nintendosdk.go index 9cca96190..fe8355c29 100644 --- a/internal/ui/input_nintendosdk.go +++ b/internal/ui/input_nintendosdk.go @@ -17,73 +17,23 @@ package ui import ( - "sync" - - "github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/nintendosdk" ) -type Input struct { - gamepads []nintendosdk.Gamepad - touches []nintendosdk.Touch +func (u *userInterfaceImpl) updateInputState() { + u.nativeTouches = u.nativeTouches[:0] + u.nativeTouches = nintendosdk.AppendTouches(u.nativeTouches) - m sync.Mutex -} - -func (i *Input) update(context *context) { - i.m.Lock() - defer i.m.Unlock() - - gamepad.Update() - - i.touches = i.touches[:0] - i.touches = nintendosdk.AppendTouches(i.touches) - - for idx, t := range i.touches { - x, y := context.adjustPosition(float64(t.X), float64(t.Y), deviceScaleFactor) - i.touches[idx].X = int(x) - i.touches[idx].Y = int(y) + for i := range u.inputState.Touches { + u.inputState.Touches[i].Valid = false } -} - -func (i *Input) AppendInputChars(runes []rune) []rune { - return nil -} - -func (i *Input) AppendTouchIDs(touchIDs []TouchID) []TouchID { - i.m.Lock() - defer i.m.Unlock() - - for _, t := range i.touches { - touchIDs = append(touchIDs, TouchID(t.ID)) - } - return touchIDs -} - -func (i *Input) CursorPosition() (x, y int) { - return 0, 0 -} - -func (i *Input) IsKeyPressed(key Key) bool { - return false -} - -func (i *Input) IsMouseButtonPressed(button MouseButton) bool { - return false -} - -func (i *Input) TouchPosition(id TouchID) (x, y int) { - i.m.Lock() - defer i.m.Unlock() - - for _, t := range i.touches { - if TouchID(t.ID) == id { - return t.X, t.Y + for i, t := range u.nativeTouches { + x, y := u.context.adjustPosition(float64(t.X), float64(t.Y), deviceScaleFactor) + u.inputState.Touches[i] = Touch{ + Valid: true, + ID: TouchID(t.ID), + X: int(x), + Y: int(y), } } - return 0, 0 -} - -func (i *Input) Wheel() (xoff, yoff float64) { - return 0, 0 } diff --git a/internal/ui/keys.go b/internal/ui/keys.go index ec019359a..99e2bbb21 100644 --- a/internal/ui/keys.go +++ b/internal/ui/keys.go @@ -132,6 +132,7 @@ const ( KeyReserved1 KeyReserved2 KeyReserved3 + KeyMax = KeyReserved3 ) func (k Key) String() string { diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 9b28a1662..ed2e66ea4 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -21,18 +21,6 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/mipmap" ) -type MouseButton int - -const ( - MouseButton0 MouseButton = iota // The 'left' button - MouseButton1 // The 'right' button - MouseButton2 // The 'middle' button - MouseButton3 // The additional button (usually browser-back) - MouseButton4 // The additional button (usually browser-forward) -) - -type TouchID int - // RegularTermination represents a regular termination. // Run can return this error, and if this error is received, // the game loop should be terminated as soon as possible. diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 6b66ee94c..60287297c 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -100,8 +100,8 @@ type userInterfaceImpl struct { fpsModeInited bool - input Input - iwindow glfwWindow + inputState InputState + iwindow glfwWindow sizeCallback glfw.SizeCallback closeCallback glfw.CloseCallback @@ -137,7 +137,6 @@ func init() { origWindowPosX: invalidPos, origWindowPosY: invalidPos, } - theUI.input.ui = &theUI.userInterfaceImpl theUI.iwindow.ui = &theUI.userInterfaceImpl } @@ -697,8 +696,13 @@ func (u *userInterfaceImpl) createWindow(width, height int) error { return nil } -func (u *userInterfaceImpl) beginFrame() { +func (u *userInterfaceImpl) beginFrame(inputState *InputState) { atomic.StoreUint32(&u.inFrame, 1) + + u.m.Lock() + defer u.m.Unlock() + *inputState = u.inputState + u.inputState.resetForFrame() } func (u *userInterfaceImpl) endFrame() { @@ -958,6 +962,7 @@ func (u *userInterfaceImpl) init(options *RunOptions) error { u.registerWindowSetSizeCallback() u.registerWindowCloseCallback() u.registerWindowFramebufferSizeCallback() + u.registerInputCallbacks() return nil } @@ -1046,7 +1051,7 @@ func (u *userInterfaceImpl) update() (float64, float64, error) { } else { glfw.WaitEvents() } - if err := u.input.update(u.window, u.context); err != nil { + if err := u.updateInputState(); err != nil { return 0, 0, err } @@ -1408,15 +1413,15 @@ func monitorFromWindow(window *glfw.Window) *glfw.Monitor { } func (u *userInterfaceImpl) resetForTick() { - u.input.resetForTick() - u.m.Lock() + defer u.m.Unlock() u.windowBeingClosed = false - u.m.Unlock() } -func (u *userInterfaceImpl) Input() *Input { - return &u.input +func (u *userInterfaceImpl) readInputState(inputState *InputState) { + u.m.Lock() + defer u.m.Unlock() + *inputState = u.inputState } func (u *userInterfaceImpl) Window() Window { diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index 490aebd72..038e1d929 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -86,8 +86,10 @@ type userInterfaceImpl struct { err error - context *context - input Input + context *context + inputState InputState + origCursorX int + origCursorY int m sync.Mutex } @@ -96,7 +98,6 @@ func init() { theUI.userInterfaceImpl = userInterfaceImpl{ runnableOnUnfocused: true, } - theUI.input.ui = &theUI.userInterfaceImpl } var ( @@ -475,7 +476,7 @@ func init() { if theUI.cursorMode == CursorModeCaptured { theUI.recoverCursorMode() } - theUI.input.recoverCursorPosition() + theUI.recoverCursorPosition() return nil })) document.Call("addEventListener", "pointerlockerror", js.FuncOf(func(this js.Value, args []js.Value) any { @@ -516,7 +517,7 @@ func setCanvasEventHandlers(v js.Value) { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -525,7 +526,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "keyup", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -539,7 +540,7 @@ func setCanvasEventHandlers(v js.Value) { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -548,7 +549,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "mouseup", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -557,7 +558,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -566,7 +567,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "wheel", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -580,7 +581,7 @@ func setCanvasEventHandlers(v js.Value) { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -589,7 +590,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "touchend", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -598,7 +599,7 @@ func setCanvasEventHandlers(v js.Value) { v.Call("addEventListener", "touchmove", js.FuncOf(func(this js.Value, args []js.Value) any { e := args[0] e.Call("preventDefault") - if err := theUI.input.updateFromEvent(e); err != nil && theUI.err != nil { + if err := theUI.updateInputFromEvent(e); err != nil && theUI.err != nil { theUI.err = err return nil } @@ -680,18 +681,15 @@ func (u *userInterfaceImpl) SetScreenTransparent(transparent bool) { } func (u *userInterfaceImpl) resetForTick() { - u.input.resetForTick() -} - -func (u *userInterfaceImpl) Input() *Input { - return &u.input } func (u *userInterfaceImpl) Window() Window { return &nullWindow{} } -func (u *userInterfaceImpl) beginFrame() { +func (u *userInterfaceImpl) beginFrame(inputState *InputState) { + *inputState = u.inputState + u.inputState.resetForFrame() } func (u *userInterfaceImpl) endFrame() { diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index 7e5d52d2f..bc5b3fc26 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -60,7 +60,6 @@ func init() { outsideWidth: 640, outsideHeight: 480, } - theUI.input.ui = &theUI.userInterfaceImpl } // Update is called from mobile/ebitenmobileview. @@ -108,7 +107,7 @@ type userInterfaceImpl struct { context *context - input Input + inputState InputState fpsMode FPSModeType renderRequester RenderRequester @@ -127,7 +126,7 @@ func (u *userInterfaceImpl) appMain(a app.App) { var glctx gl.Context var sizeInited bool - touches := map[touch.Sequence]Touch{} + touches := map[touch.Sequence]TouchForInput{} keys := map[Key]struct{}{} for e := range a.Events() { @@ -180,12 +179,10 @@ func (u *userInterfaceImpl) appMain(a app.App) { switch e.Type { case touch.TypeBegin, touch.TypeMove: s := deviceScale() - x, y := float64(e.X)/s, float64(e.Y)/s - // TODO: Is it ok to cast from int64 to int here? - touches[e.Sequence] = Touch{ + touches[e.Sequence] = TouchForInput{ ID: TouchID(e.Sequence), - X: int(x), - Y: int(y), + X: float64(e.X) / s, + Y: float64(e.Y) / s, } case touch.TypeEnd: delete(touches, e.Sequence) @@ -212,11 +209,11 @@ func (u *userInterfaceImpl) appMain(a app.App) { } if updateInput { - var ts []Touch + var ts []TouchForInput for _, t := range touches { ts = append(ts, t) } - u.input.update(keys, runes, ts) + u.updateInputState(keys, runes, ts) } } } @@ -422,25 +419,14 @@ func (u *userInterfaceImpl) DeviceScaleFactor() float64 { } func (u *userInterfaceImpl) resetForTick() { - u.input.resetForTick() -} - -func (u *userInterfaceImpl) Input() *Input { - return &u.input } func (u *userInterfaceImpl) Window() Window { return &nullWindow{} } -type Touch struct { - ID TouchID - X int - Y int -} - -func (u *userInterfaceImpl) UpdateInput(keys map[Key]struct{}, runes []rune, touches []Touch) { - u.input.update(keys, runes, touches) +func (u *userInterfaceImpl) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) { + u.updateInputState(keys, runes, touches) if u.fpsMode == FPSModeVsyncOffMinimum { u.renderRequester.RequestRenderIfNeeded() } @@ -462,7 +448,12 @@ func (u *userInterfaceImpl) ScheduleFrame() { } } -func (u *userInterfaceImpl) beginFrame() { +func (u *userInterfaceImpl) beginFrame(inputState *InputState) { + u.m.Lock() + defer u.m.Unlock() + + *inputState = u.inputState + u.inputState.resetForFrame() } func (u *userInterfaceImpl) endFrame() { diff --git a/internal/ui/ui_nintendosdk.go b/internal/ui/ui_nintendosdk.go index 7eeb4c342..dda1a2df3 100644 --- a/internal/ui/ui_nintendosdk.go +++ b/internal/ui/ui_nintendosdk.go @@ -19,6 +19,7 @@ package ui import ( "runtime" + "github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl" "github.com/hajimehoshi/ebiten/v2/internal/nintendosdk" @@ -52,8 +53,9 @@ func init() { type userInterfaceImpl struct { graphicsDriver graphicsdriver.Graphics - context *context - input Input + context *context + inputState InputState + nativeTouches []nintendosdk.Touch } func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error { @@ -66,7 +68,8 @@ func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error { nintendosdk.InitializeGame() for { nintendosdk.BeginFrame() - u.input.update(u.context) + gamepad.Update() + u.updateInputState() w, h := nintendosdk.ScreenSize() if err := u.context.updateFrame(u.graphicsDriver, float64(w), float64(h), deviceScaleFactor, u); err != nil { @@ -126,15 +129,13 @@ func (*userInterfaceImpl) SetFPSMode(mode FPSModeType) { func (*userInterfaceImpl) ScheduleFrame() { } -func (*userInterfaceImpl) Input() *Input { - return &theUI.input -} - func (*userInterfaceImpl) Window() Window { return &nullWindow{} } -func (u *userInterfaceImpl) beginFrame() { +func (u *userInterfaceImpl) beginFrame(inputState *InputState) { + *inputState = u.inputState + u.inputState.resetForFrame() } func (u *userInterfaceImpl) endFrame() { diff --git a/mobile/ebitenmobileview/input.go b/mobile/ebitenmobileview/input.go index 1e552a2bd..3a410f87f 100644 --- a/mobile/ebitenmobileview/input.go +++ b/mobile/ebitenmobileview/input.go @@ -32,16 +32,16 @@ var ( ) var ( - touchSlice []ui.Touch + touchSlice []ui.TouchForInput ) func updateInput() { touchSlice = touchSlice[:0] for id, position := range touches { - touchSlice = append(touchSlice, ui.Touch{ + touchSlice = append(touchSlice, ui.TouchForInput{ ID: id, - X: position.x, - Y: position.y, + X: float64(position.x), + Y: float64(position.y), }) }