Skip to content

Commit

Permalink
Added controller vibration capabilities in macOS
Browse files Browse the repository at this point in the history
This implementation complies with Godot's documentation regarding
start_joy_vibration.

Relates to godotengine#14634 and fixes it for macOS.
  • Loading branch information
JezerM committed Oct 8, 2023
1 parent 456f8a3 commit da70848
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 9 deletions.
4 changes: 3 additions & 1 deletion platform/macos/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ def configure(env: "Environment"):
"-framework",
"IOKit",
"-framework",
"ForceFeedback",
"GameController",
"-framework",
"CoreHaptics",
"-framework",
"CoreVideo",
"-framework",
Expand Down
19 changes: 19 additions & 0 deletions platform/macos/joypad_macos.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "core/input/input.h"
#import <CoreHaptics/CoreHaptics.h>
#import <GameController/GameController.h>

@interface JoypadMacOSObserver : NSObject
Expand All @@ -38,6 +40,19 @@

@end

@interface Joypad : NSObject

@property(assign, nonatomic) BOOL force_feedback;
@property(assign, nonatomic) NSInteger ff_effect_timestamp;
@property(strong, nonatomic) GCController *controller;
@property(strong, nonatomic) CHHapticEngine *motion_engine;
@property(strong, nonatomic) id<CHHapticPatternPlayer> pattern_player;

- (instancetype)init;
- (instancetype)init:(GCController *)controller;

@end

class JoypadMacOS {
private:
JoypadMacOSObserver *observer;
Expand All @@ -46,5 +61,9 @@ class JoypadMacOS {
JoypadMacOS();
~JoypadMacOS();

void joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp);
void joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp);

void start_processing();
void process_joypads();
};
186 changes: 178 additions & 8 deletions platform/macos/joypad_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
/**************************************************************************/

#import "joypad_macos.h"
#include <Foundation/Foundation.h>

#import "os_macos.h"

Expand All @@ -37,6 +38,48 @@
#include "core/string/ustring.h"
#include "main/main.h"

@implementation Joypad

- (instancetype)init {
self = [super init];
return self;
}
- (instancetype)init:(GCController *)controller {
self = [super init];
self.controller = controller;
self.pattern_player = nil;

CHHapticEngine *default_engine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault];

NSSet<GCHapticsLocality> *localities = controller.haptics.supportedLocalities;

// If for some reason the default engine locality does not exists, we try
// to create an engine for all supported localities.
if (default_engine == nil) {
for (GCHapticsLocality locality in [localities allObjects]) {
CHHapticEngine *engine = [controller.haptics createEngineWithLocality:locality];
if (engine != nil) {
self.motion_engine = default_engine;
break;
}
}
} else {
self.motion_engine = default_engine;
}

if (self.motion_engine != nil) {
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];
Expand All @@ -53,14 +96,95 @@
if (observer) {
[observer startProcessing];
}
process_joypads();
}

CHHapticPattern *get_vibration_pattern(float p_weak_magnitude, float p_strong_magnitude, float p_duration) {
// Creates a vibration pattern with 2 events, one per weak/strong magnitude.
NSDictionary *hapticDict = @{
CHHapticPatternKeyPattern : @[
@{
CHHapticPatternKeyEvent : @{
CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous,
CHHapticPatternKeyTime : @(CHHapticTimeImmediate),
CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration],

CHHapticPatternKeyEventParameters : @[
@{
CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity,
CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_weak_magnitude / 2]
},
],
},
CHHapticPatternKeyEvent : @{
CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous,
CHHapticPatternKeyTime : @(CHHapticTimeImmediate),
CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration],

CHHapticPatternKeyEventParameters : @[
@{
CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity,
CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_strong_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 (p_joypad.pattern_player != nil) {
joypad_vibration_stop(p_joypad, p_timestamp);
}

// Gets the default vibration pattern and creates a player with it.
NSError *error;
CHHapticPattern *pattern = get_vibration_pattern(p_weak_magnitude, p_strong_magnitude, p_duration);
id<CHHapticPatternPlayer> player = [p_joypad.motion_engine createPlayerWithPattern:pattern error:&error];

p_joypad.pattern_player = player;

// When all players have stopped for an engine, stop the engine.
[p_joypad.motion_engine notifyWhenPlayersFinished:^CHHapticEngineFinishedAction(NSError *_Nullable error) {
return CHHapticEngineFinishedActionStopEngine;
}];

// Starts the engine and execute the vibration pattern
[p_joypad.motion_engine startWithCompletionHandler:^(NSError *returnedError) {
NSError *error;
[player startAtTime:0 error:&error];
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 (p_joypad.pattern_player == nil) {
return;
}

NSError *error;
[p_joypad.pattern_player stopAtTime:0 error:&error];
p_joypad.pattern_player = nil;
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;
@property(strong, nonatomic) NSMutableDictionary<NSNumber *, Joypad *> *connectedJoypads;
@property(strong, nonatomic) NSMutableArray<Joypad *> *joypadsQueue;

@end

Expand Down Expand Up @@ -133,8 +257,22 @@ - (void)dealloc {
[self finishObserving];
}

- (NSArray<NSNumber *> *)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.connectedJoypads allKeysForObject:controller];
NSArray *keys = [self getAllKeysForController:controller];

for (NSNumber *key in keys) {
int joy_id = [key intValue];
Expand All @@ -161,8 +299,10 @@ - (void)addMacOSJoypad:(GCController *)controller {
// 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:controller forKey:[NSNumber numberWithInt:joy_id]];
[self.connectedJoypads setObject:joypad forKey:[NSNumber numberWithInt:joy_id]];

// set our input handler
[self setControllerInputHandler:controller];
Expand All @@ -177,10 +317,11 @@ - (void)controllerWasConnected:(NSNotification *)notification {
return;
}

if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) {
if ([[self getAllKeysForController:controller] count] > 0) {
print_verbose("Controller is already registered.");
} else if (!self.isProcessing) {
[self.joypadsQueue addObject:controller];
Joypad *joypad = [[Joypad alloc] init:controller];
[self.joypadsQueue addObject:joypad];
} else {
[self addMacOSJoypad:controller];
}
Expand All @@ -194,7 +335,7 @@ - (void)controllerWasDisconnected:(NSNotification *)notification {
return;
}

NSArray *keys = [self.connectedJoypads allKeysForObject:controller];
NSArray *keys = [self getAllKeysForController:controller];
for (NSNumber *key in keys) {
// tell Godot this joystick is no longer there
int joy_id = [key intValue];
Expand All @@ -214,7 +355,8 @@ - (GCControllerPlayerIndex)getFreePlayerIndex {
if (self.connectedJoypads == nil) {
NSArray *keys = [self.connectedJoypads allKeys];
for (NSNumber *key in keys) {
GCController *controller = [self.connectedJoypads objectForKey:key];
Joypad *joypad = [self.connectedJoypads objectForKey:key];
GCController *controller = joypad.controller;
if (controller.playerIndex == GCControllerPlayerIndex1) {
have_player_1 = true;
} else if (controller.playerIndex == GCControllerPlayerIndex2) {
Expand Down Expand Up @@ -351,3 +493,31 @@ - (void)setControllerInputHandler:(GCController *)controller {
}

@end

void JoypadMacOS::process_joypads() {
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 > 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);
}
}
}
}
}

0 comments on commit da70848

Please sign in to comment.