mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2025-07-03 08:15:31 +00:00
Add rumble support on iOS 14
This commit is contained in:
parent
2d24b0ec7b
commit
e8c7eb67c6
@ -6,7 +6,10 @@
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import GameController;
|
||||
@import CoreHaptics;
|
||||
|
||||
@interface Controller : NSObject
|
||||
|
||||
@ -21,7 +24,8 @@
|
||||
@property (nonatomic) short lastLeftStickY;
|
||||
@property (nonatomic) short lastRightStickX;
|
||||
@property (nonatomic) short lastRightStickY;
|
||||
@property (nonatomic) unsigned short lowFreqMotor;
|
||||
@property (nonatomic) unsigned short highFreqMotor;
|
||||
|
||||
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
|
||||
|
||||
@end
|
||||
|
@ -20,7 +20,6 @@
|
||||
@implementation ControllerSupport {
|
||||
NSLock *_controllerStreamLock;
|
||||
NSMutableDictionary *_controllers;
|
||||
NSTimer *_rumbleTimer;
|
||||
id<GamepadPresenceDelegate> _presenceDelegate;
|
||||
|
||||
OnScreenControls *_osc;
|
||||
@ -41,20 +40,6 @@
|
||||
#define UPDATE_BUTTON_FLAG(controller, x, y) \
|
||||
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
|
||||
|
||||
-(void) rumbleController: (Controller*)controller
|
||||
{
|
||||
#if 0
|
||||
// Only vibrate if the amplitude is large enough
|
||||
if (controller.lowFreqMotor > 0x5000 || controller.highFreqMotor > 0x5000) {
|
||||
// If the gamepad is nil (on-screen controls) or it's attached to the device,
|
||||
// then vibrate the device itself
|
||||
if (controller.gamepad == nil || [controller.gamepad isAttachedToDevice]) {
|
||||
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||
{
|
||||
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||
@ -67,12 +52,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the motor levels for the rumble timer to grab next iteration
|
||||
controller.lowFreqMotor = lowFreqMotor;
|
||||
controller.highFreqMotor = highFreqMotor;
|
||||
|
||||
// Rumble now to ensure short vibrations aren't missed
|
||||
[self rumbleController:controller];
|
||||
[controller.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[controller.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
}
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
|
||||
@ -212,6 +193,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(void) initializeControllerHaptics:(Controller*) controller
|
||||
{
|
||||
controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad];
|
||||
controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad];
|
||||
}
|
||||
|
||||
-(void) cleanupControllerHaptics:(Controller*) controller
|
||||
{
|
||||
[controller.lowFreqMotor cleanup];
|
||||
[controller.highFreqMotor cleanup];
|
||||
}
|
||||
|
||||
-(void) registerControllerCallbacks:(GCController*) controller
|
||||
{
|
||||
if (controller != NULL) {
|
||||
@ -402,6 +395,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare controller haptics for use
|
||||
[self initializeControllerHaptics:limeController];
|
||||
|
||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
Log(LOG_I, @"Assigning controller index: %d", i);
|
||||
@ -461,23 +457,6 @@
|
||||
return mask;
|
||||
}
|
||||
|
||||
-(void) rumbleTimer
|
||||
{
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
|
||||
if (controller == nil && i == 0 && _oscEnabled) {
|
||||
// No physical controller, but we have on-screen controls
|
||||
controller = _player0osc;
|
||||
}
|
||||
if (controller == nil) {
|
||||
// No connected controller for this player
|
||||
continue;
|
||||
}
|
||||
|
||||
[self rumbleController:controller];
|
||||
}
|
||||
}
|
||||
|
||||
-(NSUInteger) getConnectedGamepadCount
|
||||
{
|
||||
return _controllers.count;
|
||||
@ -499,12 +478,6 @@
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
|
||||
|
||||
_rumbleTimer = [NSTimer scheduledTimerWithTimeInterval:0.20
|
||||
target:self
|
||||
selector:@selector(rumbleTimer)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
|
||||
Log(LOG_I, @"Multi-controller: %d", _multiController);
|
||||
|
||||
@ -553,6 +526,10 @@
|
||||
|
||||
// Unset the GCController on this object (in case it is the OSC, which will persist)
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
// Stop haptics on this controller
|
||||
[self cleanupControllerHaptics:limeController];
|
||||
|
||||
limeController.gamepad = nil;
|
||||
|
||||
// Inform the server of the updated active gamepads before removing this controller
|
||||
@ -570,12 +547,15 @@
|
||||
|
||||
-(void) cleanup
|
||||
{
|
||||
[_rumbleTimer invalidate];
|
||||
_rumbleTimer = nil;
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
||||
[_controllers removeAllObjects];
|
||||
_controllerNumbers = 0;
|
||||
|
||||
for (Controller* controller in [_controllers allValues]) {
|
||||
[self cleanupControllerHaptics:controller];
|
||||
}
|
||||
[_controllers removeAllObjects];
|
||||
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
|
20
Limelight/Input/HapticContext.h
Normal file
20
Limelight/Input/HapticContext.h
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// HapticContext.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@interface HapticContext : NSObject
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||
-(void)cleanup;
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
|
||||
|
||||
@end
|
131
Limelight/Input/HapticContext.m
Normal file
131
Limelight/Input/HapticContext.m
Normal file
@ -0,0 +1,131 @@
|
||||
//
|
||||
// HapticContext.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@implementation HapticContext {
|
||||
GCControllerPlayerIndex _playerIndex;
|
||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0));
|
||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0));
|
||||
}
|
||||
|
||||
-(void)cleanup API_AVAILABLE(ios(14.0)) {
|
||||
if (_hapticPlayer != nil) {
|
||||
[_hapticPlayer cancelAndReturnError:nil];
|
||||
_hapticPlayer = nil;
|
||||
}
|
||||
if (_hapticEngine != nil) {
|
||||
[_hapticEngine stopWithCompletionHandler:nil];
|
||||
_hapticEngine = nil;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0)) {
|
||||
NSError* error;
|
||||
|
||||
// Cancel the last haptic effect
|
||||
if (_hapticPlayer != nil) {
|
||||
[_hapticPlayer stopAtTime:0 error:&error];
|
||||
_hapticPlayer = nil;
|
||||
}
|
||||
|
||||
// Check if the haptic engine died
|
||||
if (_hapticEngine == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't bother queuing a 0 amplitude haptic event
|
||||
if (amplitude == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:amplitude / 65536.0f];
|
||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:intensityParameter, nil] relativeTime:0 duration:GCHapticDurationInfinite];
|
||||
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
[_hapticPlayer startAtTime:0 error:&error];
|
||||
if (error != nil) {
|
||||
_hapticPlayer = nil;
|
||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0)) {
|
||||
if (gamepad.haptics == nil) {
|
||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_playerIndex = gamepad.playerIndex;
|
||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||
|
||||
NSError* error;
|
||||
[_hapticEngine startAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_hapticEngine = nil;
|
||||
};
|
||||
_hapticEngine.resetHandler = ^{
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||
me->_hapticPlayer = nil;
|
||||
[me->_hapticEngine startAndReturnError:nil];
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@ -8,6 +8,8 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
693B3A9B218638CD00982F7B /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 693B3A9A218638CD00982F7B /* Settings.bundle */; };
|
||||
9827E7A32514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
|
||||
9827E7A42514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
|
||||
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832D1351BBCD5C50036EF48 /* TemporaryApp.m */; };
|
||||
9865DC30213260B40005B9B9 /* libmoonlight-common-tv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A68152132509400507771 /* libmoonlight-common-tv.a */; };
|
||||
9865DC31213260F10005B9B9 /* mkcert.c in Sources */ = {isa = PBXBuildFile; fileRef = FB89460719F646E200339C8A /* mkcert.c */; };
|
||||
@ -157,6 +159,8 @@
|
||||
/* Begin PBXFileReference section */
|
||||
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = "<group>"; };
|
||||
9827E7A22514366900F25707 /* HapticContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HapticContext.m; sourceTree = "<group>"; };
|
||||
9827E7A7251436EA00F25707 /* HapticContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HapticContext.h; sourceTree = "<group>"; };
|
||||
9832D1341BBCD5C50036EF48 /* TemporaryApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryApp.h; path = Database/TemporaryApp.h; sourceTree = "<group>"; };
|
||||
9832D1351BBCD5C50036EF48 /* TemporaryApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryApp.m; path = Database/TemporaryApp.m; sourceTree = "<group>"; };
|
||||
98517B1B21CE0A9000481377 /* Moonlight v1.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.3.xcdatamodel"; sourceTree = "<group>"; };
|
||||
@ -533,6 +537,8 @@
|
||||
FB1A674C2131E65900507771 /* KeyboardSupport.m */,
|
||||
9897B6A0221260EF00966419 /* Controller.m */,
|
||||
9897B6A32212610800966419 /* Controller.h */,
|
||||
9827E7A22514366900F25707 /* HapticContext.m */,
|
||||
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
||||
);
|
||||
path = Input;
|
||||
sourceTree = "<group>";
|
||||
@ -945,6 +951,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9827E7A42514366900F25707 /* HapticContext.m in Sources */,
|
||||
9897B6A62212732C00966419 /* Controller.m in Sources */,
|
||||
9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */,
|
||||
9865DC36213287F30005B9B9 /* AppDelegate.m in Sources */,
|
||||
@ -1023,6 +1030,7 @@
|
||||
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
||||
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */,
|
||||
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
|
||||
9827E7A32514366900F25707 /* HapticContext.m in Sources */,
|
||||
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */,
|
||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
||||
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
||||
|
Loading…
x
Reference in New Issue
Block a user