Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
Classes/SessionManager.m
/* |
File: SessionManager.m |
Abstract: Manages the GKSession and GKVoiceChatService. While the game is |
running, it transfers game packets to and from the game and the peer. |
Version: 1.0 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2010 Apple Inc. All Rights Reserved. |
*/ |
#import <AudioToolbox/AudioToolbox.h> |
#import "SessionManager.h" |
#define SESSION_ID @"gkrocket" |
@implementation SessionManager |
@synthesize currentConfPeerID; |
@synthesize peerList; |
@synthesize lobbyDelegate; |
@synthesize gameDelegate; |
#pragma mark - |
#pragma mark NSObject Methods |
- (id)init |
{ |
if (self = [super init]) { |
// Peers need to have the same sessionID set on their GKSession to see each other. |
sessionID = SESSION_ID; |
peerList = [[NSMutableArray alloc] init]; |
// Set up starting/stopping session on application hiding/terminating |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(willTerminate:) |
name:UIApplicationWillTerminateNotification |
object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(willTerminate:) |
name:UIApplicationWillResignActiveNotification |
object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(willResume:) |
name:UIApplicationDidBecomeActiveNotification |
object:nil]; |
[self setupVoice]; |
} |
return self; |
} |
- (void)dealloc |
{ |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
[GKVoiceChatService defaultVoiceChatService].client = nil; |
if (myGKSession) [self destroySession]; |
myGKSession = nil; |
sessionID = nil; |
[peerList release]; |
[super dealloc]; |
} |
#pragma mark - |
#pragma mark Session logic |
// Creates a GKSession and advertises availability to Peers |
- (void) setupSession |
{ |
// GKSession will default to using the device name as the display name |
myGKSession = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModePeer]; |
myGKSession.delegate = self; |
[myGKSession setDataReceiveHandler:self withContext:nil]; |
myGKSession.available = YES; |
sessionState = ConnectionStateDisconnected; |
[lobbyDelegate peerListDidChange:self]; |
} |
// Initiates a GKSession connection to a selected peer. |
-(void) connect:(NSString *) peerID |
{ |
[myGKSession connectToPeer:peerID withTimeout:10.0]; |
currentConfPeerID = [peerID retain]; |
sessionState = ConnectionStateConnecting; |
} |
// Called from GameLobbyController if the user accepts the invitation alertView |
-(BOOL) didAcceptInvitation |
{ |
NSError *error = nil; |
if (![myGKSession acceptConnectionFromPeer:currentConfPeerID error:&error]) { |
NSLog(@"%@",[error localizedDescription]); |
} |
return (gameDelegate == nil); |
} |
// Called from GameLobbyController if the user declines the invitation alertView |
-(void) didDeclineInvitation |
{ |
// Deny the peer. |
if (sessionState != ConnectionStateDisconnected) { |
[myGKSession denyConnectionFromPeer:currentConfPeerID]; |
[currentConfPeerID release]; |
currentConfPeerID = nil; |
sessionState = ConnectionStateDisconnected; |
} |
// Go back to the lobby if the game screen is open. |
[gameDelegate willDisconnect:self]; |
} |
-(BOOL) comparePeerID:(NSString*)peerID |
{ |
return [peerID compare:myGKSession.peerID] == NSOrderedAscending; |
} |
// Called to check if the session is ready to start a voice chat. |
-(BOOL) isReadyToStart |
{ |
return sessionState == ConnectionStateConnected; |
} |
// When the voice chat starts, tell the game it can begin. |
-(void) voiceChatDidStart |
{ |
[gameDelegate session:self didConnectAsInitiator:![self comparePeerID:currentConfPeerID]]; |
} |
// Called by RocketController and VoiceManager to send data to the peer |
-(void) sendPacket:(NSData*)data ofType:(PacketType)type |
{ |
NSMutableData * newPacket = [NSMutableData dataWithCapacity:([data length]+sizeof(uint32_t))]; |
// Both game and voice data is prefixed with the PacketType so the peer knows where to send it. |
uint32_t swappedType = CFSwapInt32HostToBig((uint32_t)type); |
[newPacket appendBytes:&swappedType length:sizeof(uint32_t)]; |
[newPacket appendData:data]; |
NSError *error; |
if (currentConfPeerID) { |
if (![myGKSession sendData:newPacket toPeers:[NSArray arrayWithObject:currentConfPeerID] withDataMode:GKSendDataReliable error:&error]) { |
NSLog(@"%@",[error localizedDescription]); |
} |
} |
} |
// Clear the connection states in the event of leaving a call or error. |
-(void) disconnectCurrentCall |
{ |
[gameDelegate willDisconnect:self]; |
if (sessionState != ConnectionStateDisconnected) { |
if(sessionState == ConnectionStateConnected) { |
[[GKVoiceChatService defaultVoiceChatService] stopVoiceChatWithParticipantID:currentConfPeerID]; |
} |
// Don't leave a peer hangin' |
if (sessionState == ConnectionStateConnecting) { |
[myGKSession cancelConnectToPeer:currentConfPeerID]; |
} |
[myGKSession disconnectFromAllPeers]; |
myGKSession.available = YES; |
sessionState = ConnectionStateDisconnected; |
[currentConfPeerID release]; |
currentConfPeerID = nil; |
} |
} |
// Application is exiting or becoming inactive, end the session. |
- (void)destroySession |
{ |
[self disconnectCurrentCall]; |
myGKSession.delegate = nil; |
[myGKSession setDataReceiveHandler:nil withContext:nil]; |
[myGKSession release]; |
[peerList removeAllObjects]; |
} |
// Called when notified the application is exiting or becoming inactive. |
- (void)willTerminate:(NSNotification *)notification |
{ |
[self destroySession]; |
} |
// Called after the app comes back from being hidden by something like a phone call. |
- (void)willResume:(NSNotification *)notification |
{ |
[self setupSession]; |
} |
#pragma mark - |
#pragma mark GKSessionDelegate Methods and Helpers |
// Received an invitation. If we aren't already connected to someone, open the invitation dialog. |
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID |
{ |
if (sessionState == ConnectionStateDisconnected) { |
currentConfPeerID = [peerID retain]; |
sessionState = ConnectionStateConnecting; |
[lobbyDelegate didReceiveInvitation:self fromPeer:[myGKSession displayNameForPeer:peerID]]; |
} else { |
[myGKSession denyConnectionFromPeer:peerID]; |
} |
} |
// Unable to connect to a session with the peer, due to rejection or exiting the app |
- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error |
{ |
NSLog(@"%@",[error localizedDescription]); |
if (sessionState != ConnectionStateDisconnected) { |
[lobbyDelegate invitationDidFail:self fromPeer:[myGKSession displayNameForPeer:peerID]]; |
// Make self available for a new connection. |
[currentConfPeerID release]; |
currentConfPeerID = nil; |
myGKSession.available = YES; |
sessionState = ConnectionStateDisconnected; |
} |
} |
// The running session ended, potentially due to network failure. |
- (void)session:(GKSession *)session didFailWithError:(NSError*)error |
{ |
NSLog(@"%@",[error localizedDescription]); |
[self disconnectCurrentCall]; |
} |
// React to some activity from other peers on the network. |
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state |
{ |
switch (state) { |
case GKPeerStateAvailable: |
// A peer became available by starting app, exiting settings, or ending a call. |
if (![peerList containsObject:peerID]) { |
[peerList addObject:peerID]; |
} |
[lobbyDelegate peerListDidChange:self]; |
break; |
case GKPeerStateUnavailable: |
// Peer unavailable due to joining a call, leaving app, or entering settings. |
[peerList removeObject:peerID]; |
[lobbyDelegate peerListDidChange:self]; |
break; |
case GKPeerStateConnected: |
// Connection was accepted, set up the voice chat. |
currentConfPeerID = [peerID retain]; |
myGKSession.available = NO; |
[gameDelegate voiceChatWillStart:self]; |
sessionState = ConnectionStateConnected; |
// Compare the IDs to decide which device will invite the other to a voice chat. |
if([self comparePeerID:peerID]) { |
NSError *error; |
if (![[GKVoiceChatService defaultVoiceChatService] startVoiceChatWithParticipantID:peerID error:&error]) { |
NSLog(@"%@",[error localizedDescription]); |
} |
} |
break; |
case GKPeerStateDisconnected: |
// The call ended either manually or due to failure somewhere. |
[self disconnectCurrentCall]; |
[peerList removeObject:peerID]; |
[lobbyDelegate peerListDidChange:self]; |
break; |
case GKPeerStateConnecting: |
// Peer is attempting to connect to the session. |
break; |
default: |
break; |
} |
} |
// Called when voice or game data is received over the network from the peer |
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession *)session context:(void *)context |
{ |
PacketType header; |
uint32_t swappedHeader; |
if ([data length] >= sizeof(uint32_t)) { |
[data getBytes:&swappedHeader length:sizeof(uint32_t)]; |
header = (PacketType)CFSwapInt32BigToHost(swappedHeader); |
NSRange payloadRange = {sizeof(uint32_t), [data length]-sizeof(uint32_t)}; |
NSData* payload = [data subdataWithRange:payloadRange]; |
// Check the header to see if this is a voice or a game packet |
if (header == PacketTypeVoice) { |
[[GKVoiceChatService defaultVoiceChatService] receivedData:payload fromParticipantID:peer]; |
} else { |
[gameDelegate session:self didReceivePacket:payload ofType:header]; |
} |
} |
} |
- (NSString *) displayNameForPeer:(NSString *)peerID |
{ |
return [myGKSession displayNameForPeer:peerID]; |
} |
@end |
#pragma mark - |
#pragma mark AudioSession Setup |
// Sets up the audio session to use the speakerphone |
void EnableSpeakerPhone () |
{ |
UInt32 dataSize = sizeof(CFStringRef); |
CFStringRef currentRoute = NULL; |
OSStatus result = noErr; |
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &dataSize, ¤tRoute); |
// Set the category to use the speakers and microphone. |
UInt32 sessionCategory = kAudioSessionCategory_PlayAndRecord; |
result = AudioSessionSetProperty ( |
kAudioSessionProperty_AudioCategory, |
sizeof (sessionCategory), |
&sessionCategory |
); |
assert(result == kAudioSessionNoError); |
Float64 sampleRate = 44100.0; |
dataSize = sizeof(sampleRate); |
result = AudioSessionSetProperty ( |
kAudioSessionProperty_PreferredHardwareSampleRate, |
dataSize, |
&sampleRate |
); |
assert(result == kAudioSessionNoError); |
// Default to speakerphone if a headset isn't plugged in. |
UInt32 route = kAudioSessionOverrideAudioRoute_Speaker; |
dataSize = sizeof(route); |
result = AudioSessionSetProperty ( |
// This requires iPhone OS 3.1 |
kAudioSessionProperty_OverrideCategoryDefaultToSpeaker, |
dataSize, |
&route |
); |
assert(result == kAudioSessionNoError); |
AudioSessionSetActive(YES); |
} |
// Called when audio is interrupted by a call or alert. Since we are using |
// UIApplicationWillResignActiveNotification to deal with ending the game, |
// this just resumes speakerphone after an audio interruption. |
void InterruptionListenerCallback (void *inUserData, UInt32 interruptionState) |
{ |
if (interruptionState == kAudioSessionEndInterruption) { |
EnableSpeakerPhone(); |
} |
} |
@implementation SessionManager (VoiceManager) |
- (void)setupVoice |
{ |
// Set up audio to default to speakerphone but use the headset if one is plugged in. |
AudioSessionInitialize(NULL, NULL, InterruptionListenerCallback, self); |
EnableSpeakerPhone(); |
[GKVoiceChatService defaultVoiceChatService].client = self; |
[[GKVoiceChatService defaultVoiceChatService] setInputMeteringEnabled:YES]; |
[[GKVoiceChatService defaultVoiceChatService] setOutputMeteringEnabled:YES]; |
} |
// GKVoiceChatService Client Method. For convenience, we are using the same ID for the GKSession and GKVoiceChatService. |
- (NSString *)participantID |
{ |
return myGKSession.peerID; |
} |
// GKVoiceChatService Client Method. Sends voice data over the GKSession to the peer. |
- (void)voiceChatService:(GKVoiceChatService *)voiceChatService sendData:(NSData *)data toParticipantID:(NSString *)participantID |
{ |
[self sendPacket:data ofType:PacketTypeVoice]; |
} |
// GKVoiceChatService Client Method. Received a voice chat invitation from the connected peer. |
- (void)voiceChatService:(GKVoiceChatService *)voiceChatService didReceiveInvitationFromParticipantID:(NSString *)participantID callID:(NSInteger)callID |
{ |
if ([self isReadyToStart]) { |
NSError *error; |
if (![[GKVoiceChatService defaultVoiceChatService] acceptCallID:callID error:&error]) { |
NSLog(@"%@",[error localizedDescription]); |
[self disconnectCurrentCall]; |
} |
} else { |
[[GKVoiceChatService defaultVoiceChatService] denyCallID:callID]; |
[self disconnectCurrentCall]; |
} |
} |
// GKVoiceChatService Client Method. In the event something weird happened and the voice chat failed, disconnect. |
- (void)voiceChatService:(GKVoiceChatService *)voiceChatService didNotStartWithParticipantID:(NSString *)participantID error:(NSError *)error |
{ |
NSLog(@"%@",[error localizedDescription]); |
[self disconnectCurrentCall]; |
} |
// GKVoiceChatService Client Method. The voice chat with the connected peer successfully started. |
- (void)voiceChatService:(GKVoiceChatService *)voiceChatService didStartWithParticipantID:(NSString *)participantID |
{ |
// Since the session and voice chat are up, we can tell the game to start. |
[self voiceChatDidStart]; |
} |
@end |
Copyright © 2011 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2011-03-15