|
@@ -42,6 +42,7 @@
|
|
|
#if !TARGET_OS_TV
|
|
|
#import <CoreMotion/CoreMotion.h>
|
|
|
#endif
|
|
|
+#import <CoreHaptics/CoreHaptics.h>
|
|
|
|
|
|
#ifdef SDL_JOYSTICK_MFI
|
|
|
#import <GameController/GameController.h>
|
|
@@ -771,10 +772,163 @@ IOS_MFIJoystickUpdate(SDL_Joystick * joystick)
|
|
|
#endif /* SDL_JOYSTICK_MFI */
|
|
|
}
|
|
|
|
|
|
+@interface SDL_RumbleMotor : NSObject
|
|
|
+@end
|
|
|
+
|
|
|
+@implementation SDL_RumbleMotor {
|
|
|
+ CHHapticEngine *engine API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
|
+ id<CHHapticPatternPlayer> player API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
|
+}
|
|
|
+
|
|
|
+-(void)cleanup
|
|
|
+{
|
|
|
+ if (self->player != nil) {
|
|
|
+ [self->player cancelAndReturnError:nil];
|
|
|
+ self->player = nil;
|
|
|
+ }
|
|
|
+ if (self->engine != nil) {
|
|
|
+ [self->engine stopWithCompletionHandler:nil];
|
|
|
+ self->engine = nil;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+-(int)setIntensity:(float)intensity
|
|
|
+{
|
|
|
+ if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
|
+ NSError *error;
|
|
|
+
|
|
|
+ if (self->player != nil) {
|
|
|
+ [self->player stopAtTime:0 error:&error];
|
|
|
+ self->player = nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (self->engine == nil) {
|
|
|
+ return SDL_SetError("Haptics engine not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (intensity <= 0.01f) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ CHHapticEventParameter *param = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:intensity];
|
|
|
+ CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:param, nil] relativeTime:0 duration:GCHapticDurationInfinite];
|
|
|
+ CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:event] parameters:[[NSArray alloc] init] error:&error];
|
|
|
+ if (error != nil) {
|
|
|
+ return SDL_SetError("Couldn't create haptic pattern: %s", [error.localizedDescription UTF8String]);
|
|
|
+ }
|
|
|
+
|
|
|
+ self->player = [self->engine createPlayerWithPattern:pattern error:&error];
|
|
|
+ if (error != nil) {
|
|
|
+ return SDL_SetError("Couldn't create haptic player: %s", [error.localizedDescription UTF8String]);
|
|
|
+ }
|
|
|
+
|
|
|
+ [self->player startAtTime:0 error:&error];
|
|
|
+ if (error != nil) {
|
|
|
+ self->player = nil;
|
|
|
+ return SDL_SetError("Couldn't start playback: %s", [error.localizedDescription UTF8String]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+-(id) initWithController:(GCController*)controller locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0))
|
|
|
+{
|
|
|
+ NSError *error;
|
|
|
+
|
|
|
+ self->engine = [controller.haptics createEngineWithLocality:locality];
|
|
|
+ if (self->engine == nil) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ [self->engine startAndReturnError:&error];
|
|
|
+ if (error != nil) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ __weak typeof(self) weakSelf = self;
|
|
|
+ self->engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
|
|
+ SDL_RumbleMotor *_this = weakSelf;
|
|
|
+ if (_this == nil) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _this->player = nil;
|
|
|
+ _this->engine = nil;
|
|
|
+ };
|
|
|
+ self->engine.resetHandler = ^{
|
|
|
+ SDL_RumbleMotor *_this = weakSelf;
|
|
|
+ if (_this == nil) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _this->player = nil;
|
|
|
+ [_this->engine startAndReturnError:nil];
|
|
|
+ };
|
|
|
+
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+@interface SDL_RumbleContext : NSObject
|
|
|
+@end
|
|
|
+
|
|
|
+@implementation SDL_RumbleContext {
|
|
|
+ SDL_RumbleMotor *low_frequency_motor;
|
|
|
+ SDL_RumbleMotor *high_frequency_motor;
|
|
|
+}
|
|
|
+
|
|
|
+-(id) initWithLowFrequencyMotor:(SDL_RumbleMotor*)low_frequency_motor andHighFrequencyMotor:(SDL_RumbleMotor*)high_frequency_motor
|
|
|
+{
|
|
|
+ self->low_frequency_motor = low_frequency_motor;
|
|
|
+ self->high_frequency_motor = high_frequency_motor;
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+-(int) rumbleWithLowFrequency:(Uint16)low_frequency_rumble andHighFrequency:(Uint16)high_frequency_rumble
|
|
|
+{
|
|
|
+ int result = 0;
|
|
|
+
|
|
|
+ result += [self->low_frequency_motor setIntensity:((float)low_frequency_rumble / 65535.0f)];
|
|
|
+ result += [self->high_frequency_motor setIntensity:((float)high_frequency_rumble / 65535.0f)];
|
|
|
+ return ((result < 0) ? -1 : 0);
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+static SDL_RumbleContext *IOS_JoystickInitRumble(GCController *controller)
|
|
|
+{
|
|
|
+ @autoreleasepool {
|
|
|
+ if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
|
+ SDL_RumbleMotor *low_frequency_motor = [[SDL_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle];
|
|
|
+ SDL_RumbleMotor *high_frequency_motor = [[SDL_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle];
|
|
|
+ if (low_frequency_motor && high_frequency_motor) {
|
|
|
+ return [[SDL_RumbleContext alloc] initWithLowFrequencyMotor:low_frequency_motor andHighFrequencyMotor:high_frequency_motor];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil;
|
|
|
+}
|
|
|
+
|
|
|
static int
|
|
|
IOS_JoystickRumble(SDL_Joystick * joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
|
|
|
{
|
|
|
- return SDL_Unsupported();
|
|
|
+ SDL_JoystickDeviceItem *device = joystick->hwdata;
|
|
|
+ if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
|
+ if (!device->rumble && device->controller && device->controller.haptics) {
|
|
|
+ SDL_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);
|
|
|
+ if (rumble) {
|
|
|
+ device->rumble = (void *)CFBridgingRetain(rumble);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (device->rumble) {
|
|
|
+ SDL_RumbleContext *rumble = (__bridge SDL_RumbleContext *)device->rumble;
|
|
|
+ return [rumble rumbleWithLowFrequency:low_frequency_rumble andHighFrequency:high_frequency_rumble];
|
|
|
+ } else {
|
|
|
+ return SDL_Unsupported();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
static void
|
|
@@ -805,6 +959,11 @@ IOS_JoystickClose(SDL_Joystick * joystick)
|
|
|
device->joystick = NULL;
|
|
|
|
|
|
@autoreleasepool {
|
|
|
+ if (device->rumble) {
|
|
|
+ CFRelease(device->rumble);
|
|
|
+ device->rumble = NULL;
|
|
|
+ }
|
|
|
+
|
|
|
if (device->accelerometer) {
|
|
|
#if !TARGET_OS_TV
|
|
|
[motionManager stopAccelerometerUpdates];
|