SpriteKit: Collisions with 300 nodes, very low FPS - ios

I want to make a simple SpriteKit app where I can add "rocks" and they fall to bottom of the screen. Just like this: http://aamukasa.fi/II-13-347. The implementation is quite easy but I will get huge performance issues when there are more than 100 nodes. The FPS goes under 10 when all the blocks collides together. Is there anyway to achieve this kind of functionality with good FPS and about 300-400 blocks?
#implementation MyScene
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithWhite:1 alpha:1];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.scaleMode = SKSceneScaleModeAspectFit;
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor blackColor] size:CGSizeMake(20,10)];
rock.position = location;
rock.name = #"rock";
rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
[self addChild:rock];
}
}
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
}
#end

You might want to try to give your rocks a circular physicsbody. I don't think it will solve the issue but might increase framerate a bit! As far as I've understood, circular physicsbodies are better for performance than rectangular ones.

You should test on a device to see actual FPS. In the simulator I get 60 FPS until I reach 200 bricks.
Then it starts dropping but on the device you could get better results.

Not sure if you are still looking, but could try something like this:
-(void)didSimulatePhysics
{
[self enumerateChildNodesWithName:#"rock" usingBlock:^(SKNode *node, BOOL *stop) {
if (node.position.y < 0)
[node removeFromParent];
}];
}
Otherwise the nodes will stay in memory and FPS will worsen overtime.

Related

Handling Thousand of SKSpriteNodes in a scene

