ebiten: Add the standard gamepad layout

This change introduces the standard gamepad layout. This changes adds
these APIs:

  * func HasGamepadStandardLayoutMapping
  * func IsGamepadStandardButtonPressed
  * func GamepadStandardAxisValue
  * type StandardGamepadButton
  * type StandardGamepadAxis

The standard gamepad layout is based on the web standard. See
https://www.w3.org/TR/gamepad/#remapping.

On desktops, the SDL's gamecontrllerdb.txt is used. If the gamepad is
listed in the text file, the mapping works. GLFW's mapping featrue is
not used.

On browsers, the property of a gamepad 'mapping' is used. When the
mapping value is 'standard', the gamepad is recognized to have the
standard mapping.

On mobiles, the implementation is still WIP.

Updates #1557
This commit is contained in:
Hajime Hoshi 2021-07-18 23:28:26 +09:00
parent f882dbda77
commit aa694be6f6
17 changed files with 1841 additions and 12 deletions

View File

@ -85,6 +85,109 @@ func (g *Game) Update() error {
return nil return nil
} }
func standardMap(id ebiten.GamepadID) string {
m := ` [FBL] [FBR]
[FTL] [FTR]
[LT] [CC] [RT]
[LL][LR] [CL][CR] [RL][RR]
[LB] [RB]
(LS) (RS)
`
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightBottom) {
m = strings.Replace(m, "[RB]", "[**]", 1)
} else {
m = strings.Replace(m, "[RB]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightRight) {
m = strings.Replace(m, "[RR]", "[**]", 1)
} else {
m = strings.Replace(m, "[RR]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightLeft) {
m = strings.Replace(m, "[RL]", "[**]", 1)
} else {
m = strings.Replace(m, "[RL]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightTop) {
m = strings.Replace(m, "[RT]", "[**]", 1)
} else {
m = strings.Replace(m, "[RT]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonFrontTopLeft) {
m = strings.Replace(m, "[FTL]", "[**]", 1)
} else {
m = strings.Replace(m, "[FTL]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonFrontTopRight) {
m = strings.Replace(m, "[FTR]", "[**]", 1)
} else {
m = strings.Replace(m, "[FTR]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonFrontBottomLeft) {
m = strings.Replace(m, "[FBL]", "[**]", 1)
} else {
m = strings.Replace(m, "[FBL]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonFrontBottomRight) {
m = strings.Replace(m, "[FBR]", "[**]", 1)
} else {
m = strings.Replace(m, "[FBR]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonCenterLeft) {
m = strings.Replace(m, "[CL]", "[**]", 1)
} else {
m = strings.Replace(m, "[CL]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonCenterRight) {
m = strings.Replace(m, "[CR]", "[**]", 1)
} else {
m = strings.Replace(m, "[CR]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonLeftStick) {
m = strings.Replace(m, "(LS)", "[**]", 1)
} else {
m = strings.Replace(m, "(LS)", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonRightStick) {
m = strings.Replace(m, "(RS)", "[**]", 1)
} else {
m = strings.Replace(m, "(RS)", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonLeftBottom) {
m = strings.Replace(m, "[LB]", "[**]", 1)
} else {
m = strings.Replace(m, "[LB]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonLeftRight) {
m = strings.Replace(m, "[LR]", "[**]", 1)
} else {
m = strings.Replace(m, "[LR]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonLeftLeft) {
m = strings.Replace(m, "[LL]", "[**]", 1)
} else {
m = strings.Replace(m, "[LL]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonLeftTop) {
m = strings.Replace(m, "[LT]", "[**]", 1)
} else {
m = strings.Replace(m, "[LT]", "[ ]", 1)
}
if ebiten.IsStandardGamepadButtonPressed(id, ebiten.StandardGamepadButtonCenterCenter) {
m = strings.Replace(m, "[CC]", "[**]", 1)
} else {
m = strings.Replace(m, "[CC]", "[ ]", 1)
}
m += fmt.Sprintf(" Left Stick: X: %+0.2f, Y: %+0.2f\n Right Stick: X: %+0.2f, Y: %+0.2f",
ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickHorizontal),
ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisLeftStickVertical),
ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal),
ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical))
return m
}
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
// Draw the current gamepad status. // Draw the current gamepad status.
str := "" str := ""
@ -97,9 +200,18 @@ func (g *Game) Draw(screen *ebiten.Image) {
return ids[a] < ids[b] return ids[a] < ids[b]
}) })
for _, id := range ids { for _, id := range ids {
str += fmt.Sprintf("Gamepad (ID: %d, SDL ID: %s):\n", id, ebiten.GamepadSDLID(id)) var standard string
if ebiten.HasGamepadStandardLayoutMapping(id) {
standard = " (Standard Layout)"
}
str += fmt.Sprintf("Gamepad (ID: %d, SDL ID: %s)%s:\n", id, ebiten.GamepadSDLID(id), standard)
str += fmt.Sprintf(" Axes: %s\n", strings.Join(g.axes[id], ", ")) str += fmt.Sprintf(" Axes: %s\n", strings.Join(g.axes[id], ", "))
str += fmt.Sprintf(" Buttons: %s\n", strings.Join(g.pressedButtons[id], ", ")) str += fmt.Sprintf(" Buttons: %s\n", strings.Join(g.pressedButtons[id], ", "))
if ebiten.HasGamepadStandardLayoutMapping(id) {
str += "\n"
str += standardMap(id)
str += "\n"
}
str += "\n" str += "\n"
} }
} else { } else {

View File

@ -18,7 +18,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/driver"
) )
// A GamepadButton represents a gamepad button. // GamepadButton represents a gamepad button.
type GamepadButton = driver.GamepadButton type GamepadButton = driver.GamepadButton
// GamepadButtons // GamepadButtons
@ -57,3 +57,44 @@ const (
GamepadButton31 GamepadButton = driver.GamepadButton31 GamepadButton31 GamepadButton = driver.GamepadButton31
GamepadButtonMax GamepadButton = GamepadButton31 GamepadButtonMax GamepadButton = GamepadButton31
) )
// StandardGamepadButton represents a gamepad button in the standard layout.
//
// The layout and the button values are based on the web standard.
// See https://www.w3.org/TR/gamepad/#remapping.
type StandardGamepadButton = driver.StandardGamepadButton
// StandardGamepadButtons
const (
StandardGamepadButtonRightBottom StandardGamepadButton = driver.StandardGamepadButtonRightBottom
StandardGamepadButtonRightRight StandardGamepadButton = driver.StandardGamepadButtonRightRight
StandardGamepadButtonRightLeft StandardGamepadButton = driver.StandardGamepadButtonRightLeft
StandardGamepadButtonRightTop StandardGamepadButton = driver.StandardGamepadButtonRightTop
StandardGamepadButtonFrontTopLeft StandardGamepadButton = driver.StandardGamepadButtonFrontTopLeft
StandardGamepadButtonFrontTopRight StandardGamepadButton = driver.StandardGamepadButtonFrontTopRight
StandardGamepadButtonFrontBottomLeft StandardGamepadButton = driver.StandardGamepadButtonFrontBottomLeft
StandardGamepadButtonFrontBottomRight StandardGamepadButton = driver.StandardGamepadButtonFrontBottomRight
StandardGamepadButtonCenterLeft StandardGamepadButton = driver.StandardGamepadButtonCenterLeft
StandardGamepadButtonCenterRight StandardGamepadButton = driver.StandardGamepadButtonCenterRight
StandardGamepadButtonLeftStick StandardGamepadButton = driver.StandardGamepadButtonLeftStick
StandardGamepadButtonRightStick StandardGamepadButton = driver.StandardGamepadButtonRightStick
StandardGamepadButtonLeftTop StandardGamepadButton = driver.StandardGamepadButtonLeftTop
StandardGamepadButtonLeftBottom StandardGamepadButton = driver.StandardGamepadButtonLeftBottom
StandardGamepadButtonLeftLeft StandardGamepadButton = driver.StandardGamepadButtonLeftLeft
StandardGamepadButtonLeftRight StandardGamepadButton = driver.StandardGamepadButtonLeftRight
StandardGamepadButtonCenterCenter StandardGamepadButton = driver.StandardGamepadButtonCenterCenter
)
// StandardGamepadAxis represents a gamepad axis in the standard layout.
//
// The layout and the button values are based on the web standard.
// See https://www.w3.org/TR/gamepad/#remapping.
type StandardGamepadAxis = driver.StandardGamepadAxis
// StandardGamepadAxes
const (
StandardGamepadAxisLeftStickHorizontal StandardGamepadAxis = driver.StandardGamepadAxisLeftStickHorizontal
StandardGamepadAxisLeftStickVertical StandardGamepadAxis = driver.StandardGamepadAxisLeftStickVertical
StandardGamepadAxisRightStickHorizontal StandardGamepadAxis = driver.StandardGamepadAxisRightStickHorizontal
StandardGamepadAxisRightStickVertical StandardGamepadAxis = driver.StandardGamepadAxisRightStickVertical
)

View File

@ -209,6 +209,31 @@ func IsGamepadButtonPressed(id GamepadID, button GamepadButton) bool {
return uiDriver().Input().IsGamepadButtonPressed(id, button) return uiDriver().Input().IsGamepadButtonPressed(id, button)
} }
// StandardGamepadAxisValue returns a float value [-1.0 - 1.0] of the given gamepad (id)'s standard axis (axis).
//
// StandardGamepadAxisValue returns 0 when the gamepad doesn't have a standard gamepad layout mapping.
//
// StandardGamepadAxisValue is concurrent safe.
func StandardGamepadAxisValue(id GamepadID, axis StandardGamepadAxis) float64 {
return uiDriver().Input().StandardGamepadAxisValue(id, axis)
}
// IsStandardGamepadButtonPressed reports whether the given gamepad (id)'s standard gamepad button (button) is pressed.
//
// IsStandardGamepadButtonPressed returns false when the gamepad doesn't have a standard gamepad layout mapping.
//
// IsStandardGamepadButtonPressed is concurrent safe.
func IsStandardGamepadButtonPressed(id GamepadID, button StandardGamepadButton) bool {
return uiDriver().Input().IsStandardGamepadButtonPressed(id, button)
}
// HasGamepadStandardLayoutMapping reports whether the gamepad (id) has the standard gamepad layout.
//
// HasGamepadStandardLayoutMapping is concurrent-safe.
func HasGamepadStandardLayoutMapping(id GamepadID) bool {
return uiDriver().Input().HasGamepadStandardLayoutMapping(id)
}
// TouchID represents a touch's identifier. // TouchID represents a touch's identifier.
type TouchID = driver.TouchID type TouchID = driver.TouchID

View File

@ -52,3 +52,36 @@ const (
) )
const GamepadButtonNum = 32 const GamepadButtonNum = 32
type StandardGamepadButton int
// https://www.w3.org/TR/gamepad/#remapping
const (
StandardGamepadButtonRightBottom StandardGamepadButton = iota
StandardGamepadButtonRightRight
StandardGamepadButtonRightLeft
StandardGamepadButtonRightTop
StandardGamepadButtonFrontTopLeft
StandardGamepadButtonFrontTopRight
StandardGamepadButtonFrontBottomLeft
StandardGamepadButtonFrontBottomRight
StandardGamepadButtonCenterLeft
StandardGamepadButtonCenterRight
StandardGamepadButtonLeftStick
StandardGamepadButtonRightStick
StandardGamepadButtonLeftTop
StandardGamepadButtonLeftBottom
StandardGamepadButtonLeftLeft
StandardGamepadButtonLeftRight
StandardGamepadButtonCenterCenter
)
type StandardGamepadAxis int
// https://www.w3.org/TR/gamepad/#remapping
const (
StandardGamepadAxisLeftStickHorizontal StandardGamepadAxis = iota
StandardGamepadAxisLeftStickVertical
StandardGamepadAxisRightStickHorizontal
StandardGamepadAxisRightStickVertical
)

View File

@ -28,9 +28,12 @@ type Input interface {
GamepadAxisValue(id GamepadID, axis int) float64 GamepadAxisValue(id GamepadID, axis int) float64
GamepadAxisNum(id GamepadID) int GamepadAxisNum(id GamepadID) int
GamepadButtonNum(id GamepadID) int GamepadButtonNum(id GamepadID) int
HasGamepadStandardLayoutMapping(id GamepadID) bool
IsGamepadButtonPressed(id GamepadID, button GamepadButton) bool IsGamepadButtonPressed(id GamepadID, button GamepadButton) bool
IsKeyPressed(key Key) bool IsKeyPressed(key Key) bool
IsMouseButtonPressed(button MouseButton) bool IsMouseButtonPressed(button MouseButton) bool
IsStandardGamepadButtonPressed(id GamepadID, button StandardGamepadButton) bool
StandardGamepadAxisValue(id GamepadID, button StandardGamepadAxis) float64
TouchPosition(id TouchID) (x, y int) TouchPosition(id TouchID) (x, y int)
Wheel() (xoff, yoff float64) Wheel() (xoff, yoff float64)
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,379 @@
// Copyright 2021 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.
// gamecontrollerdb.txt is downloaded at https://github.com/gabomdq/SDL_GameControllerDB.
//go:generate file2byteslice -package gamepaddb -input=./gamecontrollerdb.txt -output=./gamecontrollerdb.txt.go -var=gamecontrollerdbTxt
package gamepaddb
import (
"bufio"
"bytes"
"fmt"
"io"
"runtime"
"strconv"
"strings"
"github.com/hajimehoshi/ebiten/v2/internal/driver"
)
type platform int
const (
platformUnknown platform = iota
platformWindows
platformMacOS
platformUnix
platformAndroid
platformIOS
)
func init() {
var currentPlatform platform
if runtime.GOOS == "windows" {
currentPlatform = platformWindows
} else if runtime.GOOS == "aix" ||
runtime.GOOS == "dragonfly" ||
runtime.GOOS == "freebsd" ||
runtime.GOOS == "hurd" ||
runtime.GOOS == "illumos" ||
runtime.GOOS == "linux" ||
runtime.GOOS == "netbsd" ||
runtime.GOOS == "openbsd" ||
runtime.GOOS == "solaris" {
currentPlatform = platformUnix
} else if runtime.GOOS == "android" {
currentPlatform = platformAndroid
} else if isIOS {
currentPlatform = platformIOS
} else if runtime.GOOS == "darwin" {
currentPlatform = platformMacOS
}
if currentPlatform == platformUnknown {
return
}
r := bufio.NewReader(bytes.NewReader(gamecontrollerdbTxt))
for {
line, err := r.ReadString('\n')
if err != nil && err != io.EOF {
panic(err)
}
if err := processLine(line, currentPlatform); err != nil {
panic(err)
}
if err == io.EOF {
break
}
}
}
type mappingType int
const (
mappingTypeButton mappingType = iota
mappingTypeAxis
mappingTypeHat
)
const (
HatUp = 1
HatRight = 2
HatDown = 4
HatLeft = 8
)
type mapping struct {
Type mappingType
Index int
AxisScale int
AxisOffset int
HatState int
}
var (
gamepadButtonMappings = map[string]map[driver.StandardGamepadButton]*mapping{}
gamepadAxisMappings = map[string]map[driver.StandardGamepadAxis]*mapping{}
)
func processLine(line string, platform platform) error {
line = strings.TrimSpace(line)
if len(line) == 0 {
return nil
}
if line[0] == '#' {
return nil
}
tokens := strings.Split(line, ",")
id := tokens[0]
for _, token := range tokens[2:] {
if len(token) == 0 {
continue
}
tks := strings.Split(token, ":")
if tks[0] == "platform" {
switch tks[1] {
case "Windows":
if platform != platformWindows {
return nil
}
case "Mac OS X":
if platform != platformMacOS {
return nil
}
case "Linux":
if platform != platformUnix {
return nil
}
case "Android":
if platform != platformAndroid {
return nil
}
case "iOS":
if platform != platformIOS {
return nil
}
default:
return fmt.Errorf("gamepaddb: unexpected platform: %s", tks[1])
}
continue
}
gb, err := parseMappingElement(tks[1])
if err != nil {
return err
}
if b, ok := toStandardGamepadButton(tks[0]); ok {
m, ok := gamepadButtonMappings[id]
if !ok {
m = map[driver.StandardGamepadButton]*mapping{}
gamepadButtonMappings[id] = m
}
m[b] = gb
continue
}
if a, ok := toStandardGamepadAxis(tks[0]); ok {
m, ok := gamepadAxisMappings[id]
if !ok {
m = map[driver.StandardGamepadAxis]*mapping{}
gamepadAxisMappings[id] = m
}
m[a] = gb
continue
}
// The buttons like "misc1" are ignored so far.
// There is no corresponding button in the Web standard gamepad layout.
}
return nil
}
func parseMappingElement(str string) (*mapping, error) {
switch {
case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"):
min := -1
max := 1
numstr := str[1:]
if str[0] == '+' {
numstr = str[2:]
min = 0
} else if str[0] == '-' {
numstr = str[2:]
max = 0
}
scale := 2 / (max - min)
offset := -(max + min)
if numstr[len(numstr)-1] == '~' {
numstr = numstr[:len(numstr)-1]
scale = -scale
offset = -offset
}
index, err := strconv.Atoi(numstr)
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeAxis,
Index: index,
AxisScale: scale,
AxisOffset: offset,
}, nil
case str[0] == 'b':
index, err := strconv.Atoi(str[1:])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeButton,
Index: index,
}, nil
case str[0] == 'h':
tokens := strings.Split(str[1:], ".")
if len(tokens) < 2 {
return nil, fmt.Errorf("gamepaddb: unexpected hat: %s", str)
}
index, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, err
}
hat, err := strconv.Atoi(tokens[1])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeHat,
Index: index,
HatState: hat,
}, nil
}
return nil, fmt.Errorf("gamepaddb: unepxected mapping: %s", str)
}
func toStandardGamepadButton(str string) (driver.StandardGamepadButton, bool) {
switch str {
case "a":
return driver.StandardGamepadButtonRightBottom, true
case "b":
return driver.StandardGamepadButtonRightRight, true
case "x":
return driver.StandardGamepadButtonRightLeft, true
case "y":
return driver.StandardGamepadButtonRightTop, true
case "back":
return driver.StandardGamepadButtonCenterLeft, true
case "start":
return driver.StandardGamepadButtonCenterRight, true
case "guide":
return driver.StandardGamepadButtonCenterCenter, true
case "leftshoulder":
return driver.StandardGamepadButtonFrontTopLeft, true
case "rightshoulder":
return driver.StandardGamepadButtonFrontTopRight, true
case "leftstick":
return driver.StandardGamepadButtonLeftStick, true
case "rightstick":
return driver.StandardGamepadButtonRightStick, true
case "dpup":
return driver.StandardGamepadButtonLeftTop, true
case "dpright":
return driver.StandardGamepadButtonLeftRight, true
case "dpdown":
return driver.StandardGamepadButtonLeftBottom, true
case "dpleft":
return driver.StandardGamepadButtonLeftLeft, true
case "lefttrigger":
return driver.StandardGamepadButtonFrontBottomLeft, true
case "righttrigger":
return driver.StandardGamepadButtonFrontBottomRight, true
default:
return 0, false
}
}
func toStandardGamepadAxis(str string) (driver.StandardGamepadAxis, bool) {
switch str {
case "leftx":
return driver.StandardGamepadAxisLeftStickHorizontal, true
case "lefty":
return driver.StandardGamepadAxisLeftStickVertical, true
case "rightx":
return driver.StandardGamepadAxisRightStickHorizontal, true
case "righty":
return driver.StandardGamepadAxisRightStickVertical, true
default:
return 0, false
}
}
func HasStandardLayoutMapping(id string) bool {
if _, ok := gamepadButtonMappings[id]; ok {
return true
}
if _, ok := gamepadAxisMappings[id]; ok {
return true
}
return false
}
type GamepadState interface {
Axis(index int) float64
Button(index int) bool
Hat(index int) int
}
func AxisValue(id string, axis driver.StandardGamepadAxis, state GamepadState) float64 {
mappings, ok := gamepadAxisMappings[id]
if !ok {
return 0
}
switch m := mappings[axis]; m.Type {
case mappingTypeAxis:
v := state.Axis(m.Index)*float64(m.AxisScale) + float64(m.AxisOffset)
if v > 1 {
return 1
} else if v < -1 {
return -1
}
return v
case mappingTypeButton:
if state.Button(m.Index) {
return 1
} else {
return -1
}
case mappingTypeHat:
if state.Hat(m.Index)&m.HatState != 0 {
return 1
} else {
return -1
}
}
return 0
}
func IsButtonPressed(id string, button driver.StandardGamepadButton, state GamepadState) bool {
mappings, ok := gamepadButtonMappings[id]
if !ok {
return false
}
switch m := mappings[button]; m.Type {
case mappingTypeAxis:
v := state.Axis(m.Index)*float64(m.AxisScale) + float64(m.AxisOffset)
if m.AxisOffset < 0 || m.AxisOffset == 0 && m.AxisScale > 0 {
return v >= 0
} else {
return v <= 0
}
case mappingTypeButton:
return state.Button(m.Index)
case mappingTypeHat:
return state.Hat(m.Index)&m.HatState != 0
}
return false
}

