Physics Bodies are being offset from their node upon creation - objective-c

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];

Related

Revolve a SCNode object in the 3D Space using SceneKit

I am trying to learn my way around SceneKit and as an example, I thought I'd try to build a solar system using the SDK. I am able to add SCNode objects and configure its material properties like pattern and lighting. Also, I am able to rotate the planets, but I can't seem to understand how do I "revolve" them along a particular path.
My code so far looks like:
// create a new scene
SCNScene *scene = [SCNScene scene];
// create and add a camera to the scene
SCNNode *cameraNode = [SCNNode node];
cameraNode.camera = [SCNCamera camera];
[scene.rootNode addChildNode:cameraNode];
// place the camera
cameraNode.position = SCNVector3Make(0, 0, 2);
// create and add a 3d sphere - sun to the scene
SCNNode *sphereNode = [SCNNode node];
sphereNode.geometry = [SCNSphere sphereWithRadius:0.3];
[scene.rootNode addChildNode:sphereNode];
// create and configure a material
SCNMaterial *material = [SCNMaterial material];
material.diffuse.contents = [NSImage imageNamed:#"SunTexture"];
material.specular.contents = [NSColor whiteColor];
material.specular.intensity = 0.2;
material.locksAmbientWithDiffuse = YES;
// set the material to the 3d object geometry
sphereNode.geometry.firstMaterial = material;
// create and add a 3d sphere - earth to the scene
SCNNode *earthNode = [SCNNode node];
earthNode.geometry = [SCNSphere sphereWithRadius:0.15];
NSLog(#"Sun position = %f, %f, %f", sphereNode.position.x, sphereNode.position.y, sphereNode.position.z);
earthNode.position = SCNVector3Make(0.7, 0.7, 0.7);
NSLog(#"Earth position = %f, %f, %f", earthNode.position.x, earthNode.position.y, earthNode.position.z);
//earthNode.physicsBody = [SCNPhysicsBody dynamicBody];
[scene.rootNode addChildNode:earthNode];
SCNMaterial *earthMaterial = [SCNMaterial material];
earthMaterial.diffuse.contents = [NSImage imageNamed:#"EarthTexture"];
earthNode.geometry.firstMaterial = earthMaterial;
I read somewhere in AppleDocs that you need to add planets in the Sun's co-ordinate system and not in the root node but I am not sure if I understood that properly anyway.
Let me know how do you get about doing this.
SceneKit doesn't provide an animate-along-path API. There are several ways to set up something like a solar system, though.
Ever seen an orrery? This is the model that bit in the SCNNode documentation is talking about; it's also what the WWDC presentation does on the "Scene Graph Summary" slide. The trick is that you use one node to represent the frame of reference for a planet's revolution around the sun — like the armature that holds a planet in an orrery, if you make this node rotate around its central axis, any child node you attach to it will follow a circular path. The relevant code for this is in ASCSceneGraphSummary.m in that sample code project (condensed to just show node hierarchy setup here):
// Sun
_sunNode = [SCNNode node];
_sunNode.position = SCNVector3Make(0, 30, 0);
[self.contentNode addChildNode:_sunNode];
// Earth-rotation (center of rotation of the Earth around the Sun)
SCNNode *earthRotationNode = [SCNNode node];
[_sunNode addChildNode:earthRotationNode];
// Earth-group (will contain the Earth, and the Moon)
_earthGroupNode = [SCNNode node];
_earthGroupNode.position = SCNVector3Make(15, 0, 0);
[earthRotationNode addChildNode:_earthGroupNode];
// Earth
_earthNode = [_wireframeBoxNode copy];
_earthNode.position = SCNVector3Make(0, 0, 0);
[_earthGroupNode addChildNode:_earthNode];
// Moon-rotation (center of rotation of the Moon around the Earth)
SCNNode *moonRotationNode = [SCNNode node];
[_earthGroupNode addChildNode:moonRotationNode];
// Moon
_moonNode = [_wireframeBoxNode copy];
_moonNode.position = SCNVector3Make(5, 0, 0);
[moonRotationNode addChildNode:_moonNode];
You can't make an animation that follows a specific path you model in 3D space, but you could make a keyframe animation that interpolates between several positions in 3D space. The (untested) code below moves a node around a square shape in the xz-plane... add more points to get a rounder shape.
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPaths:#"position"];
anim.values = #[
[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, 1)],
[NSValue valueWithSCNVector3:SCNVector3Make(1, 0, 0)],
[NSValue valueWithSCNVector3:SCNVector3Make(0, 0, -1)],
[NSValue valueWithSCNVector3:SCNVector3Make(-1, 0, 0)],
];
[node addAnimation:anim forKey:"orbit"];
Use physics! The SCNPhysicsField class models radial gravity, so you can just drop a gravity field into a scene, add some physics bodies for planets, and sit back and watch your solar system cataclysmically destroy itself come to life! Getting realistic behavior here takes a lot of trial and error — you'll need to set an initial velocity for each planet tangential to what you expect its orbit to be, and tweak that velocity to get it right. Researching some ephemeris data for our solar system might help.

SKShapeNode not maintaining center position while applying scale

I'm starting to develop a game, and in a moment I need to show a circle which increases its size, I got that, but the problem is that it increases its size and change its position and finally disappear from the screen. I want it to stay on its initial position and increase its size from the initial center.
My code is the following:
CGRect circle = CGRectMake(skRand(0, self.size.width), skRand(0, self.size.height), 20, 20);
SKShapeNode *shapeNode = [[SKShapeNode alloc] init];
shapeNode.position = CGPointMake(skRand(0, self.size.width), skRand(0, self.size.height));
shapeNode.path = [UIBezierPath bezierPathWithOvalInRect:circle].CGPath;
shapeNode.fillColor = [SKColor blueColor];
shapeNode.strokeColor = nil;
[self addChild:shapeNode];
SKAction* zoom = [SKAction scaleTo:15.0 duration:10.0];
[shapeNode runAction:zoom];
Any ideas?
Than you very much!
If you want your circle to increase size from the center, try constructing the circle not by using CGRectMakebut instead use a CGMutablePathRef, make the path and set the circle's path to the one you created. I have something similar in my game and this worked for me:
SKShapeNode *turnSphere = [[SKShapeNode alloc] init];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, 0, 0, 30, 0.0, (2 * M_PI), NO);
turnSphere.path = path;
turnSphere.position = CGPointMake(220, 440);
[self addChild:turnSphere];
[turnSphere runAction:[SKAction scaleBy:3 duration:2]];
The circle should now scale up from the center. Hope this works!

Is it possible to use a circle (SKShapeNode) as a mask in Sprite Kit?

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.

SKPhysicsBody and [SKNode setScale:]

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.

Changing the layer shape for CPTPlotSpaceAnnotation?

I want to add a widget to my graph that a user can click and drag. I've managed to do this using CPTPlotSpaceAnnonation (because I also want it to also scroll with the data) and CPTBorderedLayer. The basic functionality works and now I want to improve the look of the widget. So far it's just a green circle.
The follow code generates the circle below, notice how the shadow is clipped to the rect of the layer and the border follows the layer and not the circle,
How can I stroke the circle and not the layer? I guess I can avoid clipping by making the frame of the layer larger. Is possible to use CAShapeLayer class with the Core Plot annotations? This would be an easy way of drawing the widgets.
// Add a circular annotation to graph with fill and shadow
annotation = [[CPTPlotSpaceAnnotation alloc]
initWithPlotSpace:graph.defaultPlotSpace
anchorPlotPoint:anchorPoint];
// Choosing line styles and fill for the widget
NSRect frame = NSMakeRect(0, 0, 50.0, 50.0);
CPTBorderedLayer *borderedLayer = [[CPTBorderedLayer alloc] initWithFrame:frame];
// Circular shape with green fill
CGPathRef outerPath = CGPathCreateWithEllipseInRect(frame, NULL);
borderedLayer.outerBorderPath = outerPath;
CPTFill *fill = [CPTFill fillWithColor:[CPTColor greenColor]];
borderedLayer.fill = fill;
// Basic shadow
CPTMutableShadow *shadow = [CPTMutableShadow shadow];
shadow.shadowOffset = CGSizeMake(0.0, 0.0);
shadow.shadowBlurRadius = 6.0;
shadow.shadowColor = [[CPTColor blackColor] colorWithAlphaComponent:1.0];
borderedLayer.shadow = shadow;
// Add to graph
annotation.contentLayer = borderedLayer;
annotation.contentLayer.opacity = 1.0;
[graph addAnnotation:annotation];
Don't set the border paths of the layer. Instead, just set the cornerRadius to half the width of the frame.
borderedLayer.cornerRadius = frame.size.width * 0.5;