I am building a game using sprite kit and its a game where you send balls into a bucket and grow the bucket. As the buckets grow the balls (SKSpriteNodes) stay on the scene. Im trying to see how to keep high performance while managing thousands of nodes. Any idea how i can do this? After 700 or so the FPS in simulator goes below 10 tps.
Here is my code from my scene. Any help is appreciated.
//
// GameScene.m
//
#import "GameScene.h"
#implementation GameScene
#synthesize _flowIsON;
NSString *const kFlowTypeRed = #"RED_FLOW_PARTICLE";
const float kRED_DELAY_BETWEEN_PARTICLE_DROP = 0.1; //delay for particle drop in seconds
static const uint32_t kRedParticleCategory = 0x1 << 0;
static const uint32_t kInvisbleWallCategory = 0x1 << 1;
NSString *const kStartBtn = #"START_BTN";
NSString *const kLever = #"Lever";
NSString *const START_BTN_TEXT = #"Start Game";
CFTimeInterval lastTime;
-(void)didMoveToView:(SKView *)view {
[self initializeScene];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode: self];
SKNode *node = [self nodeAtPoint:location];
if ([node.name isEqualToString:kStartBtn]) {
[node removeFromParent];
//initalize to ON
_flowIsON = YES;
//[self initializeScene];
} else if ([node.name isEqualToString:kLever]) {
_leverNode = (SKSpriteNode *)node;
[self selectNodeForTouch:location];
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPoint previousPosition = [touch previousLocationInNode:self];
CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);
[self panForTranslation:translation];
}
-(void)update:(CFTimeInterval)currentTime {
float deltaTimeInSeconds = currentTime - lastTime;
//NSLog(#"Time is %f and flow is %d",deltaTimeInSeconds, _flowIsON);
if ((deltaTimeInSeconds > kRED_DELAY_BETWEEN_PARTICLE_DROP) && _flowIsON) {
[self startFlow:kFlowTypeRed];
//only if its been past 1 second do we set the lasttime to the current time
lastTime = currentTime;
}
}
- (void) initializeScene {
SKLabelNode *startBtn = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
startBtn.text = START_BTN_TEXT;
startBtn.name = kStartBtn;
startBtn.fontSize = 45;
startBtn.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[self addChild:startBtn];
//init to flow off
_flowIsON = NO;
// Set physics body delegate
self.physicsWorld.contactDelegate = self;
self.shouldRasterize = YES;
self.view.showsDrawCount = YES;
self.view.showsQuadCount = YES;
//Set collision mask for invisible wall
_nonWallNode = (SKSpriteNode *) [self.scene childNodeWithName:#"NonWall"];
_nonWallNode.physicsBody.categoryBitMask = kInvisbleWallCategory;
_nonWallNode.physicsBody.collisionBitMask = kRedParticleCategory;
_nonWallNode.physicsBody.contactTestBitMask = kRedParticleCategory | kInvisbleWallCategory;
}
- (void) startFlow:(NSString *)flowKey {
// //SKSpriteNode *redParticleEmitter = [SKSpriteNode spriteNodeWithImageNamed:#"RedFlowParticles"];
//
// SKShapeNode *redParticleEmitter = [[SKShapeNode alloc] init];
//
// CGMutablePathRef myPath = CGPathCreateMutable();
// CGPathAddArc(myPath, NULL, 0,0, 15, 0, M_PI*2, YES);
// redParticleEmitter.path = myPath;
//
// redParticleEmitter.lineWidth = 1.0;
// redParticleEmitter.fillColor = [SKColor blueColor];
// redParticleEmitter.strokeColor = [SKColor whiteColor];
// redParticleEmitter.glowWidth = 0.5;
//
// //set size to 20px x 20px
// //redParticleEmitter.size = CGSizeMake(10, 10);
SKSpriteNode *redParticleEmitter = [SKSpriteNode spriteNodeWithImageNamed:#"RedFlowParticles"];
//set size to 20px x 20px
redParticleEmitter.size = CGSizeMake(10, 10);
SKPhysicsBody *redParticleEmitterPB = [SKPhysicsBody bodyWithCircleOfRadius:redParticleEmitter.frame.size.width/2];
redParticleEmitterPB.categoryBitMask = kRedParticleCategory;
redParticleEmitterPB.collisionBitMask = kRedParticleCategory;
redParticleEmitterPB.contactTestBitMask = kRedParticleCategory | kInvisbleWallCategory;
//set this to 5% of the width of the scene
redParticleEmitter.position = CGPointMake(self.frame.size.width*0.05, self.frame.size.height);
redParticleEmitter.physicsBody =redParticleEmitterPB;
redParticleEmitter.name = #"RedParticle";
[self addChild:redParticleEmitter];
}
- (void)selectNodeForTouch:(CGPoint)touchLocation {
//1
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
//2
if(![_leverNode isEqual:touchedNode]) {
[_leverNode removeAllActions];
[_leverNode runAction:[SKAction rotateToAngle:0.0f duration:0.1]];
_leverNode = touchedNode;
//3
if([[touchedNode name] isEqualToString:kLever]) {
SKAction *sequence = [SKAction sequence:#[[SKAction rotateByAngle:degToRad(-4.0f) duration:0.1],
[SKAction rotateByAngle:0.0 duration:0.1],
[SKAction rotateByAngle:degToRad(4.0f) duration:0.1]]];
[_leverNode runAction:[SKAction repeatActionForever:sequence]];
}
}
}
float degToRad(float degree) {
return degree / 180.0f * M_PI;
}
- (CGPoint)boundLayerPos:(CGPoint)newPos {
CGSize winSize = self.size;
CGPoint retval = newPos;
retval.x = MIN(retval.x, 0);
retval.x = MAX(retval.x, -[self size].width+ winSize.width);
retval.y = [self position].y;
return retval;
}
- (void)panForTranslation:(CGPoint)translation {
CGPoint position = [_leverNode position];
if([[_leverNode name] isEqualToString:kLever]) {
[_leverNode setPosition:CGPointMake(position.x + translation.x, position.y + translation.y)];
}
// else {
// CGPoint newPos = CGPointMake(position.x + translation.x, position.y + translation.y);
// [_background setPosition:[self boundLayerPos:newPos]];
// }
}
# pragma mark -- SKPhysicsContactDelegate Methods
- (void)didBeginContact:(SKPhysicsContact *) contact {
if (([contact.bodyA.node.name isEqualToString:#"RedParticle"] && [contact.bodyB.node.name isEqualToString:#"NonWall"]) ||
([contact.bodyB.node.name isEqualToString:#"RedParticle"] && [contact.bodyA.node.name isEqualToString:#"NonWall"])) {
//NSLog(#"Red particle Hit nonwall");
//contact.bodyA.node.physicsBody.pinned = YES;
//once red particle passes the invisible wall we need to stop it from going back through the wall
}
}
- (void)didEndContact:(SKPhysicsContact *) contact {
//NSLog(#"didEndContact called");
if (([contact.bodyA.node.name isEqualToString:#"RedParticle"] && [contact.bodyB.node.name isEqualToString:#"NonWall"]) ||
([contact.bodyB.node.name isEqualToString:#"RedParticle"] && [contact.bodyA.node.name isEqualToString:#"NonWall"])) {
//NSLog(#"Red particle left");
contact.bodyB.collisionBitMask = kRedParticleCategory | kInvisbleWallCategory;
//once red particle passes the invisible wall we need to stop it from going back through the wall
}
}
#end
Try this:
Create an additional sprite node on screen to display all your static balls as a whole (explained below).
Create an array of CGPoint to keep track of the positions of all balls that stopped.
At regular intervals, check all active ball sprites to see which ones have come to a stop.
For each ball that has stopped, remove that srpite from the scene and instead add its position (CGPoint) to the array described in #2.
Render an image consisting of one ball instance at each position in the array, and assign that image (texture) to the sprite node described in #1.
Go back to #3 and repeat.
Note: I haven't used SpriteKit for a while and I'm not sure how to implement point #5, but it shouldn't be too difficult. SKEffectNode has an option (shouldRasterize) to cache its appearance -i.e., render once and reuse the same image on all subsequent frames.
Regarding the "regular intervals" described in step #3, the actual value (for example, every 10 frames) will depend on your measured performance and the dynamics of your actual game; you need to find it yourself. If it is too often, the overhead of rendering the static balls texture over and over will cause a performance hit. Too far apart, and you will spend more frames than necessary rendering many still, separate sprites that could have otherwise been "grouped".
Alternative Solution:
Instead of removing the sprites from screen when each ball becomes static, you could instead move them into a different container node (as children of it), and have that node be rasterized instead of rendering anew each frame.
This keeps each ball as a separate SKSpriteNode instance (even when the ones that are stopped) and allows for SpriteKit physics bodies (not sure if sprites with different parents can collide with each other, though. Never used SpriteKit physics).
In any case, the performance hit due to collision detection will increase with the number of balls, independent of whether you draw them each frame or not.
I don't know exactly what optimizations SpriteKit's physics does (e.g., prunning, etc.), but the naïve approach to collision between n objects is to test each object against every other object, so the worse case is O(n^2).
Final Thoughts:
Because you can safely assume that still balls do not move anymore, the "group" of still balls remains in the same shape all along (until new balls stop and are added, that is).
Ideally, you could calculate the "envelope" (a possibly non-convex polygon, with rounded corners) and collision-test the moving balls against that. Still not a trivial task, but at least it helps you skip collision-testing against the static balls in the inside of the group, which should never collide anyway (they are "shielded" by the balls in the boundary of the group).
Well your problem here is all those physics bodies, you have a 1000 sprites checking 1000 other sprites whether or not they need to be colliding. One way you can make this a little faster is to break your screen into sub sets and having your nodes collission detection only check the surrounding neighbor quadrants and its own for sprites. EG. break the screen into 9 sections, the top left section has its own bit mask, and can only collide with sprites in the top left,middle top, middle center, and let center sections. If this sprite moves to the middle top section, its category becomes middle top, and will only check sprites in the top left, middle top, top right, left center, middle center, and right center. The less checks the nodes have to make the better.

How can I measure sprite's jump strength?

Hello,
I am new to spriteKit and I am trying to make a game. In the game I have a player that jumps from stair to stair, which comes from the top of the screen infinitely (Like in Doodle Jump, only the jump is controlled by the player's touch). I am trying to make a jump, by applying an impulse on the player, but I want to control the jump strength by the player's touch duration. How can I do so? The jump executes when the player STARTS touching the screen so I can't measure the jump intensity (By calculating the touch duration) ... Any ideas?
Thanks in advance!!! (:
Here's a simple demo to apply the impulse on a node with the duration of touch. The method is straightforward: set a BOOL variable YES when the touch began, and NO when the touch ended. When touching, it will apply a constant impulse in update method.
To make the game more natural, you might want to refine the impulse action, or scroll the background down as the node is ascending.
GameScene.m:
#import "GameScene.h"
#interface GameScene ()
#property (nonatomic) SKSpriteNode *node;
#property BOOL touchingScreen;
#property CGFloat jumpHeightMax;
#end
#implementation GameScene
- (void)didMoveToView:(SKView *)view
{
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
// Generate a square node
self.node = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(50.0, 50.0)];
self.node.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
self.node.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.node.size];
self.node.physicsBody.allowsRotation = NO;
[self addChild:self.node];
}
const CGFloat kJumpHeight = 150.0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchingScreen = YES;
self.jumpHeightMax = self.node.position.y + kJumpHeight;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchingScreen = NO;
self.jumpHeightMax = 0;
}
- (void)update:(CFTimeInterval)currentTime
{
if (self.touchingScreen && self.node.position.y <= self.jumpHeightMax) {
self.node.physicsBody.velocity = CGVectorMake(0, 0);
[self.node.physicsBody applyImpulse:CGVectorMake(0, 50)];
} else {
self.jumpHeightMax = 0;
}
}
#end

Spritekit: move SKSpritekitNode in arc with SKPhysicsBody

I'm trying to move an SKSpriteNode in an arc with physics in spritekit like so:
But am unsure as to which physic I should apply to it (applyImpulse, applyForce, applyTorque).
Currently using applyTorque, the code does not actually work, and produces no movement on the object:
_boy.physicsBody.velocity = CGVectorMake(1, 1);
CGVector thrustVector = CGVectorMake(0,100);
[_boy.physicsBody applyTorque:(CGFloat)atan2(_boy.physicsBody.velocity.dy, _boy.physicsBody.velocity.dx)];
applyTorque is not the right method for this. Torque is a twisting force that causes your node to rotate around its center point.
There is no one easy command for what you are looking to do. You also fail to mention your method of movement for your node. Apply force, impulse, etc... You are going to have to come up with a hack for this one.
The sample project below does what you are looking for and it will point you in the right direction. You are going to have to modify the code to suit your specific project's needs though.
Tap/click the screen once to start moving the node and tap/click the screen a second time to start to 90 degree movement change.
#import "GameScene.h"
#implementation GameScene {
int touchCounter;
BOOL changeDirection;
SKSpriteNode *node0;
}
-(void)didMoveToView:(SKView *)view {
self.backgroundColor = [SKColor whiteColor];
node0 = [SKSpriteNode spriteNodeWithColor:[SKColor grayColor] size:CGSizeMake(50, 50)];
node0.position = CGPointMake(150, 200);
node0.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:node0.size];
node0.physicsBody.affectedByGravity = NO;
[self addChild:node0];
touchCounter = 0;
changeDirection = NO;
}
-(void)update:(CFTimeInterval)currentTime {
if(changeDirection)
[self changeMovement];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
touchCounter++;
for (UITouch *touch in touches) {
if(touchCounter == 1)
[node0.physicsBody applyImpulse:CGVectorMake(25, 0)];
if(touchCounter == 2)
changeDirection = YES;
}
}
-(void)changeMovement {
if(node0.physicsBody.velocity.dy<200) {
[node0.physicsBody applyImpulse:CGVectorMake(-0.1, 0.1)];
} else {
changeDirection = NO;
node0.physicsBody.velocity = CGVectorMake(0, node0.physicsBody.velocity.dy);
}
}

Sprite kit moving sprite

I have made this sprite that i can grab and move around.
My issue is that i want to be able to "throw" the sprite. Meaning, when i release the sprite i want it to continue in the direction that i moved it. Just like throwing a ball.
What should i do?
#implementation NPMyScene
{
SKSpriteNode *sprite;
}
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
sprite.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:sprite.size.width/2];
sprite.physicsBody.dynamic = YES;
self.scaleMode = SKSceneScaleModeAspectFit;
sprite = [SKSpriteNode spriteNodeWithImageNamed:#"GreenBall"];
sprite.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
sprite.physicsBody.velocity = self.physicsBody.velocity;
sprite.physicsBody.affectedByGravity = false;
sprite.physicsBody.dynamic = true;
sprite.physicsBody.friction = 0;
[sprite.physicsBody isDynamic];
[sprite.physicsBody allowsRotation];
[self addChild:sprite];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesMoved:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[sprite runAction:[SKAction moveTo:[[touches anyObject] locationInNode:self]duration:0.21]];
}
If you are talking about applying physics to the ball, then you need to think deeper about how Sprite Kit deals with physics.
When you specify the 'moveTo' action, you're not actually using Sprite kit's physics engine at all. You're simply specifying a point to animate that sprite to.
What you should be doing to achieve the effect you're looking for is attaching the sprite's position to your finger as it's moved around on the screen like so:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
sprite.position = [[touches anyObject] locationInNode:self];
}
Then when the finger is lifted, you need to calculate the direction and speed in which your finger just moved and apply the appropriate amount of force to the sprite's physics body using the applyForce method:
[sprite.physicsBody applyForce:calculatedForce];
You will need to figure out how to calculate the force to be applied, but play around with the applyForce method and look at what information you are able to get back from the touchedMoved: event to help you apply that force. Hope that helps.

Throwing ball in SpriteKit

Last days, I experimented some time with spriteKit and (amongst other things) tried to solve the problem to "throw" a sprite by touching it and dragging.
The same question is on Stackexchange, but they told me to first remove the bug and then let the code be reviewed.
I have tackled the major hurdles, and the code is working fine, but there consist one little problem.
(Additionally, I'd be interested if somebody has a more polished or better working solution for this. I'd also love to hear suggestions about how to perfect the feeling of realism in this interaction.)
Sometimes, the ball just gets stuck.
If you want to reproduce that, just swipe the ball really fast and short. I suspect the gestureRecognizer to make "touchesMoved" and "touchesEnded" callback asynchronous and through that some impossible state occurs in the physics simulation.
Can anybody provide a more reliable way to reproduce the issue, and what could be the solution for that?
The project is called ballThrow and BT is the class prefix.
#import "BTMyScene.h"
#import "BTBall.h"
#interface BTMyScene()
#property (strong, nonatomic) NSMutableArray *balls;
#property (nonatomic) CGFloat yPosition;
#property (nonatomic) CGFloat xCenter;
#property (nonatomic) BOOL updated;
#end
#implementation BTMyScene
const CGFloat BALLDISTANCE = 80;
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
_balls = [NSMutableArray arrayWithCapacity:5];
//define the region where the balls will spawn
_yPosition = size.height/2.0;
_xCenter = size.width/2.0;
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
}
return self;
}
-(void)didMoveToView:(SKView *)view {
//Make an invisible border
//this seems to be offset... Why the heck is this?
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:view.frame];
[self createBalls:2];
//move balls with pan gesture
//could be improved by combining with touchesBegan for first locating the touch
[self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(moveBall:)]];
}
-(void)moveBall:(UIPanGestureRecognizer *)pgr {
//depending on the touch phase do different things to the ball
if (pgr.state == UIGestureRecognizerStateBegan) {
[self attachBallToTouch:pgr];
}
else if (pgr.state == UIGestureRecognizerStateChanged) {
[self moveBallToTouch:pgr];
}
else if (pgr.state == UIGestureRecognizerStateEnded) {
[self stopMovingTouch:pgr];
}
else if (pgr.state == UIGestureRecognizerStateCancelled) {
[self stopMovingTouch:pgr];
}
}
-(void)attachBallToTouch:(UIPanGestureRecognizer *)touch {
//determine the ball to move
for (BTBall *ball in self.balls) {
if ([self isMovingBall:ball forGestureRecognizer:touch])
{
//stop ball movement
[ball.physicsBody setAffectedByGravity:NO];
[ball.physicsBody setVelocity:CGVectorMake(0, 0)];
//the ball might not be touched right in its center, so save the relative location
ball.touchLocation = [self convertPoint:[self convertPointFromView:[touch locationInView:self.view]] toNode:ball];
//update location once, just in case...
[self setBallPosition:ball toTouch:touch];
if (_updated) {
_updated = NO;
[touch setTranslation:CGPointZero inView:self.view];
}
}
}
}
-(void)moveBallToTouch:(UIPanGestureRecognizer *)touch {
for (BTBall *ball in self.balls) {
if ([self isMovingBall:ball forGestureRecognizer:touch])
{
//update the position of the ball and reset translation
[self setBallPosition:ball toTouch:touch];
if (_updated) {
_updated = NO;
[touch setTranslation:CGPointZero inView:self.view];
}
break;
}
}
}
-(void)setBallPosition:(BTBall *)ball toTouch:(UIPanGestureRecognizer *)touch {
//gesture recognizers only deliver locations in views, thus convert to node
CGPoint touchPosition = [self convertPointFromView:[touch locationInView:self.view]];
//update the location to the touche´s location, offset by touch position in ball
[ball setNewPosition:CGPointApplyAffineTransform(touchPosition,
CGAffineTransformMakeTranslation(-ball.touchLocation.x,
-ball.touchLocation.y))];
//save the velocity between the last two touch records for later release
CGPoint velocity = [touch velocityInView:self.view];
//why the hell is the y coordinate inverted??
[ball setLastVelocity:CGVectorMake(velocity.x, -velocity.y)];
}
-(void)stopMovingTouch:(UIPanGestureRecognizer *)touch {
for (BTBall *ball in self.balls) {
if ([self isMovingBall:ball forGestureRecognizer:touch]) {
//release the ball: enable gravity impact and make it move
[ball.physicsBody setAffectedByGravity:YES];
[ball.physicsBody setVelocity:CGVectorMake(ball.lastVelocity.dx, ball.lastVelocity.dy)];
break;
}
}
}
-(BOOL)isMovingBall:(BTBall *)ball forGestureRecognizer:(UIPanGestureRecognizer *)touch {
//latest location of touch
CGPoint touchPosition = [touch locationInView:self.view];
//distance covered since the last call
CGPoint touchTranslation = [touch translationInView:self.view];
//position, where the ball must be, if it is the one
CGPoint translatedPosition = CGPointApplyAffineTransform(touchPosition,
CGAffineTransformMakeTranslation(-touchTranslation.x,
-touchTranslation.y));
CGPoint inScene = [self convertPointFromView:translatedPosition];
//determine weather the last touch location was on the ball
//if last touch location was on the ball, return true
return [[self nodesAtPoint:inScene] containsObject:ball];
}
-(void)update:(CFTimeInterval)currentTime {
//updating the ball position here improved performance dramatically
for (BTBall *ball in self.balls) {
//balls that move are not gravity affected
//easiest way to determine movement
if ([ball.physicsBody affectedByGravity] == NO) {
[ball setPosition:ball.newPosition];
}
}
//ball positions are refreshed
_updated = YES;
}
-(void)createBalls:(int)numberOfBalls {
for (int i = 0; i<numberOfBalls; i++) {
BTBall *ball;
//reuse balls (not necessary yet, but imagine balls spawning)
if(i<[self.balls count]) {
ball = self.balls[i];
}
else {
ball = [BTBall newBall];
}
[ball.physicsBody setAffectedByGravity:NO];
//calculate ballposition
CGPoint ballPosition = CGPointMake(self.xCenter-BALLSIZE/2+(i-(numberOfBalls-1)/2.0)*BALLDISTANCE, self.yPosition);
[ball setNewPosition:ballPosition];
[self.balls addObject:ball];
[self addChild:ball];
}
}
#end
The BTBall (subclass of SKShapeNode, because of the custom properties needed)
#import <SpriteKit/SpriteKit.h>
#interface BTBall : SKShapeNode
const extern CGFloat BALLSIZE;
//some properties for the throw animation
#property (nonatomic) CGPoint touchLocation;
#property (nonatomic) CGPoint newPosition;
#property (nonatomic) CGVector lastVelocity;
//create a standard ball
+(BTBall *)newBall;
#end
The BTBall.m with a class method to create new balls
#import "BTBall.h"
#implementation BTBall
const CGFloat BALLSIZE = 80;
+(BTBall *)newBall {
BTBall *ball = [BTBall node];
//look
[ball setPath:CGPathCreateWithEllipseInRect(CGRectMake(-BALLSIZE/2,-BALLSIZE/2,BALLSIZE,BALLSIZE), nil)];
[ball setFillColor:[UIColor redColor]];
[ball setStrokeColor:[UIColor clearColor]];
//physics
SKPhysicsBody *ballBody = [SKPhysicsBody bodyWithCircleOfRadius:BALLSIZE/2.0];
[ball setPhysicsBody:ballBody];
[ball.physicsBody setAllowsRotation:NO];
//ball is not moving at the beginning
ball.lastVelocity = CGVectorMake(0, 0);
return ball;
}
#end
1. A couple of problems (see comments in code) are related to the spriteKit coordinate system. I just do not get the border of the scene align with its actual frame, though I make it with the exact same code that Apple gives us in the programming guide. I have moved it from initWithSize to didMoveToView due to a suggestion here on Stackoverflow, but that did not help. It is possible to manually offset the border with hardcoded values, but that does not satisfy me.
2. Does anybody know a debugging tool, which colors the physics body of a sprite, in order to see its size and whether it is at the same position as the sprite?
Update: Problems above solved by using YMC Physics Debugger:
This lines of code are correct:
[ball setPath:CGPathCreateWithEllipseInRect(CGRectMake(-BALLSIZE/2,-BALLSIZE/2,BALLSIZE,BALLSIZE), nil)];
SKPhysicsBody *ballBody = [SKPhysicsBody bodyWithCircleOfRadius:BALLSIZE/2.0];
Because 0,0 is the center of the physics body, the origin of the path must be translated.

Resources