From b2a40338485acc6115c2e6cbb19b1fa30e61e76c Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 16 Mar 2024 15:35:25 +0900 Subject: [PATCH] ebiten: add GamepadMapping Closes #2775 --- examples/gamepad/main.go | 28 +++++ gamepad.go | 29 +++++ internal/gamepad/gamepad.go | 94 ++++++++++++++++ internal/gamepaddb/gamepaddb.go | 183 +++++++++++++++++--------------- 4 files changed, 247 insertions(+), 87 deletions(-) diff --git a/examples/gamepad/main.go b/examples/gamepad/main.go index 64628a288..5af8335a4 100644 --- a/examples/gamepad/main.go +++ b/examples/gamepad/main.go @@ -193,6 +193,34 @@ func (g *Game) Draw(screen *ebiten.Image) { if ebiten.IsStandardGamepadLayoutAvailable(id) { str += "\n" str += standardMap(id) + "\n" + + // Print unmapped buttons. + buttonIDs := map[ebiten.GamepadButton]struct{}{} + for i := 0; i < ebiten.GamepadButtonCount(id); i++ { + buttonIDs[ebiten.GamepadButton(i)] = struct{}{} + } + for _, m := range ebiten.GamepadMapping(id) { + if m.PhysicalType == ebiten.GamepadMappingTypeButton { + delete(buttonIDs, ebiten.GamepadButton(m.PhysicalIndex)) + } + } + if len(buttonIDs) > 0 { + unmappedButtonIDs := make([]ebiten.GamepadButton, 0, len(buttonIDs)) + for id := range buttonIDs { + unmappedButtonIDs = append(unmappedButtonIDs, id) + } + sort.Slice(unmappedButtonIDs, func(i, j int) bool { + return unmappedButtonIDs[i] < unmappedButtonIDs[j] + }) + str += " Unmapped Buttons: " + for i, id := range unmappedButtonIDs { + str += fmt.Sprintf("%d", id) + if i < len(unmappedButtonIDs)-1 { + str += ", " + } + } + str += "\n" + } } str += "\n" } diff --git a/gamepad.go b/gamepad.go index 42adf3ccc..56e50179d 100644 --- a/gamepad.go +++ b/gamepad.go @@ -105,3 +105,32 @@ const ( StandardGamepadAxisRightStickVertical StandardGamepadAxis = gamepaddb.StandardAxisRightStickVertical StandardGamepadAxisMax StandardGamepadAxis = StandardGamepadAxisRightStickVertical ) + +type GamepadMappingType = gamepaddb.MappingType + +const ( + GamepadMappingTypeButton GamepadMappingType = gamepaddb.MappingTypeButton + GamepadMappingTypeAxis GamepadMappingType = gamepaddb.MappingTypeAxis +) + +type GamepadMappingItem struct { + StandardType GamepadMappingType + StandardIndex int + PhysicalType GamepadMappingType + PhysicalIndex int + PhysicalToStandardAxisScale float64 + PhysicalToStandardAxisOffset float64 +} + +func GamepadMapping(id GamepadID) []GamepadMappingItem { + g := gamepad.Get(id) + if g == nil { + return nil + } + m := g.Mapping() + mapping := make([]GamepadMappingItem, len(m)) + for i, v := range m { + mapping[i] = GamepadMappingItem(v) + } + return mapping +} diff --git a/internal/gamepad/gamepad.go b/internal/gamepad/gamepad.go index f0f0189e3..f9ef34572 100644 --- a/internal/gamepad/gamepad.go +++ b/internal/gamepad/gamepad.go @@ -408,3 +408,97 @@ func (g *Gamepad) Vibrate(duration time.Duration, strongMagnitude float64, weakM g.native.vibrate(duration, strongMagnitude, weakMagnitude) } + +type MappingItem struct { + StandardType gamepaddb.MappingType + StandardIndex int + PhysicalType gamepaddb.MappingType + PhysicalIndex int + PhysicalToStandardAxisScale float64 + PhysicalToStandardAxisOffset float64 +} + +func (g *Gamepad) Mapping() []MappingItem { + g.m.Lock() + defer g.m.Unlock() + + if g.native.hasOwnStandardLayoutMapping() { + var mapping []MappingItem + + buttonCount := g.native.buttonCount() + g.native.hatCount()*4 + if buttonCount > int(gamepaddb.StandardButtonMax)+1 { + buttonCount = int(gamepaddb.StandardButtonMax) + 1 + } + for i := 0; i < buttonCount; i++ { + mapping = append(mapping, MappingItem{ + StandardType: gamepaddb.MappingTypeButton, + StandardIndex: i, + PhysicalType: gamepaddb.MappingTypeButton, + PhysicalIndex: i, + }) + } + + axisCount := g.native.axisCount() + if axisCount > int(gamepaddb.StandardAxisMax)+1 { + axisCount = int(gamepaddb.StandardAxisMax) + 1 + } + for i := 0; i < axisCount; i++ { + mapping = append(mapping, MappingItem{ + StandardType: gamepaddb.MappingTypeAxis, + StandardIndex: i, + PhysicalType: gamepaddb.MappingTypeAxis, + PhysicalIndex: i, + PhysicalToStandardAxisScale: 1, + }) + } + + return mapping + } + + var mapping []MappingItem + buttons, axes := gamepaddb.UnsafeMapping(g.sdlID) + for button, item := range buttons { + item := g.convertMappingItem(item) + item.StandardType = gamepaddb.MappingTypeButton + item.StandardIndex = int(button) + mapping = append(mapping, item) + } + for axis, item := range axes { + item := g.convertMappingItem(item) + item.StandardType = gamepaddb.MappingTypeAxis + item.StandardIndex = int(axis) + mapping = append(mapping, item) + } + + return mapping +} + +func (g *Gamepad) convertMappingItem(item gamepaddb.MappingItem) MappingItem { + var r MappingItem + switch item.Type { + case gamepaddb.MappingTypeButton: + r.PhysicalType = gamepaddb.MappingTypeButton + r.PhysicalIndex = item.Index + case gamepaddb.MappingTypeAxis: + r.PhysicalType = gamepaddb.MappingTypeAxis + r.PhysicalIndex = item.Index + r.PhysicalToStandardAxisScale = item.AxisScale + r.PhysicalToStandardAxisOffset = item.AxisOffset + case gamepaddb.MappingTypeHat: + // Convert a hat value to a button value. + r.PhysicalType = gamepaddb.MappingTypeButton + var offset int + switch item.HatState { + case hatUp: + offset = 0 + case hatRight: + offset = 1 + case hatDown: + offset = 2 + case hatLeft: + offset = 3 + } + r.PhysicalIndex = g.native.buttonCount() + 4*item.Index + offset + } + return r +} diff --git a/internal/gamepaddb/gamepaddb.go b/internal/gamepaddb/gamepaddb.go index d4d287041..c72516b50 100644 --- a/internal/gamepaddb/gamepaddb.go +++ b/internal/gamepaddb/gamepaddb.go @@ -102,12 +102,12 @@ func init() { } } -type mappingType int +type MappingType int const ( - mappingTypeButton mappingType = iota - mappingTypeAxis - mappingTypeHat + MappingTypeButton MappingType = iota + MappingTypeAxis + MappingTypeHat ) const ( @@ -117,8 +117,8 @@ const ( HatLeft = 8 ) -type mapping struct { - Type mappingType +type MappingItem struct { + Type MappingType Index int AxisScale float64 AxisOffset float64 @@ -127,12 +127,12 @@ type mapping struct { var ( gamepadNames = map[string]string{} - gamepadButtonMappings = map[string]map[StandardButton]mapping{} - gamepadAxisMappings = map[string]map[StandardAxis]mapping{} + gamepadButtonMappings = map[string]map[StandardButton]MappingItem{} + gamepadAxisMappings = map[string]map[StandardAxis]MappingItem{} mappingsM sync.RWMutex ) -func parseLine(line string, platform platform) (id string, name string, buttons map[StandardButton]mapping, axes map[StandardAxis]mapping, err error) { +func parseLine(line string, platform platform) (id string, name string, buttons map[StandardButton]MappingItem, axes map[StandardAxis]MappingItem, err error) { line = strings.TrimSpace(line) if len(line) == 0 { return "", "", nil, nil, nil @@ -192,7 +192,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons if b, ok := toStandardGamepadButton(tks[0]); ok { if buttons == nil { - buttons = map[StandardButton]mapping{} + buttons = map[StandardButton]MappingItem{} } buttons[b] = gb continue @@ -200,7 +200,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons if a, ok := toStandardGamepadAxis(tks[0]); ok { if axes == nil { - axes = map[StandardAxis]mapping{} + axes = map[StandardAxis]MappingItem{} } axes[a] = gb continue @@ -213,7 +213,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons return tokens[0], tokens[1], buttons, axes, nil } -func parseMappingElement(str string) (mapping, error) { +func parseMappingElement(str string) (MappingItem, error) { switch { case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"): var tilda bool @@ -259,11 +259,11 @@ func parseMappingElement(str string) (mapping, error) { index, err := strconv.Atoi(numstr) if err != nil { - return mapping{}, err + return MappingItem{}, err } - return mapping{ - Type: mappingTypeAxis, + return MappingItem{ + Type: MappingTypeAxis, Index: index, AxisScale: scale, AxisOffset: offset, @@ -272,34 +272,34 @@ func parseMappingElement(str string) (mapping, error) { case str[0] == 'b': index, err := strconv.Atoi(str[1:]) if err != nil { - return mapping{}, err + return MappingItem{}, err } - return mapping{ - Type: mappingTypeButton, + return MappingItem{ + Type: MappingTypeButton, Index: index, }, nil case str[0] == 'h': tokens := strings.Split(str[1:], ".") if len(tokens) < 2 { - return mapping{}, fmt.Errorf("gamepaddb: unexpected hat: %s", str) + return MappingItem{}, fmt.Errorf("gamepaddb: unexpected hat: %s", str) } index, err := strconv.Atoi(tokens[0]) if err != nil { - return mapping{}, err + return MappingItem{}, err } hat, err := strconv.Atoi(tokens[1]) if err != nil { - return mapping{}, err + return MappingItem{}, err } - return mapping{ - Type: mappingTypeHat, + return MappingItem{ + Type: MappingTypeHat, Index: index, HatState: hat, }, nil } - return mapping{}, fmt.Errorf("gamepaddb: unepxected mapping: %s", str) + return MappingItem{}, fmt.Errorf("gamepaddb: unepxected mapping: %s", str) } func toStandardGamepadButton(str string) (StandardButton, bool) { @@ -358,7 +358,7 @@ func toStandardGamepadAxis(str string) (StandardAxis, bool) { } } -func buttonMappings(id string) map[StandardButton]mapping { +func buttonMapping(id string) map[StandardButton]MappingItem { if m, ok := gamepadButtonMappings[id]; ok { return m } @@ -370,7 +370,7 @@ func buttonMappings(id string) map[StandardButton]mapping { return nil } -func axisMappings(id string) map[StandardAxis]mapping { +func axisMapping(id string) map[StandardAxis]MappingItem { if m, ok := gamepadAxisMappings[id]; ok { return m } @@ -386,7 +386,7 @@ func HasStandardLayoutMapping(id string) bool { mappingsM.RLock() defer mappingsM.RUnlock() - return buttonMappings(id) != nil || axisMappings(id) != nil + return buttonMapping(id) != nil || axisMapping(id) != nil } type GamepadState interface { @@ -406,7 +406,7 @@ func HasStandardAxis(id string, axis StandardAxis) bool { mappingsM.RLock() defer mappingsM.RUnlock() - mappings := axisMappings(id) + mappings := axisMapping(id) if mappings == nil { return false } @@ -418,7 +418,7 @@ func StandardAxisValue(id string, axis StandardAxis, state GamepadState) float64 mappingsM.RLock() defer mappingsM.RUnlock() - mappings := axisMappings(id) + mappings := axisMapping(id) if mappings == nil { return 0 } @@ -429,7 +429,7 @@ func StandardAxisValue(id string, axis StandardAxis, state GamepadState) float64 } switch mapping.Type { - case mappingTypeAxis: + case MappingTypeAxis: v := state.Axis(mapping.Index)*mapping.AxisScale + mapping.AxisOffset if v > 1 { return 1 @@ -437,13 +437,13 @@ func StandardAxisValue(id string, axis StandardAxis, state GamepadState) float64 return -1 } return v - case mappingTypeButton: + case MappingTypeButton: if state.Button(mapping.Index) { return 1 } else { return -1 } - case mappingTypeHat: + case MappingTypeHat: if state.Hat(mapping.Index)&mapping.HatState != 0 { return 1 } else { @@ -458,7 +458,7 @@ func HasStandardButton(id string, button StandardButton) bool { mappingsM.RLock() defer mappingsM.RUnlock() - mappings := buttonMappings(id) + mappings := buttonMapping(id) if mappings == nil { return false } @@ -474,7 +474,7 @@ func StandardButtonValue(id string, button StandardButton, state GamepadState) f } func standardButtonValue(id string, button StandardButton, state GamepadState) float64 { - mappings := buttonMappings(id) + mappings := buttonMapping(id) if mappings == nil { return 0 } @@ -485,7 +485,7 @@ func standardButtonValue(id string, button StandardButton, state GamepadState) f } switch mapping.Type { - case mappingTypeAxis: + case MappingTypeAxis: v := state.Axis(mapping.Index)*mapping.AxisScale + mapping.AxisOffset if v > 1 { v = 1 @@ -494,12 +494,12 @@ func standardButtonValue(id string, button StandardButton, state GamepadState) f } // Adjust [-1, 1] to [0, 1] return (v + 1) / 2 - case mappingTypeButton: + case MappingTypeButton: if state.Button(mapping.Index) { return 1 } return 0 - case mappingTypeHat: + case MappingTypeHat: if state.Hat(mapping.Index)&mapping.HatState != 0 { return 1 } @@ -530,18 +530,27 @@ func IsStandardButtonPressed(id string, button StandardButton, state GamepadStat } switch mapping.Type { - case mappingTypeAxis: + case MappingTypeAxis: v := standardButtonValue(id, button, state) return v > ButtonPressedThreshold - case mappingTypeButton: + case MappingTypeButton: return state.Button(mapping.Index) - case mappingTypeHat: + case MappingTypeHat: return state.Hat(mapping.Index)&mapping.HatState != 0 } return false } +// UnsafeMapping returns the mapping of the gamepad. +// UnsafeMapping is unsafe. The returned values must not be modified. +func UnsafeMapping(id string) (map[StandardButton]MappingItem, map[StandardAxis]MappingItem) { + mappingsM.RLock() + defer mappingsM.RUnlock() + + return buttonMapping(id), axisMapping(id) +} + // Update adds new gamepad mappings. // The string must be in the format of SDL_GameControllerDB. // @@ -556,8 +565,8 @@ func Update(mappingData []byte) error { type parsedLine struct { id string name string - buttons map[StandardButton]mapping - axes map[StandardAxis]mapping + buttons map[StandardButton]MappingItem + axes map[StandardAxis]MappingItem } var lines []parsedLine @@ -611,155 +620,155 @@ func addAndroidDefaultMappings(id string) bool { return false } - gamepadButtonMappings[id] = map[StandardButton]mapping{} - gamepadAxisMappings[id] = map[StandardAxis]mapping{} + gamepadButtonMappings[id] = map[StandardButton]MappingItem{} + gamepadAxisMappings[id] = map[StandardAxis]MappingItem{} // For mappings, see mobile/ebitenmobileview/input_android.go. if buttonMask&(1<= 30, add this code: // - // gamepadButtonMappings[id][StandardButtonCenterCenter] = mapping{ + // gamepadButtonMappings[id][StandardButtonCenterCenter] = MappingItem{ // Type: mappingTypeButton, // Index: SDLControllerButtonGuide, // } } if buttonMask&(1<