View File

@ -0,0 +1,20 @@
// Copyright 2021 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.
//go:build darwin && ios
// +build darwin,ios
package gamepaddb
const isIOS = true

View File

@ -0,0 +1,20 @@
// Copyright 2021 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.
//go:build !darwin || !ios
// +build !darwin !ios
package gamepaddb
const isIOS = false

View File

@ -22,16 +22,17 @@ import (
) )
type ( type (
Action int Action int
ErrorCode int ErrorCode int
Hint int Hint int
InputMode int InputMode int
Joystick int Joystick int
Key int JoystickHatState int
ModifierKey int Key int
MouseButton int ModifierKey int
PeripheralEvent int MouseButton int
StandardCursor int PeripheralEvent int
StandardCursor int
) )
const ( const (
@ -154,3 +155,15 @@ const (
HResizeCursor = StandardCursor(0x00036005) HResizeCursor = StandardCursor(0x00036005)
VResizeCursor = StandardCursor(0x00036006) VResizeCursor = StandardCursor(0x00036006)
) )
const (
HatCentered = JoystickHatState(0)
HatUp = JoystickHatState(1)
HatRight = JoystickHatState(2)
HatDown = JoystickHatState(4)
HatLeft = JoystickHatState(8)
HatRightUp = HatRight | HatUp
HatRightDown = HatRight | HatDown
HatLeftUp = HatLeft | HatUp
HatLeftDown = HatLeft | HatDown
)

View File

@ -281,6 +281,14 @@ func (j Joystick) GetButtons() []Action {
return bs return bs
} }
func (j Joystick) GetHats() []JoystickHatState {
var hats []JoystickHatState
for _, s := range glfw.Joystick(j).GetHats() {
hats = append(hats, JoystickHatState(s))
}
return hats
}
func GetMonitors() []*Monitor { func GetMonitors() []*Monitor {
ms := []*Monitor{} ms := []*Monitor{}
for _, m := range glfw.GetMonitors() { for _, m := range glfw.GetMonitors() {

View File

@ -393,6 +393,18 @@ func (j Joystick) GetButtons() []byte {
return bs return bs
} }
func (j Joystick) GetHats() []JoystickHatState {
var l int32
ptr := glfwDLL.call("glfwGetJoystickHats", uintptr(j), uintptr(unsafe.Pointer(&l)))
panicError()
hats := make([]JoystickHatState, l)
for i := int32(0); i < l; i++ {
hats[i] = *(*JoystickHatState)(unsafe.Pointer(ptr))
ptr++
}
return hats
}
func GetMonitors() []*Monitor { func GetMonitors() []*Monitor {
var l int32 var l int32
ptr := glfwDLL.call("glfwGetMonitors", uintptr(unsafe.Pointer(&l))) ptr := glfwDLL.call("glfwGetMonitors", uintptr(unsafe.Pointer(&l)))

View File

@ -25,6 +25,7 @@ import (
"unicode" "unicode"
"github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/gamepaddb"
"github.com/hajimehoshi/ebiten/v2/internal/glfw" "github.com/hajimehoshi/ebiten/v2/internal/glfw"
) )
@ -36,6 +37,8 @@ type gamepad struct {
axes [16]float64 axes [16]float64
buttonNum int buttonNum int
buttonPressed [256]bool buttonPressed [256]bool
hatsNum int
hats [16]int
} }
type Input struct { type Input struct {
@ -344,8 +347,83 @@ func (i *Input) update(window *glfw.Window, context driver.UIContext) {
i.gamepads[id].axes[a] = float64(axes32[a]) i.gamepads[id].axes[a] = float64(axes32[a])
} }
hats := id.GetHats()
i.gamepads[id].hatsNum = len(hats)
for h := 0; h < len(i.gamepads[id].hats); h++ {
if len(hats) <= h {
i.gamepads[id].hats[h] = 0
continue
}
i.gamepads[id].hats[h] = int(hats[h])
}
// Note that GLFW's gamepad GUID follows SDL's GUID. // Note that GLFW's gamepad GUID follows SDL's GUID.
i.gamepads[id].guid = id.GetGUID() i.gamepads[id].guid = id.GetGUID()
i.gamepads[id].name = id.GetName() i.gamepads[id].name = id.GetName()
} }
} }
func (i *Input) HasGamepadStandardLayoutMapping(id driver.GamepadID) bool {
i.ui.m.Lock()
defer i.ui.m.Unlock()
if len(i.gamepads) <= int(id) {
return false
}
g := i.gamepads[int(id)]
return gamepaddb.HasStandardLayoutMapping(g.guid)
}
func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 {
i.ui.m.Lock()
defer i.ui.m.Unlock()
if len(i.gamepads) <= int(id) {
return 0
}
g := i.gamepads[int(id)]
return gamepaddb.AxisValue(g.guid, axis, &gamepadState{&g})
}
func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool {
i.ui.m.Lock()
defer i.ui.m.Unlock()
if len(i.gamepads) <= int(id) {
return false
}
g := i.gamepads[int(id)]
return gamepaddb.IsButtonPressed(g.guid, button, &gamepadState{&g})
}
func init() {
// Confirm that all the hat state values are the same.
if gamepaddb.HatUp != glfw.HatUp {
panic("glfw: gamepaddb.HatUp must equal to glfw.HatUp but not")
}
if gamepaddb.HatRight != glfw.HatRight {
panic("glfw: gamepaddb.HatRight must equal to glfw.HatRight but not")
}
if gamepaddb.HatDown != glfw.HatDown {
panic("glfw: gamepaddb.HatDown must equal to glfw.HatDown but not")
}
if gamepaddb.HatLeft != glfw.HatLeft {
panic("glfw: gamepaddb.HatLeft must equal to glfw.HatLeft but not")
}
}
type gamepadState struct {
g *gamepad
}
func (s *gamepadState) Axis(index int) float64 {
return s.g.axes[index]
}
func (s *gamepadState) Button(index int) bool {
return s.g.buttonPressed[index]
}
func (s *gamepadState) Hat(index int) int {
return s.g.hats[index]
}

