internal/gamepad: move the implementation of gamepad for GOOS=js

Updates #1957
This commit is contained in:
Hajime Hoshi 2022-01-29 01:28:53 +09:00
parent 82298edfae
commit 5edfd1b743
5 changed files with 288 additions and 177 deletions

View File

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

View File

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

View File

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

View File

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

View File

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