I am getting some behavior I can't decode from GameKit.
sometimes the player who has done the inviting gets stuck in a 'waiting...' loop, and is unable to close the deal on his invitation.
I believe it has to do with multitasking and the invite handler... it seems that if the invitee's app starts from scratch, then the invitation can be properly accepted. But the mechanism of this not so transparent to me.
Any clues as to what might be missing? I've become blinded by the documentation.
Sometimes when a match between two players starts, it's possible that one player doesn't have the connected state yet. You should check if more players are expected to connect before actually starting the game. If it's over 0, instead of starting the game, wait for the player to connect, and only start the game when that player is connected.
So the code would look something like this inside your the method where you are setting up the game:
if (currentMatch.expectedPlayerCount) {
waiting = YES;
}
And you would implement this delegate method:
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
{
if (state == GKPlayerStateConnected) {
if (waiting) {
waiting = NO;
// start the game now
}
} else if (state == GKPlayerStateDisconnected) {
// handle disconnect
}
}
Related
I'm wondering how do I get the disconnect message for local player when the game session is in progress and we're unable to communicate our data to other players. As there is nothing in documentation that says "this method will inform you whenever your connection fails", I'm at a bit of a loss.
I was trying to use this chunk of code in hopes that it would work, but it's futile. The "We're disconnected." message is never triggered.
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {
if (self.match != theMatch) return;
switch (state) {
case GKPlayerStateDisconnected:
//disconnected
NSLog(#"player status changed: disconnected");
matchStarted = NO;
GKLocalPlayer *player = [GKLocalPlayer localPlayer];
if ([playerID isEqualToString:player.playerID]) {
// We have been disconnected
NSLog(#"We're disconnected.");
}
if ([delegate respondsToSelector:#selector(matchEnded)]) {
[delegate matchEnded];
}
break;
}
}
The only other line that I found might tell us that we're unable to communicate is when we actually send data like this:
- (void)sendRandomMatchData:(NSData *)data {
GKMatch *match = [GCHelper sharedInstance].match;
BOOL success = [match sendDataToAllPlayers:data
withDataMode:GKMatchSendDataReliable
error:nil];
if (!success) {
[self matchEnded];
}
}
But I assume that "success" will also be false if the opponent has disconnected and we're unable to send messages to them.
I have a pretty strict game logics, if someone has been disconnected I need to inform them that they are unable to continue playing the match and that they have lost.
Any help is highly appreciated.
There are 2 places you can do this, both are in the GKMatchDelegate protocol.
The first is implementing:
- (void)match:(GKMatch *)match player:(NSString *)playerID
didChangeState:(GKPlayerConnectionState)state
{
}
And if it's a 2 player match, the second place you can catch the disconnect is:
- (BOOL)match:(GKMatch *)theMatch shouldReinvitePlayer:(NSString *)playerID
{
}
Both those events fire reliably when the GKMatch is terminated. If Stan's answer answered your question, then I would highly recommend getting in the practice of catching NSError wherever they are available! Can save you lots of time.
So to send data and catch the error:
NSError* nsErr ;
int result = [theMatch sendData:nsd toPlayers:theMatch.playerIDs
withDataMode:GKMatchSendDataReliable error:&nsErr] ;
if( !result ) { // NO if the match was unable to queue the data.
error( nsErr, "Failed to sendRoundtripPing" ) ;
}
What about examining the error after the following code line:
BOOL success = [match sendDataToAllPlayers:data
withDataMode:GKMatchSendDataReliable
error:nil]; //replace nil with NSError variable
Maybe error will give you extra info u need.
Another idea is to create NSTimer and set some certain time for making moves/turns. If some player didn't make it for a certain time then assume this player is disconnected. Also you could check your Internet connection state to determine you have a connection cuz maybe you just lost it and that's the reason you can't send/receive any data.
Also you could check every player periodically by sending them some short amount of data using GC just to make them answer you. This way you could ensure all players are "alive" or detect some "zombie".
Remember if player moves the game to background using Home button you won't detect it anyhow cuz code in your game wont execute. And this situation doesn't mean that player is "zombie". Player could be busy by a call or something else like another app. Player could temporary loose Internet connection. This means that he/she could return to game soon...
Im a noob in game center # games generally. Im making my second game now and implemented the game center.
If the internet is available, there is no problem, everything works well.
But just now I purposely make the internet unreachable, and when I get an achievement, obviously it does not register to the Game Center's Achievement.
How and what's the best way to handle this issue?
Thank you....
You could add the GKAchievement objects that fail to register to an array and then resend them when you get back connectivity. You should also consider committing that array to persistent storage, in case your app terminates before it has a chance to send those achievements. Try this in your completion handler:
// Load or create array of leftover achievements
if (achievementsToReport == nil) {
achievementsToReport = [[NSKeyedUnarchiver unarchiveObjectWithFile:pathForFile(kAchievementsToReportFilename)] retain];
if (achievementsToReport == nil) {
achievementsToReport = [[NSMutableArray array] retain];
}
}
#synchronized(achievementsToReport) {
if(error == nil)
{
// Achievement reporting succeded
// Resend any leftover achievements
BOOL leftoverAchievementReported = NO;
while ([achievementsToReport count] != 0) {
[self resendAchievement:[achievementsToReport lastObject]];
[achievementsToReport removeLastObject];
leftoverAchievementReported = YES;
}
// Commit leftover achievements to persistent storage
if (leftoverAchievementReported == YES) {
[NSKeyedArchiver archiveRootObject:achievementsToReport toFile:pathForFile(kAchievementsToReportFilename)];
}
}
else
{
// Achievement reporting failed
[achievementsToReport addObject:theAchievement];
[NSKeyedArchiver archiveRootObject:achievementsToReport toFile:pathForFile(kAchievementsToReportFilename)];
}
}
Hope this helps.
I see that the two answers here focus on the specific mechanisms for archiving the undelivered achievement messages. For a higher-level description of the overall approach, you can see my answer to this related question: Robust Game Center Achievement code
Achievements (and all of the gamecenter stuff like leaderboard updates) conform to NSCoding. You can store them if you get an error submitting them and submit them later. This is what apple recommends in their docs.
Your application must handle errors when it fails to report progress to Game Center. For example, the device may not have had a network when you attempted to report the progress. The proper way for your application to handle network errors is to retain the achievement object (possibly adding it to an array). Then, your application needs to periodically attempt to report the progress until it is successfully reported. The GKAchievement class supports the NSCoding protocol to allow your application to archive an achievement object when it moves into the background.
from: http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/GameKit_Guide/Achievements/Achievements.html#//apple_ref/doc/uid/TP40008304-CH7-SW13
// Submit an achievement to the server and store if submission fails
- (void)submitAchievement:(GKAchievement *)achievement
{
if (achievement)
{
// Submit the achievement.
[achievement reportAchievementWithCompletionHandler: ^(NSError *error)
{
if (error)
{
// Store achievement to be submitted at a later time.
[self storeAchievement:achievement];
}
else
{
NSLog(#"Achievement %# Submitted..",achievement);
if ([storedAchievements objectForKey:achievement.identifier]) {
// Achievement is reported, remove from store.
[storedAchievements removeObjectForKey:achievement.identifier];
}
[self resubmitStoredAchievements];
}
}];
}
}
In case anyone stumbles upon this question in the future, Apple now has sample code for submitting achievements that includes a way to archive achievements that failed to submit (due to no network connection, etc). You'll find it in the Game Center programming guide.
I have an app that streams music using AudioStreamer class by Matt Gallagher. This works fine as a background process except I want to be able to skip to the next song once the stream is finished. Unfortunately this part doesn't work.
Initially I had a timer that was monitoring the stream but realized that when the app backgrounds this timer no longer runs. So I tried adding a delegate callback in the packet read function:
void ASReadStreamCallBack(CFReadStreamRef aStream, CFStreamEventType eventType, void* inClientInfo)
{
AudioStreamer* streamer = (AudioStreamer *)inClientInfo;
double percent = [streamer progress]/[streamer duration];
if(percent>=0.98 || (percent>=0.95 && [streamer isIdle])){
if([streamer.delegate respondsToSelector:#selector(didFinishPlayingStream:)] ){
[streamer.delegate didFinishPlayingStream:streamer];
streamer.delegate = nil;
}
}
[streamer handleReadFromStream:aStream eventType:eventType];
}
This works fine when the app is in the foreground but no longer works when the app is backgrounding. The delegate method basically sends a request to get the stream URL for the next song, then once it has it creates a new AudioStreamer class
While the app is in background you can implemente the delegate to handle the different remote control states.
- (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {
switch (receivedEvent.subtype) {
case UIEventSubtypeRemoteControlTogglePlayPause:
if (player.isPlaying) {
[player pause];
} else {
[player start];
}
break;
case UIEventSubtypeRemoteControlPreviousTrack:
break;
case UIEventSubtypeRemoteControlNextTrack:
[self skipSong:nil];
break;
default:
break;
} }
Something like this works for me.
I've uploaded my AudioPlayer/streamer class inspired in part by Matt Gallagher's AudioStreamer to
https://code.google.com/p/audjustable.
One of the cooler features is its support for gapless playback. This means the AudioQueue is never closed between gaps; keeping iOS from suspending your app.
You can implement AudioPlayerDelegate:didFinishBufferingSourceWithQueueItemId and AudioPlayerDelegate:didFinishPlayingQueueItemId to queue up the next track by calling AudioPlayer:queueDataSource.
Let me know if you need help using it.
I am developing a game on Iphone using cocos2d. I have a CClayer containing 20 CCSprite. I am playing a sound and I would like to disable the touch events on all the CCSprite or on the entire layer while the sound is playing. I looked at the property of CCLayer called isTouchEnabled but the behavior doesn't propagate to the children (all the CCSprite). Unless it is not documented, there seems to be no equivalent property for CCsprite. Does anybody know an easy way to this?
Thanks
I ma using below method to disable touch on CCMenu items on a layer below on a view may be help you out.
Call the below method and disable all sub Menu or subNode.
[self MenuStatus:NO Node:self]; // to disable
method is:
-(void)MenuStatus:(BOOL)_enable Node:(id)_node
{
for (id result in ((CCNode *)_node).children)
{
if ([result isKindOfClass:[CCMenu class]])
{
for (id result1 in ((CCMenu *)result).children)
{
if ([result1 isKindOfClass:[CCMenuItem class]])
{
((CCMenuItem *)result1).isEnabled = _enable;
}
}
}
else
[self MenuStatus:_enable Node:result];
}
}
[self MenuStatus:YES Node:self]; // to enable**
A member of another forum posted this solution
So all your sprites normally receive touch events? If you know when the sound is playing, you just could have them check that and ignore the touch if the sound is playing. For example, if your sprites implement the CCTargetedTouchDelegate protocol, you could do something like:
- (BOOL)ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event {
if (soundIsPlaying) {
return NO; // i.e., the sprite is currently uninterested in the touch
}
// Other checks and behaviour here.
return YES;
}
I am working on an audio recorder application, it is working perfectly fine.
But I'm stuck with the problem of interruption. When a call comes,
- (void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder
then this method is called and the recording is paused.
And if the user rejects the call:
- (void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder
Then here I want to resume the recording from the point where it was interrupted.
But when I call the record method again, the recording starts with a new file.
The problem is solved!
I have re-written the recording code to avoid this problem. I used AudioQueues, basically the backend code is the same of SpeakHere application with some minor changes. Provided two more apis in it:
-(void)resume
{
AudioQueueStart(queueObject, NULL);
}
-(void)pause
{
AudioQueuePause(queueObject);
}
In AudioRecorder class. The basic purpose being to avoid the recording setup which is done in record method.
Setup the interrupt callback and then use this pause and resume methods appropriately in the callback. Also take care to set active the audio session based on whether your application needs it or not.
Hope this helps someone out there.
EDIT:
Audio interrupt listener callback:
void interruptionListenerCallback (void *inUserData, UInt32 interruptionState)
{
if (interruptionState == kAudioSessionBeginInterruption)
{
[self.audioRecorder pause];
}
else if (interruptionState == kAudioSessionEndInterruption)
{
// if the interruption was removed, and the app had been recording, resume recording
[self.audioRecorder resume];
}
}
Listening for audio interruption:
AudioSessionInitialize(NULL, NULL, interruptionListenerCallback, self);
The OS is probably stopping the AVAudioRecorder during the interruption. You can either present the two or more files as a single recording to the user, or you can use AudioQueue to write your code for handling interruptions (as well as saving audio data to a file).
I tried to do this a while ago and came to the conclusion that it couldn't be done properly; the operating system itself won't let you do it. Maybe that's changed in 3.1.2, but when I tried in 3.0 it just wouldn't work.
To handle the interruption, you have to use AVAudioSessionDelegate methodes instead of AVAudioRecorderDelegate methods.This is the code sample for handling interruption:
/*=================================================================
Interruption Handling Method during Recording
==================================================================*/
- (void) beginInterruption
{
if(self.recorder)
{
//Method to handle UI
}
}
- (void) endInterruption
{
NSError *err = noErr;
[[AVAudioSession sharedInstance] setActive: YES error: &err];
if(err != noErr)
{
NSLog([err description]);
}
//method to handle UI
}
First method automatically deactivate the audio session.So,in second method,you have to reactivate the audio session.First method will pause the recording and when interruption ends you can resume with second method.I have tried this on version 3.0 and upper version.