View File

@ -61,12 +61,17 @@ type pos struct {
type gamepad struct { type gamepad struct {
name string name string
mapping string
axisNum int axisNum int
axes [16]float64 axes [16]float64
buttonNum int buttonNum int
buttonPressed [256]bool buttonPressed [256]bool
} }
func (g *gamepad) hasStandardLayoutMapping() bool {
return g.mapping == "standard"
}
type Input struct { type Input struct {
keyPressed map[int]bool keyPressed map[int]bool
keyPressedEdge map[int]bool keyPressedEdge map[int]bool
@ -298,6 +303,7 @@ func (i *Input) updateGamepads() {
id := driver.GamepadID(gp.Get("index").Int()) id := driver.GamepadID(gp.Get("index").Int())
g := gamepad{} g := gamepad{}
g.name = gp.Get("id").String() g.name = gp.Get("id").String()
g.mapping = gp.Get("mapping").String()
axes := gp.Get("axes") axes := gp.Get("axes")
axesNum := axes.Get("length").Int() axesNum := axes.Get("length").Int()
@ -480,3 +486,39 @@ func (i *Input) updateForGo2Cpp() {
i.gamepads[id] = g i.gamepads[id] = g
} }
} }
func (i *Input) HasGamepadStandardLayoutMapping(id driver.GamepadID) bool {
g, ok := i.gamepads[id]
if !ok {
return false
}
return g.hasStandardLayoutMapping()
}
func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 {
g, ok := i.gamepads[id]
if !ok {
return 0
}
if !g.hasStandardLayoutMapping() {
return 0
}
// When the gamepad's mapping is "standard", the axes IDs are already mapped as the standard layout.
// See https://www.w3.org/TR/gamepad/#remapping.
return i.GamepadAxisValue(id, int(axis))
}
func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool {
g, ok := i.gamepads[id]
if !ok {
return false
}
if !g.hasStandardLayoutMapping() {
return false
}
// When the gamepad's mapping is "standard", the button IDs are already mapped as the standard layout.
// See https://www.w3.org/TR/gamepad/#remapping.
return i.IsGamepadButtonPressed(id, driver.GamepadButton(button))
}

View File

@ -132,6 +132,21 @@ func (i *Input) IsGamepadButtonPressed(id driver.GamepadID, button driver.Gamepa
return false return false
} }
func (i *Input) HasGamepadStandardLayoutMapping(id driver.GamepadID) bool {
// TODO: Implement this (#1557)
return false
}
func (i *Input) IsStandardGamepadButtonPressed(id driver.GamepadID, button driver.StandardGamepadButton) bool {
// TODO: Implement this (#1557)
return false
}
func (i *Input) StandardGamepadAxisValue(id driver.GamepadID, axis driver.StandardGamepadAxis) float64 {
// TODO: Implement this (#1557)
return 0
}
func (i *Input) AppendTouchIDs(touchIDs []driver.TouchID) []driver.TouchID { func (i *Input) AppendTouchIDs(touchIDs []driver.TouchID) []driver.TouchID {
i.ui.m.RLock() i.ui.m.RLock()
defer i.ui.m.RUnlock() defer i.ui.m.RUnlock()

View File

@ -70,6 +70,8 @@ const (
sourceJoystick = 0x01000010 sourceJoystick = 0x01000010
) )
// TODO: Can we map these values to the standard gamepad buttons?
var androidKeyToGamepadButton = map[int]driver.GamepadButton{ var androidKeyToGamepadButton = map[int]driver.GamepadButton{
keycodeButtonA: driver.GamepadButton0, keycodeButtonA: driver.GamepadButton0,
keycodeButtonB: driver.GamepadButton1, keycodeButtonB: driver.GamepadButton1,