GameKit: who's the Master? - objective-c

I'm building an application (already on the app store by the way, called MotAMot): a boggle, free app.
This game proposes several playing modes: standalone, multi players using the same iDevice and MULTI in which 2 players play the same gameboard at the same time but each using his own iDevices.
I'm using a GKPeerPickerController instance to manage the connection between the 2 devices.
My only concern is that I can't decide who's iPhone will play the server role and who's the client. When a player sends an invitation to the other, its cool, I can decide that this player will be the server.
Say for instance that player 1 invites player 2 to join the game. But at the same time (or just a few seconds after), player 2 invites player 1 before he gets the invitation popup displayed on his device. My question: how can I be sure that player 1 invited player 2 first (or the opposite)? I mean, of course I got some callbacks because I'm implementing the GKPeerPickerControllerDelegate protocol. But it seems that I get the same messages on both devices, regardless of who really initiates the connection.
// New connection
- (void)peerPickerController:(GKPeerPickerController *)picker
didConnectPeer:(NSString *)peer
toSession:(GKSession *)session
{
// Set the session
[self setCurrentSession:session];
// I'm implementing the GKSessionDelegate protocol
[session setDelegate:self];
// I'll handle any received data from my enemy
[session setDataReceiveHandler:self withContext:nil];
// I'm cleaning the room
[myPicker setDelegate:nil];
[myPicker dismiss];
[myPicker autorelease];
myPicker = nil;
}
// A peer changed its state
- (void)session:(GKSession *)session peer:(NSString *)peer didChangeState: (GKPeerConnectionState)state
{
switch (state)
{
case GKPeerStateConnected:
[self peerConnected:peer];
break;
case GKPeerStateDisconnected:
// Libération de la session
[currentSession release];
currentSession = nil;
break;
case GKPeerStateAvailable:
break;
case GKPeerStateConnecting:
break;
case GKPeerStateUnavailable:
break;
}
}
Would someone have any idea on this?

Why can't you solve this the old fashioned way? Civilization IV for example puts you into a server-room screen, in which case you are the server waiting for clients and it makes sense for you to not accept any new connections.
I would do something akin to that. it turns out this person had a problem (slightly related) and in my understanding it is solved in a similar way.

Related

DDHidLib - Implementing a joystick (Logitech 3D Extreme PRO)

