mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-14 06:57:27 +01:00
aa694be6f6
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
380 lines
8.3 KiB
Go
380 lines
8.3 KiB
Go
// 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
|
|
}
|