diff --git a/input.go b/input.go index e585c7b52..4bad2dac8 100644 --- a/input.go +++ b/input.go @@ -137,8 +137,6 @@ func GamepadSDLID(id GamepadID) string { // - Chrome: "Xbox 360 Controller (XInput STANDARD GAMEPAD)" // - Firefox: "xinput" // -// GamepadName always returns an empty string on iOS. -// // GamepadName is concurrent-safe. func GamepadName(id GamepadID) string { return uiDriver().Input().GamepadName(id) @@ -148,8 +146,6 @@ func GamepadName(id GamepadID) string { // Giving a slice that already has enough capacity works efficiently. // // AppendGamepadIDs is concurrent-safe. -// -// AppendGamepadIDs doesn't append anything on iOS. func AppendGamepadIDs(gamepadIDs []GamepadID) []GamepadID { return uiDriver().Input().AppendGamepadIDs(gamepadIDs) } @@ -164,8 +160,6 @@ func GamepadIDs() []GamepadID { // GamepadAxisNum returns the number of axes of the gamepad (id). // // GamepadAxisNum is concurrent-safe. -// -// GamepadAxisNum always returns 0 on iOS. func GamepadAxisNum(id GamepadID) int { return uiDriver().Input().GamepadAxisNum(id) } @@ -173,8 +167,6 @@ func GamepadAxisNum(id GamepadID) int { // GamepadAxisValue returns a float value [-1.0 - 1.0] of the given gamepad (id)'s axis (axis). // // GamepadAxisValue is concurrent-safe. -// -// GamepadAxisValue always returns 0 on iOS. func GamepadAxisValue(id GamepadID, axis int) float64 { return uiDriver().Input().GamepadAxisValue(id, axis) } @@ -189,8 +181,6 @@ func GamepadAxis(id GamepadID, axis int) float64 { // GamepadButtonNum returns the number of the buttons of the given gamepad (id). // // GamepadButtonNum is concurrent-safe. -// -// GamepadButtonNum always returns 0 on iOS. func GamepadButtonNum(id GamepadID) int { return uiDriver().Input().GamepadButtonNum(id) } @@ -204,8 +194,6 @@ func GamepadButtonNum(id GamepadID) int { // // The relationships between physical buttons and buttion IDs depend on environments. // There can be differences even between Chrome and Firefox. -// -// IsGamepadButtonPressed always returns false on iOS. func IsGamepadButtonPressed(id GamepadID, button GamepadButton) bool { return uiDriver().Input().IsGamepadButtonPressed(id, button) } diff --git a/internal/uidriver/mobile/gamepad_android.go b/internal/uidriver/mobile/gamepad_android.go index 55e8b4694..76fe7b9b9 100644 --- a/internal/uidriver/mobile/gamepad_android.go +++ b/internal/uidriver/mobile/gamepad_android.go @@ -21,13 +21,17 @@ import ( // UpdateGamepads updates the gamepad states. // UpdateGamepads is called when the gamepad states are given from outside like Android. func (u *UserInterface) UpdateGamepads(gamepads []Gamepad) { - u.input.updateGamepads(gamepads) + u.input.updateGamepadsFromOutside(gamepads) if u.fpsMode == driver.FPSModeVsyncOffMinimum { u.renderRequester.RequestRenderIfNeeded() } } -func (i *Input) updateGamepads(gamepads []Gamepad) { +func (i *Input) updateGamepadsFromOutside(gamepads []Gamepad) { i.gamepads = i.gamepads[:0] i.gamepads = append(i.gamepads, gamepads...) } + +func (i *Input) updateGamepads() { + // Do nothing. +} diff --git a/internal/uidriver/mobile/gamepad_ios.go b/internal/uidriver/mobile/gamepad_ios.go index ae01b6c42..1a71fb37d 100644 --- a/internal/uidriver/mobile/gamepad_ios.go +++ b/internal/uidriver/mobile/gamepad_ios.go @@ -17,4 +17,495 @@ package mobile -// TODO: Implement gamepad detection for iOS (#1105). +// #cgo CFLAGS: -x objective-c +// #cgo LDFLAGS: -framework Foundation -framework GameController +// +// #import +// +// static NSMutableArray* controllers = nil; +// static NSMutableArray* controllerProperties = nil; +// static NSString* GCInputXboxShareButton = @"Button Share"; +// static uint32_t currentId = 0; +// +// static uint32_t genNextId() { +// currentId++; +// return currentId; +// } +// +// enum ControllerButton { +// kControllerButtonInvalid = -1, +// kControllerButtonA, +// kControllerButtonB, +// kControllerButtonX, +// kControllerButtonY, +// kControllerButtonBack, +// kControllerButtonGuide, +// kControllerButtonStart, +// kControllerButtonLeftStick, +// kControllerButtonRightStick, +// kControllerButtonLeftShoulder, +// kControllerButtonRightShoulder, +// kControllerButtonDpadUp, +// kControllerButtonDpadDown, +// kControllerButtonDpadLeft, +// kControllerButtonDpadRight, +// kControllerButtonMisc1, +// kControllerButtonPaddle1, +// kControllerButtonPaddle2, +// kControllerButtonPaddle3, +// kControllerButtonPaddle4, +// kControllerButtonTouchpad, +// kControllerButtonMax, +// }; +// +// enum HatState : uint8_t { +// kHatCentered = 0x00, +// kHatUp = 0x01, +// kHatRight = 0x02, +// kHatDown = 0x04, +// kHatLeft = 0x08, +// kHatRightUp = kHatRight | kHatUp, +// kHatRightDown = kHatRight | kHatDown, +// kHatLeftUp = kHatLeft | kHatUp, +// kHatLeftDown = kHatLeft | kHatDown, +// }; +// +// enum USBVendor { +// kUSBVendorApple = 0x05ac, +// kUSBVendorMicrosoft = 0x045e, +// kUSBVendorSony = 0x054c, +// }; +// +// enum USBProduct { +// kUSBProductSonyDS4Slim = 0x09cc, +// kUSBProductSonyDS5 = 0x0ce6, +// kUSBProductXboxOneEliteSeries2Bluetooth = 0x0b05, +// kUSBProductXboxOneSRev1Bluetooth = 0x02e0, +// kUSBProductXboxSeriesXBluetooth = 0x0b13, +// }; +// +// struct ControllerProperty { +// uint32_t id; +// uint8_t nButtons; +// uint8_t nAxes; +// uint8_t nHats; +// uint16_t buttonMask; +// uint8_t guid[16]; +// uint8_t name[256]; +// bool hasDualshockTouchpad; +// bool hasXboxPaddles; +// bool hasXboxShareButton; +// }; +// +// static size_t min(size_t a, size_t b) { +// return a < b ? a : b; +// } +// +// static void getControllerPropertyFromController(GCController* controller, struct ControllerProperty* property) { +// @autoreleasepool { +// uint16_t vendor = 0; +// uint16_t product = 0; +// uint16_t subtype = 0; +// +// const char* name = nil; +// if (controller.vendorName) { +// name = controller.vendorName.UTF8String; +// } +// if (!name) { +// name = "MFi Gamepad"; +// } +// memcpy(property->name, name, min(sizeof(property->name), strlen(name))); +// +// if (controller.extendedGamepad) { +// GCExtendedGamepad* gamepad = controller.extendedGamepad; +// +// bool isXbox = false; +// bool isPS4 = false; +// bool isPS5 = false; +// if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { +// NSString* productCategory = [controller productCategory]; +// if ([productCategory isEqualToString:@"DualShock 4"]) { +// isPS4 = 1; +// } else if ([productCategory isEqualToString:@"DualSense"]) { +// isPS5 = 1; +// } else if ([productCategory isEqualToString:@"Xbox One"]) { +// isXbox = 1; +// } +// } else { +// NSString* vendorName = [controller vendorName]; +// if ([vendorName isEqualToString:@"DUALSHOCK"]) { +// isPS4 = 1; +// } else if ([vendorName isEqualToString:@"DualSense"]) { +// isPS5 = 1; +// } else if ([vendorName isEqualToString:@"Xbox"]) { +// isXbox = 1; +// } +// } +// +// property->buttonMask |= (1 << kControllerButtonA); +// property->buttonMask |= (1 << kControllerButtonB); +// property->buttonMask |= (1 << kControllerButtonX); +// property->buttonMask |= (1 << kControllerButtonY); +// property->buttonMask |= (1 << kControllerButtonLeftShoulder); +// property->buttonMask |= (1 << kControllerButtonRightShoulder); +// property->nButtons += 6; +// +// #pragma clang diagnostic push +// #pragma clang diagnostic ignored "-Wunguarded-availability-new" +// +// if ([gamepad respondsToSelector:@selector(leftThumbstickButton)] && gamepad.leftThumbstickButton) { +// property->buttonMask |= (1 << kControllerButtonLeftStick); +// property->nButtons++; +// } +// if ([gamepad respondsToSelector:@selector(rightThumbstickButton)] && gamepad.rightThumbstickButton) { +// property->buttonMask |= (1 << kControllerButtonRightStick); +// property->nButtons++; +// } +// if ([gamepad respondsToSelector:@selector(buttonOptions)] && gamepad.buttonOptions) { +// property->buttonMask |= (1 << kControllerButtonBack); +// property->nButtons++; +// } +// if ([gamepad respondsToSelector:@selector(buttonHome)] && gamepad.buttonHome) { +// property->buttonMask |= (1 << kControllerButtonGuide); +// property->nButtons++; +// } +// +// property->buttonMask |= (1 << kControllerButtonStart); +// property->nButtons++; +// +// if ([controller respondsToSelector:@selector(physicalInputProfile)]) { +// if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) { +// property->hasDualshockTouchpad = true; +// property->buttonMask |= (1 << kControllerButtonMisc1); +// property->nButtons++; +// } +// if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) { +// property->hasXboxPaddles = true; +// property->buttonMask |= (1 << kControllerButtonPaddle1); +// property->nButtons++; +// } +// if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo] != nil) { +// property->hasXboxPaddles = true; +// property->buttonMask |= (1 << kControllerButtonPaddle2); +// property->nButtons++; +// } +// if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree] != nil) { +// property->hasXboxPaddles = true; +// property->buttonMask |= (1 << kControllerButtonPaddle3); +// property->nButtons++; +// } +// if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour] != nil) { +// property->hasXboxPaddles = true; +// property->buttonMask |= (1 << kControllerButtonPaddle4); +// property->nButtons++; +// } +// if (controller.physicalInputProfile.buttons[GCInputXboxShareButton] != nil) { +// property->hasXboxShareButton = true; +// property->buttonMask |= (1 << kControllerButtonMisc1); +// property->nButtons++; +// } +// } +// +// #pragma clang diagnostic pop +// +// if (isXbox) { +// vendor = kUSBVendorMicrosoft; +// if (property->hasXboxPaddles) { +// product = kUSBProductXboxOneEliteSeries2Bluetooth; +// subtype = 1; +// } else if (property->hasXboxShareButton) { +// product = kUSBProductXboxSeriesXBluetooth; +// subtype = 1; +// } else { +// product = kUSBProductXboxOneSRev1Bluetooth; +// subtype = 0; +// } +// } else if (isPS4) { +// vendor = kUSBVendorSony; +// product = kUSBProductSonyDS4Slim; +// if (property->hasDualshockTouchpad) { +// subtype = 1; +// } else { +// subtype = 0; +// } +// } else if (isPS5) { +// vendor = kUSBVendorSony; +// product = kUSBProductSonyDS5; +// subtype = 0; +// } else { +// vendor = kUSBVendorApple; +// product = 1; +// subtype = 1; +// } +// +// property->nAxes = 6; +// property->nHats = 1; +// } else if (controller.gamepad) { +// property->buttonMask |= (1 << kControllerButtonA); +// property->buttonMask |= (1 << kControllerButtonB); +// property->buttonMask |= (1 << kControllerButtonX); +// property->buttonMask |= (1 << kControllerButtonY); +// property->buttonMask |= (1 << kControllerButtonLeftShoulder); +// property->buttonMask |= (1 << kControllerButtonRightShoulder); +// // This button's detection actually does not happen. +// property->buttonMask |= (1 << kControllerButtonStart); +// property->nButtons += 7; +// +// vendor = kUSBVendorApple; +// product = 2; +// subtype = 2; +// property->nAxes = 0; +// property->nHats = 1; +// } +// +// const int kSDLHardwareBusBluetooth = 0x05; +// property->guid[0] = (uint8_t)(kSDLHardwareBusBluetooth); +// property->guid[1] = (uint8_t)(kSDLHardwareBusBluetooth >> 8); +// property->guid[2] = 0; +// property->guid[3] = 0; +// property->guid[4] = (uint8_t)(vendor); +// property->guid[5] = (uint8_t)(vendor >> 8); +// property->guid[6] = 0; +// property->guid[7] = 0; +// property->guid[8] = (uint8_t)(product); +// property->guid[9] = (uint8_t)(product >> 8); +// property->guid[10] = 0; +// property->guid[11] = 0; +// property->guid[12] = (uint8_t)(property->buttonMask); +// property->guid[13] = (uint8_t)(property->buttonMask >> 8); +// if (vendor == kUSBVendorApple) { +// property->guid[14] = 'm'; +// } else { +// property->guid[14] = 0; +// } +// property->guid[15] = subtype; +// } +// } +// +// static void addController(GCController* controller) { +// // Ignore if the controller is not an actual controller. +// if (!controller.extendedGamepad && !controller.gamepad && controller.microGamepad) { +// return; +// } +// +// [controllers addObject:controller]; +// +// struct ControllerProperty property = {}; +// getControllerPropertyFromController(controller, &property); +// property.id = genNextId(); +// NSValue* value = [NSValue valueWithBytes:&property objCType:@encode(struct ControllerProperty)]; +// [controllerProperties addObject:value]; +// } +// +// static void removeController(GCController* controller) { +// int index = [controllers indexOfObject:controller]; +// [controllers removeObjectAtIndex:index]; +// // NSValue should be released by the autorelease pool. +// [controllerProperties removeObjectAtIndex:index]; +// } +// +// static void getControllerPropertyFromIndex(int index, struct ControllerProperty* property) { +// NSValue* value = [controllerProperties objectAtIndex:index]; +// [value getValue:property]; +// } +// +// struct ControllerState { +// uint8_t buttons[32]; +// float axes[32]; +// enum HatState hat; +// }; +// +// static enum HatState getHatState(GCControllerDirectionPad* dpad) { +// enum HatState hat = 0; +// if (dpad.up.isPressed) { +// hat |= kHatUp; +// } else if (dpad.down.isPressed) { +// hat |= kHatDown; +// } +// if (dpad.left.isPressed) { +// hat |= kHatLeft; +// } else if (dpad.right.isPressed) { +// hat |= kHatRight; +// } +// return hat; +// } +// +// static void getControllerStateFromIndex(int index, struct ControllerState* controllerState) { +// @autoreleasepool { +// GCController* controller = [controllers objectAtIndex:index]; +// struct ControllerProperty property = {}; +// getControllerPropertyFromIndex(index, &property); +// +// if (controller.extendedGamepad) { +// GCExtendedGamepad* gamepad = controller.extendedGamepad; +// +// controllerState->axes[0] = gamepad.leftThumbstick.xAxis.value; +// controllerState->axes[1] = -gamepad.leftThumbstick.yAxis.value; +// controllerState->axes[2] = gamepad.leftTrigger.value * 2 - 1; +// controllerState->axes[3] = gamepad.rightThumbstick.xAxis.value; +// controllerState->axes[4] = -gamepad.rightThumbstick.yAxis.value; +// controllerState->axes[5] = gamepad.rightTrigger.value * 2 - 1; +// +// int buttonCount = 0; +// controllerState->buttons[buttonCount++] = gamepad.buttonA.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonB.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonX.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonY.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.leftShoulder.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.rightShoulder.isPressed; +// +// #pragma clang diagnostic push +// #pragma clang diagnostic ignored "-Wunguarded-availability-new" +// +// if (property.buttonMask & (1 << kControllerButtonLeftStick)) { +// controllerState->buttons[buttonCount++] = gamepad.leftThumbstickButton.isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonRightStick)) { +// controllerState->buttons[buttonCount++] = gamepad.rightThumbstickButton.isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonBack)) { +// controllerState->buttons[buttonCount++] = gamepad.buttonOptions.isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonGuide)) { +// controllerState->buttons[buttonCount++] = gamepad.buttonHome.isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonStart)) { +// controllerState->buttons[buttonCount++] = gamepad.buttonMenu.isPressed; +// } +// +// if (property.hasDualshockTouchpad) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].isPressed; +// } +// if (property.hasXboxPaddles) { +// if (property.buttonMask & (1 << kControllerButtonPaddle1)) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonPaddle2)) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonPaddle3)) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].isPressed; +// } +// if (property.buttonMask & (1 << kControllerButtonPaddle4)) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].isPressed; +// } +// } +// if (property.hasXboxShareButton) { +// controllerState->buttons[buttonCount++] = controller.physicalInputProfile.buttons[GCInputXboxShareButton].isPressed; +// } +// +// #pragma clang diagnostic pop +// +// if (property.nHats) { +// controllerState->hat = getHatState(gamepad.dpad); +// } +// } else if (controller.gamepad) { +// GCGamepad* gamepad = controller.gamepad; +// +// int buttonCount = 0; +// controllerState->buttons[buttonCount++] = gamepad.buttonA.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonB.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonX.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.buttonY.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.leftShoulder.isPressed; +// controllerState->buttons[buttonCount++] = gamepad.rightShoulder.isPressed; +// +// if (property.nHats) { +// controllerState->hat = getHatState(gamepad.dpad); +// } +// } +// } +// } +// +// static void init(void) { +// controllers = [NSMutableArray array]; +// controllerProperties = [NSMutableArray array]; +// +// @autoreleasepool { +// for (GCController* controller in [GCController controllers]) { +// addController(controller); +// } +// NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; +// [center addObserverForName:GCControllerDidConnectNotification +// object:nil +// queue:nil +// usingBlock:^(NSNotification* notification) { +// addController(notification.object); +// }]; +// [center addObserverForName:GCControllerDidDisconnectNotification +// object:nil +// queue:nil +// usingBlock:^(NSNotification* notification) { +// removeController(notification.object); +// }]; +// } +// } +// +// static int getControllersNum(void) { +// return [controllers count]; +// } +import "C" + +import ( + "encoding/hex" + + "github.com/hajimehoshi/ebiten/v2/internal/driver" +) + +func init() { + C.init() +} + +func (i *Input) updateGamepads() { + i.gamepads = i.gamepads[:0] + + num := int(C.getControllersNum()) + for idx := 0; idx < num; idx++ { + var property C.struct_ControllerProperty + C.getControllerPropertyFromIndex(C.int(idx), &property) + var guid [16]byte + for i := range property.guid { + guid[i] = byte(property.guid[i]) + } + + name := make([]byte, 0, len(property.name)) + for _, c := range property.name { + if c == 0 { + break + } + name = append(name, byte(c)) + } + + gamepad := Gamepad{ + ID: driver.GamepadID(property.id), + SDLID: hex.EncodeToString(guid[:]), + Name: string(name), + ButtonNum: int(property.nButtons), + AxisNum: int(property.nAxes), + } + + var state C.struct_ControllerState + C.getControllerStateFromIndex(C.int(idx), &state) + + for j := 0; j < gamepad.ButtonNum; j++ { + gamepad.Buttons[j] = state.buttons[j] != 0 + } + + // Follow the GLFW way to process hats. + // See _glfwInputJoystickHat. + if property.nHats > 0 { + base := gamepad.ButtonNum + gamepad.ButtonNum += 4 + gamepad.Buttons[base] = state.hat&0x01 != 0 + gamepad.Buttons[base+1] = state.hat&0x02 != 0 + gamepad.Buttons[base+2] = state.hat&0x04 != 0 + gamepad.Buttons[base+3] = state.hat&0x08 != 0 + } + + for j := 0; j < gamepad.AxisNum; j++ { + gamepad.Axes[j] = float32(state.axes[j]) + } + + i.gamepads = append(i.gamepads, gamepad) + } +} diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index fac5b7acf..3e8ca62a0 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -83,6 +83,8 @@ func (u *UserInterface) Update() error { return nil } + u.input.updateGamepads() + renderCh <- struct{}{} go func() { <-renderEndCh