I'm using the root viewcontroller to display a couple of labels to keep track of scores etc, which is working fine. However, the root viewcontroller is not responding to messages from the scene right away when the scene is loaded.
The situation is this: UIViewController presents SKView where I initialize MyScene on top of that (normal SpriteKit stuff). Immediately in MyScene I load the level from a json-file, which contains the number of possible points on that specific level. When the level is done loaded, I try to send the number of possible points back to my root viewcontroller, so it can display it in a label.
I've tried setting a property myScene.mainViewController = self from the viewWillLayoutSubviews on ViewController, and then just [self.mainViewController setInitialScore:_bgLayer.maxScore];, but the method setInitialScore never fires. I've also tried using the NSNotificationCenter, but this does not work either.
However, I call a method in self.mainViewController whenever the player collects a point (which happens a little bit later), and that works fine. So the problems seems to be that the ViewController is not done loading or something, so it doesn't respond right away.
How could I solve this?
EDIT 1:
Here's some code by request:
ViewController.m
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
SKView *skView = (SKView *)self.view;
if (!skView.scene) {
skView.showsFPS = YES;
skView.showsNodeCount = YES;
// Create and configure the scene.
MyScene* scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
scene.mainViewController = self;
// Present the scene.
[skView presentScene:scene];
self.numLives = bStartNumLives;
self.score = 0;
[self resetLabels];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(setInitialRings:) name:#"setInitialRings" object:nil];
}
}
- (void)setInitialRings:(NSNotification *)notification {
NSLog(#"Initial rings set");
NSDictionary *userInfo = [notification userInfo];
NSInteger rings = [(NSNumber *)[userInfo objectForKey:#"rings"] integerValue];
self.numRings = rings;
[self resetLabels];
}
MyScene.m
- (void)createWorld {
_bgLayer = [self createScenery];
NSLog(#"world created setting initial rings");
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithInteger:_bgLayer.numRings] forKey:#"rings"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"setInitialRings" object:userInfo];
_worldNode = [SKNode node];
[_worldNode addChild:_bgLayer];
[self addChild:_worldNode];
self.anchorPoint = CGPointMake(0.5, 0.5);
_worldNode.position = CGPointMake(-_bgLayer.layerSize.width / 2, -_bgLayer.layerSize.height / 2);
}
As you can see from this snippet of code, I'm trying to use the NSNotificationCenter. I've also tried calling [self.mainViewController setInitialRings:_bgLayer.numRings]; manually (but changing the setInitialRings-method to take an integer instead of a notification).
Related
This seems like a huge glitch in my opinion.
I have an SKScene overlaying a SceneKit scene. In my app, a method is called to update progress on a "progress bar" (really just an SKShapeNode with a really low height). This method is called updateSaveDataNotification and takes an NSNotification. I called [self childNodeWithName:] to access a child node. The problem is, when it's called, self has no children. Even though it really has three children.
I'm changing my code so they're variables in the implementation so I can animate their scale that way, but why would this behavior even exist? Why would self have zero children in this instance?
EDIT: the progress bar is simply an SKShapeNode. Here's the code I used:
#implementation {
SKShapeNode *progressBar;
float progress;
}
…
-(void)load {
[[NSNotificationCenter defaultCenter] addObserver:self name:#"anyName" selector:#selector(rcvNotification:) object:nil];
progress = 0.0;
…
progressBar = [SKShapeNode shapeNodeWithRectOfSize:CGSizeMake(self.size.width * 0.9, 2)
progressBar.xScale = 0;
progressBar.alpha = 0;
progressBar.fillColor = [UIColor whiteColor];
progressBar.strokeColor = [UIColor whiteColor];
progressBar.position = CGPointMake(self.size.width / 2, self.size.height * 0.1);
progressBar.name = #"progressBar";
…
[self addChild:progressBar]
[progressBar runAction:[SKAction fadeAlphaTo:1 duration:1]];
}
// The notification has one KVP in its user info dictionary. This is "someKey."
// "someKey"'s float value is added to "progress."
-(void)rcvNotification:(NSNotification*)notification {
progress += [[notification.userData valueForKey:#"someKey"] floatValue];
// [self updateUI];
// note: if I call updateUI like that, self will have zero children regardless of whether or not it actually has more.
// a remedy:
[self runAction:[SKAction waitForDuration:0] completion:^{
[self updateUI];
}];
}
-(void)updateUI {
NSLog(#"count of children: %lu", self.children.count);
[progressBar runAction:[SKAction scaleXTo:progress duration:0.5] completion:
// code to run when finished animating change
// also logic for reaching 100% progress.
}];
}
My predicament was, for a solid hour, not being able to re-scale that progress bar. I figured out self temporarily had 0 children, so I made it run a 0-second animation. I want to figure out why this odd behavior occurs.
Since xCode was updated to version 6.0, the default method for SpriteKit in "GameScene" (the default scene created) changes to:
-(void) didMoveToView:(SKView *) view {
/* Scene set up in here */
}
as opposed to the older method:
-(id) initWithSize:(CGSize) size {
if (self = [super initWithSize:size]{
/* Scene set up in here */
}
}
I understand that the new method (as with the changes in the view controller) is used to help manage the import from the .sks file that is new in xCode 6. However, I was curious if I didn't want to use the new "storyboard"-type format that is the .sks file, should I still use the new method? Or should I change the method back to the initWithSize method and just delete the .sks file?
Init methods should be used for initializing but keep in mind that inside init, view is always nil. So any code that needs the view has to be moved to didMoveToView method (it's called immediately after a scene is presented by a view).
About initWithSize in Xcode 6... By default, scene is loaded from .sks file. Because of this, initWithSize is never called actually. initWithCoder is called instead:
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
// do stuff
}
return self;
}
So initializing anything inside initWithSize won't have any effect. If you decide to delete .sks file and create a scene in "old" way, you can do something like this in view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
// Create and configure the scene.
GameScene *scene = [GameScene sceneWithSize:self.view.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
After that, you can use initWithSize for initialization.
Note that in viewDidLoad the final size of a view may not be known yet, and using viewWillLayoutSubviews instead could be a right choice. Read more here.
And proper implementation of viewWillLayoutSubviews for scene initialization purpose would be:
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
//viewWillLayoutSubviews can be called multiple times (read about this in docs ) so we have to check if the scene is already created
if(!skView.scene){
// Create and configure the scene.
GameScene *scene = [GameScene sceneWithSize:self.view.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
}
Swift code:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.showsPhysics = true
skView.showsDrawCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
if(skView.scene == nil){
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
skView.presentScene(scene)
}
}
}
I think that, for all intents and purposes, there isn't much of a difference between the two methods when it comes to actually setting up your scene and using it. (initWithSize is called when the scene is initialized, while didMoveToView is always called right when the scene is presented by the view). I guess if you really prefer to see the initialization then you could just use the init method without any trouble at all.
Regarding the .sks file:
take a look at your view controller implementation file. Right above the v.controller's methods you'll see:
#implementation SKScene (Unarchive)
+ (instancetype)unarchiveFromFile:(NSString *)file {
/* Retrieve scene file path from the application bundle */
NSString *nodePath = [[NSBundle mainBundle] pathForResource:fileofType:#"sks"];
/* Unarchive the file to an SKScene object */
NSData *data = [NSData dataWithContentsOfFile:nodePath
options:NSDataReadingMappedIfSafe
error:nil];
NSKeyedUnarchiver *arch = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[arch setClass:self forClassName:#"SKScene"];
SKScene *scene = [arch decodeObjectForKey:NSKeyedArchiveRootObjectKey];
[arch finishDecoding];
return scene;
}
#end
This is the part that handles the sks. Using the file is totally optional, you aren't forced to do it and it really has no impact on you setting up your scene. However, if you do want to work with it, then you'll find that you'll need to work with this code snippet.
Both options are correct. Said that, the init method is not actually the same than the didMoveToView: method, as this last one will be called once the SKScene is presented in a SKView.
In the main ViewController class usually called 'GameViewController' there's this part in the code that can use 'GameScene' classes and you can add Nodes or Labels. This is the part in 'GameViewController.m'
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
// Create and configure the scene.
GameScene *scene = [GameScene unarchiveFromFile:#"GameScene"];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
I created another UIViewController class called 'ViewController' and a SKScene class called 'testSceneClass'. I tried doing this in 'ViewController.m' class:
- (void)viewDidLoad
{
[super viewDidLoad];
//[ViewController addChild: test];
// Configure the view.
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
// Create and configure the scene.
testSceneClass *scene = [testSceneClass unarchiveFromFile:#"testSceneClass"];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
But it gives me the error "No known class method for selector 'unarchiveFromFile'" And I copied pretty much everything from the SKScene and UIViewController so I don't know what is wrong. But when I run it, it crashes and this is the reason:
reason: '-[UIView setShowsFPS:]: unrecognized selector sent to
instance
When I add a Exception Breakpoint it showed up on this line.
skView.showsFPS = YES;
Then when I comment it, it shows a error on the next line, you can look at it in the 'ViewController.m' file I showed above. After I commented all those, an error showed up on this line:
NSData *data = [NSData dataWithContentsOfFile:nodePath
options:NSDataReadingMappedIfSafe
error:nil];
and so on...
Any help?? (Sorry I added so much lol)
I am having an issue getting the correct coordinates from a UITouch on an SKScene after the SKScene is presented the second time. I've written a simplified app to demonstrate the issue located here:
https://github.com/JenDobson/SKSceneCoordinatesIssue
In this simplified app there is 1 scene. The scene has a single SKNode. When the scene is touched, if the UITouch location intersects with the SKNode, the touchesEnded:withEvent method calls back to the ViewController to present the scene a second time. But when the scene is presented a second time, when I attempt to touch the SKNode, the coordinates of the UITouch are reversed (i.e. the x value reports the y value and vice versa). They thus don't intersect with the SKNode, and the scene is not presented again. (If I touch at the location on the screen that is the SKNode's position reflected over the line x=y, then I can get the scene to be presented again.)
I've tried playing around with "ViewWillLayoutSubviews", but in this sample app the view is only loaded once, so I believe that ViewWillLayoutSubviews is not called repeatedly and therefore cannot affect the scene coordinates.
To illustrate the issue, here is the output from touchesEnded:withEvent the first time the scene is presented:
2014-07-21 17:57:06.908 TestTouchCoordinatesInSKScene[5476:60b] scene frame size:1024.000000,768.000000
2014-07-21 17:57:06.911 TestTouchCoordinatesInSKScene[5476:60b] location:685.000000,105.500000
2014-07-21 17:57:06.912 TestTouchCoordinatesInSKScene[5476:60b] navNode Position:700.000000,100.000000
And here is the output from touchesEnded:withEvent the second time the scene is presented:
2014-07-21 17:57:14.724 TestTouchCoordinatesInSKScene[5476:60b] scene frame size:1024.000000,768.000000
2014-07-21 17:57:14.725 TestTouchCoordinatesInSKScene[5476:60b] location:107.000000,632.500000
2014-07-21 17:57:14.726 TestTouchCoordinatesInSKScene[5476:60b] navNode Position:700.000000,100.000000
Note that "location" has the x- and y- coordinates reversed for the second listing.
Below is what I think the relevant code is.
In "MyViewController.m"
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
SKView* skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
CGSize contentSize = CGSizeMake(skView.bounds.size.width,skView.bounds.size.height);
_mainMenuScene = [MyMainMenuScene sceneWithSize:contentSize];
_mainMenuScene.controller = self;
_mainMenuScene.scaleMode = SKSceneScaleModeAspectFill;
[skView presentScene:_mainMenuScene];
}
-(void)loadView
{
CGRect viewFrame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
SKView* view = [[SKView alloc] initWithFrame:viewFrame];
self.view = view;
}
-(void)returnToMainMenu
{
[(SKView*)self.view presentScene:_mainMenuScene transition:[SKTransition moveInWithDirection:SKTransitionDirectionRight duration:.3]];
}
#end
In "MyMainMenuScene.m"
-(CGPoint)navNodePosition
{
return CGPointMake(700,100);
}
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
_navNode = [SKLabelNode node];
_navNode.position = [self navNodePosition];
_navNode.text = #"Return to Main Menu";
_setupLabel.fontColor = [UIColor whiteColor];
_setupLabel.fontSize = 24;
[self addChild:_navNode];
}
return self;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
BOOL shouldSegueToMainMenu = NO;
NSLog(#"scene frame size:%f,%f",self.frame.size.width,self.frame.size.height);
for (UITouch* touch in touches) {
CGPoint touchLocation = [touch locationInNode:self];
NSLog(#"location:%f,%f",touchLocation.x,touchLocation.y);
NSLog(#"navNode Position:%f,%f",self.navNode.position.x,self.navNode.position.y);
if ([self.navNode containsPoint:touchLocation]) {
shouldSegueToMainMenu = YES;
break;
}
}
if (shouldSegueToMainMenu)
{
[self.controller returnToMainMenu];
}
}
Thanks very much!
After playing around more, I believe this is a bug in the SKView method presentScene:transition. In the returnToMainMenu method of MyViewController, if I use the plain old presentScene method rather than presentScene:transition, the problem goes away. I've filed this with Apple as bug 17770198.
Situation:
I'm getting some mysterious crashing shortly after a CCCallFunc. In short, we have a button. The button has a tag to identify it later. When the button is pressed, we run some actions to animate it, and when the animation is done, we CCCallFunc another method to transition to another scene. We crash shortly after the CCCallFunc. Source and errors below.
Point Of Crash (in cocos2d source):
// From CCActionInstant.m of cocos2d
-(void) execute
{
/*** EXC_BAD_ACCESS on line 287 of CCActionInstant.m ***/
[targetCallback_ performSelector:selector_];
}
#end
Snapshot of Thread 1:
My Code:
Below is some source taken from MenuLayer.m (a simple menu to display a button).
// from MenuLayer.m
// …
#implementation MenuLayer
-(id) init{
if((self=[super init])) {
/****** Create The Play Button (As a CCMenu) ********/
CCSprite *playSprite = [CCSprite spriteWithFile:#"playbutton.png"];
CCMenuItemSprite *playItem = [CCMenuItemSprite itemFromNormalSprite:playSprite selectedSprite:nil target:self selector:#selector(animateButton:)];
playItem.tag = 3001;
playItem.position = ccp(160.0f, 240.0f);
CCMenu *menu = [CCMenu menuWithItems:playItem, nil];
menu.position = ccp(0.0f, 0.0f);
[self addChild:menu z:0];
}
}
// ...
- (void)animateButton:(id)sender{
/*** Run an animation on the button and then call a function ***/
id a1 = [CCScaleTo actionWithDuration:0.05 scale:1.25];
id a2 = [CCScaleTo actionWithDuration:0.05 scale:1.0];
id aDone = [CCCallFunc actionWithTarget:self selector:#selector(animationDone:)];
[sender runAction:[CCSequence actions:a1,a2,aDone, nil]];
}
- (void)animationDone:(id)sender{
/*** Identify button by tag ***/
/*** Call appropriate method based on tag ***/
if([(CCNode*)sender tag] == 3001){
/*** crashes around here (see CCActionInstant.m) ***/
[self goGame:sender];
}
}
-(void)goGame:(id)sender{
/*** Switch to another scene ***/
CCScene *newScene = [CCScene node];
[newScene addChild:[StageSelectLayer node]];
if ([[CCDirector sharedDirector] runningScene]) {
[[CCDirector sharedDirector] replaceScene:newScene]];
}else {
[[CCDirector sharedDirector] runWithScene:newScene];
}
}
Use CCCallFuncN instead of CCCallFun.
CCCallFuncN passes the Node as parameter, the problem with CCCallFun is that you are loosing reference of the node.
I test your code with CCCallFuncN and works ok.
Just a hunch. Besides checking for memory leaks, try to schedule a selector with a 0 second interval instead of directly sending the goGame message. I have a suspicion that director's replaceScene causes a cleanup of the scene and all objects associated with it. That in turn could leave the CCCallFunc action in an undefined state. Although normally it works fine - which is to say that this is just another indication about something sketchy, memory- respectively object-lifetime-management-wise.
Btw, if you support iOS 4 as a minimum, use CCCallBlock instead of CCCallFunc. That's safer and cleaner.