I have recently decided to start a project which requires some kind of direct input to control my application and, in my case, I decided to use my Logitech 3D Extreme PRO joystick.
Therefore I started documenting myself online using the HID Class Device Interface Guide provided by Apple (Even the one dealing with the new HID Manager for OS X 10.5). I was able to implement the very main methods but unfortunately none of them worked.
After few days of search I have discovered a great Library developed by Daij-Djan : DDHidLib which helps a lot when dealing with direct inputs, providing great methods for discovering button presses and stick toggles, queues and lot more.
Even though this lib is a bit odd (2007), I decided to import it and give it a try..
I imported it into my project and started implementing some of it's methods which apparently seem very easy.
For instance, inside the DDHidJoystick sub-class, I found:
- (void)ddhidJoystick:(DDHidJoystick *)joystick buttonDown:(unsigned)buttonNumber
This method returns the number of the button that have been pressed.
Now, after this long introduction, let me explain my problem:
I coded a tested implementation of this class but without success (At least with my hardware).
Apparently with no reason the method reported above gets called only if I press one button on my joystick (Number 7).
Therefore I get some kind of notification {NSLog()} just in that case, even though the library is deigned to retrive any kind of input from the device.
But the most weird thing is that I am able to retrive all the other buttons/povs/sticks values only by pressing that specific button (N. 7) at the same time.
So let's say I want to get input from button 8, I will have to press button 8, than, at the same time, button number 7. Now i got a notification for both inputs.
If I want to get x-axis value, I need to:
Move x-stick
Press button n. 7
Then I see one notification both for button n.7 and x-axis moved at the same time.
To clarify, let me introduce this method:
- (void)ddhidJoystick:(DDHidJoystick *)joystick
stick:(unsigned)stick
xChanged:(int)value
As you can imagine, this method should be triggered whenever I move my X-Axis stick, however it doesn't.
Instead it gets triggered only if I press button number 7 and then, at the same time,I move my stick !
I tested out the joystick with X-Plane 10 and works just fine, so my guess is that there should be something else different from my app acquiring the input and hiding it.
I'm expecting to move my axis-sticks and se a NSLog, but that is not happening.
I'm not requesting a specific response on how to achieve my task using this lib, any other working approach will really be appreciated.
I really hope that this question is not too specific and could be helpful to somebody else in the future since nobody (apparently) tried to implement such input.
Thanks a lot to anyone who will reply to this post.
For the most curious:
I am building my own quad-copter using Arduino/Raspberry and lot of other electronics. I got a TX/RX Module operating at 2.4GHz which allows communication between 2 boards: one on the quad, and the other one plugged to the pc. I developed a lib (in C) reading POSIX documentation to read/write to serial ports and therefore I am able to send data over usb to my board, which than sends it to the quad. Finally I'am developing an OS X app to control the copter using the mentioned hardware/software and it is not far from being finished.
However for my purposes I want to use my joystick, and this is difficult.
In the end I will have a live video from onboard (FPV-Like) on the screen with telemetry all controlled by my Logitech 3D Extreme.
EDIT - I FOUND A SOLUTION
I found a solution and it seems to work pretty good!
Basically I had to edit a bit one method of the lib, adding support for the engine slider the joystick has.
Open up DDHidJoystick.m
Locate the method - (BOOL) addElement: (DDHidElement *) element;
Add the case statement case kHIDUsage_GD_Slider:
Set the action to [mStickElements addObject: element];
I will post the code here just in case somebody needs it in the future:
- (BOOL) addElement: (DDHidElement *) element;
{
DDHidUsage * usage = [element usage];
if ([usage usagePage] != kHIDPage_GenericDesktop)
return NO;
BOOL elementAdded = YES;
switch ([usage usageId])
{
case kHIDUsage_GD_X:
if (mXAxisElement == nil)
mXAxisElement = [element retain];
else
[mStickElements addObject: element];
break;
case kHIDUsage_GD_Y:
if (mYAxisElement == nil)
mYAxisElement = [element retain];
else
[mStickElements addObject: element];
break;
case kHIDUsage_GD_Z:
case kHIDUsage_GD_Rx:
case kHIDUsage_GD_Ry:
case kHIDUsage_GD_Rz:
[mStickElements addObject: element];
break;
case kHIDUsage_GD_Hatswitch:
[mPovElements addObject: element];
break;
/* EDIT HERE */
case kHIDUsage_GD_Slider:
[mStickElements addObject: element];
default:
elementAdded = NO;
}
return elementAdded;
}
Under this line you can find my whole implementation, and an image of the joystick.
(Developing on OS X 10.10 - Alberto Bellini)
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self startWatchingJoysticks];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (void)startWatchingJoysticks
{
//get an array of all joystick objects
joySticks = [[DDHidJoystick allJoysticks] retain];
//become the delegate of all available joystick objects
[joySticks makeObjectsPerformSelector:#selector(setDelegate:) withObject:self];
[joySticks makeObjectsPerformSelector:#selector(startListening) withObject:nil];
}
- (void)dealloc
{
[joySticks release];
[super dealloc];
}
//these are the DDHidLib joystick delegate methods related to buttons
- (void)ddhidJoystick:(DDHidJoystick *)joystick buttonDown:(unsigned)buttonNumber
{
NSLog(#"button down: %d", buttonNumber);
//Works only number 7
}
- (void)ddhidJoystick:(DDHidJoystick *)joystick
stick:(unsigned)stick
xChanged:(int)value
{
NSLog(#"x axis %d",value);
//Works ONCE only if presing button number 7 and moving stick
}
- (void)ddhidJoystick:(DDHidJoystick *)joystick
stick:(unsigned)stick
yChanged:(int)value
{
NSLog(#"y axis %d",value);
//Works ONCE only if presing button number 7 and moving stick
}
- (void)ddhidJoystick:(DDHidJoystick *)joystick
stick:(unsigned)stick
otherAxis:(unsigned)otherAxis
valueChanged:(int)value
{
NSLog(#"z axis %d",value);
//Works ONCE only if presing button number 7 and moving stick
}
- (void)ddhidJoystick:(DDHidJoystick *)joystick
stick:(unsigned)stick
povNumber:(unsigned)povNumber
valueChanged:(int)value
{
NSLog(#"Pov changed");
//Works ONCE only if presing button number 7 and moving stick
}
#end
Maybe I found the problem with your joystick.
Logitech Extreme 3D has no standard data packet for joysticks. It has different HID report descriptor and I can not found HID report descriptor parser in DDHidLib. I think DDHidLib just assume standard data packet.
Check this link: http://www.circuitsathome.com/mcu/using-logitech-extreme-3d-pro-joystick-with-arduino-hid-library
Unfortunately, I can not help you more because I don't know nothing about objetive-c nor OSX nor HID.
Maybe you can modify data packet structure in DDHidLib, create a HID report descriptor parser for DDHidLib or get a new joystick with standard data packet. ;)

Multipeer Connectivity Doesn't Connect Second Time

I'm using the Multipeer Connectivity Framework to transfer files between devices. I'm using the standard MCAdvertiserAssistant and MCBrowserViewController to connect the devices. On the first try from device A to device B things work fine. Same things on the first transfer from device B to device A.
If you try either direction again, after MCBrowserViewController presents its dialog to choose a peer and you select one, the popup to accept the request on the other device never appears. No error messages, no calls to delegate methods - just nothing. Has anyone come across this and any ideas?
I had the same problem and solved it with initiating all the necessary components every time I start advertising or browsing for peers. It isn't the cleanest solution but in my case it works 100%.
The code below is how I implemented it, so this is without the build-in ViewController provided by Apple.
Please be aware that [session disconnect] is an async method which sometimes take a few seconds to complete.
- (void)startBrowsing
{
// Initiate new advertiser
isAdvertising = YES;
_peerID = [[MCPeerID alloc] initWithDisplayName:#"Wallet"];
_session = [[MCSession alloc] initWithPeer:_peerID];
_session.delegate = self;
_advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:_peerID discoveryInfo:nil serviceType:#"made2pay"];
_advertiser.delegate = self;
// Start advertiser
[_advertiser startAdvertisingPeer];
}
- (void)stopBrowsing
{
[_advertiser stopAdvertisingPeer];
[_session disconnect];
_session = nil;
_peerID = nil;
_advertiser = nil;
isAdvertising = NO;
}

Multipeer Connectivity Disconnect

I'm having trouble with staying connected using the Multipeer Connectivity Framework in iOs7. Currently my app is programmatically handling the browsing and advertising using MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. I have an alert view that asks the user if he is a browser or an advertiser. On the return from that view I instantiate either an MCNearbyServiceAdvertiser or Browser accordingly.
#pragma - Alert Delegate
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0)
{
_browser = [[MCNearbyServiceBrowser alloc]initWithPeer:_peerID serviceType:#"Context-xl"];
[_browser setDelegate:self];
[self.detailViewController setRemote:YES];
[_browser startBrowsingForPeers];
} else
{
_advertiser = [[MCNearbyServiceAdvertiser alloc]initWithPeer:_peerID discoveryInfo:nil serviceType:#"Context-xl"];
[_advertiser setDelegate:self];
[self.detailViewController setRemote:NO];
[_advertiser startAdvertisingPeer];
}
[self.detailViewController configureView];
}
My session delegate method peer:...DidChangeState... is getting called twice, once for the connect and again for the disconnect. I'm not stopping the advertiser or browser at all after the session is started. Should I stop browsing/advertising?
EDIT Used a support ticket with Apple and they confirmed that calling sendData with too much data or too often can cause disconnects.
EDIT My hypothesis is that Apple has a thread or queue that is polling to check if peers are connected. If this thread / queue stalls (i.e. a breakpoint is hit or the app pegs the CPU or does something that takes a while on the main thread) it appears that this causes a disconnect.
Creating my session without encryption seems to have helped performance and with the disconnects, although they still happen.
MCPeerID* peerId = [[MCPeerID alloc] initWithDisplayName:self.displayName];
self.peer = [[MultiPeerPeer alloc] initWithDisplayName:peerId.displayName andPeer:peerId];
self.session = [[MCSession alloc] initWithPeer:peerId securityIdentity:nil encryptionPreference:MCEncryptionNone];
In addition, I have found calling sendData too often (more than 30-60 times a second) can cause the framework to get in a bad state and cause freezes and disconnects.

Why does my MCSession peer disconnect randomly?

Im using MCNearbyServiceBrowser and MCNearbyServiceAdvertiser to join two peers to a MCSession. I am able to send data between them using MCSession's sendData method. All seems to be working as expected until I randomly (and not due to any event I control) receive a MCSessionStateNotConnected via the session's MCSessionDelegate didChangeState handler. Additionally, the MCSession's connectedPeers array no longer has my peers.
Two questions: Why? and How do i keep the MCSession from disconnecting?
This is a bug, which I just reported to Apple. The docs claim the didReceiveCertificate callback is optional, but it's not. Add this method to your MCSessionDelegate:
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
certificateHandler(YES);
}
The random disconnects should cease.
UPDATE After using a support ticket to Apple, they confirmed that calling sendData too often and with too much data can cause disconnects.
I have had disconnects when hitting break points and when backgrounding. Since the break points won't happen on the app store, you need to handle the backgrounding case by beginning a background task when your app is about to enter the background. Then end this task when your app comes back to the foreground. On iOS 7 this gives you about 3 background minutes which is better than nothing.
An additional strategy would be to schedule a local notification for maybe 15 seconds before your background time expires by using [[UIApplication sharedApplication] backgroundTimeRemaining], that way you can bring the user back into the app before it suspends and the multi peer framework has to be shutdown. Perhaps the local notification would warn them that their session will expire in 10 seconds or something...
If the background task expires and the app is still in the background, you have to tear down everything related to multi-peer connectivity, otherwise you will get crashes.
- (void) createExpireNotification
{
[self killExpireNotification];
if (self.connectedPeerCount != 0) // if peers connected, setup kill switch
{
NSTimeInterval gracePeriod = 20.0f;
// create notification that will get the user back into the app when the background process time is about to expire
NSTimeInterval msgTime = UIApplication.sharedApplication.backgroundTimeRemaining - gracePeriod;
UILocalNotification* n = [[UILocalNotification alloc] init];
self.expireNotification = n;
self.expireNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:msgTime];
self.expireNotification.alertBody = TR(#"Text_MultiPeerIsAboutToExpire");
self.expireNotification.soundName = UILocalNotificationDefaultSoundName;
self.expireNotification.applicationIconBadgeNumber = 1;
[UIApplication.sharedApplication scheduleLocalNotification:self.expireNotification];
}
}
- (void) killExpireNotification
{
if (self.expireNotification != nil)
{
[UIApplication.sharedApplication cancelLocalNotification:self.expireNotification];
self.expireNotification = nil;
}
}
- (void) applicationWillEnterBackground
{
self.taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
{
[self shutdownMultiPeerStuff];
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}];
[self createExpireNotification];
}
- (void) applicationWillEnterForeground
{
[self killExpireNotification];
if (self.taskId != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:self.taskId];
self.taskId = UIBackgroundTaskInvalid;
}
}
- (void) applicationWillTerminate
{
[self killExpireNotification];
[self stop]; // shutdown multi-peer
}
You'll also want this handler in your MCSession delegate due to Apple bug:
- (void) session:(MCSession*)session didReceiveCertificate:(NSArray*)certificate fromPeer:(MCPeerID*)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
if (certificateHandler != nil) { certificateHandler(YES); }
}
There are many causes of this, and the two answers thus far are both correct in my experience. Another which you'll find in other similar questions is this: Only one peer can accept another's invitation.
So, to clarify, if you set up an app where all devices are both advertisers and browsers, any devices can freely invite any others found to join a session. However, between any two given devices, only one device can actually accept the invitation and connect to the other device. If both devices accept each others' invitations they will disconnect within a minute or less.
Note that this limitation does not prevent the desired behavior because - unlike what my intuition stated before I built my multipeer implementation - when one device accepts an invitation and connects to another device they both become connected and receive connection delegate methods and can send each other messages.
Therefore, if you are connecting devices which both browse and advertise, send invitations freely but only accept one of a pair.
The problem of only accepting one of two invitations can be solved a myriad of ways. To begin, understand that you can pass any arbitrary object or dictionary (archived as data) as the context argument in an invitation. Therefore, both devices have access to any arbitrary information about the other (and of course itself). So, you could use at least these strategies:
simply compare: the display name of the peerID. But there's no guarantee these won't be equal.
store the date your multipeer controller was initialized and use that for comparison
give each peer a UUID and send this for comparison (my technique, in which each device - indeed each user of the app on a device - has a persistent UUID it employs).
etc - any object which supports both NSCoding and compare: will do fine.
I've been having similar problems. It seems though that if I have run my app on one iOS device, and connected to another, then quit and relaunch (say when I rerun from Xcode), then I am in a situation where I get a Connected message and then a Not Connected message a little later. This was throwing me off. But looking more carefully, I can see that the Not Connected message is actually meant for a different peerId than the one that has connected.
I think the problem here is that most samples I've seen just care about the displayName of the peerID, and neglect the fact that you can get multiple peerIDs for the same device/displayName.
I am now checking the displayName first and then verifying that the peerID is the same, by doing a compare of the pointers.
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
MyPlayer *player = _players[peerID.displayName];
if ((state == MCSessionStateNotConnected) &&
(peerID != player.peerID)) {
NSLog(#"remnant connection drop");
return; // note that I don't care if player is nil, since I don't want to
// add a dictionary object for a Not Connecting peer.
}
if (player == nil) {
player = [MyPlayer init];
player.peerID = peerID;
_players[peerID.displayName] = player;
}
player.state = state;
...
I was disconnecting immediately after I accepted the connection request. Observing the state, I saw it change from MCSessionStateConnected to MCSessionStateNotConnected.
I am creating my sessions with:
[[MCSession alloc] initWithPeer:peerID]
NOT the instantiation method dealing with security certificates:
- (instancetype)initWithPeer:(MCPeerID *)myPeerID securityIdentity:(NSArray *)identity encryptionPreference:(MCEncryptionPreference)encryptionPreference
Based on Andrew's tip above, I added the delegate method
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler {
certificateHandler(YES);
}
and the disconnects stopped.

Game Center: authenticates ok, shows leaderboard ok, posts (my) score ok, only 2 players?

My game (called "Clear the Square") has gotten approved. It's getting hundreds of downloads. It's currently #44 in free word games in the US. There is only one problem: when I go to the Game Center leaderboards, it shows two users, one of them being me. And one of them being (very appropriately named) someone who isn't me. Here is a screenshot of what this looks like: http://clearthesquareapp.com/photo-9.PNG
I know that there are more than two people playing this (iAd tells me so.) I also know that the game is not unwinnable; somewhat challenging, but not unwinnable. And I know that Game Center didn't just completely go out of style overnight. In all other games that have ever been anywhere near a top-100 list, there are at least a few hundred all-time scores on the leaderboard.
Thinking that the problem might be that my development build is somehow special and goes to an alternate, "sandbox" set of leaderboards, I deleted the binary from my phone, rebooted my phone, and downloaded the game from the App Store. Same thing. So that wasn't the problem.
Here's all my GameCenter code; it comes straight from example projects. I don't know what could be missing, since it does successfully authenticate, it does show leaderboards, and it does post my score -- each time I set a high score, it is instantly reflected in the leaderboard.
I would really, really appreciate it if someone could download the free version of my game, and send me a screenshot of what the leaderboards look like on your end. You would definitely get promo codes for every future app I make.
- (void) authenticateLocalPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
[localPlayer authenticateWithCompletionHandler:^(NSError *error) {
if (localPlayer.isAuthenticated)
{
NSLog(#"authenticated in game center!");
// Perform additional tasks for the authenticated player.
}
}];
}
- (void) reportScore: (int64_t) score forCategory: (NSString*) category
{
GKScore *scoreReporter = [[[GKScore alloc] initWithCategory:category] autorelease];
scoreReporter.value = score;
[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {
if (error != nil)
{
// handle the reporting error
}
}];
}
- (void) showLeaderboard
{
GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != nil)
{
leaderboardController.leaderboardDelegate = self;
[self presentModalViewController: leaderboardController animated: YES];
}
}
- (void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController {
[self dismissModalViewControllerAnimated:YES];
}
There should be more than two players on this leaderboard. What am I missing?
Edit: also, worth noting that it has already been out for about 48 hours, and probably has about 1000 downloads so far. So I am quite sure that it's not just a matter of nobody having had time to win a game yet.
You're probably still signed in to your "sandbox" Game Center account. Log out of that, and then log into your real account, and you should see the real leaderboards.
I ran into this problem myself, and it sometimes didn't "take" to log out and back in, so try a couple times. I think I may have had to kill (via the double-tap home button) the Game Center app after logging out of the sandbox, and before logging into the real account, for it to work.