Apple's documentation has me a bit confused.
According to the documentation page : https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Nodes/Nodes.html the relationship between the visible area of the scene in points and the size of the scene should be the same according to the sentence :
"The size of the scene specifies the size of the visible portion of
the scene in points".
I created a custom scene that gets its size from the size of the device's screen (in my case I've been testing this for the iPad in portrait mode so the size should be 768 wide by 1024 high).
The following line which is being called in the scene's createContent method
NSLog(#"self.size : %f %f", self.size.width, self.size.height);
Returns
2014-07-15 14:53:28.844 SpriteWalkthrough[15888:90b] self.size : 768.000000 1024.000000
as expected.
However when I try to draw a SKSpriteNode at the position(self.size.width/2,self.size.height/2) the node is drawn in the upper right hand corner of the screen, not in the middle.
Why is this happening?
For other people who may be making similar mistakes drawing SpriteNodes to a scene, take a look at my source code for the scene, and in particular pay attention to the //self.sCar.car.position = self.sCar.position;line.
FoolAroundScene.h
#import <SpriteKit/SpriteKit.h>
#interface FoolAroundScene : SKScene
#end
FoolAroundScene.m
#import "FoolAroundScene.h"
#import "ScrollingBackground.h"
#import "SpriteCar.h"
#define BACKGROUND_NAME #"clouds.jpg"
#interface FoolAroundScene ()
#property BOOL contentCreated;
#property (strong, nonatomic) ScrollingBackground * sbg;
#property (strong, nonatomic) SpriteCar * sCar;
#end
#implementation FoolAroundScene
-(void) didMoveToView:(SKView *)view
{
if (!self.contentCreated) {
[self createContent];
self.contentCreated = !self.contentCreated;
}
}
-(void)createContent
{
self.sbg = [[ScrollingBackground alloc] initWithBackgroundImage:BACKGROUND_NAME size:self.size speed:2.0];
self.sCar = [[SpriteCar alloc] init];
[self addChild:self.sbg];
[self addChild:self.sCar];
self.sCar.position = CGPointMake(self.size.width/2, self.size.height/2);
//self.sCar.car.position = self.sCar.position;
SKSpriteNode * spriteCar = [self.sCar makeCarSprite];
spriteCar.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
NSLog(#"anchor point : %f %f", self.anchorPoint.x, self.anchorPoint.y);
NSLog(#"self.size : %f %f", self.size.width, self.size.height);
NSLog(#"self.scalemode : %d", self.scaleMode);
}
-(void) update:(NSTimeInterval)currentTime
{
[self.sbg update:currentTime];
}
#end
Even though I had set the sCar's position property to the middle of the scene, by setting its child node (the sCar's car property) position to the same value, the child's position will not be the same as its parent's. This is because the child's position is relative to its parent's.
Explained another way : the parent's position in the context of the scene was (384,512) and the child's position in the context of its parent was (384,512). However this means that the child's position in the context of the scene was actually (768,1024), which is why the car was being drawn in the upper right hand corner of the screen.
Also, in case anyone wants the implementation for the sprite car, here it is. A crappily drawn car that can be used to get a grip on how the Sprite Kit works.
SpriteCar.h
#import <SpriteKit/SpriteKit.h>
#interface SpriteCar : SKNode
-(id)init;
-(SKSpriteNode *)makeCarSprite;
#end
SpriteCar.m
#import "SpriteCar.h"
#interface SpriteCar ()
#property (nonatomic, strong) SKSpriteNode * car;
#end
#implementation SpriteCar
-(id) init
{
self = [super init];
if (self) {
self.car = [self makeCarSprite];
[self addChild:self.car];
}
return self;
}
-(SKSpriteNode *) makeCarSprite
{
SKSpriteNode * carBody1 = [[SKSpriteNode alloc] initWithColor:[SKColor redColor] size:CGSizeMake(64.0, 24.0)];
SKSpriteNode * carBody2 = [[SKSpriteNode alloc] initWithColor:[SKColor redColor] size:CGSizeMake(32.0, 32.0)];
SKSpriteNode * wheel1 = [[SKSpriteNode alloc] initWithColor:[SKColor blackColor] size:CGSizeMake(8.0, 8.0)];
SKSpriteNode * wheel2 = [wheel1 copy];
SKSpriteNode * light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(6.0, 6.0)];
carBody2.position = carBody1.position;
wheel1.position = CGPointMake(30.0, -30);
wheel2.position = CGPointMake(-30.0, -30.0);
light.position = CGPointMake(32.0, 11.0);
[carBody1 addChild:carBody2];
[carBody1 addChild:wheel1];
[carBody1 addChild:wheel2];
[carBody1 addChild:light];
SKAction * hover = [SKAction sequence:#[[SKAction moveByX:0.0 y:5.0 duration:0.1],
[SKAction waitForDuration:0.05],
[SKAction moveByX:0.0 y:-5.0 duration:0.1],
[SKAction waitForDuration:0.05]]];
[carBody1 runAction:[SKAction repeatActionForever:hover]];
return carBody1;
}
#end
I recently making a racing game with using the SpriteKit. I create an NSObject class called "GameObject" to store all the properties, like physics. I also create another NSObject class called "GameWorld" to store all the game objects, like creating player objects and other objects with their location and the functions of the direction buttons. However, when I start to write a game scene class, I cannot add the world object to the scene, which means the car doesn't show up in the scene. Would anyone can help me about this question? The codes of my SKScene class is provided below,
#interface GamePlay : SKScene
#property (nonatomic) GameWorld *world;
-(void)update:(NSTimeInterval)currentTime;
-(id) initWithSize:(CGSize)s andWorld:(GameWorld *)w;
#end
#implementation GamePlay
- (id) initWithSize:(CGSize)s andWorld:(GameWorld *)w
{
self = [super initWithSize:s];
if (self)
{
_world = w;
}
return self;
}
-(void) didMoveToView:(SKView *)view
{
if (!self.contentCreated ) {
[self createSceneContents];
self.contentCreated = YES;
}
}
-(void) createSceneContents
{
// turn off gravity for the world
self.physicsWorld.gravity = CGVectorMake(0.0f, 0.0f);
self.scaleMode = SKSceneScaleModeAspectFit;
//Create the background
SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:#"road.png"];
background.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
[self addChild:background];
//Create the buttons for directions
SKSpriteNode *up = [SKSpriteNode spriteNodeWithImageNamed:#"Up.png"];
up.position = CGPointMake(290, 115);
up.name = #"upDirection";
[self addChild:up];
SKSpriteNode *down = [SKSpriteNode spriteNodeWithImageNamed:#"Down.png"];
down.position = CGPointMake(290, 40);
down.name = #"downDirection";
[self addChild:down];
SKSpriteNode *left = [SKSpriteNode spriteNodeWithImageNamed:#"Left.png"];
left.position = CGPointMake(30, 40);
left.name = #"leftDirection";
[self addChild:left];
SKSpriteNode *right = [SKSpriteNode spriteNodeWithImageNamed:#"Right.png"];
right.position = CGPointMake(CGRectGetMidX(self.frame), 40);
right.name = #"rightDirection";
[self addChild:right];
// add objects from GameWorld to this scene using the world's worldNode property
// Here is the place I confuse. the world doesn't show up
[self addChild:_world.worldNode];
}
//making buttons for the cars
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
//setting a node to keep the position of each direction buttons
SKNode *n = [self nodeAtPoint:positionInScene];
NSLog(#"Node n:%#", n);
//compare the location of each SKSpriteNode and the touch location
SKNode *up = [self childNodeWithName:#"upDirection"];
if (n == up) {
[self.world goForward];
}
SKNode *down =[self childNodeWithName:#"downDirection"];
if (n == down) {
[self.world goBackward];
}
SKNode *left = [self childNodeWithName:#"leftDirection"];
if (n == left) {
[self.world goLeft];
}
SKNode *right = [self childNodeWithName:#"rightDirection"];
if (n == right) {
[self.world goRight];
}
}
-(void)update:(NSTimeInterval)currentTime
{
//I don't know how to start yet. = =
}
#end
The GameWorld Class is provided below:
#interface GameWorld : NSObject
#property (nonatomic) SKNode *worldNode;
#property (nonatomic) NSArray *gameObjs;
-(id)init;
-(void) initializeWorld;
-(void) goForward;
-(void) goBackward;
-(void) goLeft;
-(void) goRight;
#end
#implementation GameWorld
-(id)init
{
if (self) {
self = [super init];
GameObject *player = [[GameObject alloc] initWithImageNamed:#"pCar.png" andPosition:CGPointMake(200, 85)];
GameObject *otherCar = [[GameObject alloc]initWithImageNamed:#"AICar.png" andPosition:CGPointMake(200, 100)];
_worldNode = [SKNode node];
// now create the objects, put them in an SKNode
// create the _worldNode with a size equal to the virtual world size
// then add the game objects to that node
_worldNode.scene.size = CGSizeMake(320, 480);
_gameObjs = #[player, otherCar];
}
return self;
}
- (void) initializeWorld
{
// add skSpriteNodes to the worldNode
SKSpriteNode *player = [[SKSpriteNode alloc] initWithImageNamed:#"pCar.png"];
[_worldNode addChild:player];
SKSpriteNode *otherCar = [[SKSpriteNode alloc] initWithImageNamed:#"AICar.png"];
[_worldNode addChild:otherCar];
}
-(void) goForward
{
GameObject *thePlayer = _gameObjs[1];
thePlayer.node.position = CGPointMake(thePlayer.node.position.x, thePlayer.node.position.y + 10);
}
-(void) goBackward
{
GameObject *thePlayer = _gameObjs[1];
thePlayer.node.position = CGPointMake(thePlayer.node.position.x, thePlayer.node.position.y - 10);
}
-(void) goLeft
{
GameObject *thePlayer = _gameObjs[1];
thePlayer.node.position = CGPointMake(thePlayer.node.position.x - 10, thePlayer.node.position.y);
}
-(void) goRight
{
GameObject *thePlayer = _gameObjs[1];
thePlayer.node.position = CGPointMake(thePlayer.node.position.x + 10, thePlayer.node.position.y);
}
#end
There is no need to use NSObject is most cases. Use SKNode and SKSpriteNode instead. By subclassing these classes you can add your custom properties to them.
I suggest you the following structure of the project:
-- GameScene (SKScene)
---- BackgroundLayer (SKNode)
-------- BackgroundNode (SKSpriteNode)
---- UiLayer (SKNode)
-------- your buttons here... (SKSpriteNode)
---- CarLayer (SKNode)
-------- PlayerNode (SKSpriteNode)
-------- OtherCarNode (SKSpriteNode)
In this case you don't have to store objects in manually created arrays, such as gameObjs. For example, all buttons can be accessed from this property: uiLayer.children.
I have created a template, which I use for my Sprite Kit games, take a look on it:
https://github.com/andrew8712/spritekit-game-template
I have a view controller that has three skscenes as children.
When I transition from one to another, the old skscene doesn't get deallocated.
I want it to get deallocated as if it was never there.
Example:
When I first load the app, only 1 skscene is visible (say it takes up 100mb memory), then I transition to another (100mb more), and then the third (300mb memory).
I would end up with 300mb memory and I want to have 100 at all times.
How can I achieve this?
My view controller:
//
// ViewController.m
// Paddle Jumper
//
// Created by Chance Daniel on 1/18/14.
// Copyright (c) 2014 Max Hudson. All rights reserved.
//
#import "Flurry.h"
#import "ViewController.h"
#import "startViewController.h"
#implementation ViewController{
BOOL sceneSetUp;
}
- (void)viewWillLayoutSubviews
{
if(!sceneSetUp){
[super viewWillLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
//skView.showsFPS = YES;
//skView.showsNodeCount = YES;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([[defaults objectForKey:#"firstTime"] intValue] != 1){
[defaults setObject:[NSNumber numberWithInt:1] forKey:#"firstTime"];
[defaults setObject:#"ggr" forKey:#"skinSelected"];
[defaults setObject:[NSNumber numberWithInt:2] forKey:#"ggrOwned"];
[defaults setObject:[NSNumber numberWithInt:5000] forKey:#"gona"];
[defaults setObject:[NSNumber numberWithInt:1500] forKey:#"points"];
[defaults setObject:[NSNumber numberWithInt:7] forKey:#"livesLeftValue"];
[defaults setObject:[NSNumber numberWithInt:3] forKey:#"shieldsLeftValue"];
[defaults setObject:[NSNumber numberWithInt:2] forKey:#"lvlTwoLeftValue"];
[defaults setObject:[NSNumber numberWithInt:0] forKey:#"lvlThreeLeftValue"];
}
if(![defaults objectForKey:#"tut_game1"]){
[defaults setObject:[NSNumber numberWithInt:1] forKey:#"tut_game1"];
[defaults setObject:[NSNumber numberWithInt:1] forKey:#"tut_store"];
[defaults setObject:[NSNumber numberWithInt:1] forKey:#"tut_daily"];
}
[defaults synchronize];
// Create and configure the scene.
SKScene * startScene = [StartViewController sceneWithSize:skView.bounds.size];
startScene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:startScene];
//[skView presentScene:scene];
sceneSetUp = YES;
}
}
-(void) switchScene{
}
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return UIInterfaceOrientationMaskAllButUpsideDown;
} else {
return UIInterfaceOrientationMaskAll;
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
#end
An SKScene that won't release: //
// SettingsSKScene.m
// Paddle Jumper
//
// Created by Max Hudson on 3/15/14.
// Copyright (c) 2014 Max Hudson. All rights reserved.
//
#import "SettingsSKScene.h"
#import "gameViewController.h"
#import "StoreScene.h"
#interface SettingsSKScene (){
SKSpriteNode *bg;
SKSpriteNode *masterOverlay;
SKSpriteNode *wbox;
SKSpriteNode *gamecenter;
SKSpriteNode *max;
SKSpriteNode *chance;
SKSpriteNode *bryce;
SKSpriteNode *home;
SKSpriteNode *play;
SKSpriteNode *chance_link;
SKSpriteNode *max_link;
SKSpriteNode *bryce_link;
SKSpriteNode *fbButton;
SKSpriteNode *fbToggleYes;
SKSpriteNode *fbToggleNo;
SKLabelNode *story1;
SKLabelNode *story2;
SKLabelNode *story3;
SKLabelNode *chance_name;
SKLabelNode *max_name;
SKLabelNode *bryce_name;
SKLabelNode *chance_role;
SKLabelNode *max_role;
SKLabelNode *bryce_role;
SKLabelNode *chance_handle;
SKLabelNode *max_handle;
SKLabelNode *bryce_handle;
SKTexture *bg_texture;
SKTexture *gamecenter_texture;
SKTexture *wbox_texture;
SKTexture *max_texture;
SKTexture *chance_texture;
SKTexture *bryce_texture;
SKTexture *home_texture;
SKTexture *play_texture;
SKTexture *fb_texture;
SKTexture *toggle_yes_texture;
SKTexture *toggle_no_texture;
}
#end
#implementation SettingsSKScene
-(id) initWithSize:(CGSize)size{
if(self = [super initWithSize:size]){
masterOverlay = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:self.frame.size];
masterOverlay.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
[self addChild:masterOverlay];
bg_texture = [SKTexture textureWithImageNamed:#"settings_bg"];
wbox_texture = [SKTexture textureWithImageNamed:#"white_rect_bg"];
gamecenter_texture = [SKTexture textureWithImageNamed:#"gc"];
max_texture = [SKTexture textureWithImageNamed:#"max"];
chance_texture = [SKTexture textureWithImageNamed:#"chance"];
bryce_texture = [SKTexture textureWithImageNamed:#"bryce"];
home_texture = [SKTexture textureWithImageNamed:#"home_light"];
play_texture = [SKTexture textureWithImageNamed:#"play_light"];
fb_texture = [SKTexture textureWithImageNamed:#"fb_light"];
toggle_yes_texture = [SKTexture textureWithImageNamed:#"toggle_yes"];
toggle_no_texture = [SKTexture textureWithImageNamed:#"toggle_no"];
NSArray *to_preload = #[bg_texture, wbox_texture, gamecenter_texture, max_texture, chance_texture, bryce_texture, home_texture, play_texture, fb_texture, toggle_yes_texture, toggle_no_texture];
[SKTexture preloadTextures:to_preload withCompletionHandler:^{
[self fadeRemove:masterOverlay];
[self initialize];
}];
}
return self;
}
-(void) fadeRemove: (SKNode *) node{
double duration = arc4random() % 10;
duration *= 0.05;
SKAction *fadeOut = [SKAction fadeOutWithDuration:0.1+duration];
SKAction *remove = [SKAction runBlock:^{
[node removeFromParent];
}];
[node runAction:[SKAction sequence:#[fadeOut, remove]]];
}
-(void) initialize{
bg = [SKSpriteNode spriteNodeWithTexture:bg_texture];
bg.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
bg.zPosition = 0;
wbox = [SKSpriteNode spriteNodeWithTexture:wbox_texture];
wbox.position = CGPointMake(self.frame.size.width/2, 70);
wbox.alpha = 0.8;
wbox.zPosition = 2;
gamecenter = [SKSpriteNode spriteNodeWithTexture:gamecenter_texture];
gamecenter.position = CGPointMake(self.frame.size.width/2 + 100, self.frame.size.height/2-2);
gamecenter.name = #"gc";
gamecenter.zPosition = 2;
fbButton = [SKSpriteNode spriteNodeWithTexture:fb_texture];
fbButton.size = CGSizeMake(36, 36);
fbButton.position = CGPointMake(self.frame.size.width/2 - 115, self.frame.size.height/2-2);
fbButton.name = #"fb";
fbButton.zPosition = 2;
fbToggleNo = [SKSpriteNode spriteNodeWithTexture:toggle_no_texture];
fbToggleNo.position = CGPointMake(self.frame.size.width/2 - 50, self.frame.size.height/2-2);
fbToggleNo.size = CGSizeMake(fbToggleNo.size.width/3, fbToggleNo.size.height/3);
fbToggleNo.name = #"fbno";
fbToggleNo.zPosition = 2;
fbToggleYes = [SKSpriteNode spriteNodeWithTexture:toggle_yes_texture];
fbToggleYes.position = CGPointMake(self.frame.size.width/2 - 70, self.frame.size.height/2-2);
fbToggleYes.name = #"fbyes";
fbToggleYes.zPosition = 2;
int hpBuffer = 40;
int hpZ = 2;
home = [SKSpriteNode spriteNodeWithTexture:home_texture];
home.position = CGPointMake(hpBuffer, self.frame.size.height - hpBuffer);
home.zPosition = hpZ;
home.name = #"home";
play = [SKSpriteNode spriteNodeWithTexture:play_texture];
play.position = CGPointMake(self.frame.size.width - hpBuffer, self.frame.size.height - hpBuffer);
play.zPosition = hpZ;
play.name = #"play";
[self addChild:bg];
[self addChild:wbox];
[self addChild:gamecenter];
[self addChild:home];
[self addChild:play];
[self addChild:fbButton];
[self addChild:fbToggleNo];
[self addCredits];
[self addStory];
}
-(void) addCredits{
/* images */
int cmbZ = wbox.zPosition + 1;
int cmbY = wbox.position.y;
int cmbXUnit = 132;
int cmbX = self.frame.size.width/2 - (3*cmbXUnit)/2 + 20;
int cmbWidth = 40;
chance = [SKSpriteNode spriteNodeWithTexture:chance_texture];
max = [SKSpriteNode spriteNodeWithTexture:max_texture];
bryce = [SKSpriteNode spriteNodeWithTexture:bryce_texture];
chance.zPosition = cmbZ;
max.zPosition = cmbZ;
bryce.zPosition = cmbZ;
chance.position = CGPointMake(cmbX+cmbXUnit*0, cmbY+3);
max.position = CGPointMake(cmbX+cmbXUnit*1 + 10, cmbY);
bryce.position = CGPointMake(cmbX+cmbXUnit*2 + 10, cmbY+5);
chance.size = CGSizeMake(cmbWidth, (chance.size.height/chance.size.width)*cmbWidth);
max.size = CGSizeMake(cmbWidth, (max.size.height/max.size.width)*cmbWidth);
bryce.size = CGSizeMake(cmbWidth, (bryce.size.height/bryce.size.width)*cmbWidth);
[self addChild:chance];
[self addChild:max];
[self addChild:bryce];
/* names */
int cmb_nameXUnit = 30;
int cmb_nameY = wbox.position.y - 10;
int cmb_nameFontSize = 18;
chance_name = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
max_name = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
bryce_name = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
chance_name.text = #"Chance Daniel";
max_name.text = #"Max Hudson";
bryce_name.text = #"Bryce Daniel";
chance_name.fontColor = [SKColor blackColor];
max_name.fontColor = [SKColor blackColor];
bryce_name.fontColor = [SKColor blackColor];
chance_name.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
max_name.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
bryce_name.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
chance_name.position = CGPointMake(chance.position.x + cmb_nameXUnit, cmb_nameY);
max_name.position = CGPointMake(max.position.x + cmb_nameXUnit, cmb_nameY);
bryce_name.position = CGPointMake(bryce.position.x + cmb_nameXUnit, cmb_nameY);
chance_name.fontSize = cmb_nameFontSize;
max_name.fontSize = cmb_nameFontSize;
bryce_name.fontSize = cmb_nameFontSize;
chance_name.zPosition = cmbZ;
max_name.zPosition = cmbZ;
bryce_name.zPosition = cmbZ;
[self addChild:chance_name];
[self addChild:max_name];
[self addChild:bryce_name];
/* roles */
int cmb_roleY = wbox.position.y - 25;
int cmb_roleFontSize = 11;
chance_role = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
max_role = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
bryce_role = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
chance_role.text = #"Programmer";
max_role.text = #"Programmer";
bryce_role.text = #"Graphic Designer";
chance_role.fontColor = [SKColor darkGrayColor];
max_role.fontColor = [SKColor darkGrayColor];
bryce_role.fontColor = [SKColor darkGrayColor];
chance_role.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
max_role.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
bryce_role.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
chance_role.position = CGPointMake(chance.position.x + cmb_nameXUnit + 19, cmb_roleY);
max_role.position = CGPointMake(max.position.x + cmb_nameXUnit + 12, cmb_roleY);
bryce_role.position = CGPointMake(bryce.position.x + cmb_nameXUnit + 6, cmb_roleY);
chance_role.fontSize = cmb_roleFontSize;
max_role.fontSize = cmb_roleFontSize;
bryce_role.fontSize = cmb_roleFontSize;
chance_role.zPosition = cmbZ;
max_role.zPosition = cmbZ;
bryce_role.zPosition = cmbZ;
[self addChild:chance_role];
[self addChild:max_role];
[self addChild:bryce_role];
/* twitter handles */
int cmb_handY = wbox.position.y - 40;
int cmb_handFontSize = 10;
chance_handle = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
max_handle = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
bryce_handle = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
chance_handle.text = #"#ChanceOfThat";
max_handle.text = #"#max_hud";
bryce_handle.text = #"#BryceOfLife";
chance_handle.fontColor = [SKColor darkGrayColor];
max_handle.fontColor = [SKColor darkGrayColor];
bryce_handle.fontColor = [SKColor darkGrayColor];
chance_handle.position = CGPointMake(chance.position.x, cmb_handY);
max_handle.position = CGPointMake(max.position.x, cmb_handY);
bryce_handle.position = CGPointMake(bryce.position.x, cmb_handY);
chance_handle.fontSize = cmb_handFontSize;
max_handle.fontSize = cmb_handFontSize;
bryce_handle.fontSize = cmb_handFontSize;
chance_handle.zPosition = cmbZ;
max_handle.zPosition = cmbZ;
bryce_handle.zPosition = cmbZ;
[self addChild:chance_handle];
[self addChild:max_handle];
[self addChild:bryce_handle];
/* touchzones */
CGSize cmdL_size = CGSizeMake(120, 70);
SKColor *cmdL_color = [SKColor clearColor];
int cmdLZ = 3;
int cmdLX = cmbX + 40;
chance_link = [SKSpriteNode spriteNodeWithColor:cmdL_color size:cmdL_size];
max_link = [SKSpriteNode spriteNodeWithColor:cmdL_color size:cmdL_size];
bryce_link = [SKSpriteNode spriteNodeWithColor:cmdL_color size:cmdL_size];
chance_link.position = CGPointMake(cmdLX+cmbXUnit*0, cmbY);
max_link.position = CGPointMake(cmdLX+cmbXUnit*1 + 10, cmbY);
bryce_link.position = CGPointMake(cmdLX+cmbXUnit*2 + 10, cmbY);
chance_link.zPosition = cmdLZ;
max_link.zPosition = cmdLZ;
bryce_link.zPosition = cmdLZ;
chance_link.name = #"c_handle";
max_link.name = #"m_handle";
bryce_link.name = #"b_handle";
[self addChild:chance_link];
[self addChild:max_link];
[self addChild:bryce_link];
}
-(void) addStory{
int stX = self.frame.size.width/2;
int stZ = 2;
SKColor *stColor = [SKColor whiteColor];
story1 = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
story2 = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
story3 = [SKLabelNode labelNodeWithFontNamed:#"BebasNeue"];
story1.text = #"Gon";
story2.text = #"Help Gon-Gon and his friends get";
story3.text = #"Back to the time they came from!";
story1.fontColor = stColor;
story2.fontColor = stColor;
story3.fontColor = stColor;
story1.zPosition = stZ;
story2.zPosition = stZ;
story3.zPosition = stZ;
story1.position = CGPointMake(stX, self.frame.size.height - 55);
story1.fontSize = 50;
story2.position = CGPointMake(stX, self.frame.size.height - 95);
story2.fontSize = 30;
story3.position = CGPointMake(stX, self.frame.size.height - 120);
story3.fontSize = 20;
[self addChild:story1];
[self addChild:story2];
[self addChild:story3];
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touchUI = [touches anyObject];
CGPoint touchPoint = [touchUI locationInNode:self];
SKNode *touchNode = [self nodeAtPoint:touchPoint];
if([touchNode.name isEqualToString:#"home"]){
StartViewController *svc = [[StartViewController alloc] initWithSize:self.size];
SKTransition *fade = [SKTransition fadeWithColor :[SKColor blackColor] duration:0.4];
[self.view presentScene:svc transition:fade];
}
if([touchNode.name isEqualToString:#"play"]){
gameViewController *gvc = [[gameViewController alloc] initWithSize:self.size];
SKTransition *fade = [SKTransition fadeWithColor :[SKColor blackColor] duration:0.4];
[self.view presentScene:gvc transition:fade];
}
if([touchNode.name isEqualToString:#"gc"]){
NSDictionary * dict = [[NSDictionary alloc]initWithObjectsAndKeys:[NSNumber numberWithBool:1], #"showGC", nil];
[[NSNotificationCenter defaultCenter]postNotificationName:#"kNotificationUpdateBannerView" object:self userInfo:dict];
}
if([touchNode.name isEqualToString:#"c_handle"]){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"twitter:///user?screen_name=ChanceOfThat"]];
int worked = [[UIApplication sharedApplication] openURL:urlApp];
if(!worked){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"https://twitter.com/#!/ChanceOfThat"]];
[[UIApplication sharedApplication] openURL:urlApp];
}
}
if([touchNode.name isEqualToString:#"m_handle"]){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"twitter:///user?screen_name=max_hud"]];
int worked = [[UIApplication sharedApplication] openURL:urlApp];
if(!worked){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"https://twitter.com/#!/max_hud"]];
[[UIApplication sharedApplication] openURL:urlApp];
}
}
if([touchNode.name isEqualToString:#"b_handle"]){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"twitter:///user?screen_name=BryceOfLife"]];
int worked = [[UIApplication sharedApplication] openURL:urlApp];
if(!worked){
NSURL *urlApp = [NSURL URLWithString: [NSString stringWithFormat:#"%#", #"https://twitter.com/#!/BryceOfLife"]];
[[UIApplication sharedApplication] openURL:urlApp];
}
}
}
#end
A similar problem was faced by the person who asked this question.
When asked whether he was able to solve it, they said:
Yes, I did, there wasn't anything I could do about it from the scene
or Sprite Kit for that matter, I simply needed to remove the scene and
the view containing it completely from the parent view, cut all its
bonds to the other parts of the system, in order for the memory to be
deallocated as well.
You should use separate views for each of your scene and transition between these views. You can follow these steps to make it look natural:
1 - At the point you want to transition from one scene to the other, take a snapshot of the scene using the following code:
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, scale);
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Then, add this image as a subview of the SKView of the current scene, and remove the scene.
2 - Initialise the new scene on another view, and transition between the two views using UIView animation.
3 - Remove the first view from it's superview.
I am just starting out with SpriteKit in Swift and I had the same problem: My intro scene was playing some music that I wanted to stop after transitioning to the main menu scene, and I figured the deinit would be a good place to put the AVAudioPlayer.stop() call in, but deinit was never being called.
After looking around I learned that it may be because of some strong references to the scene, so in my GameViewController:UIViewController subclass I changed this code:
let intro = IntroScene(size: skView.bounds.size)
skView.presentScene(intro)
to
skView.presentScene(IntroScene(size: skView.bounds.size))
and in the intro scene, that I wanted to be deallocated, I changed
let mainMenu = MainMenuScene(size: self.size)
let crossFade = SKTransition.crossFadeWithDuration(1)
self.scene.view.presentScene(mainMenu, transition: crossFade)
to
self.scene.view.presentScene(MainMenuScene(size: self.size),
transition: SKTransition.crossFadeWithDuration(1))
and it worked! After the transition was complete the deinit method got called.
I assume that the outgoing scene was not being deinitialized because there were variables holding references to it.
December 2019/Swift 5
Update:
My layout:
I have a single view controller that contains 2 SKViews which each of them have their own unique SKScene that are presented at the same time. One SKView & its SKScene is the main overworld where the player character is rendered, controlled, NPC's rendered, camera tracking, the whole shebang etc., & the other SKView & its SKScene display the mini map of the overworld.
You can imagine there are also quite a number of SKSpriteNode's & lot of them ALWAYS have some kind of SKAction/animation running non-stop (swaying trees for instance). My SKScenes even contain their own arrays pointing at specific groups of SKSpriteNodes, such as, character nodes, building nodes, tree nodes. This is for quick access & convenience purposes.
Plus, I have a few singletons that either contain an array of SKTextures, or character models, etc.. They are kept around as an optimization for quick data access rather than reading from disc/accessing storage every time I need something.
There are even UIKit elements used for the game UI inside the view controller.
Many other objects, such as, my models that contain data on characters, buildings, the entire game session all have some kind of delegates pointing at someone. On top of all of this the codebase is massive.
After observing memory in the debug session I found out the sure-fire way to make sure nothing is retained is to absolutely make sure the following:
Memory handling:
Pass in nil for scene presentation & set that skview's property/pointer to nil as well
Remove all view controller primary view subviews
Absolutely set every delegate you have to nil !!!!!!!!!!!!!!
Remove all observers IF you happen to have any
Anywhere you have an array/dictionary or pointer to some kind of object YOU created, set it to nil/empty it
CRITICAL: put all of the above into a single function/method & make sure to call it right before changing view controllers!
*NOTE: IF you have 1 view controller for the entire app (congrats on squeezing everything - youz overlord squeezer), then do NOT nil everything & use caution. BUT the previous scene still needs to be set to nil during presentation.
So something like this if you're jumping between view controllers:
/// Remove all pointers to any data, nodes & views.
fileprivate func cleanUp() {
guard self.skView != nil else { return }
// Session was passing data updates to this view controller; time to nil it
self.gameSessionModel.delegate = nil
self.skView.presentScene(nil)
self.skViewMiniMap.presentScene(nil)
self.skView = nil
self.skViewMiniMap = nil
for subview in self.view.subviews {
subview.removeFromSuperview()
}
}
/// Take the user back to the main menu module/view controller.
fileprivate func handleMenuButton() {
// First, clean up everything
self.cleanUp()
// Then go to the other view controller
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "MainViewController")
self.show(view, sender: .none)
}
Call handleMenuButton() function or whatever function you use to present another view controller, but make sure the cleanUp() function is called within it!
*NOTE: Classes such as 'gameSessionModel' are entirely my own custom classes. Xcode will throw an error on such things (unless you magically have the same one...) so delete such things. They are only used as example code in case you have delegates pointing at the current view controller.
If you're only presenting different SKScene's with a single SKView, then your cleanUp() function can end up being "lighter" as such:
/// Remove all pointers to any data, nodes & views.
fileprivate func cleanUp() {
self.skView.presentScene(nil)
// Previous scene was set to nil, its deinit called. Now onto the new scene:
let gameScene = self.setupGameScene(id: "perhapsYouUseStringIDsToDisntiguishBetweenScenes?")
self.skView.presentScene(gameScene)
}
*NOTE: Do NOT forget to use the SKScene's deinit method to remove anything you won't need. It's a practice I use all the time for all my classes to untangle anything I might have missed.
There's nothing wrong with SKScene or SKView, as long as I can see. Make sure the scene instance is not strongly reference anywhere else, especially inside a block. Blocks are highly probable to be ignored.
More about weak reference inside a block: https://stackoverflow.com/a/17105368/571489
As far as I see, you do have a block strongly referencing the scene instance:
[SKTexture preloadTextures:to_preload withCompletionHandler:^{
[self fadeRemove:masterOverlay];
[self initialize];
}];
EDIT: My previous idea about an NSTimer was irrelevant
To make sure this is an issue isolated to this scene, override the dealloc methods of all scenes you might have (including this one) like this:
-(void)dealloc {
NSLog(#"Dealloc <scene name>");
}
Look at your other scene transitions, see if they deallocate properly. Find the differences between these scenes. This will help you see if it's an isolated issue or a bigger problem. Once you have the problem fixed be sure to comment out or remove the dealloc method as it is overriding the one that actually deallocates the memory. Hopefully this helps!
I'm trying to respond to Swipe Gestures in a scene and move a sprite in response. The sprite is displaying on the left side of the screen. The logging statement in the handleSwipeRight is going to the log. But the sprite isn't moving at all. Surely I'm just missing something basic on the SKAction?
Here's my Scene code :
MFFMyScene.h :
#import <SpriteKit/SpriteKit.h>
#interface MFFMyScene : SKScene <UIGestureRecognizerDelegate>
#property (nonatomic) SKSpriteNode *player;
#property (nonatomic) UISwipeGestureRecognizer * swipeRightGesture;
#property (nonatomic) UISwipeGestureRecognizer * swipeLeftGesture;
#property (nonatomic) UISwipeGestureRecognizer * swipeUpGesture;
#property (nonatomic) UISwipeGestureRecognizer * swipeDownGesture;
#end
Relevant bits from MFFMyScene.m :
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
NSLog(#"Size: %#", NSStringFromCGSize(size));
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:#"dungeon"];
SKTexture *texture = [atlas textureNamed:#"hero_trans.png"];
_player = [SKSpriteNode spriteNodeWithTexture:texture];
_player.position = CGPointMake(10, 150);
[self addChild:_player];
}
return self;
}
-(void) didMoveToView:(SKView *)view {
_swipeRightGesture = [[UISwipeGestureRecognizer alloc]
initWithTarget:self
action:#selector(handleSwipeRight:)];
_swipeRightGesture.direction = UISwipeGestureRecognizerDirectionRight;
[view addGestureRecognizer:_swipeRightGesture];
}
- (void)handleSwipeRight:(UISwipeGestureRecognizer *)sender {
if(sender.direction == UISwipeGestureRecognizerDirectionRight) {
NSLog(#"scene: SWIPE RIGHT");
SKAction *movePlayerRight = [SKAction moveByX:10.0
y:0.0
duration:100.0];
[_player runAction:movePlayerRight];
}
}
Make your properties strong, use setters to let system do right memory management for you, for instance
#property (nonatomic, strong) UISwipeGestureRecognizer *rightSwipe;
self.rightSwipe = /* init code here */
check for swipe gesture state:
- (void)swipeHandled:(UISwipeGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateRecognized) {
if (sender.direction == UISwipeGestureRecognizerDirectionRight) {
/* moving code here */
}
}
}
I was using this SKAction wrong. I was looking for an SKAction that would do 'move x/y every z seconds' or 'move x/y every frame for z seconds'. The way I originally had it written, the SKAction will move the sprite 10 points total over a period of 100 seconds.
So I think the sprite was moving after all, but my X/Y/duration values were such that the movement was so small that it looked like it wasn't moving. With x=10 and duration=100 seconds, it was moving 10 points over 100 seconds. I don't even understand point vs pixel, but that was slow movement.
I changed it to x=100, y=50, duration=1.0, and it moved just fine.
I have an issue with forward declaration in SKScene I have to show a label node's value from game scene to game over scene , for example , but the value it returns as null , here is my code :
GameOver Scene :
#import "MyScene.h"
#class MyScene;
#interface GameOver : SKScene {
MyScene *mainScene;
}
#implementation GameOver
- (void)didMoveToView:(SKView *)view {
scores = [[SKLabelNode alloc]initWithFontNamed:#"Pixel LCD7"];
scores.fontSize = 30;
scores.fontColor = [SKColor darkGrayColor];
//displaying score :
scores.text = [NSString
stringWithFormat:#"Score:%#",mainScene.scoreLabel.text];
scores.name = #"score";
scores.position = CGPointMake(CGRectGetMidX(self.frame), 230);
[self addChild:scores];
}
MyScene
#import "MyScene.h"
#class GameOver;
#interface MyScene : SKScene {
SKLabelNode *scoreLabel;
}
#property (nonatomic) SKLabelNode *scoreLabel;
It doesn't work because you initialise the label in wrong method.
Remove didMoveToView: method if you use it just to initialise and set up the label and move the code to initWithSize: method:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
// Your init code here
scores = [[SKLabelNode alloc]initWithFontNamed:#"Pixel LCD7"];
scores.fontSize = 30;
scores.fontColor = [SKColor darkGrayColor];
//displaying score :
scores.text = [NSString stringWithFormat:#"Score:%#",mainScene.scoreLabel.text];
scores.name = #"score";
scores.position = CGPointMake(CGRectGetMidX(self.frame), 230);
self addChild:scores];
}
return self;
}
You should add property to your GameOver Scene to accept your score or override initWithSize: to for example initWithSize:score: and you should update your score label when you initialise game over scene.