Why isn't GameCenter waiting long enough to fill all the player slots? - iphone

So I'm working with multiplayer for the first time and I'm confused about the whole minplayer/maxplayer options. When I set minplayer=2 and maxplayer=4 and test the code, it connects 2 players just fine, but jumps directly into the game scene without waiting for players 3-4. How do I keep the code from progressing to the main game scene before all the slots are filled? The code works fine if I set minPlayers=maxPlayers. I know match.expectedPlayerCount==0 is supposed to fire once minPlayers is satisfied, but it isn't waiting at all for additional players to join. What am I missing here?
GKMatchRequest * matchRequest = [[[GKMatchRequest alloc] init] autorelease];
matchRequest.minPlayers = 2;
matchRequest.maxPlayers = 4;
gameCenterManager.matchController = [[GKMatchmakerViewController alloc] initWithMatchRequest:matchRequest];
gameCenterManager.matchController.matchmakerDelegate = self;
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[delegate.viewController presentViewController:gameCenterManager.matchController animated:YES completion:nil];
Find Match Code
-(void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match
{
TXGameCenterManager *gameCenterManager = [TXGameCenterManager sharedTXGameCenterManager];
gameCenterManager.multiplayerMatch = match;
// The delegate of the match is HelloWorldLayer
gameCenterManager.multiplayerMatch.delegate = self;
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[delegate.viewController dismissModalViewControllerAnimated:NO];
if( match.expectedPlayerCount==0 )
{
// Launching the game without waiting for connection change messages
NSLog(#"Begin game without waiting for match connection change messages");
// Determine the host, local or remote
NSArray * playerIds = match.playerIDs;
NSLog(#"Number of players: %d", [playerIds count]);
NSLog(#"ID of player: %#", [playerIds lastObject]);
NSLog(#"I got the player ids");
[GKPlayer loadPlayersForIdentifiers:playerIds withCompletionHandler:^(NSArray *players, NSError * error)
{
//bunch of code that gets player aliases and set host player
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
ChangeState code
-(void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
{
NSArray * playerIds = [NSArray arrayWithObject:playerID];
switch (state)
{
case GKPlayerStateConnected:
// handle a new player connection.
NSLog(#"Player connected!");
[GKPlayer loadPlayersForIdentifiers:playerIds withCompletionHandler:^(NSArray *players, NSError * error)
{
//bunch of code that gets player aliases and set host player
if (match.expectedPlayerCount==0)
{
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
}];
break;
case GKPlayerStateDisconnected:
// a player just disconnected.
NSLog(#"Player disconnected!");
break;
}
-(void)StartMultiplayerGame
{
[[CCDirector sharedDirector] replaceScene:[HelloWorldLayer node]];
}

if (match.expectedPlayerCount==0)
{
//start match
[self schedule: #selector(StartMultiplayerGame) interval:5.];
}
You said it yourself, if minPlayers players have joined (which is 2 in your case) then expectedPlayerCount is 0. So as soon as 2 players have joined, you're starting the game. This is not Game Center's fault.
You could wait for a longer amount of time once expectedPlayerCount is 0 to allow other players to join.
Your code also does not consider that a second player might join, then leave again. So in that case you would be starting the game with just one player.

Related

SpriteKit App Using Excessive CPU

I wrote a SpriteKit app last year targeting 10.10 (Yosemite). Everything ran fine, but when I upgraded to El Capitan this year it freezes in one particular spot. It's a tough problem to diagnose because there is a lot of code so I'll try to be as descriptive as possible. I've also created a YOUTUBE screen recording of the issue.
App's Purpose
The app is basically a leaderboard that I created for a tournament at the school that I teach at. When the app launches, it goes to the LeaderboardScene scene and displays the leaderboard.
The app stays in this scene for the rest of the time. The sword that says "battle" is a button. When it is pressed it creates an overlay and shows the two students that will be facing each other in video form (SKVideoNode).
The videos play continuously and the user of the app eventually clicks on whichever student wins that match and then the overlay is removed from the scene and the app shows the leaderboard once again.
Potential Reasons For High CPU
Playing video: Normally the overlay shows video, but I also created an option where still images are loaded instead of video just in case I had a problem. Whether I load images or video, the CPU usage is super high.
Here's some of the code that is most likely causing this issue:
LeaderboardScene.m
//when the sword button is pressed it switches to the LB_SHOW_VERSUS_SCREEN state
-(void) update:(NSTimeInterval)currentTime {
switch (_leaderboardState) {
...
case LB_SHOW_VERSUS_SCREEN: { //Case for "Versus Screen" overlay
[self showVersusScreen];
break;
}
case LB_CHOOSE_WINNER: {
break;
}
default:
break;
}
}
...
//sets up the video overlay
-(void) showVersusScreen {
//doesn't allow the matchup screen to pop up until the producer FLASHING actions are complete
if ([_right hasActions] == NO) {
[self addChild:_matchup]; //_matchup is an object from the Matchup.m class
NSArray *producers = #[_left, _right];
[_matchup createRound:_round WithProducers:producers VideoType:YES]; //creates the matchup with VIDEO
//[_matchup createRound:_round WithProducers:producers VideoType:NO]; //creates the matchup without VIDEO
_leaderboardState = LB_CHOOSE_WINNER;
}
}
Matchup.m
//more setting up of the overlay
-(void) createRound:(NSString*)round WithProducers:(NSArray*)producers VideoType:(bool)isVideoType {
SKAction *wait = [SKAction waitForDuration:1.25];
[self loadSoundsWithProducers:producers];
[self runAction:wait completion:^{ //resets the overlay
_isVideoType = isVideoType;
[self removeAllChildren];
[self initBackground];
[self initHighlightNode];
[self initOutline];
[self initText:round];
if (_isVideoType)
[self initVersusVideoWithProducers:producers]; //this is selected
else
[self initVersusImagesWithProducers:producers];
[self animationSequence];
_currentSoundIndex = 0;
[self playAudio];
}];
}
...
//creates a VersusSprite object which represents each of the students
-(void) initVersusVideoWithProducers:(NSArray*)producers {
Producer *left = (Producer*)[producers objectAtIndex:0];
Producer *right = (Producer*)[producers objectAtIndex:1];
_leftProducer = [[VersusSprite alloc] initWithProducerVideo:left.name LeftSide:YES];
_leftProducer.name = left.name;
_leftProducer.zPosition = 5;
_leftProducer.position = CGPointMake(-_SCREEN_WIDTH/2, _SCREEN_HEIGHT/3);
[self addChild:_leftProducer];
_rightProducer = [[VersusSprite alloc] initWithProducerVideo:right.name LeftSide:NO];
_rightProducer.name = right.name;
_rightProducer.zPosition = 5;
_rightProducer.xScale = -1;
_rightProducer.position = CGPointMake(_SCREEN_WIDTH + _SCREEN_WIDTH/2, _SCREEN_HEIGHT/3);
[self addChild:_rightProducer];
}
VersusSprite.m
-(instancetype) initWithProducerVideo:(NSString*)fileName LeftSide:(bool)isLeftSide {
if (self = [super init]) {
_isVideo = YES;
_isLeftSide = isLeftSide;
self.name = fileName;
[self initVideoWithFileName:fileName]; //creates videos
[self addProducerLabel];
}
return self;
}
...
//creates the videos for the VersusSprite
-(void) initVideoWithFileName:(NSString*)fileName {
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES);
NSString *desktopPath = [paths objectAtIndex:0];
NSString *resourcePath = [NSString stringWithFormat:#"%#/vs", desktopPath];
NSString *videoPath = [NSString stringWithFormat:#"%#/%#.mp4", resourcePath, fileName];
NSURL *fileURL = [NSURL fileURLWithPath:videoPath];
AVPlayer *avPlayer = [[AVPlayer alloc] initWithURL:fileURL];
_vid = [SKVideoNode videoNodeWithAVPlayer:avPlayer];
//[_vid setScale:1];
[self addChild:_vid];
[_vid play];
avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[avPlayer currentItem]];
}
//used to get the videos to loop
- (void)playerItemDidReachEnd:(NSNotification *)notification {
AVPlayerItem *p = [notification object];
[p seekToTime:kCMTimeZero];
}
UPDATE
The issue has been identified and is very specific to my project, so it probably won't help anyone else unfortunately. When clicking on the "sword" icon that says "Battle", the scene gets blurred and then the overlay is put on top of it. The blurring occurs on a background thread as you'll see below:
[self runAction:[SKAction waitForDuration:1.5] completion:^{
[self blurSceneProgressivelyToValue:15 WithDuration:1.25];
}];
I'll have to handle the blur in another way or just remove it altogether.

Game Center Matchmaking GKTurnBasedMatch has significant lag (~1 min)

I'm implementing a turn-based game with multiplayer mode through gamecenter. I have 2 devices (1 ipad, 1 iphone) to test in sandbox mode which were working fine but lately it has started to struggle in auto matchmaking process. After I send the first turn from one user, the other device doesn't immediately recognize that game but opens up its own fresh game. Before it was able to immediately spot the game started in the other device and matchmaking was fairly straightforward. And I don't remember changing any parts relevant to matchmaking (NSCoding, GKTurnBasedEventHandler, GKTurnBasedMatchmakerViewControllerDelegate delegate methods etc).
Now I send the first turn from one device and need to wait around 1 min so the other device can successfully connect to that game. After connection occurs endTurnWithMatchData calls work without any problems, it can send and receive data within 1-2 secs. But it won't be a good UX if users start a fresh game and had to wait 1 min so another user can connect to his game. Has anyone been experiencing significant lag in auto matchmaking process? I didn't implement invitations yet, so I cannot check it. The matchdata I archive with NSKeyedArchiver seemed quite big, 3396 bytes, even for a fresh game with almost no data. And here are relevant parts of my code:
GameOptionsViewController:
- (void)turnBasedMatchmakerViewControllerWasCancelled:(GKTurnBasedMatchmakerViewController *)viewController
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFailWithError:(NSError *)error
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
[self dismissViewControllerAnimated:NO completion:nil];
self.gcMatch = match;
[self performSegueWithIdentifier:#"GameMultiplayer" sender:self];
}
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"GameMultiplayer"])
{
GameViewController *GameVC = (GameViewController *)segue.destinationViewController;
[GameVC setGameMode:GAMEMODE_MULTIPLAYER_SAMEDEVICE];
//Multiplayer game it is
if(self.gcMatch != nil)
{
[GameVC setGameMode:GAMEMODE_MULTIPLAYER_GAMECENTER];
GameVC.gcMatchDelegate = self;
GameVC.gcMatch = self.gcMatch;
NSLog(#"Game OVC Segue: Match ID | %#", self.gcMatch.matchID);
}
}
else
{
...
}
}
GameViewController:
//This method is called according to user actions
//It's the only method I use to send data to other participant
-(void) sendCurrentGameDataWithNewTurn:(BOOL) newTurn
{
NSLog(#"Sending game data current participant : %#", gcMatch.currentParticipant.playerID);
//Update match data if it is corrupted anyhow
if (gcMatch.currentParticipant == nil)
{
[GKTurnBasedMatch loadMatchWithID:gcMatch.matchID withCompletionHandler:^(GKTurnBasedMatch *match, NSError *error)
{
if (error != nil)
{
NSLog(#"Error :%#", error);
return ;
}
[self sendCurrentGameDataWithNewTurn:newTurn];
}];
}
else
{
NSData *matchData = [NSKeyedArchiver archivedDataWithRootObject:game];
//Game advances to new player, buttons are disabled
if(newTurn)
{
NSLog(#"SENDING NEW TURN");
NSUInteger currentIndex = [gcMatch.participants
indexOfObject:gcMatch.currentParticipant];
GKTurnBasedParticipant *nextParticipant;
nextParticipant = [gcMatch.participants objectAtIndex:
((currentIndex + 1) % [gcMatch.participants count])];
[gcMatch endTurnWithNextParticipants:[NSArray arrayWithObject:nextParticipant] turnTimeout:GC_TURN_TIMEOUT matchData:matchData completionHandler:^(NSError *error) {
NSLog(#"Sent");
if (error) {
NSLog(#"SNT - %#", error);
}
}];
}
else
{
NSLog(#"ONLY UPDATING DATA");
[gcMatch saveCurrentTurnWithMatchData:matchData completionHandler:^(NSError *error) {
NSLog(#"Sent");
if (error) {
NSLog(#"OUD - %#", error);
}
}];
}
}
}
-(void) updateGameDataWithGCMatch
{
//Update whole game data
self.game = [NSKeyedUnarchiver unarchiveObjectWithData:self.gcMatch.matchData];
//Update game ui
...
}
-(void) handleTurnEventForMatch:(GKTurnBasedMatch *)match didBecomeActive:(BOOL)didBecomeActive
{
//Check if I got data for the currently active match that options vc forwarded me here, if not do some debug print and return
if(![self.gcMatch.matchID isEqual:match.matchID])
{
//For debugging reasons I skip if i get info for any previous match (other player quit etc)
NSLog(#"GCMatch matchID: %# match matchID: %#",self.gcMatch.matchID,match.matchID);
return;
}
NSLog(#"Turn event handle");
self.gcMatch = match;
if([match.currentParticipant.playerID isEqualToString: [GKLocalPlayer localPlayer].playerID ])
{
//Disable field buttons
[self setFieldButtonsEnabled:TRUE];
[self turnChangeAnimationFromLeftToRight:FALSE];
}
[self updateGameDataWithGCMatch];
}
As for your question:
I myself tempered with matchmaking over Game Center quite a bit and also experienced lags quite frequently, which have been proven to not have been caused by my site but by apples game center servers.
As for additional guidance:
As far as I can see your current approach to matchmaking on a device is:
look if there is a match I can connect to --> If YES request gamedata and connect to the match ELSE start your own match and broadcast the matchdata
From my experience it is better practice to start with matchrequestbroadcasts, wait until you find a second player, define the server device (e.g. by lower checksum of game-center names) and then start the game on that device.

AVCaptureSession - Stop Running - take a long long time

I use ZXing for an app, this is mainly the same code than the ZXing original code except that I allow to scan several time in a row (ie., the ZXingWidgetController is not necesseraly dismissed as soon as something is detected).
I experience a long long freeze (sometimes it never ends) when I press the dismiss button that call
- (void)cancelled {
// if (!self.isStatusBarHidden) {
// [[UIApplication sharedApplication] setStatusBarHidden:NO];
// }
[self stopCapture];
wasCancelled = YES;
if (delegate != nil) {
[delegate zxingControllerDidCancel:self];
}
}
with
- (void)stopCapture {
decoding = NO;
#if HAS_AVFF
if([captureSession isRunning])[captureSession stopRunning];
AVCaptureInput* input = [captureSession.inputs objectAtIndex:0];
[captureSession removeInput:input];
AVCaptureVideoDataOutput* output = (AVCaptureVideoDataOutput*)[captureSession.outputs objectAtIndex:0];
[captureSession removeOutput:output];
[self.prevLayer removeFromSuperlayer];
/*
// heebee jeebees here ... is iOS still writing into the layer?
if (self.prevLayer) {
layer.session = nil;
AVCaptureVideoPreviewLayer* layer = prevLayer;
[self.prevLayer retain];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 12000000000), dispatch_get_main_queue(), ^{
[layer release];
});
}
*/
self.prevLayer = nil;
self.captureSession = nil;
#endif
}
(please notice that the dismissModalViewController that remove the view is within the delegate method)
I experience the freeze only while dismissing only if I made several scans in a row, and only with an iPhone 4 (no freeze with a 4S)
Any idea ?
Cheers
Rom
According to the AV Cam View Controller Example calling startRunning or stopRunning does not return until the session completes the requested operation. Since you are sending these messages to the session on the main thread, it freezes all the UI until the requested operation completes. What I would recommend is that you wrap your calls in an Asynchronous dispatch so that the view does not lock-up.
- (void)cancelled
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self stopCapture];
});
//You might want to think about putting the following in another method
//and calling it when the stop capture method finishes
wasCancelled = YES;
if (delegate != nil) {
[delegate zxingControllerDidCancel:self];
}
}

Misunderstanding on Cocos2D: Layers, static variables and Model View Controller. Can those coexist?

sorry if the question is too dull but I have been trying to understand as much as possible from Itterheim's book book's code example and can't figure out how this works.
In my "HelloWorldLayer.m" I create an instance of a class named "MusicLayer" that extends CCLayer The inspiration of this was that in the example named "ShootEmUp" (Chapter 8 of 1) there is an InputLayer. I thus thought that I would create a MusicLayer to deal with the various music files I have in my game and got some problems (will explain at the end of the post). I got most of the music related code code from RecipeCollection02, Ch6_FadingSoundsAndMusic of the Cocos2d Cookbook .
But first I would like to introduce some of my background thoughts that might have lead to this problem with the hope to have some clear reference on this.
I am used to a "model view controller" (MVC wikipedia link) approach as I come from a C++ and Java background and never coded a game before and it does seem to me that the gaming paradigms are a bit different from "the classic" Software Engineering University approach (don't take me wrong, I am sure that in many Universities they don't teach this stuff anymore :)).
The point is that it seems to me that the "classic" Software Engineering approach is lost in those examples (for instance in the ShootEmUp code the instance of InputLayer has a reference of the class GameScene -see below- and at the same time in InputLayer there is a reference to the static shared instance of GameScene -see below-).
//From GameScene
+(id) scene
{
CCScene* scene = [CCScene node];
GameScene* layer = [GameScene node];
[scene addChild:layer z:0 tag:GameSceneLayerTagGame];
InputLayer* inputLayer = [InputLayer node];
[scene addChild:inputLayer z:1 tag:GameSceneLayerTagInput];
return scene;
}
//From InputLayer
-(void) update:(ccTime)delta
{
totalTime += delta;
// Continuous fire
if (fireButton.active && totalTime > nextShotTime)
{
nextShotTime = totalTime + 0.5f;
GameScene* game = [GameScene sharedGameScene];
ShipEntity* ship = [game defaultShip];
//Code missing
}
}
I was told that static instances are things to avoid. Despite this I understand the benefit of having a GameScene staic instance but I still get confused on the biderectional reference (from GameScene to InputLayer and viceversa)..:
Is this common practice in Game programming?
Is there a way to avoid this approach?
I will now paste my code and I admit it, is probably rubbish and there will be probably some obvious error as I am trying to do a very simple thing and I do not manage.
So here we are. That's the MusicLayer class (it contains commented out code as I had previously tried to make it a shared class instance):
//
// MusicLayer.h
// ShootEmUp
//
// Copyright 2012 __MyCompanyName__. All rights reserved.
//
//Enumeration files should be the way those are referred from outside the class..
enum music_files {
PLAYER_STANDARD = 1,
};
enum sound_files{
A = 0,
B = 1,
C = 2,
ABC = 4,
AAC = 5,
ACA = 6,
//And so on and so forth..
};
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "CDXPropertyModifierAction.h"
#interface MusicLayer : CCLayer {
SimpleAudioEngine *sae;
NSMutableDictionary *soundSources;
NSMutableDictionary *musicSources;
}
//In this way you can load once the common sounds on the sharedMusicLayer, have the benefit of keep tracks playing on scene switching as well as being able to load tracks and sounds before each level
//+(MusicLayer*) sharedMusicLayer;
/** API **/
//PLAY - sound undtested
-(void) playBackgroundMusic:(enum music_files) file;
-(void) playSoundFile:(enum sound_files) file;
-(void) loadPopLevel;
/** Private methods **/
//Utilities methods
-(NSString *) _NSStringFromMusicFiles: (enum music_files) file;
-(NSString *) _NSStringFromSoundFiles: (enum sound_files) file;
//LOAD - Meant to be private methods
-(CDLongAudioSource*) _loadMusic:(NSString*)fn;
-(CDSoundSource*) _loadSoundEffect:(NSString*)fn;
//FADE - sound undtested
-(void) _fadeOutPlayingMusic;
-(void) _fadeInMusicFile:(NSString*)fn;
-(void) _playSoundFile:(NSString*)fn;
#end
And here we are with the .m file:
#import "MusicLayer.h"
#implementation MusicLayer
/**
static MusicLayer* instanceOfMusicLayer;
+(MusicLayer*) sharedMusicLayer
{
NSAssert(instanceOfMusicLayer != nil, #"MusicLayer instance not yet initialized!");
return instanceOfMusicLayer;
}**/
-(id) init
{
CCLOG(#"In Init");
if ((self = [super init]))
{
CCLOG(#"Inside Init");
//instanceOfMusicLayer = self;
//Initialize the audio engine
sae = [SimpleAudioEngine sharedEngine];
//Background music is stopped on resign and resumed on become active
[[CDAudioManager sharedManager] setResignBehavior:kAMRBStopPlay autoHandle:YES];
//Initialize source container
soundSources = [[NSMutableDictionary alloc] init];
musicSources = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void) loadPopLevel{
CCLOG(#"In loadPopLevel");
[self _loadSoundEffect:[self _NSStringFromSoundFiles:A]];
[self _loadSoundEffect:[self _NSStringFromSoundFiles:B]];
[self _loadSoundEffect:[self _NSStringFromSoundFiles:C]];
[self _loadSoundEffect:[self _NSStringFromSoundFiles:ACA]];
[self _loadSoundEffect:[self _NSStringFromSoundFiles:ABC]];
[self _loadSoundEffect:[self _NSStringFromSoundFiles:AAC]];
[self _loadMusic:[self _NSStringFromMusicFiles:PLAYER_STANDARD]];
[self _loadMusic:[self _NSStringFromMusicFiles:PLAYER_STANDARD]];
[self _loadMusic:[self _NSStringFromMusicFiles:PLAYER_STANDARD]];
[self _loadMusic:[self _NSStringFromMusicFiles:PLAYER_STANDARD]];
}
/** UTILITIES METHODS **/
//This function is key to define which files we are going to load..
-(NSString *) _NSStringFromMusicFiles: (enum music_files) file{
CCLOG(#"In _NSStringFromMusicFiles");
switch (file) {
case PLAYER_STANDARD:
return #"a.mp3";
default:
NSAssert(0 == 1, #"Invalid argument");
break;
}
}
-(NSString *) _NSStringFromSoundFiles: (enum sound_files) file{
CCLOG(#"In _NSStringFromSoundFiles");
switch (file) {
case A:
return #"shot.caf";
case B:
return #"shot.caf";
case C:
return #"shot.caf";
case AAC:
return #"Wow.caf";
case ABC:
return #"Wow.caf";
case ACA:
return #"Wow.caf";
default:
NSAssert(0 == 1, #"Invalid argument");
break;
}
}
/** PLAY METHODS **/
-(void) _playSoundFile:(NSString*)fn {
CCLOG(#"In _playSoundFile");
//Get sound
CDSoundSource *sound = [soundSources objectForKey:fn];
sound.looping = YES;
//Play sound
if(sound.isPlaying){
[sound stop];
}else{
[sound play];
}
}
-(void) _fadeInMusicFile:(NSString*)fn {
CCLOG(#"In _fadeInMusicFile");
//Stop music if its playing and return
CDLongAudioSource *source = [musicSources objectForKey:fn];
if(source.isPlaying){
[source stop];
return;
}
//Set volume to zero and play
source.volume = 0.0f;
[source play];
//Create fader
CDLongAudioSourceFader* fader = [[CDLongAudioSourceFader alloc] init:source interpolationType:kIT_Linear startVal:source.volume endVal:1.0f];
[fader setStopTargetWhenComplete:NO];
//Create a property modifier action to wrap the fader
CDXPropertyModifierAction* fadeAction = [CDXPropertyModifierAction actionWithDuration:1.5f modifier:fader];
[fader release];//Action will retain
[[CCActionManager sharedManager] addAction:[CCSequence actions:fadeAction, nil] target:source paused:NO];
}
-(void) _fadeOutPlayingMusic {
CCLOG(#"In _fadeOutPlayingMusic");
for(id m in musicSources){
//Release source
CDLongAudioSource *source = [musicSources objectForKey:m];
if(source.isPlaying){
//Create fader
CDLongAudioSourceFader* fader = [[CDLongAudioSourceFader alloc] init:source interpolationType:kIT_Linear startVal:source.volume endVal:0.0f];
[fader setStopTargetWhenComplete:NO];
//Create a property modifier action to wrap the fader
CDXPropertyModifierAction* fadeAction = [CDXPropertyModifierAction actionWithDuration:3.0f modifier:fader];
[fader release];//Action will retain
CCCallFuncN* stopAction = [CCCallFuncN actionWithTarget:source selector:#selector(stop)];
[[CCActionManager sharedManager] addAction:[CCSequence actions:fadeAction, stopAction, nil] target:source paused:NO];
}
}
}
/** LOADING METHODS **/
-(CDLongAudioSource*) _loadMusic:(NSString*)fn {
CCLOG(#"In _loadMusic" );
//Init source
CDLongAudioSource *source = [[CDLongAudioSource alloc] init];
source.backgroundMusic = NO;
[source load:fn];
//Add sound to container
[musicSources setObject:source forKey:fn];
return source;
}
-(CDSoundSource*) _loadSoundEffect:(NSString*)fn {
CCLOG(#"In _loadSoundEffect" );
//Pre-load sound
[sae preloadEffect:fn];
//Init sound
CDSoundSource *sound = [[sae soundSourceForFile:fn] retain];
//Add sound to container
[soundSources setObject:sound forKey:fn];
return sound;
}
/** Public methods **/
//Play music callback
-(void) playBackgroundMusicNumber:(enum music_files) file {
CCLOG(#"In playBackgroundMusic");
switch (file) {
case PLAYER_STANDARD:
[self _fadeOutPlayingMusic];
[self _fadeInMusicFile: [self _NSStringFromMusicFiles:PLAYER_STANDARD]];
break;
default:
break;
}
}
-(void) playSoundFile:(enum sound_files) file {
CCLOG(#"In playSoundFile");
switch (file) {
case A:
[self _playSoundFile:[self _NSStringFromSoundFiles:A]];
break;
case B:
[self _playSoundFile:[self _NSStringFromSoundFiles:B]];
break;
case C:
[self _playSoundFile:[self _NSStringFromSoundFiles:C]];
break;
case AAC:
[self _playSoundFile:[self _NSStringFromSoundFiles:AAC]];
break;
case ABC:
[self _playSoundFile:[self _NSStringFromSoundFiles:ABC]];
break;
case ACA:
[self _playSoundFile:[self _NSStringFromSoundFiles:ACA]];
break;
default:
break;
}
}
/** Dealloc ! **/
-(void) dealloc {
[sae stopBackgroundMusic];
for(id s in soundSources){
//Release source
CDSoundSource *source = [soundSources objectForKey:s];
if(source.isPlaying){ [source stop]; }
[source release];
}
[soundSources release];
for(id m in musicSources){
//Release source
CDLongAudioSource *source = [musicSources objectForKey:m];
if(source.isPlaying){ [source stop]; }
[source release];
}
[musicSources release];
//End engine
[SimpleAudioEngine end];
sae = nil;
//instanceOfMusicLayer = nil;
[super dealloc];
}
#end
Now, I created a new Ccoos2d Helloworld template and added the following to the .m class to test the code:
#import "cocos2d.h"
#import "MusicLayer.h"
enum tags {
MUSICLAYERTAG = 99,
};
// HelloWorldLayer
#interface HelloWorldLayer : CCLayer
{
}
+(CCScene *) scene;
#end
//
// HelloWorldLayer.m
// MusicFadingTest
//
// Import the interfaces
#import "HelloWorldLayer.h"
// HelloWorldLayer implementation
#implementation HelloWorldLayer
+(CCScene *) scene
{
CCLOG(#"In helloworld scene");
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
HelloWorldLayer *layer = [HelloWorldLayer node];
// add layer as a child to scene
[scene addChild: layer];
MusicLayer *musicLayer = [MusicLayer node];
[musicLayer loadPopLevel];
[scene addChild:musicLayer z:-1 tag:MUSICLAYERTAG];
// return the scene
return scene;
}
// on "init" you need to initialize your instance
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super" return value
if( (self=[super init])) {
// ask director the the window size
CGSize size = [[CCDirector sharedDirector] winSize];
//Add menu items
[CCMenuItemFont setFontSize:20];
CCMenuItemFont *music0Item = [CCMenuItemFont itemFromString:#"Song A" target:self selector:#selector(play:)];
music0Item.tag = 0;
CCMenuItemFont *music1Item = [CCMenuItemFont itemFromString:#"Song B" target:self selector:#selector(play:)];
music1Item.tag = 1;
CCMenuItemFont *music2Item = [CCMenuItemFont itemFromString:#"Song C" target:self selector:#selector(play:)];
music2Item.tag = 2;
CCMenuItemFont *music3Item = [CCMenuItemFont itemFromString:#"Song D" target:self selector:#selector(play:)];
music3Item.tag = 3;
CCMenuItemFont *music4Item = [CCMenuItemFont itemFromString:#"Sound A" target:self selector:#selector(play:)];
music2Item.tag = 4;
CCMenuItemFont *music5Item = [CCMenuItemFont itemFromString:#"Sound B" target:self selector:#selector(play:)];
music3Item.tag = 5;
//Create our menus
CCMenu *menu0 = [CCMenu menuWithItems:music0Item, music1Item, music2Item, music3Item, music4Item, music5Item, nil];
[menu0 alignItemsInColumns: [NSNumber numberWithUnsignedInt:6], nil];
menu0.position = ccp(240,240);
[self addChild:menu0];
}
return self;
}
//Play music callback
-(void) play:(id)sender {
CCNode* node = [self getChildByTag:MUSICLAYERTAG];
NSAssert([node isKindOfClass:[MusicLayer class]], #"not a MusicLayer");
MusicLayer *musicLayer = (MusicLayer*)node;
CCMenuItem *item = (CCMenuItem*)sender;
switch (item.tag ) {
case 1:
[musicLayer playBackgroundMusic:PLAYER_STANDARD];
break;
case 2:
[musicLayer playBackgroundMusic:PLAYER_STANDARD];
break;
case 3:
[musicLayer playBackgroundMusic:PLAYER_STANDARD];
break;
case 4:
[musicLayer playBackgroundMusic:PLAYER_STANDARD];
break;
case 5:
[musicLayer playSoundFile:A];
break;
case 6:
[musicLayer playSoundFile:B];
break;
default:
break;
}
}
- (void) dealloc
{
[super dealloc];
}
#end
But here is the error message that I get when I the simulator I try to press any button and hence trigger the play method in HelloWorld.m
2012-04-23 10:39:18.493 MusicFadingTest[1474:10a03] In _loadMusic
2012-04-23 10:39:18.510 MusicFadingTest[1474:10a03] cocos2d: Frame interval: 1
2012-04-23 10:39:19.405 MusicFadingTest[1474:10a03] *** Assertion failure in -[HelloWorldLayer play:], /Users/user/Desktop/MusicFadingTest/MusicFadingTest/HelloWorldLayer.m:76
2012-04-23 10:39:19.406 MusicFadingTest[1474:10a03] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'not a MusicLayer'
*** First throw call stack:
(0x17f6022 0x1987cd6 0x179ea48 0x11c62cb 0xc264a 0x175c4ed 0x175c407 0x3a3b5 0x3ad73 0x37772 0x17f7e99 0x92821 0x9336f 0x95221 0x8573c0 0x8575e6 0x83ddc4 0x831634 0x27b1ef5 0x17ca195 0x172eff2 0x172d8da 0x172cd84 0x172cc9b 0x27b07d8 0x27b088a 0x82f626 0xc107f 0x2955)
The code above kind of mimics what happenes in the ShootEmUp example but I am missing something. As I cannot get the child by tag..
I asked this question as despite the answer will be probably trivial I hope to get some clarification on the general NON-ModelViewController approach in game programming and the usage of static variables.
I imagine using my MusicLayer in the MainMenu class and in the various layers implementing my levels. I would preload various music files according to the level the player is playing and keep the files that are not level specific preloaded (obviously taking care on the maximum number of sound files supported by the AudioEngine).
The other approach would have been to have a different instance of MusicLayer for each level and initializing them with different music files. The disadvantage of this approach is that the sharedAudioEngine is only one and when you want to have files keep playing between one scene and the other there is the risk of not having a full control on the track numbers used in the sharedAudioEngine (I recall should be 32 caf files for sound effect and few background tracks).
I thus understand why static instances are beneficial in game programming, but still, would love to hear what you think about the biderectional reference in 1 ShootEmUp code.
Also, would like to clarify that I encourage buying 1 as it has been a good starting point.
Thank you very much!
Oh, how much code and letters... First of all, for your question about "static instance". This is not static instance. This is static constructor. So, you can use smth like
CCScene* myScene = [GameScene scene];
instead of
CCScene* myScene = [[GameScene alloc] init];
// doing smth
[myScene release];
So, you will just create an autoreleased instance of your node (in that case, of your scene).
About the music question. You have add your music layer to the scene, but self in your play method will be HelloWorldLayer instance. So if you wanna get your music layer, you can try smth like this
MusicLayer* musicLayer = [[self parent] getChildByTag:MUSICLAYERTAG];

iPhone - Online Multiplayer game ... understanding the mechanics

I am developing an online multiplayer game, but I am struggling with Apple documentation. (I have tried these tutorials by Ray Wenderlichs Part 1 and part 2, but they are not working (match never starts because inviting device never receives the match acceptance).
As this topic is vast, I will be creating a single question, then moving to create another question on SO if necessary.
I want to create an online multiplayer game that will let a user to invite from 1 to 3 people. So, it would be a 2 to 4 people match. The game is not turned based. It is live and the data to be transferred between users is minimum.
Lets start with the basic stuff.
1) the first thing I do is create a notification
if (self.gameCenterAvailable) {
NSNotificationCenter *nc =
[NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(authenticationChanged)
name:GKPlayerAuthenticationDidChangeNotificationName
object:nil];
}
to let me know when the notification changes. When this happens, authenticationChanged method will fire... here it is
- (void)authenticationChanged {
GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
// Insert application-specific code here to clean up any games in progress.
if (acceptedInvite)
{
GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
mmvc.matchmakerDelegate = self;
[self presentModalViewController:mmvc animated:YES];
}
else if (playersToInvite)
{
GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
request.minPlayers = 2;
request.maxPlayers = 4;
request.playersToInvite = playersToInvite;
GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
mmvc.matchmakerDelegate = self;
[self presentModalViewController:mmvc animated:YES];
}
};
}
I grabbed this code from Apple. My question here is this. If Apple say to run this code after the user is authenticated why it is checking for invitation or users to invite? As far as I see, users were not invited yet. Unless the code is not executed at that time, right? It will just sit in memory waiting to be called, WHEN the invitation is done, correct?
If this is the case, I now create an invitation for a match doing
[self dismissModalViewControllerAnimated:NO];
GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
request.minPlayers = minPlayers;
request.maxPlayers = maxPlayers;
request.playersToInvite = self.pendingPlayersToInvite;
GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
mmvc.matchmakerDelegate = self;
[self presentModalViewController:mmvc animated:YES];
A window will be present to all users I choose to invite. Suppose the first one taps ACCEPT on the invitation. Which method will be fired on my app, how do I get the user identity and how do I know if all users accepted?
thanks.
First of all please be aware that invitations on the Sandbox enviroment tend to work erratically, so I suggest you start by having all players search for an available match. The code would be something like this:
GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
request.minPlayers = minPlayers;
request.maxPlayers = maxPlayers;
[[GKMatchmaker sharedMatchmaker] findMatchForRequest:request
withCompletionHandler:^(GKMatch *match, NSError *error) {
if (error || !match) {
// handle the error
}
else if (match != nil){
// match found
}}];
}
Then you have to implement the protocol GKMatchDelegate. There's a method there that will be invoked for each player that joins the match and for each player that gets disconnected from it (on this method you can find out the user identity with its playerID):
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {
switch (state){
case GKPlayerStateConnected:
if (match.expectedPlayerCount == 0) {
// start the match
}
break;
case GKPlayerStateDisconnected:
// remove the player from the match, notify other players, etc
break;
}
}
Regarding your first question, the code on authenticationChanged is only registering the handlers for those methods, meaning the code that will be invoked when the notifications arrive.
EDIT: Regarding the question on your comment, if the match was started by invitations, the user that started the match has to wait until all invitations are accepted, or cancel some of them and then press Start Match (or something like that) on the Invite Screen. In this scenario the match.expectedPlayerCount == 0 condition will be satisfied once all the players that accepted the invites are connected to the match.
If the match was started by AutoMatch then Game Center does the following: once it finds minPlayers waiting to start a match it will assign them to a match and then wait a few more seconds to see if it can fill the remaining slots. At some point it will start the match with a certain number of players between minPlayers and maxPlayers. Then the condition match.expectedPlayerCount == 0 will be satisfied only once all the players have effectively join the match, but note that when the decision was taken to start a match the number of players expected for that match is already determined by Game Center.