Organizing zPosition values in Sprite Kit with enums - ios

We have been using enums to organize the zPositions of our sprites. As we started to add SKNodes with several subsprites to our game, the structure quickly began to break down. Some child sprites that were placed on the screen had to have negative values in order to be beneath other nodes with children. These negative values are hard to keep track of in relation to other sprites in separate enums.
Is there a better way to organize the zPosition of sprites (especially those with sub-sprites) than using enums?

Upon reading more into your issue, it looks like you are using multiple enums to organize your z order. I suggest using a single enum for the z - ordering of your game.

Use layers to organize your scene:
- (instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.view.ignoresSiblingOrder = YES;
[self addLayers];
}
return self;
}
- (void)addLayers {
self.backgroundLayer = [SKNode node];
self.backgroundLayer.zPosition = 100;
[self addChild:self.backgroundLayer];
self.playerLayer = [SKNode node];
self.playerLayer.zPosition = 300;
[self addChild:self.playerLayer];
self.enemyLayer = [SKNode node];
self.enemyLayer.zPosition = 400;
[self addChild:self.enemyLayer];
// UI elements must be on top of all nodes on the scene
self.uiLayer = [SKNode node];
self.uiLayer.zPosition = 1000;
}
- (void)addBackgrounds {
SKSpriteNode *backgroundNode1 = [SKSpriteNode spriteNodeWithTexture:[self backgroundTexture1]];
backgroundNode1.zPosition = 10;
[self.backgroundLayer addChild:backgroundNode1];
SKSpriteNode *backgroundNode2 = [SKSpriteNode spriteNodeWithTexture:[self backgroundTexture2]];
backgroundNode2.zPosition = 20;
[self.backgroundLayer addChild:backgroundNode2];
}
.... etc

Related

Change Physical body frame in spritekit

