I have made an iOS multiplayer GameCenter game, but right before publishing found an issue I don't know how to solve. In coding process I used Ray Wenderlich tutorial http://www.raywenderlich.com/3276/how-to-make-a-simple-multiplayer-game-with-game-center-tutorial-part-12
GameCenter view controller is shown, connection creates and game can be played until both devices are on the same Wifi network.
If I turn off Wifi on my phone and use 3G network, then try to start new game - in that case connection isn't made anymore. Both devices find each other, but hangs on "Connecting..." screen. It looks like that
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
is not called. Any ideas how to solve it or at least understand, where exactly is the problem?
I think that in your particular case the problem is that your 3G ISP restricts connections from necessary ports. The Apple docs say:
To use Game Center ... port forwarding must be enabled for ports 443 (TCP), 3478-3497 (UDP), 4398 (UDP), 5223 (TCP), 16384-16387 (UDP), and 16393-16472 (UDP)
I faced this issue too when trying to play on iPad connected via bluetooth to iPhone: there was "Connecting..." screen on each device.
But when I use built-in iPad 3G (with a different tariff plan) everything goes fine.
Just remind, in a normal match-making scenario match:player:didChangeState: may not be called. You should also check match.expectedPlayerCount:
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
//...
if (theMatch.expectedPlayerCount == 0) {
NSLog(#"Ready to start match!");
}
}
Also I expected a similar problem with "Connecting..." screen, but on Wifi network.
And it was reproduced only on iOS6 and after I tried rematch via -[GKMatch rematchWithCompletionHandler:^(GKMatch *match, NSError *error) {}] before.
One device hanged on "Connecting..." screen but on the other matchmakerViewController:didFindMatch: was successfully called, but what is interesting is that match.expectedPlayerCount was 0 and match.playerIDs array was empty at the same time.
I think such error occurred because I tried to find a new match while previous match tried to reconnect on background thread in the same time. And because of that new match was obtained corrupted.
The decision is to wait for rematchCompletion being called and only then try to find new match. There is no interface in GKMatch to cancel rematch, so I use [[GKMatchmaker sharedMatchmaker] cancel] and after several seconds rematchCompletion is called with error and we are ready to start finding new match.
Also I figured out that old unsued GKMatch instances are not deallocated and continue to live somewhere in GameKit framework. And they may probably cause problems if the work with them is not finished correctly (i.e. not disconnected, or rematch is not canceled in my case ). So do not forget to call -[GKMatch disconnect] and finish any other kind of work before removing the last strong reference to the match object .
Related
I've been trying to implement a way to notice when my app gets disconnected from Game Center since ending a turn while disconnected breaks the app. I just started testing on my iPad with the WiFi off and noticed that even when I completely quit the app, it automatically authenticates my player and signs me into game center. It even loads game data from recent games. I then tried going on the game center app itself and the only thing that caused a network error was clicking on the "Games" tab. Obviously much of the data is being cached so I don't know what to check to see if I'm disconnected.
So how can I test if the device is connected to Game Center? Neither Match data, participants, nor any of their properties are nil, and the player is always authenticated.
Try to manage this with two things :
Network connection : with something like FXReachability and then [FXReachability isReachable]
Game Center authentification :
In your app test authentification with :
GKLocalPlayer.localPlayer.isAuthenticated
More generally, always test completion handlers errors and array existence like this :
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error) {
if (error) {
// what kind of error ?
}
if (matches) {
// do what you need to do
}}];
Hope this will help you a little bit...
I am using the MPMoviePlayerController(MPMPC) to stream audio into an application and thats working really fine. Just with one exception, during low network connectivity the app becomes unresponsive.
Now I have even tried to use AVPlayer too but with more or less same experience. And for some reason I cannot find any issues related to this on the internet. So I am not sure if this is from my end or is it how MPMoviePlayerController behaves during low connectivity.
I even tried to log any function that is being called after giving URL to the MPMPC but none of the functions are called.
I have used below three notification to get events of the MPMPC
MPMoviePlayerLoadStateDidChangeNotification
MPMoviePlayerPlaybackDidFinishNotification
MPMoviePlayerPlaybackStateDidChangeNotification
Once the available networking bandwidth is becoming too low to keep up proper playback, MPMoviePlayerController will trigger the MPMoviePlayerLoadStateDidChangeNotification and the loadState will have MPMovieLoadStateStalled set.
You may then mask the load-state within your notification handler and run any actions needed by your app for this state:
if ((movieController_.loadState & MPMovieLoadStateStalled) == MPMovieLoadStateStalled)
{
NSLog(#"playback stalled - make sure we don't block now!");
}
Once the player has recovered, once again the MPMoviePlayerLoadStateDidChangeNotification is triggered and the loadState property will have the bits for MPMovieLoadStatePlaythroughOK set:
if ((movieController_.loadState & MPMovieLoadStatePlaythroughOK) == MPMovieLoadStatePlaythroughOK)
{
NSLog(#"playback should run uninterrupted from now on.");
}
However, I never experienced any interface slowdowns of my app caused by the MPMovieLoadStateStalled state. I'ld say that must be your code acting weird, it is not MPMoviePlayerController as I know it. Additionally, those notifications are always sent, I never experienced scenarios in which they were not properly triggered.
I can only recommend to recreate this issue within a minimal test-case and work your way up from that one towards your app (possibly from both sides, test-case and your app).
For simulating bandwidth breakdowns, I would recommend using Charles proxy.
I'm using the iOS 7 Multipeer framework in my app but I'm experiencing a problem with devices disconnecting. If I open the app in two devices: device A and device B the two devices connect to each other automatically. However, after several seconds device A disconnects from device B. i.e. At first the connection is like this:
A ---> B
A <--- B
After several seconds:
A ---> B
A B
Device A maintains it's connection but device B get's a MCSessionStateNotConnected.
This means that A can send data to B but B can't reply. I tried to get around this by checking if the device is connected and if it's not, re-initiating the connection using:
[browser invitePeer:peerID toSession:_session withContext:Nil timeout:10];
But the didChangeState callback just get's called with MCSessionStateNotConnected.
Strangely if I send app A to the background, then re-open it, B reconnects to it and the connection is maintained.
The Multipeer API (and documentation) seems a bit sparse so I was assuming that it would just work. In this situation how should I re-connect the device?
I was having the same problem, and it seems to have been related to my app browsing and advertising at the same time, and two invitations being sent/accepted. When I stopped doing this and let one peer defer to the other for invitations the devices stayed connected.
In my browser delegate I'm checking the hash value of the discovered peer's displayName and only sending an invitation if my peer has a higher hash value:
Edit
As pointed out by #Masa the hash value of an NSString will be different on 32 and 64 bit devices, so it's safer to use the compare: method on displayName.
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info {
NSLog(#"Browser found peer ID %#",peerID.displayName);
//displayName is created with [[NSUUID UUID] UUIDString]
BOOL shouldInvite = ([_myPeerID.displayName compare:peerID.displayName]==NSOrderedDescending);
if (shouldInvite){
[browser invitePeer:peerID toSession:_session withContext:nil timeout:1.0];
}
else {
NSLog(#"Not inviting");
}
}
As you say, the documentation is sparse so who knows what Apple really wants us to do, but I've experimented with both sending and accepting invitations using a single session, and also creating a new session for each invitation accepted/sent, but this particular way of doing things has given me the most success.
For anyone interested, I created MCSessionP2P, a demo app that illustrates the ad-hoc networking features of MCSession. The app both advertises itself on the local network and programmatically connects to available peers, establishing a peer-to-peer network. Hat tip to #ChrisH for his technique of comparing hash values for inviting peers.
I liked ChrisH's solution, which reveals the key insight that only one peer should connect to the other peer, not both. Mutual connection attempts results in mutual disconnection (though not that a single-sided connection actually is, counter-intuitively, a mutual connection in terms of status and communication, so that works fine).
However, I think a better approach than one peer inviting is for both peers to invite but only one peer to accept. I use this method now and it works great, because both peers have an opportunity to pass rich information to the other via the context parameter of the invitation, as opposed to having to rely on scant information available in the foundPeer delegate method.
Therefore, I recommend a solution like so:
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
[self invitePeer:peerID];
}
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL accept, MCSession *session))invitationHandler
{
NSDictionary *hugePackageOfInformation = [NSKeyedUnarchiver unarchiveObjectWithData:context];
BOOL shouldAccept = ([hugePackageOfInformation.UUID.UUIDString compare:self.user.UUID.UUIDString] == NSOrderedDescending);
invitationHandler(shouldAccept && ![self isPeerConnected:peerID], [self openSession]);
}
I have the same issue when devices trying to connect to each other at the same time and I don't know how to find a reason because we don't have any errors with MCSessionStateNotConnected.
We can use some crafty way to solve this issue:
Put into txt records ( discovery info ) a time [[NSDate date] timeIntervalSince1970] when app started. Who started first - send invitation to others.
But I think it's not a right way ( if apps start at the same time, unlikely... :) ). We need to figure out the reason.
This is the result of a bug, which I've reported to Apple. I've explained how to fix it in my response to another question: Why does my MCSession peer disconnect randomly?
I have not flagged these questions for merging, because while the underlying bug and solution are the same, the two questions describe different problems.
Save the hash of the peer B. Using a timer check the state of the connection continuously if is not connected try to reconnect with each given period of time.
According to apple document Choosing an inviter when using Multipeer Connectivity
“In iOS 7, sending simultaneous invites can cause both invites to fail, leaving both peers unable to communicate with each other.”
But iOS 8 has fixed it.
It seems that the .notConnected message is a false positive in that the device is still receiving data. So, I manually updated local connection state to .connected
It was hard to factor out other state from other examples. So, I wrote a bare bones MCSession example for SwiftUI, here: MultiPeer
I have been using GKMatch for quite a while successfully in an app. I have been chasing down and issue with the game occasionally stopping and have tracked it down to packets being sent but not received. This happens only occasionally but I can't seem to track down why it happens.
All messages are sent using GKSendDataReliable.
Logging has shown that the packet is being sent from one device successfully, but it is never received at the target device.
//Code sample of sending method....
//self.model.match is a GKMatch instance
-(BOOL) sendDataToAllPlayers:(NSData *)data error:(NSError **)error {
[self.model.debugger addToLog:#"GKManager - sending data"];
return [self.model.match sendDataToAllPlayers:data withDataMode:GKSendDataReliable error:error];
}
...
//Code sample of receiving method....
// The match received data sent from the player.
-(void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
[self.model.debugger addToLog:#"GKManager - received data"];
[super didReceiveData:data fromPlayer:playerID];
}
What I see happen is that periodically (maybe 1 in 100 messages) is sent without error from the 'sendDataToAllPlayers' method, but the receiving device never hits the 'didReceiveData' method. My understanding is that using GKSendDataReliable should send messages and then won't send another until it receives an acknowledgement. Messages aren't received but new messages are sent from the same device.
The sending method returns 'YES' and error is nil, but the didReceiveData is never hit...!
Has anyone ever seen this? Does anyone have any ideas what this could be? I don't know what else I could do to debug this.
I confirm the bug.
I made an example project consistently reproducing the issue: https://github.com/rabovik/GKMatchPacketLostExample. I tested it on a weak internet connection (iPad 3 with Wi-Fi and iPhone 4S with EDGE; both on iOS 6.1.3), and some packets regularly get lost without any error from Game Center API. Moreover sometimes device stops receiving any data, while another still sends and receives messages successfully.
So if we are sure the bug exists, the only possible workaround is to add an extra transport layer for truly reliable delivery.
I wrote a simple lib for this purpose: https://github.com/rabovik/RoUTP. It saves all sent messages until acknowledgement for each received, resends lost and buffers received messages in case of broken sequence.
In my tests combination "RoUTP + GKMatchSendDataUnreliable" works even beter than "RoUTP + GKMatchSendDataReliable" (and of course better than pure GKMatchSendDataReliable which is not really reliable).
[Edit: RoUTP no longer seems to work properly in iOS9]
I did some testing yesterday at the edge of my wifi's range where packet loss was occuring. What happens is that when packets are lost using GKMatchSendDataReliable the player is abruptly disconnected from the GKMatch session. match:player:didChangeState is called with GKPlayerStateDisconnected and the player's ID is removed from the playerIDs dictionary. This happens with only slight packet loss. I can still browse the internet from this connection for instance.
Now, if I switch to sending packets unreliably, then match:player:didChangeState never fires and the match keeps going without a problem (except losing the occasional packet which might be important). It will only disconnect if the packet loss becomes substantial. Now this is where Yan's RoUTP library is handy, since we can keep track of and retry these important messages without having our players disconnected when they encounter slight packet loss.
Also, data sending using GKMatchSendDataReliable will only return YES if the message has been successfully queued for delivery. It does not tell you whether or not the message was successfully delivered. How could it? It returns right away.
I am using iOS 6 Game Center API for turn based games.
When the device is disconnected from internet
In the completion handler of the method
[currentMatch endTurnWithNextParticipant:nextParticipant matchData:data completionHandler:^(NSError *error) {
if (error) {
NSLog(#"%#", error);
} else {
//save the new state of the game
}
I get an error. But then, game center standard UI that displays matches list, says "Their turn". when connected again it changes to "Your turn".
The code from famous tutorial at http://www.raywenderlich.com/5509/beginning-turn-based-gaming-with-ios-5-part-2 has the same exact problem.
How I should handle this problem?
If you are using iOS 6 Game Center API then you will have to use
-endTurnWithNextParticipants:turnTimeout:matchData:completionHandler:
because...
–endTurnWithNextParticipant:matchData:completionHandler: Deprecated in iOS 6.0
http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKTurnBasedMatch_Ref/Reference/Reference.html
The thing is, that when you use GC methods that change status of the match (matchData and synchronization info in this case), data is uploaded to the GC server so that other player(s) get the update. If you're disconnected and ignore the error, your local GKTurnBasedMatch and its matchData change, as well as your synchronization info (which is used to determine if it is your turn to act among other things).
However, since you are diconnected, only your local instance of GCTurnBasedMatch is updated (you get error so that you app is aware of that). When you're reconnected, your app authenticates the user and updates match state (if you're following the tutorial code). Updating match data reverts the sync data (so it's still your turn).
At this point, you should either submit the turn again (provided that you cached gameData that was passed to GC while you were disconnected) and/or call updateMatchData so that your local GKTurnBasedMatch and its matchData get in sync with what's on the server. You should also re-layout your game board with previous turn's data if you didn't re-submit turn after reconnection.