diff --git a/internal/gamepad/gamepad.go b/internal/gamepad/gamepad.go index 95a57cdc4..471e598dc 100644 --- a/internal/gamepad/gamepad.go +++ b/internal/gamepad/gamepad.go @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build darwin && !ios -// +build darwin,!ios +//go:build (darwin && !ios) || js +// +build darwin,!ios js package gamepad import ( "sync" + "time" "github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/gamepaddb" @@ -47,6 +48,7 @@ var theGamepads gamepads func init() { theGamepads.nativeGamepads.init() + theGamepads.nativeGamepads.gamepads = &theGamepads } func AppendGamepadIDs(ids []driver.GamepadID) []driver.GamepadID { @@ -62,9 +64,6 @@ func Get(id driver.GamepadID) *Gamepad { } func (g *gamepads) appendGamepadIDs(ids []driver.GamepadID) []driver.GamepadID { - g.m.Lock() - defer g.m.Unlock() - for i, gp := range g.gamepads { if gp != nil && gp.present() { ids = append(ids, driver.GamepadID(i)) @@ -77,6 +76,7 @@ func (g *gamepads) update() { g.m.Lock() defer g.m.Unlock() + g.nativeGamepads.update() for _, gp := range g.gamepads { if gp != nil { gp.update() @@ -87,7 +87,10 @@ func (g *gamepads) update() { func (g *gamepads) get(id driver.GamepadID) *Gamepad { g.m.Lock() defer g.m.Unlock() + return g.getImpl(id) +} +func (g *gamepads) getImpl(id driver.GamepadID) *Gamepad { if id < 0 || int(id) >= len(g.gamepads) { return nil } @@ -97,7 +100,10 @@ func (g *gamepads) get(id driver.GamepadID) *Gamepad { func (g *gamepads) find(cond func(*Gamepad) bool) *Gamepad { g.m.Lock() defer g.m.Unlock() + return g.findImpl(cond) +} +func (g *gamepads) findImpl(cond func(*Gamepad) bool) *Gamepad { for _, gp := range g.gamepads { if gp == nil { continue @@ -112,7 +118,10 @@ func (g *gamepads) find(cond func(*Gamepad) bool) *Gamepad { func (g *gamepads) add(name, sdlID string) *Gamepad { g.m.Lock() defer g.m.Unlock() + return g.addImpl(name, sdlID) +} +func (g *gamepads) addImpl(name, sdlID string) *Gamepad { for i, gp := range g.gamepads { if gp == nil { gp := &Gamepad{ @@ -135,7 +144,10 @@ func (g *gamepads) add(name, sdlID string) *Gamepad { func (g *gamepads) remove(cond func(*Gamepad) bool) { g.m.Lock() defer g.m.Unlock() + g.removeImpl(cond) +} +func (g *gamepads) removeImpl(cond func(*Gamepad) bool) { for i, gp := range g.gamepads { if gp == nil { continue @@ -220,17 +232,42 @@ func (g *Gamepad) IsStandardLayoutAvailable() bool { g.m.Lock() defer g.m.Unlock() - return gamepaddb.HasStandardLayoutMapping(g.sdlID) + if gamepaddb.HasStandardLayoutMapping(g.sdlID) { + return true + } + return g.hasOwnStandardLayoutMapping() } func (g *Gamepad) StandardAxisValue(axis driver.StandardGamepadAxis) float64 { - return gamepaddb.AxisValue(g.sdlID, axis, g) + if gamepaddb.HasStandardLayoutMapping(g.sdlID) { + return gamepaddb.AxisValue(g.sdlID, axis, g) + } + if g.hasOwnStandardLayoutMapping() { + return g.nativeGamepad.axisValue(int(axis)) + } + return 0 } func (g *Gamepad) StandardButtonValue(button driver.StandardGamepadButton) float64 { - return gamepaddb.ButtonValue(g.sdlID, button, g) + if gamepaddb.HasStandardLayoutMapping(g.sdlID) { + return gamepaddb.ButtonValue(g.sdlID, button, g) + } + if g.hasOwnStandardLayoutMapping() { + return g.nativeGamepad.buttonValue(int(button)) + } + return 0 } func (g *Gamepad) IsStandardButtonPressed(button driver.StandardGamepadButton) bool { - return gamepaddb.IsButtonPressed(g.sdlID, button, g) + if gamepaddb.HasStandardLayoutMapping(g.sdlID) { + return gamepaddb.IsButtonPressed(g.sdlID, button, g) + } + if g.hasOwnStandardLayoutMapping() { + return g.nativeGamepad.isButtonPressed(int(button)) + } + return false +} + +func (g *Gamepad) Vibrate(duration time.Duration, strongMagnitude float64, weakMagnitude float64) { + g.nativeGamepad.vibrate(duration, strongMagnitude, weakMagnitude) } diff --git a/internal/gamepad/gamepad_darwin.go b/internal/gamepad/gamepad_darwin.go index 21bebd1e1..6f5d0ef08 100644 --- a/internal/gamepad/gamepad_darwin.go +++ b/internal/gamepad/gamepad_darwin.go @@ -20,6 +20,7 @@ package gamepad import ( "fmt" "sort" + "time" "unsafe" ) @@ -57,6 +58,8 @@ import ( import "C" type nativeGamepads struct { + gamepads *gamepads + hidManager C.IOHIDManagerRef } @@ -170,6 +173,10 @@ func (g *nativeGamepad) update() { } } +func (g *nativeGamepad) hasOwnStandardLayoutMapping() bool { + return false +} + func (g *nativeGamepad) axisNum() int { return len(g.axisValues) } @@ -189,6 +196,10 @@ func (g *nativeGamepad) axisValue(axis int) float64 { return g.axisValues[axis] } +func (g *nativeGamepad) buttonValue(button int) float64 { + panic("gamepad: buttonValue is not implemented") +} + func (g *nativeGamepad) isButtonPressed(button int) bool { if button < 0 || button >= len(g.buttonValues) { return false @@ -203,6 +214,10 @@ func (g *nativeGamepad) hatState(hat int) int { return g.hatValues[hat] } +func (g *nativeGamepad) vibrate(duration time.Duration, strongMagnitude float64, weakMagnitude float64) { + // TODO: Implement this (#1452) +} + func (g *nativeGamepads) init() { var dicts []C.CFDictionaryRef @@ -399,3 +414,6 @@ func ebitenGamepadRemovalCallback(ctx unsafe.Pointer, res C.IOReturn, sender uns return g.device == device }) } + +func (g *nativeGamepads) update() { +} diff --git a/internal/gamepad/gamepad_js.go b/internal/gamepad/gamepad_js.go new file mode 100644 index 000000000..7a45e011c --- /dev/null +++ b/internal/gamepad/gamepad_js.go @@ -0,0 +1,183 @@ +// Copyright 2022 The Ebiten 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 gamepad + +import ( + "encoding/hex" + "syscall/js" + "time" +) + +var ( + object = js.Global().Get("Object") + go2cpp = js.Global().Get("go2cpp") +) + +type nativeGamepads struct { + gamepads *gamepads + + indices map[int]struct{} +} + +func (g *nativeGamepads) init() { +} + +func (g *nativeGamepads) update() { + // TODO: Use the gamepad events instead of navigator.getGamepads after go2cpp is removed. + + defer func() { + for k := range g.indices { + delete(g.indices, k) + } + }() + + nav := js.Global().Get("navigator") + if !nav.Truthy() { + return + } + + gps := nav.Call("getGamepads") + if !gps.Truthy() { + return + } + + l := gps.Length() + for idx := 0; idx < l; idx++ { + gp := gps.Index(idx) + if !gp.Truthy() { + continue + } + index := gp.Get("index").Int() + + if g.indices == nil { + g.indices = map[int]struct{}{} + } + g.indices[index] = struct{}{} + + // The gamepad is not registered yet, register this. + gamepad := g.gamepads.findImpl(func(gamepad *Gamepad) bool { + return index == gamepad.index + }) + if gamepad == nil { + name := gp.Get("id").String() + + // This emulates the implementation of EMSCRIPTEN_JoystickGetDeviceGUID. + // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/src/joystick/emscripten/SDL_sysjoystick.c#L385 + var sdlID [16]byte + copy(sdlID[:], []byte(name)) + + gamepad = g.gamepads.addImpl(name, hex.EncodeToString(sdlID[:])) + gamepad.index = index + gamepad.mapping = gp.Get("mapping").String() + } + gamepad.value = gp + } + + // Remove an unused gamepads. + g.gamepads.removeImpl(func(gamepad *Gamepad) bool { + _, ok := g.indices[gamepad.index] + return !ok + }) +} + +type nativeGamepad struct { + value js.Value + index int + mapping string +} + +func (g *nativeGamepad) present() bool { + return g.value.Truthy() +} + +func (g *nativeGamepad) hasOwnStandardLayoutMapping() bool { + // With go2cpp, the controller must have the standard + if go2cpp.Truthy() { + return true + } + return g.mapping == "standard" +} + +func (g *nativeGamepad) update() { +} + +func (g *nativeGamepad) axisNum() int { + return g.value.Get("axes").Length() +} + +func (g *nativeGamepad) buttonNum() int { + return g.value.Get("buttons").Length() +} + +func (g *nativeGamepad) hatNum() int { + return 0 +} + +func (g *nativeGamepad) axisValue(axis int) float64 { + axes := g.value.Get("axes") + if axis < 0 || axis >= axes.Length() { + return 0 + } + return axes.Index(axis).Float() +} + +func (g *nativeGamepad) buttonValue(button int) float64 { + buttons := g.value.Get("buttons") + if button < 0 || button >= buttons.Length() { + return 0 + } + return buttons.Index(button).Get("value").Float() +} + +func (g *nativeGamepad) isButtonPressed(button int) bool { + buttons := g.value.Get("buttons") + if button < 0 || button >= buttons.Length() { + return false + } + return buttons.Index(button).Get("pressed").Bool() +} + +func (g *nativeGamepad) hatState(hat int) int { + return hatCentered +} + +func (g *nativeGamepad) vibrate(duration time.Duration, strongMagnitude float64, weakMagnitude float64) { + // vibrationActuator is avaialble on Chrome. + if va := g.value.Get("vibrationActuator"); va.Truthy() { + if !va.Get("playEffect").Truthy() { + return + } + + prop := object.New() + prop.Set("startDelay", 0) + prop.Set("duration", float64(duration/time.Millisecond)) + prop.Set("strongMagnitude", strongMagnitude) + prop.Set("weakMagnitude", weakMagnitude) + va.Call("playEffect", "dual-rumble", prop) + return + } + + // hapticActuators is available on Firefox. + if ha := g.value.Get("hapticActuators"); ha.Truthy() { + // TODO: Is this order correct? + if ha.Length() > 0 { + ha.Index(0).Call("pulse", strongMagnitude, float64(duration/time.Millisecond)) + } + if ha.Length() > 1 { + ha.Index(1).Call("pulse", weakMagnitude, float64(duration/time.Millisecond)) + } + return + } +} diff --git a/internal/uidriver/glfw/gamepad_native.go b/internal/uidriver/glfw/gamepad_native.go index f340f870f..db8a6f0e6 100644 --- a/internal/uidriver/glfw/gamepad_native.go +++ b/internal/uidriver/glfw/gamepad_native.go @@ -129,5 +129,9 @@ func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button drive } func (i *Input) VibrateGamepad(id driver.GamepadID, duration time.Duration, strongMagnitude float64, weakMagnitude float64) { - // TODO: Implement this (#1452) + g := gamepadpkg.Get(id) + if g == nil { + return + } + g.Vibrate(duration, strongMagnitude, weakMagnitude) } diff --git a/internal/uidriver/js/input_js.go b/internal/uidriver/js/input_js.go index f33ec4074..c47e3b9b7 100644 --- a/internal/uidriver/js/input_js.go +++ b/internal/uidriver/js/input_js.go @@ -15,16 +15,14 @@ package js import ( - "encoding/hex" "syscall/js" "time" "unicode" "github.com/hajimehoshi/ebiten/v2/internal/driver" + "github.com/hajimehoshi/ebiten/v2/internal/gamepad" ) -var object = js.Global().Get("Object") - var ( stringKeydown = js.ValueOf("keydown") stringKeypress = js.ValueOf("keypress") @@ -62,30 +60,6 @@ type pos struct { Y int } -type gamepad struct { - value js.Value - - name string - mapping string - axisNum int - axes [16]float64 - buttonNum int - buttonPressed [256]bool - buttonValues [256]float64 - - standardButtonPressed [driver.StandardGamepadButtonMax + 1]bool - standardButtonValues [driver.StandardGamepadButtonMax + 1]float64 - standardAxisValues [driver.StandardGamepadAxisMax + 1]float64 -} - -func (g *gamepad) hasStandardLayoutMapping() bool { - // With go2cpp, the controller must have the standard - if go2cpp.Truthy() { - return true - } - return g.mapping == "standard" -} - type Input struct { keyPressed map[int]bool keyPressedEdge map[int]bool @@ -96,7 +70,6 @@ type Input struct { origCursorY int wheelX float64 wheelY float64 - gamepads map[driver.GamepadID]gamepad touches map[driver.TouchID]pos runeBuffer []rune ui *UserInterface @@ -111,71 +84,58 @@ func (i *Input) CursorPosition() (x, y int) { } func (i *Input) GamepadSDLID(id driver.GamepadID) string { - // This emulates the implementation of EMSCRIPTEN_JoystickGetDeviceGUID. - // https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/src/joystick/emscripten/SDL_sysjoystick.c#L385 - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return "" } - var sdlid [16]byte - copy(sdlid[:], []byte(g.name)) - return hex.EncodeToString(sdlid[:]) + return g.SDLID() } // GamepadName returns a string containing some information about the controller. // A PS2 controller returned "810-3-USB Gamepad" on Firefox // A Xbox 360 controller returned "xinput" on Firefox and "Xbox 360 Controller (XInput STANDARD GAMEPAD)" on Chrome func (i *Input) GamepadName(id driver.GamepadID) string { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return "" } - return g.name + return g.Name() } func (i *Input) AppendGamepadIDs(gamepadIDs []driver.GamepadID) []driver.GamepadID { - for id := range i.gamepads { - gamepadIDs = append(gamepadIDs, id) - } - return gamepadIDs + return gamepad.AppendGamepadIDs(gamepadIDs) } func (i *Input) GamepadAxisNum(id driver.GamepadID) int { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return 0 } - return g.axisNum + return g.AxisNum() } func (i *Input) GamepadAxisValue(id driver.GamepadID, axis int) float64 { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return 0 } - if g.axisNum <= axis { - return 0 - } - return g.axes[axis] + return g.Axis(axis) } func (i *Input) GamepadButtonNum(id driver.GamepadID) int { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return 0 } - return g.buttonNum + return g.ButtonNum() } func (i *Input) IsGamepadButtonPressed(id driver.GamepadID, button driver.GamepadButton) bool { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return false } - if g.buttonNum <= int(button) { - return false - } - return g.buttonPressed[button] + return g.Button(int(button)) } func (i *Input) AppendTouchIDs(touchIDs []driver.TouchID) []driver.TouchID { @@ -293,63 +253,7 @@ func (i *Input) mouseUp(code int) { } func (i *Input) updateGamepads() { - nav := js.Global().Get("navigator") - if !nav.Truthy() { - return - } - - gamepads := nav.Call("getGamepads") - if !gamepads.Truthy() { - return - } - - for k := range i.gamepads { - delete(i.gamepads, k) - } - - l := gamepads.Length() - for idx := 0; idx < l; idx++ { - gp := gamepads.Index(idx) - if !gp.Truthy() { - continue - } - - id := driver.GamepadID(gp.Get("index").Int()) - g := gamepad{ - value: gp, - } - g.name = gp.Get("id").String() - g.mapping = gp.Get("mapping").String() - - axes := gp.Get("axes") - axesNum := axes.Length() - g.axisNum = axesNum - for a := 0; a < axesNum; a++ { - g.axes[a] = axes.Index(a).Float() - } - - buttons := gp.Get("buttons") - buttonsNum := buttons.Length() - g.buttonNum = buttonsNum - for b := 0; b < buttonsNum; b++ { - btn := buttons.Index(b) - g.buttonPressed[b] = btn.Get("pressed").Bool() - g.buttonValues[b] = btn.Get("value").Float() - } - - if g.mapping == "standard" { - // When the gamepad's mapping is "standard", the button and axis IDs are already mapped as the standard layout. - // See https://www.w3.org/TR/gamepad/#remapping. - copy(g.standardButtonPressed[:], g.buttonPressed[:]) - copy(g.standardButtonValues[:], g.buttonValues[:]) - copy(g.standardAxisValues[:], g.axes[:]) - } - - if i.gamepads == nil { - i.gamepads = map[driver.GamepadID]gamepad{} - } - i.gamepads[id] = g - } + gamepad.Update() } func (i *Input) updateFromEvent(e js.Value) { @@ -474,76 +378,41 @@ func (i *Input) updateForGo2Cpp() { } func (i *Input) IsStandardGamepadLayoutAvailable(id driver.GamepadID) bool { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return false } - return g.hasStandardLayoutMapping() + return g.IsStandardLayoutAvailable() } func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return 0 } - if !g.hasStandardLayoutMapping() { - return 0 - } - return g.standardAxisValues[axis] + return g.StandardAxisValue(axis) } func (i *Input) StandardGamepadButtonValue(id driver.GamepadID, button driver.StandardGamepadButton) float64 { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return 0 } - if !g.hasStandardLayoutMapping() { - return 0 - } - return g.standardButtonValues[button] + return g.StandardButtonValue(button) } func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool { - g, ok := i.gamepads[id] - if !ok { + g := gamepad.Get(id) + if g == nil { return false } - if !g.hasStandardLayoutMapping() { - return false - } - return g.standardButtonPressed[button] + return g.IsStandardButtonPressed(button) } func (i *Input) VibrateGamepad(id driver.GamepadID, duration time.Duration, strongMagnitude float64, weakMagnitude float64) { - g, ok := i.gamepads[id] - if !ok { - return - } - - // vibrationActuator is avaialble on Chrome. - if va := g.value.Get("vibrationActuator"); va.Truthy() { - if !va.Get("playEffect").Truthy() { - return - } - - prop := object.New() - prop.Set("startDelay", 0) - prop.Set("duration", float64(duration/time.Millisecond)) - prop.Set("strongMagnitude", strongMagnitude) - prop.Set("weakMagnitude", weakMagnitude) - va.Call("playEffect", "dual-rumble", prop) - return - } - - // hapticActuators is available on Firefox. - if ha := g.value.Get("hapticActuators"); ha.Truthy() { - // TODO: Is this order correct? - if ha.Length() > 0 { - ha.Index(0).Call("pulse", strongMagnitude, float64(duration/time.Millisecond)) - } - if ha.Length() > 1 { - ha.Index(1).Call("pulse", weakMagnitude, float64(duration/time.Millisecond)) - } + g := gamepad.Get(id) + if g == nil { return } + g.Vibrate(duration, strongMagnitude, weakMagnitude) }