In my current project using SpriteKit, I have a bunch of sprites that need to be scaled up and down independently at various times. The problem is that when I scale the node, the physics body doesn't scale with it so it screws up the physics. Here's a small example I put together for the purpose of this question:
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[n1 setScale:0.5];
Notice how the blue sprite (scaled down) sits on top of the red one but you can tell its physics body still has the dimension I told it, and it didn't scale.
So obviously, scaling down the node doesn't scale down the physicsBody. So my question is if I have to manually do it, how do I go about it?
I tried swapping the body with one of the right size when scaling, but then things get really convoluted if the old body had joints, etc... It'd be a lot simpler if I could just scale the existing body somehow.
Using either an SKAction or the Update loop, you can create a new SKPhysicsBody with the proper scale and apply it to the object. However, by doing so, you will lose the velocity. To fix this, you can do the following:
SKPhysicsBody *newBody = [SKPhysicsBody bodyWithRectangleOfSize:boxObject.size];
newBody.velocity = boxObject.physicsBody.velocity;
boxObject.physicsBody = newBody;
Physics body shapes can't be scaled. Definitely not in Box2D which Sprite Kit uses internally.
Besides internal optimizations, scaling a physics body would have far reaching implications. For example scaling a body's shape up could get the body stuck in collisions. It would affect how joints interact. It would definitely change the body's density or mass and thus its behavior.
You could use a SKNode to which you add the sprite without a physics body, and then add additional SKNode with bodies of given sizes. You could then enable or disable the bodies when you start scaling the sprite. If you time this right the player won't notice that the collision shape simply went from full size to half size while the sprite animates that scaling transition.
You would have to calculate the body's density and perhaps other properties according to its size though.
For anyone else reading this answer, I believe that now Physics Bodies scale with the SKNode. I am using Xcode 8.2.1 and Swift 3.0.2. I start a new Project, select Game, fill in the details. Change 2 files:
GameViewController.swift (add one line)
view.showsFPS = true
view.showsNodeCount = true
// add this to turn on Physics Edges
view.showsPhysics = true
Game Scene.swift (replace the sceneDidLoad()) function with this:
override func sceneDidLoad() {
var found = false
self.enumerateChildNodes(withName: "testSprite") {
node, stop in
found = true
}
if !found {
let testSprite = SKSpriteNode(color: UIColor.white, size: CGSize(width: 200.0, height: 200.0))
testSprite.physicsBody = SKPhysicsBody(polygonFrom: CGPath(rect: CGRect(x: -100.0, y: -100.0, width: 200.0, height: 200.0), transform: nil))
testSprite.physicsBody?.affectedByGravity = false
testSprite.name = "testSprite"
let scaleAction = SKAction.repeatForever(SKAction.sequence([
SKAction.scale(to: 2.0, duration: 2.0),
SKAction.scale(to: 0.5, duration: 2.0)
]))
testSprite.run(scaleAction)
self.addChild(testSprite)
}
}
The physics boundary is scaling with the sprite node.
I had the exact same problem, I think its a bug in sprite kit.
Try using an action to scale, this works on whole scene.
[self runAction:[SKAction scaleTo:0.5 duration:0]];
My original question
Is this what you want to happen
- (void) anthoertest
{
CGSize objectSize = CGSizeMake(100, 100);
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 0, self.size.width, self.size.height)];
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n1.size];
n1.physicsBody.affectedByGravity = YES;
[self addChild:n1];
SKSpriteNode *n2 = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:objectSize];
n2.position = CGPointMake(self.size.width/2, self.size.height/3);
n2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:n2.size];
n2.physicsBody.affectedByGravity = YES;
[self addChild:n2];
[self shrinkMe:n1];
}
- (void) shrinkMe:(SKSpriteNode *) s
{
[s setScale:0.5];
s.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:s.size];
}
The physics body depends on the CGPath of the SKNode; you'll need to change that somehow. You might try scaling the CGPath with something like the following:
//scale up
CGFloat scaleFactor = 1.1;
CGAffineTransform scaleTransform = CGAffineTransformIdentity;
scaleTransform = CGAffineTransformScale(scaleTransform, scaleFactor, scaleFactor);
CGPathRef scaledPathRef = CGPathCreateCopyByTransformingPath(exampleNode.path, &scaleTransform);
exampleNode.path = scaledPathRef;
But keep in mind this won't update the appearance of nodes already in an SKScene. You might need to combine resizing the CGPath with an SKAction that scales the node.
I had this same problem, all I did was define my physics body in my update current time function and it would scale with it.
For you it would be something like this:
override func sceneDidLoad() {
SKSpriteNode *n1 = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:objectSize];
n1.position = CGPointMake(self.size.width/2, 2*self.size.height/3);
}
override func update(_ currentTime: TimeInterval) {
n1.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:objectSize];
n1.physicsBody.affectedByGravity = YES;
}
The only problem is that you have to redefine your physics properties every time in updateCurrentTime. You can make some of these properties like it's restitution equal a variable so you can change its properties by changing the variable elsewhere. Redefining it anywhere else only works until the next frame is called.
Related
I've written this code for the game over scene I have for a game:
#import "GameOverScene.h"
#import "SharedInfo.h"
#implementation GameOverScene
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
[self setupView];
}
-(void)showGameEndingWithGameInformation:(NSDictionary *)gameEndingInformation{
}
-(void)setupView{
SKLabelNode *GOTitle = [SKLabelNode labelNodeWithFontNamed:#"Generica Bold"];
GOTitle.fontSize = 40.f;
NSString* text = [NSString stringWithFormat:#"GAME OVER"];
[GOTitle setText:text];
GOTitle.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height- GOTitle.frame.size.height*1.5);
[GOTitle setFontColor:[[SharedInfo sharedManager]colorFromHexString:#"#2EB187"]];
[self addChild: GOTitle];
SKLabelNode *replayButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
replayButton.fontSize = 25.f;
NSString* replayText = [NSString stringWithFormat:#"Play Again"];
[replayButton setText:replayText];
replayButton.name = kGOSceneReplayButton;
replayButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5);
[replayButton setFontColor:[SKColor whiteColor]];
SKShapeNode *bgNode = [SKShapeNode shapeNodeWithRectOfSize:replayButton.frame.size];
[bgNode setFillColor:[UIColor redColor]];
[replayButton addChild:bgNode];
[self addChild:replayButton];
NSLog(#"replay dimensions: %#",NSStringFromCGRect(replayButton.frame));
SKLabelNode *returnButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
returnButton.fontSize = 25.f;
NSString* returnText = [NSString stringWithFormat:#"Return To Main Menu"];
[returnButton setText:returnText];
returnButton.name = kGOSceneReturnToMainButton;
returnButton.position = CGPointMake(CGRectGetMidX(self.frame), replayButton.position.y -self.frame.size.height/7 );
[returnButton setFontColor:[SKColor whiteColor]];
[self addChild:returnButton];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *sprite = [self nodeAtPoint:location];
NSLog(#"sprite name: %#",sprite.name);
if ([sprite.name isEqualToString:kGOSceneReturnToMainButton]||[sprite.name isEqualToString:kGOSceneReturnToMainButton]) {
//return to main menu or retry
[self.gameEndingSceneDelegate goToScene:sprite.name withOptions:nil]; //Sort out the options later on.
}
}
#end
When I run it though, I get this:
There are two issues I'm really confused about. Firstly, why do I have 8 nodes in the scene, where I should really have 4? I think something is doubling the nodes, but that's just a guess.
The more confusing issue is the red SKShapeNode positioning. I've read that scaling the parent node can cause problems to the child SKShapeNode, but I'm not scaling anything. Also, why does it place my red rectangle at a random position (it's not the middle of the parent, or corresponding with the bottom).
Thanks a lot for all the help in advance.
UPDATE 1: So following the suggestion, I checked if my method is being called twice, and thus creating the duplicates. No luck there, as it is only called once. The mystery still going strong!
As for the positioning shenanigans, I changed the code slightly to set the position of the red rectangle to match its parent node:
SKLabelNode *replayButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
replayButton.fontSize = 25.f;
NSString* replayText = [NSString stringWithFormat:#"Play Again"];
[replayButton setText:replayText];
replayButton.name = kGOSceneReplayButton;
replayButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5);
[replayButton setFontColor:[SKColor whiteColor]];
SKShapeNode *bgNode = [SKShapeNode shapeNodeWithRectOfSize:replayButton.frame.size];
[bgNode setFillColor:[UIColor redColor]];
[self addChild:replayButton];
bgNode.position = replayButton.position;
[replayButton addChild:bgNode];
But after updating, I got this:
In case it helps, this is what I do to present the scene:
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
scene = [GameOverScene sceneWithSize:self.view.frame.size];
[(GameOverScene*)scene setGameEndingSceneDelegate:self];
[(GameOverScene*)scene setScaleMode: SKSceneScaleModeAspectFill];
[(GameOverScene*)scene showGameEndingWithGameInformation:self.gameEndingInfo];
// Present the scene.
[skView presentScene:scene transition:sceneTransition];
Also, this is the output of my NSLog:
replay dimensions: {{221, 91}, {127, 25}
I've got a feeling that because I set my scene's setScaleMode, it gets strange, but nothing else is out of ordinary, so not sure what to do. I'm thinking maybe just create an image for my label and change the SKLabelNode to SKSpriteNode and set the image, so I skip adding the red rectangle as background for the label node. The reason I wanted to add the rectangle is actually to provide bigger hit target for when the 'Button' is tapped, so if anyone knows an easier, more straightforward way, I'd really appreciate it.
UPDATE 3:
I also tried setting the position of the rectangle to match that of parent label node:
bgNode.position = [replayButton convertPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5) fromNode:self];
the rectangle ends up at the same place as the last update (all the way to the right of the screen)
There are few issues with your code:
lineWidth property and it's default value of 1.0. It should be 0.0f
verticalAlignmentMode property and it's default baseline alignment. It should be SKLabelVerticalAlignmentModeCenter.
Wrong positioning of a shape node. It should be (0,0)
To fix it, change label's vertical alignment:
replayButton.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
set shapenode's lineWidth property to 0.0f:
bgNode.lineWidth = 0.0f;
and remove this line:
//bgNode.position should be CGPointZero which is (0,0)
bgNode.position = replayButton.position;
Still, I would stay away of this approach. SKShapeNode is not needed in this situation. You can do the same with SKSpriteNode. What is important is that both SKShapeNode and SKLabelNode can't be drawn in batches, which means, can't be drawn in a single draw pass when rendered like SKSpriteNode. Take a look at this. Your example is too simple to make performance issues, but in general you should keep all this in mind.
If your button's text never change during the game, you should consider using SKSpriteNode initialized with texture. If interested in a pre made buttons for SpriteKit, take a look at SKAButton.
Hope this helps!
I have been working on a puzzle game that uses a customizable grid with levels that are preset and created when the scene inits. They are comprised of static blocks scattered around the grid and a movable player controlled block. The gravity of the world is controllable and the block falls in that direction (up, down, left, right). I am using SKContactDelegate to track when the player block touches a block or the edge of the grid and stops it in place.
The problems I am having involve the physics bodies of the blocks and grid edge.
I am using bodyWithRectOfSize for the blocks and bodyWithEdgeLoopFromRect for the grid border.
The physics bodies of 1x1 blocks placed in the grid and the player 1x1 block have normal physics bodies (as they should be). However, larger blocks ex: 1x5, the bodies are shifted down on the y axis for no reason. Also depending on the grid size, the grid edge would be offset by a random number. Note: the bodies are offset and the nodes themselves are in the right place.
This is the code for creating the blocks; this one works fine
(cellsize is the size of each grid space)
Basic 1x1 block
-(SKShapeNode *)basic {
CGRect blockRect = CGRectMake(10, 10, self.cellSize, self.cellSize);
UIBezierPath *blockPath = [UIBezierPath bezierPathWithRoundedRect:blockRect cornerRadius:8];
SKShapeNode *blockNode = [SKShapeNode node];
blockNode.path = blockPath.CGPath;
blockNode.fillColor =[UIColor blackColor];
blockNode.lineWidth = 0;
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size];
blockNode.physicsBody.categoryBitMask = blockCategory;
blockNode.physicsBody.dynamic = NO;
return blockNode;
}
and custom blocks (offset down on the y axis)
-(SKShapeNode *)basicWithWidth:(int)width WithHeight:(int)height {
CGRect blockRect = CGRectMake(10, 10, self.cellSize * width, self.cellSize * height);
UIBezierPath *blockPath = [UIBezierPath bezierPathWithRoundedRect:blockRect cornerRadius:8];
SKShapeNode *blockNode = [self basic];
blockNode.path = blockPath.CGPath;
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size];
blockNode.physicsBody.categoryBitMask = blockCategory;
blockNode.physicsBody.dynamic = NO;
return blockNode;
}
And here is the grid edge
SKShapeNode *edge = [SKShapeNode node];
CGRect edgeRect = CGRectMake(10, 10, 260, 260);
UIBezierPath *edgeShape = [UIBezierPath bezierPathWithRoundedRect:edgeRect cornerRadius:8];
edge.path = edgeShape.CGPath;
edge.lineWidth = 0;
edge.fillColor = [UIColor grayColor];
edge.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:edgeRect];
edge.physicsBody.categoryBitMask = boardCategory;
[self addChild:edge];
Note: The edge and blocks are children to a "board" node that is a child of the game scene
Thank you for reading this.
When you use SKPhysicsBody's bodyWithRectangleOfSize:, it is centered on its node's origin. What you want to do in your case is center them on the center of the rect that defines your node. To do that, use bodyWithRectangleOfSize:center: instead.
For example, you define your block with the rect:
CGRect blockRect = CGRectMake(10, 10, self.cellSize, self.cellSize);
So, you'll want your physicsBody centered on the center of that rect. You easily get the center of a CGRect with:
CGPoint center = CGPointMake(CGRectGetMidX(blockRect), CGRectGetMidY(blockRect));
To create the physicsBody centered there, use:
blockNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:blockRect.size center:center];
Note: bodyWithRectangleOfSize:center: is only available in iOS 7.1 and later
iOS 7.0 method
For iOS 7.0, you can use bodyWithPolygonFromPath: instead, it just takes an additional line of code:
CGPathRef blockPath = CGPathCreateWithRect(blockRect, nil);
blockNode.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath: blockPath];
Here is my code:
// setting background
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.name = #"ground";
_ground.zPosition = 1;
_ground.anchorPoint = CGPointMake(0, 0);
_ground.position = CGPointMake(0, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.physicsBody.dynamic = NO;
_ground.physicsBody.affectedByGravity = NO;
_ground.physicsBody.categoryBitMask = groundCategory;
_ground.physicsBody.collisionBitMask = elementCategory;
_ground.physicsBody.contactTestBitMask = elementCategory;
Green rectangle positioning is ok, but SKPhysicsBody is not representing this rectangle correctly. It looks like SKPhysicsBody is not "moving" its body according to sprite position and anchorPoint. SKPhysicsBody is moved left for _ground.width points.
What am I doing wrong?
PS.
Changing to this (code) solved my problem, but I really dont like this solution, it seems an ugly way to position an element.
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
+removing:
_ground.position = CGPointMake(0, 0);
I had exactly the same issue yesterday ;)
For me I solved this by using the default sprite positioning by SpriteKit. I just set the position and leave the anchor point at it's default. Then you set the physicsbody to the size of the sprite.
Change your code to the following and check if it works:
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_ground.size];
If you still have no luck with this you can also try to use another initialisation for your sprite (this works in my project). Try it with a texture:
[[SKSpriteNode alloc] initWithTexture:[SKTexture textureWithCGImage:[UIImage imageWithColor:[UIColor whiteColor]].CGImage] color:[UIColor whiteColor] size:CGSizeMake(PLAYER_WIDTH, PLAYER_HEIGHT)];
don't forget to implement the (UIImage*)imageWithColor: method to create a 1x1 pixel image of your desired color for you (see here).
I've been trying to apply a mask to a SKShapeNode using SKCropNode, and so far without success. Thinking that it's a SpriteKit bug - Here is the code snippet:
SKNode* contentNode = [SKNode node];
// picture - use an image bigger than 50x50
SKSpriteNode *pictureNode = [SKSpriteNode spriteNodeWithImageNamed:#"tree"];
// triangle
SKShapeNode* triangleNode = [SKShapeNode node];
UIBezierPath* triangleNodeBezierPath = [[UIBezierPath alloc] init];
[triangleNodeBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(0.0, 100.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(50.0, 100.0)];
[triangleNodeBezierPath closePath];
triangleNode.path = triangleNodeBezierPath.CGPath;
triangleNode.fillColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1];
// create a mask
SKSpriteNode *mask = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size: CGSizeMake(50, 50)]; //50 by 50 is the size of the mask
// create a SKCropNode
SKCropNode *cropNode = [SKCropNode node];
[cropNode addChild: contentNode];
[cropNode setMaskNode: mask];
[self addChild: cropNode];
[contentNode addChild:pictureNode]; // pictureNode is being cropped
[contentNode addChild:triangleNode]; // triangleNode is not
cropNode.position = CGPointMake( CGRectGetMidX (self.frame), CGRectGetMidY (self.frame));
Does anyone have a workaround about this issue? Thanks a lot!
This had been bugging me for most of the day. I had planned to create a timer similar to the excellent TCProgressTimer by Tony Chamblee. However, as my application uses multiple progress timers I didn't want to have to design dozens of different sized sprites for use at different resolutions.
My solution was to convert SKShapeNode objects to SKSpriteNode objects. I ended up having to go back to basics and use Core Graphics to do the heavy lifting. This is a rather messy way of doing things, I'm sure, but I wanted quick results to dynamically create objects that would resemble the results obtained when using SKShapeNode.
I am only interested in making circle objects at present, so I did it like this:
-(SKSpriteNode *)createSpriteMatchingSKShapeNodeWithRadius:(float)radius color:(SKColor *)color {
CALayer *drawingLayer = [CALayer layer];
CALayer *circleLayer = [CALayer layer];
circleLayer.frame = CGRectMake(0,0,radius*2.0f,radius*2.0f);
circleLayer.backgroundColor = color.CGColor;
circleLayer.cornerRadius = circleLayer.frame.size.width/2.0;
circleLayer.masksToBounds = YES;
[drawingLayer addSublayer:circleLayer];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(circleLayer.frame.size.width, circleLayer.frame.size.height), NO, [UIScreen mainScreen].scale);
CGContextSetAllowsAntialiasing(UIGraphicsGetCurrentContext(), TRUE);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [UIColor clearColor].CGColor);
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,circleLayer.frame.size.width,circleLayer.frame.size.height));
[drawingLayer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *layerImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:layerImage]];
return sprite;
}
The resulting sprite can now be masked by an SKCropNode as expected. As these sprites are all generated before the scene begins, I do not notice a performance hit. However, I would imagine this method is highly inefficient if you are generating multiple nodes on the fly.
I would be eager to hear solutions from other users. Hope that helps.
-DC
I have the similar task in my app. I need to draw multiple irregular shapes based on user input and then use them as crop node's masks.
The solution that works for me is to:
create a SKShapeNode with the required path
retrieve a SKTexture from it using SKView method textureFromNode:crop:
create a SKSpriteNode from that texture.
use SKSprite node as a mask.
Your title is correct. I've also discovered having a ShapeNode in the CropNode's hierarchy affects the children above it as well. I created a quick experiment. You can create a new game project and try it yourself. By commenting out one of the addChild:shapeContent lines you can see how it affects the cropping of the spaceship image.
As DoctorClod points out the current solution seems to be making sure all children of the cropNode are SpriteNodes.
-(void)didMoveToView:(SKView *)view {
SKSpriteNode* colorBackground = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(800, 600)];
SKSpriteNode *spaceshipImage = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
SKShapeNode* shapeContent = [SKShapeNode shapeNodeWithRect:CGRectMake(0, 0, 200, 100)];
shapeContent.fillColor = [SKColor greenColor];
SKSpriteNode *maskShape = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:CGSizeMake(500, 100)];
SKCropNode *cropNode = [SKCropNode new];
[cropNode addChild:colorBackground];
[cropNode addChild:shapeContent]; // comment this one out
[cropNode addChild:spaceshipImage];
[cropNode addChild:shapeContent]; // or comment this one out
[cropNode setMaskNode:maskShape];
cropNode.position = CGPointMake(CGRectGetMidX (self.frame), CGRectGetMidY(self.frame));
[self addChild:cropNode];
}
I'm trying to create a circular mask in a Sprite Kit project. I create the circle like this (positioning it at the centre of the screen):
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES);
circleMask.path = circle;
circleMask.lineWidth = 0;
circleMask.fillColor = [SKColor blueColor];
circleMask.name=#"circleMask";
and further down the code, I set it as the mask for the cropNode:
[cropNode setMaskNode:circleMask];
... but instead of the content showing inside a circle, the mask appears as a square.
Is it possible to use a SKShapeNode as a mask, or do I need to use an image?
After much swearing, scouring the web, and experimentation in Xcode, I have a really hacky fix.
Keep in mind that this is a really nasty hack - but you can blame that on Sprite Kit's implementation of SKShapeNode. Adding a fill to a path causes the following:
adds an extra node to your scene
the path becomes unmaskable - it appears 'over' the mask
makes any non-SKSpriteNode sibling nodes unmaskable (e.g. SKLabelNodes)
Not an ideal state of affairs.
Inspired by Tony Chamblee's progress timer, the 'fix' is to dispense with the fill altogether, and just use the stroke of the path:
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES); // replace 50 with HALF the desired radius of the circle
circleMask.path = circle;
circleMask.lineWidth = 100; // replace 100 with DOUBLE the desired radius of the circle
circleMask.strokeColor = [SKColor whiteColor];
circleMask.name=#"circleMask";
[cropNode setMaskNode:circleMask];
As commented, you need to set the radius to half of what you'd normally have, and the line width to double the radius.
Hopefully Apple will look at this in future; for now, this hack is the best solution I've found (other than using an image, which doesn't really work if your mask needs to be dynamic).
Since I cannot use a SKShapeNode as mask I decided to convert it to a SKSpriteNode.
Here it is my Swift code:
let shape : SKShapeNode = ... // create your SKShapeNode
var view : SKView = ... // you can get this from GameViewController
let texture = view.textureFromNode(shape)
let sprite = SKSpriteNode(texture: texture)
sprite.position = ...
cropNode.mask = sprite
And it does work :)
Yes, it is impossible to use fill-colored shapenode in current Sprite-Kit realization. It's a bug, I think.
But!
You always can render the shape to texture and use it as mask!
For ex, let edge is SKShapeNode created before.
First, render it to texture before adding to view (in this case it will clean from another nodes)
SKTexture *Mask = [self.view textureFromNode:edge];
[self addChild:edge]; //if you need physics borders
Second,
SKCropNode *cropNode = [[SKCropNode alloc] init];
[cropNode setMaskNode:[SKSpriteNode spriteNodeWithTexture:Mask]];
[cropNode addChild: [SKSpriteNode spriteNodeWithTexture:rocksTiles size:CGSizeMake(w,h)]];
cropNode.position = CGPointMake(Mask.size.width/2,Mask.size.height/2);
//note, anchorpoint of shape in 0,0, but rendered texture is in 0.5,0.5, so we need to dispose it
[self addChild:cropNode];
There is a slightly awkward solution to this issue that I believe is slightly less nasty than any of the mooted hacks around this issue. Simply wrap your SKShapeNode in an SKNode. I haven't tested what kind of performance hit this might cause, but given that SKNode is non-drawing, I'm hoping it won't be too significant.
i.e.
let background = SKSpriteNode(imageNamed: "Background")
let maskNode = SKShapeNode(path: MyPath)
let wrapperNode = SKNode()
let cropNode = SKCropNode()
wrapperNode.addChild(maskNode)
cropNode.maskNode = wrapperNode
cropNode.addChild(background)
Setting alpha of mask node to .0 does the trick:
circleMask.alpha = .0;
EDIT:
Seems to work for Mac OS only.