I'm using the semi-singleton approach for the main game layer/scene of my cocos2d game as shown in the later code.
The objective is to properly restart/recreate this singleton scene using a button in a pause or gameover layers by calling: [[GameLayer sharedGameLayer] restart] method.
The problem is if I use a CCTransition effect for that, and override the GameLayer's dealloc method with sharedGameLayer = nil; line (to ensure the resetting of the static variable), the sharedGameLayer variable stays nil after the first restart (aka. after the first dealloc), so calling the restart method does nothing.
What works with suspicion is not to override the dealloc method at all, but before restarting the scene using replaceScene:, I set the sharedGameLayer to nil.
Question: Is this the right approach for restarting/recreating this semi-singleton class?
Thanks in advance.
Code:
GameLayer.m:
static GameLayer *sharedGameLayer;
#implementation GameLayer
- (id)init
{
NSLog(#"%s", __PRETTY_FUNCTION__);
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if (self = [super initWithColor:ccc4(255, 255, 255, 255) width:[CCDirector sharedDirector].winSize.width
height:[CCDirector sharedDirector].winSize.height])
{
// Set the sharedGameLayer instance to self.
sharedGameLayer = self;
// Set the initial game state.
self.gameState = kGameStateRunning;
// Register with the notification center in order to pause the game when game resigns the active state.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(pauseGame) name:UIApplicationWillResignActiveNotification object:nil];
// Enable touches and multi-touch.
CCDirector *director = [CCDirector sharedDirector];
[director.touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:YES];
director.view.multipleTouchEnabled = YES;
// Set the initial score.
self.score = 5;
// Create the spiders batch node.
[self createSpidersBatchNode];
// Load the game assets.
[self loadAssets];
// Play Background music.
if (![SimpleAudioEngine sharedEngine].isBackgroundMusicPlaying)
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:#"backgroundmusic.mp3" loop:YES];
// Preload sound effects.
[[SimpleAudioEngine sharedEngine] preloadEffect:#"inbucket.mp3"];
[[SimpleAudioEngine sharedEngine] preloadEffect:#"outbucket.mp3"];
[[SimpleAudioEngine sharedEngine] preloadEffect:#"gameover.mp3"];
// Schdule updates.
[self scheduleUpdate];
[self schedule:#selector(releaseSpiders) interval:0.7];
}
return self;
}
- (void)createSpidersBatchNode
{
NSLog(#"%s", __PRETTY_FUNCTION__);
// BatchNode. (For Animation Optimization)
self.spidersBatchNode = [CCSpriteBatchNode batchNodeWithFile:#"spiderAtlas.png"];
// Spider sprite + BatchNode + animation action.
for (int i = 0; i < 50; i++) {
Spider *spider = [[Spider alloc] init];
spider.spiderSprite.visible = NO;
}
[self addChild:self.spidersBatchNode];
}
- (void)restartGame
{
// Play button pressed sound effect.
[[SimpleAudioEngine sharedEngine] playEffect:#"button.mp3"];
// Nil'ing the static variable.
sharedGameLayer = nil;
// Restart game.
[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:[GameLayer scene]]];
}
+ (CCScene *)scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// Game Layer.
// 'gameLayer' is an autorelease object.
GameLayer *gameLayer = [GameLayer node];
// add gameLayer as a child to scene
[scene addChild: gameLayer];
// HUD Layer.
HUDLayer *hudLayer = [HUDLayer node];
[scene addChild:hudLayer];
gameLayer.hud = hudLayer;
// return the scene
return scene;
}
+ (GameLayer *)sharedGameLayer
{
return sharedGameLayer;
}
- (void)cleanup
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[CCDirector sharedDirector].touchDispatcher removeDelegate:self];
[self stopAllActions];
[self unscheduleAllSelectors];
[self unscheduleUpdate];
[self removeAllChildrenWithCleanup:YES];
self.hud = nil;
[super cleanup];
}
//- (void)dealloc
//{
// sharedGameLayer = nil;
//}
#end
I bet your new game layer is created before the first one deallocates, therefore it sets the static var to nil. Check that sharedGameLayer equals self in dealloc.
Do not use dealloc to set the static variable to nil. Dealloc happens after your object has stopped being used. Never use it for controlling the behaviour of your app.
For all you know, 30 minutes might have passed between when you set sharedGameLayer to nil and when the dealloc method gets called. Or perhaps dealloc will never be called at all.
Your code looks good, just delete the commented out "dealloc" code.
Also, this code:
[[NSNotificationCenter defaultCenter] removeObserver:self];
Should be done in the -dealloc method in addition to your custom -cleanup method. It is perfectly fine to remove an observer twice, if it's already been removed nothing will happen.
Another note, +sharedGameLayer should return a variable of type instancetype and it should be responsible for setting the sharedGameLayer variable. If you sharedGameLayer inside -init, you will run into bugs.
For example:
+ (instancetype)sharedGameLayer
{
if (sharedGameLayer)
return sharedGameLayer;
sharedGameLayer = [[[self class] alloc] init];
return sharedGameLayer;
}
- (id)init
{
if (!(self = [super init]))
return nil;
.
.
.
return self;
}
Related
I am pausing my game with a willResignActive notification, and it seems to pause the game, but when didBecomeActive is called, it seems to un-pause on its own.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(applicationWillResign)
name:UIApplicationWillResignActiveNotification
object:NULL];
- (void) applicationWillResign {
self.scene.view.paused = TRUE;
NSLog(#"About to lose focus");
}
How do I get it to stay paused? Do I actually need to pause it in my AppDelegate?
Here's a way to keep the view paused after returning from background mode. It's a bit of a hack, but it does work.
1) Define an SKView subclass with a boolean named stayPaused...
#interface MyView : SKView
#property BOOL stayPaused;
#end
#implementation MyView
// Override the paused setter to conditionally un-pause the view
- (void) setPaused:(BOOL)paused
{
if (!_stayPaused || paused) {
// Call the superclass's paused setter
[super setPaused:paused];
}
_stayPaused = NO;
}
- (void) setStayPaused
{
_stayPaused = YES;
}
#end
2) In the storyboard, change the class of the view to MyView
3) In the view controller, define the view as MyView
4) Add a notifier to set the stayPaused flag
#implementation GameViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Define view using the subclass
MyView * skView = (MyView *)self.view;
// Add an observer for a method that sets the stay pause flag when notified
[[NSNotificationCenter defaultCenter] addObserver:skView selector:#selector(setStayPaused)
name:#"stayPausedNotification" object:nil];
...
5) In AppDelegate.m, post a notification to set the stay paused flag when the app becomes active
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[NSNotificationCenter defaultCenter] postNotificationName:#"stayPausedNotification" object:nil];
}
I have a game where I've successfully implemented a pause feature when the home button is pressed. In my View Controller that has the main scene, I pause using:
- (void)appWillEnterBackground{
SKView *skView = (SKView *)self.view;
skView.paused = YES;
bubbleOn=NO; //turns bubble spawn off
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterForeground)
name:UIApplicationDidBecomeActiveNotification
object:NULL];
}
To unpause,
- (void)appWillEnterForeground{
SKView * skView = (SKView *)self.view;
skView.paused=NO;
bubbleOn=YES; Allows recursive method to run until bubbleOn = YES
[NSTimer scheduledTimerWithTimeInterval:slowMo target:scene selector:#selector(spawnNew) userInfo:nil repeats:NO]; //Recursive spawn method
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterBackground)
name:UIApplicationWillResignActiveNotification
object:NULL];}
This works well until my the game ends and a new scene (End Scene) is presented. After the End Scene shows a score, the user can tap again to start again and main scene is presented. The main scene's initWithSize method begins the recursive method spawnNew. If the app goes to the background, the scene pauses and spawnNew stops.
But when the app goes to foreground, the scene does resume, but the spawnNew method does not work. It gets called and outputs a correct NSLog message, but the the method doesn't spawn bubble nodes.
The spawnNew method is in my main scene's implementation:
-(void) spawnNew{
if (bubbleOn==YES){
bubble = [SKSpriteNode spriteNodeWithImageNamed:ballName];
bubble.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:bubble.size.width/2];
...
//bubble properties
...
[self addChild:bubble];
NSLog(#"Spawn!");
[NSTimer scheduledTimerWithTimeInterval:slowMo target:self selector:#selector(spawnNew) userInfo:nil repeats:NO];
return;
} else{
return;
}
I'm out of ideas at this point! Any suggestions?
The easiest way is to get rid of NSTimer. You can use SKAction instead.
In SKScene's initWithSize method:
- (instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
....
__weak GameScene *weakSelf = self;
SKAction *spawnBubble = [SKAction runBlock:^{
[weakSelf spawnNew];
}];
SKAction *wait = [SKAction waitForDuration:slowMo];
[self runAction:[SKAction repeatActionForever:
[SKAction sequence:#[spawnBubble,wait]]]];
}
return self;
}
In this case you don't even need to use bubbleOn variable, as scene stops executing any actions as soon as it's SKView is paused.
I have huge issue with my newest game for iPhone made with Games by Tutorials book's help.
NOTE : method from SpriteKit- the right way to multitask doesn't work.
So, in my ViewController.m file, I'm defining private variable SKView *_skView.
Then, I do something like this :
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(!_skView)
{
_skView = [[SKView alloc] initWithFrame:self.view.bounds];
_skView.showsFPS = NO;
_skView.showsNodeCount = NO;
// Create and configure the scene.
SKScene * scene = [MainMenuScene sceneWithSize:_skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[_skView presentScene:scene];
[self.view addSubview:_skView];
}
}
And I have my _skView defined, and everything works just fine.
But, when I interrupt game, it resets its state to initial, so, for example, if I'm currently playing, and somebody calls me, game switches back to main menu. It can't be like this.
Based on the site I mentioned above, I created this :
- (void)applicationWillResignActive:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
view.paused = YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
view.paused = NO;
}
But game crashes as soon as it is launched, because second method is called and SKView* view is nil.
Getting it from self.window.rootViewController.view doesn't work.
I also tried to get it from self.window.rootViewController.view.subviews, but it doesn't work either.
I can't define my SKView (in ViewController.m) like this :
SKView * skView = (SKView *)self.view;
because then I have errors with my GameCenterController.
Can someone help me how correctly get actual skView and pause it properly??
You could subscribe to these notifications yourself within the ViewController that manages the SKView. Then you don't have to navigate some weird hierarchy to obtain it.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(willEnterBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(willEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
- (void)willEnterForeground {
self.skView.paused = NO;
}
- (void)willEnterBackground {
// pause it and remember to resume the animation when we enter the foreground
self.skView.paused = YES;
}
I am trying to figure out touch handling on multiple CCNodes .
I have
Main CCLayer
------> z:2 Hud CCNode
On main layer I choose an object, on hud layer I want to control it. I followed this q&a its very helpful Cocos2d handling touch with multiple layers
On Main Layer touch events, below is Hud Node work:
-(void) registerWithTouchDispatcher
{
[[CCDirector sharedDirector].touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event
{
//detects touched Object and sends it to hud
if (object != nil)
{
//sends it to hud
return true;
}
return false;
}
-(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent*)event
-(void) ccTouchEnded:(UITouch*)touch withEvent:(UIEvent*)event
-(void) ccTouchCancelled:(UITouch*)touch withEvent:(UIEvent*)event
On Hud CCNode none of ccTouchBegan/moved/ended methods are fired
-(void) registerWithTouchDispatcher
{
[[CCDirector sharedDirector].touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event
{
if (object!=nil) {
NSLog(#"Touch began");
return YES;
}
else
return NO;
}
EDIT:
I have a button to set speed of the object on HUD Node , it has nothing to do with object!=nil because when I put breakpoints I see that -(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent*)event is never called
-(void)speed1Tapped:(id)sender
{
if (object!=nil) {
NSLog(#"Moving Object is %#:",object!=nil);
}
}
On Log I get:
object is <ObjectATC: 0x13763850>
Why ccTouchBegan/moved/ended methods are not fired in CCNode?
How can I handle touches on multiple CCNodes, CCLayers ?
I guess you dont receive touch method because you did not register the touchDipatcher
either in your CCNodes init method or onEnter method try to register it.
//register touches
- (void)onEnter {
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
[super onEnter];
}
- (void)onExit {
[[[CCDirector sharedDirector] touchDispatcher]removeDelegate:self];
[super onExit];
}
In your touch methods, you have a conditional that says if (object != nil), but object is not defined anywhere in the code that you have shown. Is it defined elsewhere? Otherwise, this is why it is not firing.
Currently I have this code in my UIViewController:
//Cocos2D methods
-(id) init {
if((self=[super init]))
{
CCScene *scene = [CCScene node];
CCLayer *layer = [CCLayer node];
[scene addChild:layer];
[[CCDirector sharedDirector] runWithScene:scene];
[[CCDirector sharedDirector] setDisplayFPS:NO];
[self performSelector:#selector(cocosgameLoop:) withObject:nil afterDelay:1/60.0f];
}
return self;
}
- (void)cocosgameLoop:(ccTime)dT {
//Check for collisions here and add gravity, etc....
}
- (void)viewDidUnload {
[super viewDidUnload];
[[CCDirector sharedDirector] end];
}
//
My view controller is somewhat Objective-C code, but I want to add Cocos2D to the UIView. I think the code above is initializing it, but I am not sure. Also should the CCScene have its own class dedicated to everything that goes on in the scene? if so how would I do that?
I know these are a lot of questions, but my game is very similar to Doodle Jump and I need to know where to go from the current state I am in.
Does anyone have any ideas/tips?
Thanks!
performSelector will call the cocosgameLoop only once. You will want to call CCDirector startAnimation, and then schedule updates in your cocos2d nodes.
My Learn Cocos2D book (2nd Edition) explains how to add and setup Cocos2D in a UIKit view (regular iPhone) app.