I develop an iOS game using SpriteKit (such a helpful framework to quickly make a game). I add texture and configure a physical body for a main character as image
The green rectangle is the frame of the physical body. I'm using the following code to create it
#interface MainCharacter : SKSpriteNode
#end
#implementation MainCharacter
+ (instancetype)mainCharacterAtPosition:(CGPoint)pos {
MainCharacter* mainChar = [[MainCharacter alloc] initWithTexture:[SKTexture textureWithImageNamed:#"stand_up"]];
mainChar.position = pos;
mainChar.xScale = 0.5f;
mainChar.yScale = 0.5f;
return mainChar;
}
- (instancetype)initWithTexture:(SKTexture *)texture {
if (self = [super initWithTexture:texture]) {
self.name = kCharacterName;
self.anchorPoint = CGPointMake(0.5f, 0.0f);
[self standup];
CGSize spriteSize = self.size;
CGPoint center = CGPointMake(spriteSize.width*(self.anchorPoint.x-0.5f), spriteSize.height*(0.5f-self.anchorPoint.y));
self.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:spriteSize center:center];
self.physicsBody.dynamic = NO;
self.physicsBody.categoryBitMask = kCharacterCategory;
self.physicsBody.contactTestBitMask = 0x0;
self.physicsBody.collisionBitMask = 0x0;
}
return self;
}
- (void)standup {
SKAction* standupAction = [SKAction setTexture:self.standupTexture resize:YES];
[self runAction:standupAction];
}
- (void)standdown {
SKAction* standownAction = [SKAction setTexture:self.standdownTexture resize:YES];
[self runAction:standownAction completion:^{
}];
[self performSelector:#selector(standup) withObject:nil afterDelay:1.0f];
}
MainCharacter is a class that inherits from SKSPriteNode, just an convienient class to manage a main character. Stand Up is a first state of the character. I have another state, temporarily called stand down (demonstrate as following image)
I add a swipe down gesture to make character stand down.
The green rectangle also the physical body but it's too large for the character. I want to make a physical body frame as the red rectangle.
Can anyone help me how to make the physical body smaller when my character stand down and enlarge the physical body after it stands up
You can destroy the current physics body self.physicsBody = nil; and then simply create a new one with the new size requirements.
I solve this problem by using 2 nodes for 2 states (as a suggestion): stand up state and stand down state. I named it
standupNode and standdownNode
First, add the standupNode to the game scene. If swipe donw gesture recognize, I remove the standupNode from game scene and add the standdownNode instead. On contrary, removing the standdownNode from the game scene then add the standupNode if character stands up

How to have my character "ride" along with moving platforms?

Im experiencing a problem with my Hero Character, when he lands on a moving platform, rather then moving along with it, he literally stands on the same X position until the platform moves offscreen. I searched many answers with no avail.
Here is my methods for my hero character and the moving platforms.
-(void)HeroAdd
{
_Hero = [SKSpriteNode spriteNodeWithImageNamed:#"Hero-1"];
_Hero.name = #"Daniel";
_Hero.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_Hero.size];
_Hero.physicsBody.categoryBitMask = fPlayerCategory;
_Hero.physicsBody.contactTestBitMask = fPlatformCategory | fEnemyCategory;
_Hero.physicsBody.usesPreciseCollisionDetection = YES;
_Hero.physicsBody.affectedByGravity = YES;
_Hero.physicsBody.dynamic = YES;
_Hero.physicsBody.friction = .9;
_Hero.physicsBody.restitution = 0;
_Hero.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
_Hero.position = CGPointMake(_Hero.position.x - 252, _Hero.position.y + 50);
if (self.size.width == 480) {
_Hero.position = CGPointMake(_Hero.position.x + 44, _Hero.position.y);
}
[self addChild:_Hero];
}
My moving platform code
-(void)createPlatform {
SKTexture *objectTexture;
switch (arc4random_uniform(2)) {
case (0):
objectTexture = [SKTexture textureWithImageNamed:#"shortPlatform"];
break;
case (1):
objectTexture = [SKTexture textureWithImageNamed:#"highPlatform"];
default:
break;
}
SKSpriteNode *variaPlatform = [SKSpriteNode spriteNodeWithTexture:objectTexture];
variaPlatform.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
variaPlatform.position = CGPointMake(variaPlatform.position.x + 500, variaPlatform.position.y - 140);
variaPlatform.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:variaPlatform.size];
variaPlatform.physicsBody.usesPreciseCollisionDetection = YES;
variaPlatform.physicsBody.categoryBitMask = fPlatformCategory;
variaPlatform.physicsBody.contactTestBitMask = fPlatformCategory |fPlayerCategory | fEnemyCategory;
variaPlatform.physicsBody.dynamic = NO;
variaPlatform.physicsBody.affectedByGravity = NO;
SKAction *moveLeft = [SKAction moveTo:CGPointMake(180, variaPlatform.position.y) duration:3];
SKAction *moveDown = [SKAction moveTo:CGPointMake(180, -700) duration:4];
SKAction *removeFromParent = [SKAction removeFromParent];
SKAction *AllThree = [SKAction sequence:#[moveLeft, moveDown, removeFromParent]];
[self addChild:variaPlatform];
[variaPlatform runAction:AllThree];
}
Any type of information would be truly appreciated.
I ran into the same issue when I added conveyer belts into a game. Sprite Kit does not drag objects no matter how much resistance is added. You currently have 2 options.
Option 1 - Add a ledge at each end of your platform. This is the easiest to implement but is less graceful and still allows the player to slide off if he lands on the ledge.
Option 2 -
Step 1: Add code which makes the player move in sync with the horizontal moving platform. You can either use something like self.physicsBody.velocity = CGVectorMake(-50, self.physicsBody.velocity.dy); or use self.position = CGPointMake(self.position.x+10, self.position.y);. You will have to play around with the x values to sync them to the platform's speed.
Step 2: Activate the above code whenever the player makes contact with the platform and deactivate when contact is lost.
Step 3: In case the platform switches directions, set up left and right limits which notify you via contact when the platform switches direction. Depending on the platform's direction you apply either +x or -x movement values to your player.
I know this option sounds complicated but it is not. You just need to go step by step.
* EDIT to provide sample code *
This is the logic I have behind the horizontal moving platforms:
PLATFORMS
If you have more than 1 horizontal moving platform, you will need to store them in an array in the GameScene (my Levels). I have created my own class for them but you do not have to do this.
You will have to set left and right limits (invisible SKNodes with contacts) to set a BOOL property for the platform which tells the player class which way to push as each platform will probably not be the same length. This is why you need to keep a reference to each platform (hence the array).
PLAYER
When the player jumps on the platform, set a Player class property BOOL to TRUE which activates the constant left or right push depending on which way the platform is currently moving. On the flip side, losing the contact cancels the push.
// This code in my "Levels class" which is the default GameScene class.
- (void)didBeginContact:(SKPhysicsContact *)contact
{
uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask);
if (collision == (CategoryPlatformHorizontal | CategoryPlayer))
{
[_player setPlatformHorizontalContact:true];
for(Platform *platformObject in platformArray)
{
if(([platformObject.name isEqualToString:contact.bodyB.node.name]) || ([platformObject.name isEqualToString:contact.bodyA.node.name]))
{
_player.currentPlatform = platformObject;
}
}
}
}
- (void)didEndContact:(SKPhysicsContact *)contact
{
uint32_t collision = (contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask);
if (collision == (CategoryPlatformHorizontal | CategoryPlayer))
{
[_player setPlatformHorizontalContact:false];
}
}
// This code is in Player.h
#property (strong) Platform *currentPlatform;
#property BOOL platformHorizontalContact;
// This code is in Player.m
- (void)update:(NSTimeInterval)currentTime
{
if(self.platformHorizontalContact == true)
{
if(self.currentPlatform.movingLeft == true)
{
self.physicsBody.velocity = CGVectorMake(-75, self.physicsBody.velocity.dy); // set your own value depending on your platform speed
} else {
self.physicsBody.velocity = CGVectorMake(75, self.physicsBody.velocity.dy); // set your own value depending on your platform speed
}
}
}

Image doesn't fit SKSpriteNode frame

I'm trying to create a player node on my scene. But the image "player.png" doesn't fit the physics body. They are both separate.
PhysicsBody is in the ground and the image is floating on the air. What's wrong with my code? How do I make them both together?
#import "Player.h"
#implementation Player
- (instancetype)init {
self = [super initWithImageNamed:#"player.png"];
self.name = #"player";
self.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(50, 50)];
self.physicsBody.dynamic = YES;
self.physicsBody.allowsRotation = NO;
self.physicsBody.affectedByGravity = YES;
self.zPosition = 100;
self.anchorPoint = CGPointMake(0.5, 0);
return self;
}
#end
// myScene.h
-(void)createSceneContents {
self.currentBackground = [Background generateBackground];
[self addChild: self.currentBackground];
self.physicsWorld.gravity = CGVectorMake(0, _gravity);
self.physicsWorld.contactDelegate = self;
Player *player = [[Player alloc]init];
player.position = CGPointMake([UIScreen mainScreen].applicationFrame.size.width/2, 50);
[self addChild:player];
}
Anchor point is the center of your physics body also here.
Your point 0.5, 0.0 means center x and zero y.
So, the center of your physics body is at the bottom edge of your sprite node.
Likely the bottom center of the image.
But the physics body extends downward from there.
This is because of the method you used to create the physics body.
Anchor points are confusing.
They play dual roles sometimes.
They include a lot of poorly documented implicit behaviors.
Unless you have logic relying on the anchor point, it's best to shy away from changing them.
With a physics body, what matters is where the body is in the physics world.
Keep your model of the sprite as simple as possible.
Refine constantly towards the simplest model to get the job done. It will simplify your game logic.

Access property of current SKScene?

I'm testing out a few different technologies to integrate into my Sprite Kit-powered game, and I'm stumbling on what I'm sure is a simple task.
The test project I'm using came as part of a framework that integrates Spine with Sprite Kit. Everything is contained (and defined) in a single ViewController (which creates one of four views depending on which option of a UISegmentedControl is currently selected). At the bottom of the screen I have a UISwitch which is hooked up to an action:
-(IBAction)changeCostume:(UISwitch *)sender {
if(sender.on) {
NSLog(#"New costume");
} else {
NSLog(#"Old costume");
}
}
The scene I'm working on is defined like this:
+ (SKScene *) buildAvatarWithSize:(CGSize) size
{
SpineSkeleton *avatar = [DZSpineSceneBuilder loadSkeletonName:#"test-avatar" scale:1]; //json
spSkeleton_setSkinByName(avatar.spineContext->skeleton, "test-avatar");
spSkeleton_setSlotsToSetupPose(avatar.spineContext->skeleton);
DZSpineSceneDescription *sceneDesc = [DZSpineSceneDescription description];
... define animations etc. here...
NSArray *nodes = [sceneDesc buildScene];
SKNode *placeHolder = [SKNode node];
placeHolder.position = CGPointMake(100, size.height);
placeHolder.name = #"root";
... more setup code...
SKScene *scene = [[SKScene alloc] initWithSize:size];
scene.scaleMode = SKSceneScaleModeAspectFill;
scene.backgroundColor = [UIColor whiteColor];
[scene addChild:placeHolder];
return scene;
}
The property I want to access is sceneDesc - I need to modify its contents on the fly, when the UISwitch is pressed. I know that it's possible from within the scene setup code, but I'm having trouble figuring out how to do so once the scene's on screen.
Changing values of this property will alter the SKTexture used in various nodes.
It probably doesn't help that I'm still new to Objective-C, and the setup of the project is rather different to how I've been working so far (splitting up scenes into their own classes, with properties that can be easily accessed programatically).
You can use NSNotificationCenter to send a message to the scene when the switch is pressed.
I actually gave up on trying to access sceneDesc programatically, and just changed the textures of the relevant SKSpriteNodes:
SKSpriteNode *root = (SKSpriteNode *)[placeHolder.children[0] childNodeWithName:#"root"];
SKSpriteNode *arm = (SKSpriteNode *)[root childNodeWithName:#"upper arm"];
SKSpriteNode *upperArm = (SKSpriteNode *)[arm childNodeWithName:#"upper arm"];
SKSpriteNode *lowerArm = (SKSpriteNode *)[[arm childNodeWithName:#"bone3"] childNodeWithName:#"lower arm"];
upperArm.texture = [SKTexture textureWithImageNamed:#"blue_sleeve_upper"];
lowerArm.texture = [SKTexture textureWithImageNamed:#"blue_sleeve_lower"];

SKPhysicsContactDelegate Not Working

I have the following code:
in my scene:
static const uint32_t enermyCategory = 0x1 << 0;
static const uint32_t fatherCategory = 0x1 << 1;
self.physicsWorld.contactDelegate = self;
//init ship
Ship *ship = [Ship getFather];
ship.position = CGPointMake(CGRectGetMaxX(self.frame) - ship.frame.size.width , ship.frame.size.height);
[self addChild: ship];
//init enermy
Enermy *ene = [[Enermy alloc] initWithImageNamed:enermyName gameScene:self];
ene.position = ship.position;
[self addChild:ene];
#pragma mark - Physics Delegate Methods
- (void)didBeginContact:(SKPhysicsContact *)contact{
NSLog(#"contact detected");
}
As you can see, I set both ship and energy at the same location to start with, so they will always collide.
For both classes, I have the following code in their init method:
//setup physics body
self.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.size];
self.physicsBody.dynamic = NO;
self.physicsBody.categoryBitMask = enermyCategory; #shipCategory for ship
self.physicsBody.collisionBitMask = 0;
self.physicsBody.contactTestBitMask = shipCategory; #enermyCategory for ship
What I found is that the NSLog is never get called, so that the physical collision detection never works, I've read a lot from apple developer tutorials and it seems all the same of what they had, not sure why is not working. I can see the ship and energy images on screen collide each other.
self.physicsBody.dynamic = NO;
Static (non-dynamic) bodies do not generate contact events. Make them dynamic.
Also, I see you're passing gameScene to an instance of Enemy. If Enemy has a strong reference to game scene (an ivar) then you may be creating a retain cycle here (enemy retains scene, scene retains enemy).
In Sprite Kit you can simply use self.scene to access the scene and if you need the scene during init, move that code into a setup method and call [ene setup] right after adding it as child to perform setup steps involving the scene.
Having the physicsBody set to dynamic = NO doesn't affect contacts. The more likely case is that you are setting the same contactBitMask and categoryBitMask for both nodes. You should be setting the ship to have the following:
self.physicsBody.categoryBitMask = shipCategory;
self.physicsBody.collisionBitMask = 0;
self.physicsBody.contactTestBitMask = enermyCategory;
The enemy should have the following:
self.physicsBody.categoryBitMask = enermyCategory;
self.physicsBody.collisionBitMask = 0;
self.physicsBody.contactTestBitMask = shipCategory;
Bottom line: they should be swapped in the ship.

Resources