I am planning a platforming game for iOS using SpriteKit and Swift. I did some research on how to handle collisions of the player sprite and stumbled upon this article.
http://www.learn-cocos2d.com/2013/08/physics-engine-platformer-terrible-idea/
That article advices you not to use the build-in physics engine of SpriteKit but to implement things like moving, jumping and collision handling on your own. The platform tutorial on Ray Wenderlichs site suggests a similar approach.
So far so good but let's talk about floor tiles on which the player can stand upon. A custom physics implementation would be easy as long as the tiles are rectangular and have a flat surface (like in the tutorial from Ray Wenderlich), since you would use CGRectIntersectsRect to detect a collision. But how would you check the player collision on curved or sloped tiles? From what I have read, CGRectIntersectsRect does not account for tranparent pixel inside a sprites rect.
http://i.stack.imgur.com/lRP2Q.png
Look at the above tile for example. The white area (upper left) would be transparent. Now, if the player sprite would drop on this tile, the collision would be detected at the upper border of the tiles rectangle, although there are no ground pixels there (blue area, lower right). So ultimately the player would hover in mid-air above this tile. I could push the player sprite down a few pixels but that is a bit hacky and gets harder if the curved floor tiles have different slope angles.
So the question is, how can I handle these types of collision with SpriteKit alone (no extra frameworks like Cocos2D, Kobold Kit, ...)? Or is this approach entirely wrong and collisions in platformer with SpriteKit should be done fundamentally different?
Any help is very much appreciated!
I disagree with not using physics to handle collisions and contacts. You are really just trying to reinvent the wheel here by not using physics and implementing your own custom code.
If you are using the Tiled app then assigning a physics body is a simple task. Use the Objects in Tiled to assign various body types. In your code you can then go about creating a specific body for each object type.
For example:
In the above image I have created a 45 degree right side sloped floor. The object I added is called floor45R.
The next step is to parse your map objects. In case of the 45floorR, you create a physics body like this:
NSArray *arrayObjects = [group objectsNamed:#"floor45R"];
for (NSDictionary *dicObj in arrayObjects) {
CGFloat x = [dicObj[#"x"] floatValue];
CGFloat y = [dicObj[#"y"] floatValue];
CGFloat w = [dicObj[#"width"] floatValue];
CGFloat h = [dicObj[#"height"] floatValue];
SKSpriteNode *myNode = [SKSpriteNode spriteNodeWithColor:[SKColor clearColor] size:CGSizeMake(w, h)];
myNode.position = CGPointMake(x, y);
myNode.zPosition = 100;
CGMutablePathRef trianglePath = CGPathCreateMutable();
CGPathMoveToPoint(trianglePath, nil, -myNode.size.width/2, myNode.size.height/2);
CGPathAddLineToPoint(trianglePath, nil, myNode.size.width/2, -myNode.size.height/2);
CGPathAddLineToPoint(trianglePath, nil, -myNode.size.width/2, -myNode.size.height/2);
CGPathAddLineToPoint(trianglePath, nil, -myNode.size.width/2, myNode.size.height/2);
myNode.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:trianglePath];
myNode.physicsBody.dynamic = NO;
myNode.physicsBody.restitution = 0;
myNode.physicsBody.friction = 0.0;
myNode.physicsBody.categoryBitMask = CategoryFloor45;
myNode.physicsBody.collisionBitMask = 0x00000000;
CGPathRelease(trianglePath);
[worldNode addChild:myNode];
}
This works for any degree floor slope. Remember to set your player's and any other node's collision bit mask to collide with the floor.
In order for your player move smoothly over sloped floor, I recommend building the player's physics body in 2 pieces. A circle at the bottom and a rectangle at the top. The circle will prevent getting stuck in any potential cracks caused by 2 joining physics bodies.
SKPhysicsBody *firstBody = [SKPhysicsBody bodyWithCircleOfRadius:10 center:CGPointMake(0, 0)];
SKPhysicsBody *secondBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(5, 50) center:CGPointMake(0, 10)];
self.physicsBody = [SKPhysicsBody bodyWithBodies:#[firstBody, secondBody]];
You will have to adjust the position and center coordinates to match your sprite's image.
You can make use of a level editor, Tiled to handle curved and sloped floor tiles along with JSTileMap which processes the TMX map.
It will be able to able to handle contact with curved and sloped floor tiles but as for adjusting the angle of objects standing on top of those tiles, you would have to utilize some math functions, a good example is found here
You can still make use of SpriteKit's collision detection which will simplify things for you but create your own jumping or movement engine.
Related
I have some simple code that adds a block sprite at the leftmost part of a tile like this :
block.position = CGPointMake((-self.size.width * 0.5), 0);
[self addChild:block];
Since the anchor point is in the middle of the block sprite. self refers to the actual tile.
This works fine and indeed adds the block on the left side of the tile sprite.
Now I also have a player sprite that can collide with that block if it tries to go through it. That also works just fine.
The problem happened when i tried to get the block sprite to show in the exact same spot using another anchor point (i need a new anchor point for a shrink effect i wanted to create - which appears to work fine btw).
The new code becomes :
block.position = CGPointMake(-(self.size.width * 0.5), -(self.size.width * 0.5));
block.anchorPoint = CGPointZero;
[self addChild:block];
The new block appears in a similar to the first case position (though not totally identical).
I am not sure why the position is not identical but i can fix that by adding/subtracting 1 or 2 from the x,y points.
The weird problem is that if my player sprite now tries to go below that block on the tile below (which is an open tile without any blocks), i get a contact between the player and the block.
I have even added debug paths with SKShapeNode to make sure that the player and block sprites do not actually collide. And they don't ! But i still get a collision event.
The player scale is (0.8, 0.9), but i don't think this would play much of a role.
I really don't get why this could be happening. Any ideas guys ?
Changing the sprite's anchor point have no effect on the physics body.
When talking about CGRect, the rect origin is at point {0, 0},
So what is happening is that you now have a sprite that its image is centred around anchor point {0, 0} but with a physics body, that starts at {0, 0} and is the size of the sprite, meaning that it is centred around {0.5, 0.5}.
So even that the player doesn't collide with the image of the sprite, it does collide with its physics body.
What is happening is that you have a physics body, that before had the same centre point as the sprite,
But as oppose to before, where the sprite anchor point were in the middle, which would 'fit' into the physics body,
Now the sprite's anchor point is {0, 0}, which causes the physics body centre point, to actually be the most bottom left point of the sprite.
To resolve this, you need to offset your physics body, so it will be centred around the new anchor point.
For example:
block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size center:CGPointZero];
CGPoint newCenter = CGPointMake(block.size.width, block.size.height);
block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size centre:newCenter];
Good luck mate.
EDIT- On a second thought, I think I got confused with the offset directions.
An edit has been made to correct it.
I am using Sprite Kit in Xcode and I was wondering how to change gravity direction.As default gravity direction to "X" you can imagine on below axes graphic.What about if I would like to change to "Y".
My goal is giving to object the falling effect.Its like falling from hight point and touching the ground than getting respond with physics!
(Could be dices on board game)
//Default gravity direction is X
SKSpriteNode *myNode =[SKSpriteNode spriteNodeWithImageNamed:#"ball"];
myNode.physicsBody=[SKPhysicsBody bodyWithCircleOfRadius:self.frame.size.width/2];
[self addChild: myNode];
Thanks in advance!
You can apply a vector to the Physics World of your scene using this code
self.physicsWorld.gravity=CGVectorMake(0,-10);
In SpriteKit, X and Y are the default coordinates that you see on the screen, and the Z coordinate is the order in which the objects are positioned (the zPosition). Since SpriteKit uses a 2D game engine, you do not have a third dimension, Z, to utilize. You can change the gravity between Y and X (Top/Bottom and Left/Right of screen respectively), but not between the Z coordinate. If you want to recreate a "dice falling" effect, I would recommend you create a Sprite called Dice scaled to a large amount, and once you add it to the scene you scale it down in x amounts of seconds.
[self runAction:[SKAction scaleBy:negativeFloatHere duration:3]];
This will make the dice appear to be falling, and you might want to add some spinning animations for it if you with. If you want to use the 3D engine, go try out Metal or SceneKit
I have created a SKShapeNode and I have assigned a physicsBody to it. However, it is not being triggered when there is contact.
Creation of SKShapeNode code:
-(SKShapeNode*)gravityline{
//SKSpriteNode *lolo=[[SKSpriteNode alloc]init];
SKShapeNode *lolo = [[SKShapeNode alloc] init];
CGPoint fff=CGPointMake(ray1.position.x, ray1.position.y);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, fff.x, fff.y);
CGPathAddLineToPoint(path, 0,rayoriginpoint.x,rayoriginpoint.y );
CGPathCloseSubpath(path);
lolo.path = path;
lolo.name=#"gravityline";
lolo.strokeColor=[SKColor greenColor];
lolo.glowWidth=.1;
CGPathRelease(path);
lolo.physicsBody=[SKPhysicsBody bodyWithEdgeFromPoint:fff toPoint:rayoriginpoint];
//lolo.physicsBody=[SKPhysicsBody bodyWithEdgeLoopFromPath:path];
//lolo.physicsBody=[SKPhysicsBody bodyWithPolygonFromPath:path];
lolo.physicsBody.categoryBitMask=raylightCategory;
lolo.physicsBody.collisionBitMask=batCategory;
lolo.physicsBody.contactTestBitMask=batCategory;
lolo.physicsBody.usesPreciseCollisionDetection=YES;
lolo.physicsBody.linearDamping=0;
lolo.physicsBody.restitution=1.0;
lolo.physicsBody.dynamic=NO;
return lolo;
}
Here is the trigering code :
- (void)didBeginContact:(SKPhysicsContact *)contact
{
SKPhysicsBody *firstBody, *secondBody;
if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask)
{
firstBody = contact.bodyA;
secondBody = contact.bodyB;
}
else
{
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
if (firstBody.categoryBitMask == raylightCategory && secondBody.categoryBitMask==batCategory)
{
NSLog(#"Contact with bat have been made");
[secondBody.node removeFromParent];
}
}
If anybody has a clue what I did wrong, why the SKShapeNode is not activating the physicsBody, please let me know.
This certainly won't work:
lolo.physicsBody=[SKPhysicsBody bodyWithEdgeFromPoint:fff toPoint:rayoriginpoint];
If anything this will return a body already assigned to a different node. But I guess it simply returns nil.
This commented line will not work either:
//lolo.physicsBody=[SKPhysicsBody bodyWithEdgeLoopFromPath:path];
Edge shapes will create static (as in: immovable) bodies. Hence this node won't move through physics and if I'm not mistaken you also won't get contact response from contacts with static bodies, only dynamic bodies.
This one should work:
//lolo.physicsBody=[SKPhysicsBody bodyWithPolygonFromPath:path];
But then you're setting the body to a static body here:
lolo.physicsBody.dynamic=NO;
Hence the same rules apply as if you were creating a body with an edge loop.
Your code doesn't quite show what your "gravityLine" is colliding with. I can only speculate from its description that it's not detecting a collision with a round shaped object (a ball and a bat).
The "gravityLine" method seems to be returning a SKShapeNode that is defined by an edge-based shape:
lolo.physicsBody=[SKPhysicsBody bodyWithEdgeFromPoint:fff toPoint:rayoriginpoint];
When it comes to collisions, it is important to read Apple's Sprite Kit Programming Guide, specifically it's explanation of three types of shapes.
1) A dynamic volume simulates a physical object with volume and mass that can be affected by forces and collisions in the system. Use dynamic volumes to represent items in the scene that need to move around and collide with each other.
2) A static volume is similar to a dynamic volume, but its velocity is ignored and it is unaffected by forces or collisions. However, because it still has volume, other objects can bounce off it or interact with it. Use static volumes to represent items that take up space in the scene, but that should not be moved by the simulation. For example, you might use static volumes to represent the walls of a maze.
While it is useful to think of static and dynamic volumes as distinct entities, in practice these are two different modes you can apply to any volume-based physics body. This can be useful because you can selectively enable or disable effects for a body.
3) An edge is a static volume-less body. Edges are never moved by the simulation and their mass doesn’t matter. Edges are used to represent negative space within a scene (such as a hollow spot inside another entity) or an uncrossable, invisibly thin boundary. For example, edges are frequently used to represent the boundaries of your scene.
The main difference between a edge and a volume is that an edge permits movement inside its own boundaries, while a volume is considered a solid object. If edges are moved through other means, they only interact with volumes, not with other edges.
Based on the above info, if you read the documentation for the method that you used bodyWithEdgeFromPoint:toPoint:, you will see that you are creating an "Edge-based" physics body.
Return Value
A new edge-based physics body.
Discussion
An edge has no volume or mass and is always treated as if the dynamic property is equal to NO. Edges may only collide with volume-based physics bodies.
To make your collision work, you have to make sure that your edge is colliding with a volume-based physics body. Every physics body shape creation method documents what type if shape it's creating.
If you are using a volume-based physics body that is colliding with your edge, then another possibility may be due to the size or speed of the involved objects. Again, reading Apple's docs makes it clear.
Specify High Precision Collisions for Small or Fast-Moving Objects:
When Sprite Kit performs collision detection, it first determines the locations of all of the physics bodies in the scene. Then it determines whether collisions or contacts occurred. This computational method is fast, but can sometimes result in missed collisions. A small body might move so fast that it completely passes through another physics body without ever having a frame of animation where the two touch each other.
If you have physics bodies that must collide, you can hint to Sprite Kit to use a more precise collision model to check for interactions. This model is more expensive, so it should be used sparingly. When either body uses precise collisions, multiple movement positions are contacted and tested to ensure that all contacts are detected
ship.physicsBody.usesPreciseCollisionDetection = YES;
Other possibilities might mess up collisions as well, such as wrong position info for the path that you're assigning to the physics body. It's important to understand that when you set a path to a node, that the shape is being set using the local coordinate system of the node. It's important to remember that the origin in Sprite Kit is located bottom left corner (not UIKit's top left corner), and when you assign a path to a physics body of a node, that the path is placed relative to the anchor point of the node.
For example:
SKShapeNode *ball = [[SKShapeNode alloc] init];
CGRect ballFrame = CGRectMake(-25.0, -25.0, 50.0, 50.0);
[ball setPath:[UIBezierPath bezierPathWithOvalInRect:ballFrame].CGPath];
[ball setPosition:CGPointMake(100.0, 450.0)];
[ball setFillColor:[UIColor redColor]];
[ball setPhysicsBody:[SKPhysicsBody bodyWithCircleOfRadius:25.0]];
If I had set the origin of "ballFrame" at (0,0), then the circle of the physics body with radius of 25.0, would not coincide with the shape of the ball, as the bottom left corner of the physics body will be placed at the anchor point of the ball, which is at points x = 0 and y = 0, in the local coordinate system of the ball.
I'm having problems detecting collisions in Cocos2D, because the coordinates I use to see if an object collided are always wrong.
I have the following objects:
All the sprites from a tiled map's layer, containing all the obstacles;
A sprite which is child of another sprite, which is child of the hello world layer (the main layer I'm using, which returns the scene). This sprites continuously moves in the map, and it may collide with the obstacles.
To detect collision I just see look at the distance between 2 sprite's bounding boxes:
inline BOOL collision(CGRect r1, CGRect r2)
{
CGPoint c1= RectCenter(r1);
CGPoint c2= RectCenter(r2);
BOOL result= (fabs(c1.x-c2.x)<MAX(r1.size.width/2.0,r2.size.width/2.0)) && (fabs(c1.y-c2.y)<MAX(r1.size.height/2.0,r2.size.height/2.0));
return result;
}
I use the boundingBox property to get the coordinate and the size of every sprite. But the coordinates are wrong, and it doesn't detect collisions correctly.
I'm pretty sure that I'm doing something wrong, could someone tell me how is the way to manage all sprites to have the same coordinate system? I also tried with convertToWordSpace, but with no luck.
You can do away with collision method in favor of the builtin function for seeing if two CGRects intersect:
CGRectIntersectsRect(r1, r2);
so I have a sprite that is created every second and that is moving at a random position (sprite1) and another sprite that has a fixed position (sprite2). I would like that when sprite1 collide with sprite2, sprite1 is like sticked to it (it stops moving and is sticked to it) . How can I do this please ? sorry for my english I'm french :/
p.s : sprite2 is rotating with accelerometer, so if sprite1 collide with it I would like that it rotate too :)
I think, you can try to use box2d to do this. It will help to detect collisions and to manage rotations, movement, etc.
I think, you can do it simply in Cocos2d.
1) First set the rect for sprite1 and sprite2 using CGRectMake(x,y,width,height)
2) As you told sprite1 is moving at random position and sprite2 is fixed to particular position, you can check them collide by using CGRectIntersectsRect([sprite1 bounds],[sprite2 bounds]).
3) if it intersects, set sprite1.position = sprite2.position
Note: you said sprite1 is rotating, rect can be fit only to the regular bodies. if you want exact collision or physical properties for sprite better you can go for box2d.
If you don't want to use Box2d (which can handle circle collisions), you can try something like this:
1.) Detect collision, is the distance between the two circles center point (x,y), less than the sum of the two circles radius.
2.) Make the Sprite1 stick to Sprite2, Stop the movement of Sprite1, and save the relative delta (x,y) to Sprite2, then whenever Sprite2 moves or rotates apply the same delta movement and rotation to Sprite1.