diff --git a/drivers/SCsub b/drivers/SCsub index 03ad70649b2..153298c99e9 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -26,7 +26,9 @@ if env["xaudio2"]: print_error("Target platform '{}' does not support the XAudio2 audio driver".format(env["platform"])) Exit(255) SConscript("xaudio2/SCsub") - +# Shared Apple platform drivers +if env["platform"] in ["macos", "ios"]: + SConscript("apple/SCsub") # Midi drivers SConscript("alsamidi/SCsub") SConscript("coremidi/SCsub") diff --git a/drivers/apple/SCsub b/drivers/apple/SCsub new file mode 100644 index 00000000000..83ac27f4b61 --- /dev/null +++ b/drivers/apple/SCsub @@ -0,0 +1,7 @@ +#!/usr/bin/env python +from misc.utility.scons_hints import * + +Import("env") + +# Driver source files +env.add_source_files(env.drivers_sources, "*.mm") diff --git a/platform/ios/joypad_ios.h b/drivers/apple/joypad_apple.h similarity index 65% rename from platform/ios/joypad_ios.h rename to drivers/apple/joypad_apple.h index a61e9afafb9..909648e253d 100644 --- a/platform/ios/joypad_ios.h +++ b/drivers/apple/joypad_apple.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* joypad_ios.h */ +/* joypad_apple.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,23 +28,44 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ +#include "core/input/input.h" + +#define Key _QKey #import +#undef Key -@interface JoypadIOSObserver : NSObject +@class GCController; +class RumbleContext; -- (void)startObserving; -- (void)startProcessing; -- (void)finishObserving; +struct GameController { + int joy_id; + GCController *controller; + RumbleContext *rumble_context API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) = nil; + NSInteger ff_effect_timestamp = 0; + bool force_feedback = false; -@end + GameController(int p_joy_id, GCController *p_controller); + ~GameController(); +}; -class JoypadIOS { +class JoypadApple { private: - JoypadIOSObserver *observer; + id connect_observer = nil; + id disconnect_observer = nil; + HashMap joypads; + HashMap controller_to_joy_id; + + GCControllerPlayerIndex get_free_player_index(); + + void add_joypad(GCController *p_controller); + void remove_joypad(GCController *p_controller); public: - JoypadIOS(); - ~JoypadIOS(); + JoypadApple(); + ~JoypadApple(); - void start_processing(); + void joypad_vibration_start(GameController &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)); + void joypad_vibration_stop(GameController &p_joypad, uint64_t p_timestamp) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)); + + void process_joypads(); }; diff --git a/drivers/apple/joypad_apple.mm b/drivers/apple/joypad_apple.mm new file mode 100644 index 00000000000..148ec4cb271 --- /dev/null +++ b/drivers/apple/joypad_apple.mm @@ -0,0 +1,427 @@ +/**************************************************************************/ +/* joypad_apple.mm */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#import "joypad_apple.h" + +#include +#import + +#include "core/config/project_settings.h" +#include "main/main.h" + +class API_AVAILABLE(macos(11), ios(14.0), tvos(14.0)) RumbleMotor { + CHHapticEngine *engine; + id player; + bool is_started; + + RumbleMotor(GCController *p_controller, GCHapticsLocality p_locality) { + engine = [p_controller.haptics createEngineWithLocality:p_locality]; + engine.autoShutdownEnabled = YES; + } + +public: + static RumbleMotor *create(GCController *p_controller, GCHapticsLocality p_locality) { + if ([p_controller.haptics.supportedLocalities containsObject:p_locality]) { + return memnew(RumbleMotor(p_controller, p_locality)); + } + return nullptr; + } + + _ALWAYS_INLINE_ bool has_active_player() { + return player != nil; + } + + void execute_pattern(CHHapticPattern *p_pattern) { + NSError *error; + if (!is_started) { + ERR_FAIL_COND_MSG(![engine startAndReturnError:&error], "Couldn't start controller haptic engine: " + String::utf8(error.localizedDescription.UTF8String)); + is_started = YES; + } + + player = [engine createPlayerWithPattern:p_pattern error:&error]; + ERR_FAIL_COND_MSG(error, "Couldn't create controller haptic pattern player: " + String::utf8(error.localizedDescription.UTF8String)); + ERR_FAIL_COND_MSG(![player startAtTime:CHHapticTimeImmediate error:&error], "Couldn't execute controller haptic pattern: " + String::utf8(error.localizedDescription.UTF8String)); + } + + void stop() { + id old_player = player; + player = nil; + + NSError *error; + ERR_FAIL_COND_MSG(![old_player stopAtTime:CHHapticTimeImmediate error:&error], "Couldn't stop controller haptic pattern: " + String::utf8(error.localizedDescription.UTF8String)); + } +}; + +class API_AVAILABLE(macos(11), ios(14.0), tvos(14.0)) RumbleContext { + RumbleMotor *weak_motor; + RumbleMotor *strong_motor; + +public: + RumbleContext(GCController *p_controller) { + weak_motor = RumbleMotor::create(p_controller, GCHapticsLocalityRightHandle); + strong_motor = RumbleMotor::create(p_controller, GCHapticsLocalityLeftHandle); + } + + ~RumbleContext() { + if (weak_motor) { + memdelete(weak_motor); + } + if (strong_motor) { + memdelete(strong_motor); + } + } + + _ALWAYS_INLINE_ bool has_motors() { + return weak_motor != nullptr && strong_motor != nullptr; + } + + _ALWAYS_INLINE_ bool has_active_players() { + if (!has_motors()) { + return false; + } + return (weak_motor && weak_motor->has_active_player()) || (strong_motor && strong_motor->has_active_player()); + } + + void stop() { + if (weak_motor) { + weak_motor->stop(); + } + if (strong_motor) { + strong_motor->stop(); + } + } + + void play_weak_pattern(CHHapticPattern *p_pattern) { + if (weak_motor) { + weak_motor->execute_pattern(p_pattern); + } + } + + void play_strong_pattern(CHHapticPattern *p_pattern) { + if (strong_motor) { + strong_motor->execute_pattern(p_pattern); + } + } +}; + +GameController::GameController(int p_joy_id, GCController *p_controller) : + joy_id(p_joy_id), controller(p_controller) { + force_feedback = NO; + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + if (controller.haptics != nil) { + // Create a rumble context for the controller. + rumble_context = memnew(RumbleContext(p_controller)); + + // If the rumble motors aren't available, disable force feedback. + force_feedback = rumble_context->has_motors(); + } + } + + int l_joy_id = joy_id; + + auto BUTTON = [l_joy_id](JoyButton p_button) { + return ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_button(l_joy_id, p_button, pressed); + }; + }; + + if (controller.extendedGamepad != nil) { + GCExtendedGamepad *gamepad = controller.extendedGamepad; + + gamepad.buttonA.pressedChangedHandler = BUTTON(JoyButton::A); + gamepad.buttonB.pressedChangedHandler = BUTTON(JoyButton::B); + gamepad.buttonX.pressedChangedHandler = BUTTON(JoyButton::X); + gamepad.buttonY.pressedChangedHandler = BUTTON(JoyButton::Y); + gamepad.leftShoulder.pressedChangedHandler = BUTTON(JoyButton::LEFT_SHOULDER); + gamepad.rightShoulder.pressedChangedHandler = BUTTON(JoyButton::RIGHT_SHOULDER); + gamepad.dpad.up.pressedChangedHandler = BUTTON(JoyButton::DPAD_UP); + gamepad.dpad.down.pressedChangedHandler = BUTTON(JoyButton::DPAD_DOWN); + gamepad.dpad.left.pressedChangedHandler = BUTTON(JoyButton::DPAD_LEFT); + gamepad.dpad.right.pressedChangedHandler = BUTTON(JoyButton::DPAD_RIGHT); + + gamepad.leftThumbstick.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::LEFT_X, xValue); + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::LEFT_Y, -yValue); + }; + + gamepad.rightThumbstick.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::RIGHT_X, xValue); + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::RIGHT_Y, -yValue); + }; + gamepad.leftTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::TRIGGER_LEFT, value); + }; + gamepad.rightTrigger.valueChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + Input::get_singleton()->joy_axis(l_joy_id, JoyAxis::TRIGGER_RIGHT, value); + }; + + if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) { + gamepad.leftThumbstickButton.pressedChangedHandler = BUTTON(JoyButton::LEFT_STICK); + gamepad.rightThumbstickButton.pressedChangedHandler = BUTTON(JoyButton::RIGHT_STICK); + } + + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + gamepad.buttonOptions.pressedChangedHandler = BUTTON(JoyButton::BACK); + gamepad.buttonMenu.pressedChangedHandler = BUTTON(JoyButton::START); + } + + if (@available(macOS 11, iOS 14.0, tvOS 14.0, *)) { + gamepad.buttonHome.pressedChangedHandler = BUTTON(JoyButton::GUIDE); + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + xboxGamepad.paddleButton1.pressedChangedHandler = BUTTON(JoyButton::PADDLE1); + xboxGamepad.paddleButton2.pressedChangedHandler = BUTTON(JoyButton::PADDLE2); + xboxGamepad.paddleButton3.pressedChangedHandler = BUTTON(JoyButton::PADDLE3); + xboxGamepad.paddleButton4.pressedChangedHandler = BUTTON(JoyButton::PADDLE4); + } + } + + if (@available(macOS 12, iOS 15.0, tvOS 15.0, *)) { + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + xboxGamepad.buttonShare.pressedChangedHandler = BUTTON(JoyButton::MISC1); + } + } + } else if (controller.microGamepad != nil) { + GCMicroGamepad *gamepad = controller.microGamepad; + + gamepad.buttonA.pressedChangedHandler = BUTTON(JoyButton::A); + gamepad.buttonX.pressedChangedHandler = BUTTON(JoyButton::X); + gamepad.dpad.up.pressedChangedHandler = BUTTON(JoyButton::DPAD_UP); + gamepad.dpad.down.pressedChangedHandler = BUTTON(JoyButton::DPAD_DOWN); + gamepad.dpad.left.pressedChangedHandler = BUTTON(JoyButton::DPAD_LEFT); + gamepad.dpad.right.pressedChangedHandler = BUTTON(JoyButton::DPAD_RIGHT); + } + + // TODO: Need to add support for controller.motion which gives us access to + // the orientation of the device (if supported). +} + +GameController::~GameController() { + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + if (rumble_context) { + memdelete(rumble_context); + } + } +} + +JoypadApple::JoypadApple() { + connect_observer = [NSNotificationCenter.defaultCenter + addObserverForName:GCControllerDidConnectNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + GCController *controller = notification.object; + if (!controller) { + return; + } + add_joypad(controller); + }]; + + disconnect_observer = [NSNotificationCenter.defaultCenter + addObserverForName:GCControllerDidDisconnectNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + GCController *controller = notification.object; + if (!controller) { + return; + } + remove_joypad(controller); + }]; + + if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) { + GCController.shouldMonitorBackgroundEvents = YES; + } +} + +JoypadApple::~JoypadApple() { + for (KeyValue &E : joypads) { + memdelete(E.value); + E.value = nullptr; + } + + [NSNotificationCenter.defaultCenter removeObserver:connect_observer]; + [NSNotificationCenter.defaultCenter removeObserver:disconnect_observer]; +} + +// Finds the rightmost set bit in a number, n. +// variation of https://www.geeksforgeeks.org/position-of-rightmost-set-bit/ +int rightmost_one(int n) { + return __builtin_ctz(n & -n) + 1; +} + +GCControllerPlayerIndex JoypadApple::get_free_player_index() { + // player_set will be a bitfield where each bit represents a player index. + __block uint32_t player_set = 0; + for (const KeyValue &E : controller_to_joy_id) { + player_set |= 1U << E.key.playerIndex; + } + + // invert, as we want to find the first unset player index. + int n = rightmost_one((int)(~player_set)); + if (n >= 5) { + return GCControllerPlayerIndexUnset; + } + + return (GCControllerPlayerIndex)(n - 1); +} + +void JoypadApple::add_joypad(GCController *p_controller) { + if (controller_to_joy_id.has(p_controller)) { + return; + } + + // Get a new id for our controller. + int joy_id = Input::get_singleton()->get_unused_joy_id(); + + if (joy_id == -1) { + print_verbose("Couldn't retrieve new joy ID."); + return; + } + + // Assign our player index. + if (p_controller.playerIndex == GCControllerPlayerIndexUnset) { + p_controller.playerIndex = get_free_player_index(); + } + + // Tell Godot about our new controller. + Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8(p_controller.vendorName.UTF8String)); + + // Assign our player index. + joypads.insert(joy_id, memnew(GameController(joy_id, p_controller))); + controller_to_joy_id.insert(p_controller, joy_id); +} + +void JoypadApple::remove_joypad(GCController *p_controller) { + if (!controller_to_joy_id.has(p_controller)) { + return; + } + + int joy_id = controller_to_joy_id[p_controller]; + controller_to_joy_id.erase(p_controller); + + // Tell Godot this joystick is no longer there. + Input::get_singleton()->joy_connection_changed(joy_id, false, ""); + + // And remove it from our dictionary. + GameController **old = joypads.getptr(joy_id); + memdelete(*old); + *old = nullptr; + joypads.erase(joy_id); +} + +API_AVAILABLE(macos(10.15), ios(13.0), tvos(14.0)) +CHHapticPattern *get_vibration_pattern(float p_magnitude, float p_duration) { + // Creates a vibration pattern with an intensity and duration. + NSDictionary *hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{ + CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration], + + CHHapticPatternKeyEventParameters : @[ + @{ + CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity, + CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_magnitude] + }, + ], + }, + }, + ], + }; + NSError *error; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; + return pattern; +} + +void JoypadApple::joypad_vibration_start(GameController &p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { + if (!p_joypad.force_feedback || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { + return; + } + + // If there is active vibration players, stop them. + if (p_joypad.rumble_context->has_active_players()) { + joypad_vibration_stop(p_joypad, p_timestamp); + } + + // Gets the default vibration pattern and creates a player for each motor. + CHHapticPattern *weak_pattern = get_vibration_pattern(p_weak_magnitude, p_duration); + CHHapticPattern *strong_pattern = get_vibration_pattern(p_strong_magnitude, p_duration); + + p_joypad.rumble_context->play_weak_pattern(weak_pattern); + p_joypad.rumble_context->play_strong_pattern(strong_pattern); + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +void JoypadApple::joypad_vibration_stop(GameController &p_joypad, uint64_t p_timestamp) { + if (!p_joypad.force_feedback) { + return; + } + // If there is no active vibration players, exit. + if (!p_joypad.rumble_context->has_active_players()) { + return; + } + + p_joypad.rumble_context->stop(); + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +void JoypadApple::process_joypads() { + if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) { + for (KeyValue &E : joypads) { + int id = E.key; + GameController &joypad = *E.value; + + if (joypad.force_feedback) { + Input *input = Input::get_singleton(); + uint64_t timestamp = input->get_joy_vibration_timestamp(id); + + if (timestamp > (unsigned)joypad.ff_effect_timestamp) { + Vector2 strength = input->get_joy_vibration_strength(id); + float duration = input->get_joy_vibration_duration(id); + if (duration == 0) { + duration = GCHapticDurationInfinite; + } + + if (strength.x == 0 && strength.y == 0) { + joypad_vibration_stop(joypad, timestamp); + } else { + joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); + } + } + } + } + } +} diff --git a/platform/ios/SCsub b/platform/ios/SCsub index 959a657aacc..d16dcb1a95a 100644 --- a/platform/ios/SCsub +++ b/platform/ios/SCsub @@ -68,7 +68,6 @@ ios_lib = [ "ios.mm", "rendering_context_driver_vulkan_ios.mm", "display_server_ios.mm", - "joypad_ios.mm", "godot_view.mm", "tts_ios.mm", "display_layer.mm", diff --git a/platform/ios/joypad_ios.mm b/platform/ios/joypad_ios.mm deleted file mode 100644 index 38d3ce728aa..00000000000 --- a/platform/ios/joypad_ios.mm +++ /dev/null @@ -1,362 +0,0 @@ -/**************************************************************************/ -/* joypad_ios.mm */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#import "joypad_ios.h" - -#import "godot_view.h" -#import "os_ios.h" - -#include "core/config/project_settings.h" -#import "drivers/coreaudio/audio_driver_coreaudio.h" -#include "main/main.h" - -JoypadIOS::JoypadIOS() { - observer = [[JoypadIOSObserver alloc] init]; - [observer startObserving]; -} - -JoypadIOS::~JoypadIOS() { - if (observer) { - [observer finishObserving]; - observer = nil; - } -} - -void JoypadIOS::start_processing() { - if (observer) { - [observer startProcessing]; - } -} - -@interface JoypadIOSObserver () - -@property(assign, nonatomic) BOOL isObserving; -@property(assign, nonatomic) BOOL isProcessing; -@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; -@property(strong, nonatomic) NSMutableArray *joypadsQueue; - -@end - -@implementation JoypadIOSObserver - -- (instancetype)init { - self = [super init]; - - if (self) { - [self godot_commonInit]; - } - - return self; -} - -- (void)godot_commonInit { - self.isObserving = NO; - self.isProcessing = NO; -} - -- (void)startProcessing { - self.isProcessing = YES; - - for (GCController *controller in self.joypadsQueue) { - [self addiOSJoypad:controller]; - } - - [self.joypadsQueue removeAllObjects]; -} - -- (void)startObserving { - if (self.isObserving) { - return; - } - - self.isObserving = YES; - - self.connectedJoypads = [NSMutableDictionary dictionary]; - self.joypadsQueue = [NSMutableArray array]; - - // get told when controllers connect, this will be called right away for - // already connected controllers - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasConnected:) - name:GCControllerDidConnectNotification - object:nil]; - - // get told when controllers disconnect - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; -} - -- (void)finishObserving { - if (self.isObserving) { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - } - - self.isObserving = NO; - self.isProcessing = NO; - - self.connectedJoypads = nil; - self.joypadsQueue = nil; -} - -- (void)dealloc { - [self finishObserving]; -} - -- (int)getJoyIdForController:(GCController *)controller { - NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; - - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - } - - return -1; -} - -- (void)addiOSJoypad:(GCController *)controller { - // get a new id for our controller - int joy_id = Input::get_singleton()->get_unused_joy_id(); - - if (joy_id == -1) { - print_verbose("Couldn't retrieve new joy ID."); - return; - } - - // assign our player index - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [self getFreePlayerIndex]; - } - - // tell Godot about our new controller - Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); - - // add it to our dictionary, this will retain our controllers - [self.connectedJoypads setObject:controller forKey:[NSNumber numberWithInt:joy_id]]; - - // set our input handler - [self setControllerInputHandler:controller]; -} - -- (void)controllerWasConnected:(NSNotification *)notification { - // get our controller - GCController *controller = (GCController *)notification.object; - - if (!controller) { - print_verbose("Couldn't retrieve new controller."); - return; - } - - if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) { - print_verbose("Controller is already registered."); - } else if (!self.isProcessing) { - [self.joypadsQueue addObject:controller]; - } else { - [self addiOSJoypad:controller]; - } -} - -- (void)controllerWasDisconnected:(NSNotification *)notification { - // find our joystick, there should be only one in our dictionary - GCController *controller = (GCController *)notification.object; - - if (!controller) { - return; - } - - NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; - for (NSNumber *key in keys) { - // tell Godot this joystick is no longer there - int joy_id = [key intValue]; - Input::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // and remove it from our dictionary - [self.connectedJoypads removeObjectForKey:key]; - } -} - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (self.connectedJoypads == nil) { - NSArray *keys = [self.connectedJoypads allKeys]; - for (NSNumber *key in keys) { - GCController *controller = [self.connectedJoypads objectForKey:key]; - if (controller.playerIndex == GCControllerPlayerIndex1) { - have_player_1 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex2) { - have_player_2 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex3) { - have_player_3 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex4) { - have_player_4 = true; - } - } - } - - if (!have_player_1) { - return GCControllerPlayerIndex1; - } else if (!have_player_2) { - return GCControllerPlayerIndex2; - } else if (!have_player_3) { - return GCControllerPlayerIndex3; - } else if (!have_player_4) { - return GCControllerPlayerIndex4; - } else { - return GCControllerPlayerIndexUnset; - } -} - -- (void)setControllerInputHandler:(GCController *)controller { - // Hook in the callback handler for the correct gamepad profile. - // This is a bit of a weird design choice on Apples part. - // You need to select the most capable gamepad profile for the - // gamepad attached. - if (controller.extendedGamepad != nil) { - // The extended gamepad profile has all the input you could possibly find on - // a gamepad but will only be active if your gamepad actually has all of - // these... - _weakify(self); - _weakify(controller); - - controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - Input::get_singleton()->joy_button(joy_id, JoyButton::B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - Input::get_singleton()->joy_button(joy_id, JoyButton::Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - - if (element == gamepad.leftThumbstick) { - float value = gamepad.leftThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_X, value); - value = -gamepad.leftThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_Y, value); - } else if (element == gamepad.rightThumbstick) { - float value = gamepad.rightThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_X, value); - value = -gamepad.rightThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_Y, value); - } else if (element == gamepad.leftTrigger) { - float value = gamepad.leftTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_LEFT, value); - } else if (element == gamepad.rightTrigger) { - float value = gamepad.rightTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); - } - - if (@available(iOS 13, *)) { - // iOS uses 'buttonOptions' and 'buttonMenu' names for BACK and START joy buttons. - if (element == gamepad.buttonOptions) { - Input::get_singleton()->joy_button(joy_id, JoyButton::BACK, - gamepad.buttonOptions.isPressed); - } else if (element == gamepad.buttonMenu) { - Input::get_singleton()->joy_button(joy_id, JoyButton::START, - gamepad.buttonMenu.isPressed); - } - } - - if (@available(iOS 14, *)) { - // iOS uses 'buttonHome' for the GUIDE joy button. - if (element == gamepad.buttonHome) { - Input::get_singleton()->joy_button(joy_id, JoyButton::GUIDE, - gamepad.buttonHome.isPressed); - } - } - }; - } else if (controller.microGamepad != nil) { - // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad - _weakify(self); - _weakify(controller); - - controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, gamepad.dpad.right.isPressed); - } - }; - } - - ///@TODO need to add support for controller.motion which gives us access to - /// the orientation of the device (if supported) - - ///@TODO need to add support for controllerPausedHandler which should be a - /// toggle -} - -@end diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index cd688077aca..94b38f2c40c 100644 --- a/platform/ios/os_ios.h +++ b/platform/ios/os_ios.h @@ -34,8 +34,8 @@ #ifdef IOS_ENABLED #import "ios.h" -#import "joypad_ios.h" +#import "drivers/apple/joypad_apple.h" #import "drivers/coreaudio/audio_driver_coreaudio.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" @@ -58,7 +58,7 @@ private: iOS *ios = nullptr; - JoypadIOS *joypad_ios = nullptr; + JoypadApple *joypad_apple = nullptr; MainLoop *main_loop = nullptr; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 88e19a0a1cb..2493c5b8fb2 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -130,12 +130,12 @@ void OS_IOS::initialize_modules() { ios = memnew(iOS); Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); - joypad_ios = memnew(JoypadIOS); + joypad_apple = memnew(JoypadApple); } void OS_IOS::deinitialize_modules() { - if (joypad_ios) { - memdelete(joypad_ios); + if (joypad_apple) { + memdelete(joypad_apple); } if (ios) { @@ -169,6 +169,8 @@ bool OS_IOS::iterate() { DisplayServer::get_singleton()->process_events(); } + joypad_apple->process_joypads(); + return Main::iteration(); } @@ -176,10 +178,6 @@ void OS_IOS::start() { if (Main::start() == EXIT_SUCCESS) { main_loop->initialize(); } - - if (joypad_ios) { - joypad_ios->start_processing(); - } } void OS_IOS::finalize() { diff --git a/platform/macos/SCsub b/platform/macos/SCsub index 598444ae24b..429cec22344 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -123,7 +123,6 @@ files = [ "native_menu_macos.mm", "dir_access_macos.mm", "tts_macos.mm", - "joypad_macos.mm", "rendering_context_driver_vulkan_macos.mm", "gl_manager_macos_angle.mm", "gl_manager_macos_legacy.mm", diff --git a/platform/macos/joypad_macos.h b/platform/macos/joypad_macos.h deleted file mode 100644 index b37a1b24f3f..00000000000 --- a/platform/macos/joypad_macos.h +++ /dev/null @@ -1,89 +0,0 @@ -/**************************************************************************/ -/* joypad_macos.h */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#include "core/input/input.h" - -#define Key _QKey -#import -#import -#undef Key - -@interface JoypadMacOSObserver : NSObject - -- (void)startObserving; -- (void)startProcessing; -- (void)finishObserving; - -@end - -API_AVAILABLE(macosx(11)) -@interface RumbleMotor : NSObject -@property(strong, nonatomic) CHHapticEngine *engine; -@property(strong, nonatomic) id player; -@end - -API_AVAILABLE(macosx(11)) -@interface RumbleContext : NSObject -// High frequency motor, it's usually the right engine. -@property(strong, nonatomic) RumbleMotor *weak_motor; -// Low frequency motor, it's usually the left engine. -@property(strong, nonatomic) RumbleMotor *strong_motor; -@end - -// Controller support for macOS begins with macOS 10.9+, -// however haptics (vibrations) are only supported in macOS 11+. -@interface Joypad : NSObject - -@property(assign, nonatomic) BOOL force_feedback; -@property(assign, nonatomic) NSInteger ff_effect_timestamp; -@property(strong, nonatomic) GCController *controller; -@property(strong, nonatomic) RumbleContext *rumble_context API_AVAILABLE(macosx(11)); - -- (instancetype)init; -- (instancetype)init:(GCController *)controller; - -@end - -class JoypadMacOS { -private: - JoypadMacOSObserver *observer; - -public: - JoypadMacOS(); - ~JoypadMacOS(); - - API_AVAILABLE(macosx(11)) - void joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); - API_AVAILABLE(macosx(11)) - void joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp); - - void start_processing(); - void process_joypads(); -}; diff --git a/platform/macos/joypad_macos.mm b/platform/macos/joypad_macos.mm deleted file mode 100644 index beb32d9129c..00000000000 --- a/platform/macos/joypad_macos.mm +++ /dev/null @@ -1,611 +0,0 @@ -/**************************************************************************/ -/* joypad_macos.mm */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#import "joypad_macos.h" - -#include - -#import "os_macos.h" - -#include "core/config/project_settings.h" -#include "core/os/keyboard.h" -#include "core/string/ustring.h" -#include "main/main.h" - -@implementation RumbleMotor - -- (instancetype)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality { - self = [super init]; - self.engine = [controller.haptics createEngineWithLocality:locality]; - self.player = nil; - return self; -} - -- (void)execute_pattern:(CHHapticPattern *)pattern { - NSError *error; - id player = [self.engine createPlayerWithPattern:pattern error:&error]; - - // When all players have stopped for an engine, stop the engine. - [self.engine notifyWhenPlayersFinished:^CHHapticEngineFinishedAction(NSError *_Nullable error) { - return CHHapticEngineFinishedActionStopEngine; - }]; - - self.player = player; - - // Starts the engine and returns if an error was encountered. - if (![self.engine startAndReturnError:&error]) { - print_verbose("Couldn't start controller haptic engine"); - return; - } - if (![self.player startAtTime:0 error:&error]) { - print_verbose("Couldn't execute controller haptic pattern"); - } -} - -- (void)stop { - NSError *error; - [self.player stopAtTime:0 error:&error]; - self.player = nil; -} - -@end - -@implementation RumbleContext - -- (instancetype)init { - self = [super init]; - self.weak_motor = nil; - self.strong_motor = nil; - return self; -} - -- (bool)hasMotors { - return self.weak_motor != nil && self.strong_motor != nil; -} -- (bool)hasActivePlayers { - if (![self hasMotors]) { - return NO; - } - return self.weak_motor.player != nil && self.strong_motor.player != nil; -} - -@end - -@implementation Joypad - -- (instancetype)init { - self = [super init]; - return self; -} -- (instancetype)init:(GCController *)controller { - self = [super init]; - self.controller = controller; - - if (@available(macOS 11, *)) { - // Haptics within the controller is only available in macOS 11+. - self.rumble_context = [[RumbleContext alloc] init]; - - // Create Weak and Strong motors for controller. - self.rumble_context.weak_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle]; - self.rumble_context.strong_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle]; - - // If the rumble motors aren't available, disable force feedback. - if (![self.rumble_context hasMotors]) { - self.force_feedback = NO; - } else { - self.force_feedback = YES; - } - } else { - self.force_feedback = NO; - } - - self.ff_effect_timestamp = 0; - - return self; -} - -@end - -JoypadMacOS::JoypadMacOS() { - observer = [[JoypadMacOSObserver alloc] init]; - [observer startObserving]; - - if (@available(macOS 11.3, *)) { - GCController.shouldMonitorBackgroundEvents = YES; - } -} - -JoypadMacOS::~JoypadMacOS() { - if (observer) { - [observer finishObserving]; - observer = nil; - } -} - -void JoypadMacOS::start_processing() { - if (observer) { - [observer startProcessing]; - } - process_joypads(); -} - -API_AVAILABLE(macosx(10.15)) -CHHapticPattern *get_vibration_pattern(float p_magnitude, float p_duration) { - // Creates a vibration pattern with an intensity and duration. - NSDictionary *hapticDict = @{ - CHHapticPatternKeyPattern : @[ - @{ - CHHapticPatternKeyEvent : @{ - CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, - CHHapticPatternKeyTime : @(CHHapticTimeImmediate), - CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration], - - CHHapticPatternKeyEventParameters : @[ - @{ - CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity, - CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_magnitude] - }, - ], - }, - }, - ], - }; - NSError *error; - CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; - return pattern; -} - -void JoypadMacOS::joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { - if (!p_joypad.force_feedback || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { - return; - } - - // If there is active vibration players, stop them. - if ([p_joypad.rumble_context hasActivePlayers]) { - joypad_vibration_stop(p_joypad, p_timestamp); - } - - // Gets the default vibration pattern and creates a player for each motor. - CHHapticPattern *weak_pattern = get_vibration_pattern(p_weak_magnitude, p_duration); - CHHapticPattern *strong_pattern = get_vibration_pattern(p_strong_magnitude, p_duration); - - RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; - RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; - - [weak_motor execute_pattern:weak_pattern]; - [strong_motor execute_pattern:strong_pattern]; - - p_joypad.ff_effect_timestamp = p_timestamp; -} - -void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp) { - if (!p_joypad.force_feedback) { - return; - } - // If there is no active vibration players, exit. - if (![p_joypad.rumble_context hasActivePlayers]) { - return; - } - - RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; - RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; - - [weak_motor stop]; - [strong_motor stop]; - - p_joypad.ff_effect_timestamp = p_timestamp; -} - -@interface JoypadMacOSObserver () - -@property(assign, nonatomic) BOOL isObserving; -@property(assign, nonatomic) BOOL isProcessing; -@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; -@property(strong, nonatomic) NSMutableArray *joypadsQueue; - -@end - -@implementation JoypadMacOSObserver - -- (instancetype)init { - self = [super init]; - - if (self) { - [self godot_commonInit]; - } - - return self; -} - -- (void)godot_commonInit { - self.isObserving = NO; - self.isProcessing = NO; -} - -- (void)startProcessing { - self.isProcessing = YES; - - for (GCController *controller in self.joypadsQueue) { - [self addMacOSJoypad:controller]; - } - - [self.joypadsQueue removeAllObjects]; -} - -- (void)startObserving { - if (self.isObserving) { - return; - } - - self.isObserving = YES; - - self.connectedJoypads = [NSMutableDictionary dictionary]; - self.joypadsQueue = [NSMutableArray array]; - - // Get told when controllers connect, this will be called right away for - // already connected controllers. - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasConnected:) - name:GCControllerDidConnectNotification - object:nil]; - - // Get told when controllers disconnect. - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; -} - -- (void)finishObserving { - if (self.isObserving) { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - } - - self.isObserving = NO; - self.isProcessing = NO; - - self.connectedJoypads = nil; - self.joypadsQueue = nil; -} - -- (void)dealloc { - [self finishObserving]; -} - -- (NSArray *)getAllKeysForController:(GCController *)controller { - NSArray *keys = [self.connectedJoypads allKeys]; - NSMutableArray *final_keys = [NSMutableArray array]; - - for (NSNumber *key in keys) { - Joypad *joypad = [self.connectedJoypads objectForKey:key]; - if (joypad.controller == controller) { - [final_keys addObject:key]; - } - } - - return final_keys; -} - -- (int)getJoyIdForController:(GCController *)controller { - NSArray *keys = [self getAllKeysForController:controller]; - - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - } - - return -1; -} - -- (void)addMacOSJoypad:(GCController *)controller { - // Get a new id for our controller. - int joy_id = Input::get_singleton()->get_unused_joy_id(); - - if (joy_id == -1) { - print_verbose("Couldn't retrieve new joy ID."); - return; - } - - // Assign our player index. - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [self getFreePlayerIndex]; - } - - // Tell Godot about our new controller. - Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); - - Joypad *joypad = [[Joypad alloc] init:controller]; - - // Add it to our dictionary, this will retain our controllers. - [self.connectedJoypads setObject:joypad forKey:[NSNumber numberWithInt:joy_id]]; - - // Set our input handler. - [self setControllerInputHandler:controller]; -} - -- (void)controllerWasConnected:(NSNotification *)notification { - // Get our controller. - GCController *controller = (GCController *)notification.object; - - if (!controller) { - print_verbose("Couldn't retrieve new controller."); - return; - } - - if ([[self getAllKeysForController:controller] count] > 0) { - print_verbose("Controller is already registered."); - } else if (!self.isProcessing) { - [self.joypadsQueue addObject:controller]; - } else { - [self addMacOSJoypad:controller]; - } -} - -- (void)controllerWasDisconnected:(NSNotification *)notification { - // Find our joystick, there should be only one in our dictionary. - GCController *controller = (GCController *)notification.object; - - if (!controller) { - return; - } - - NSArray *keys = [self getAllKeysForController:controller]; - for (NSNumber *key in keys) { - // Tell Godot this joystick is no longer there. - int joy_id = [key intValue]; - Input::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // And remove it from our dictionary. - [self.connectedJoypads removeObjectForKey:key]; - } -} - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (self.connectedJoypads == nil) { - NSArray *keys = [self.connectedJoypads allKeys]; - for (NSNumber *key in keys) { - Joypad *joypad = [self.connectedJoypads objectForKey:key]; - GCController *controller = joypad.controller; - if (controller.playerIndex == GCControllerPlayerIndex1) { - have_player_1 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex2) { - have_player_2 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex3) { - have_player_3 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex4) { - have_player_4 = true; - } - } - } - - if (!have_player_1) { - return GCControllerPlayerIndex1; - } else if (!have_player_2) { - return GCControllerPlayerIndex2; - } else if (!have_player_3) { - return GCControllerPlayerIndex3; - } else if (!have_player_4) { - return GCControllerPlayerIndex4; - } else { - return GCControllerPlayerIndexUnset; - } -} - -- (void)setControllerInputHandler:(GCController *)controller { - // Hook in the callback handler for the correct gamepad profile. - // This is a bit of a weird design choice on Apples part. - // You need to select the most capable gamepad profile for the - // gamepad attached. - if (controller.extendedGamepad != nil) { - // The extended gamepad profile has all the input you could possibly find on - // a gamepad but will only be active if your gamepad actually has all of - // these... - _weakify(self); - _weakify(controller); - - controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - Input::get_singleton()->joy_button(joy_id, JoyButton::B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - Input::get_singleton()->joy_button(joy_id, JoyButton::Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - - if (element == gamepad.leftThumbstick) { - float value = gamepad.leftThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_X, value); - value = -gamepad.leftThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_Y, value); - } else if (element == gamepad.rightThumbstick) { - float value = gamepad.rightThumbstick.xAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_X, value); - value = -gamepad.rightThumbstick.yAxis.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_Y, value); - } else if (element == gamepad.leftTrigger) { - float value = gamepad.leftTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_LEFT, value); - } else if (element == gamepad.rightTrigger) { - float value = gamepad.rightTrigger.value; - Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); - } - - if (@available(macOS 10.14.1, *)) { - if (element == gamepad.leftThumbstickButton) { - Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_STICK, - gamepad.leftThumbstickButton.isPressed); - } else if (element == gamepad.rightThumbstickButton) { - Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_STICK, - gamepad.rightThumbstickButton.isPressed); - } - } - - if (@available(macOS 10.15, *)) { - if (element == gamepad.buttonOptions) { - Input::get_singleton()->joy_button(joy_id, JoyButton::BACK, - gamepad.buttonOptions.isPressed); - } else if (element == gamepad.buttonMenu) { - Input::get_singleton()->joy_button(joy_id, JoyButton::START, - gamepad.buttonMenu.isPressed); - } - } - - if (@available(macOS 11, *)) { - if (element == gamepad.buttonHome) { - Input::get_singleton()->joy_button(joy_id, JoyButton::GUIDE, - gamepad.buttonHome.isPressed); - } - if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { - GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; - if (element == xboxGamepad.paddleButton1) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE1, - xboxGamepad.paddleButton1.isPressed); - } else if (element == xboxGamepad.paddleButton2) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE2, - xboxGamepad.paddleButton2.isPressed); - } else if (element == xboxGamepad.paddleButton3) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE3, - xboxGamepad.paddleButton3.isPressed); - } else if (element == xboxGamepad.paddleButton4) { - Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE4, - xboxGamepad.paddleButton4.isPressed); - } - } - } - - if (@available(macOS 12, *)) { - if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { - GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; - if (element == xboxGamepad.buttonShare) { - Input::get_singleton()->joy_button(joy_id, JoyButton::MISC1, - xboxGamepad.buttonShare.isPressed); - } - } - } - }; - } else if (controller.microGamepad != nil) { - // Micro gamepads were added in macOS 10.11 and feature just 2 buttons and a d-pad. - _weakify(self); - _weakify(controller); - - controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - _strongify(self); - _strongify(controller); - - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - Input::get_singleton()->joy_button(joy_id, JoyButton::A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - Input::get_singleton()->joy_button(joy_id, JoyButton::X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, - gamepad.dpad.up.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, - gamepad.dpad.down.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, - gamepad.dpad.left.isPressed); - Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, - gamepad.dpad.right.isPressed); - } - }; - } - - // TODO: Need to add support for controller.motion which gives us access to - // the orientation of the device (if supported). -} - -@end - -void JoypadMacOS::process_joypads() { - if (@available(macOS 11, *)) { - // Process vibrations in macOS 11+. - NSArray *keys = [observer.connectedJoypads allKeys]; - - for (NSNumber *key in keys) { - int id = key.intValue; - Joypad *joypad = [observer.connectedJoypads objectForKey:key]; - - if (joypad.force_feedback) { - Input *input = Input::get_singleton(); - uint64_t timestamp = input->get_joy_vibration_timestamp(id); - - if (timestamp > (unsigned)joypad.ff_effect_timestamp) { - Vector2 strength = input->get_joy_vibration_strength(id); - float duration = input->get_joy_vibration_duration(id); - if (duration == 0) { - duration = GCHapticDurationInfinite; - } - - if (strength.x == 0 && strength.y == 0) { - joypad_vibration_stop(joypad, timestamp); - } else { - joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); - } - } - } - } - } -} diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 194ba235e45..334c8eab030 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -32,16 +32,16 @@ #define OS_MACOS_H #include "crash_handler_macos.h" -#import "joypad_macos.h" #include "core/input/input.h" +#import "drivers/apple/joypad_apple.h" #import "drivers/coreaudio/audio_driver_coreaudio.h" #import "drivers/coremidi/midi_driver_coremidi.h" #include "drivers/unix/os_unix.h" #include "servers/audio_server.h" class OS_MacOS : public OS_Unix { - JoypadMacOS *joypad_macos = nullptr; + JoypadApple *joypad_apple = nullptr; #ifdef COREAUDIO_ENABLED AudioDriverCoreAudio audio_driver; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index d0ef4d1e389..fd8156b68fe 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -142,13 +142,13 @@ void OS_MacOS::finalize() { delete_main_loop(); - if (joypad_macos) { - memdelete(joypad_macos); + if (joypad_apple) { + memdelete(joypad_apple); } } void OS_MacOS::initialize_joypads() { - joypad_macos = memnew(JoypadMacOS()); + joypad_apple = memnew(JoypadApple()); } void OS_MacOS::set_main_loop(MainLoop *p_main_loop) { @@ -834,7 +834,7 @@ void OS_MacOS::run() { if (DisplayServer::get_singleton()) { DisplayServer::get_singleton()->process_events(); // Get rid of pending events. } - joypad_macos->start_processing(); + joypad_apple->process_joypads(); if (Main::iteration()) { quit = true;