How to draw a line in SpriteKit efficiently - ios

In my SpriteKit scene, user should be able to draw line with his/her finger. I have a working solution, if the line is long, FPS gets down to 4-6 and the line starts to get polygonal, as the image below:
To draw myline (SKShapeNode*), I collect points of touches movement in an NSMutableArray* noteLinePoints in this way
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint touchPoint = [[touches anyObject] locationInNode:self.scene];
SKNode *node = [self nodeAtPoint:touchPoint];
if(noteWritingActive)
{
[noteLinePoints removeAllObjects];
touchPoint = [[touches anyObject] locationInNode:_background];
[noteLinePoints addObject:[NSValue valueWithCGPoint:touchPoint]];
myline = (SKShapeNode*)node;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
if(myline)
{
CGPoint touchPoint = [[touches anyObject] locationInNode:_background];
[noteLinePoints addObject:[NSValue valueWithCGPoint:touchPoint]];
[self drawCurrentNoteLine];
}
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if(myline)
{
myline.name = #"note";
myline = nil;
}
NSLog(#"touch ended");
}
and I draw the line in this way
- (CGPathRef)createPathOfCurrentNoteLine
{
CGMutablePathRef ref = CGPathCreateMutable();
for(int i = 0; i < [noteLinePoints count]; ++i)
{
CGPoint p = [noteLinePoints[i] CGPointValue];
if(i == 0)
{
CGPathMoveToPoint(ref, NULL, p.x, p.y);
}
else
{
CGPathAddLineToPoint(ref, NULL, p.x, p.y);
}
}
return ref;
}
- (void)drawCurrentNoteLine
{
if(myline)
{
SKNode* oldLine = [self childNodeWithName:#"line"];
if(oldLine)
[self removeChildrenInArray:[NSArray arrayWithObject:oldLine]];
myline = nil;
myline = [SKShapeNode node];
myline.name = #"line";
[myline setStrokeColor:[SKColor grayColor]];
CGPathRef path = [self createPathOfCurrentNoteLine];
myline.path = path;
CGPathRelease(path);
[_background addChild:myline];
}
}
How can I fix this problem? because all subsequent lines are all polygonal only, I think because the fps is very low and sampling rate of touches automatically get also very low...
Please note that for the test I used an iPad 3 (my app needs to be working from iPad 2 model with iOS7)

Do not constantly create new SKShapeNodes, there is a bug which is causing your slowdown. Instead, only use 1 SKShapeNode (Or create a bunch but reuse them), and append the path with new info (So there is no need to constantly add the myline to the background)
Alternative:
Use 1 community SKSkapeNode for rendering of the path, then convert the SKShapeNode to a texture with view.textureFromNode, then add an SKSpriteNode with this new texture instead of the shape node

Related

Speed of touch varies with number of objects on Scene

I am creating game in which user can hit the object falling from the top of the screen with the racket. user can continuously move the racket but if it is at minimal speed or is at rest it should not hit the object, but if it above the minimal speed user should hit them. I have achieved that but the issue is when user start touching the racket which continously move with the user touch, the speed varition is their it does not start with the same speed and while touch is moving at that time also some times speed is very less even though the movement is fast. Here is my piece of code
-(void)didMoveToView:(SKView *)view {
self.physicsWorld.contactDelegate = (id)self;
racketNode = [SKSpriteNode spriteNodeWithImageNamed:#"racket"];
racketNode.size = CGSizeMake(50,50);
racketNode.position = CGPointMake(self.frame.origin.x + self.frame.size.width - 50,50);
racketNode.name = #"racket";
[self addChild:racketNode];
}
-(void) didBeginContact:(SKPhysicsContact *)contact {
SKSpriteNode *nodeA = (SKSpriteNode *)contact.bodyA.node ;
SKSpriteNode *nodeB = (SKSpriteNode *) contact.bodyB.node;
if (([nodeA.name isEqualToString:#"racket"] && [nodeB.name isEqualToString:#"fallingObject"])) {
if (racketNode.speed > kMinSpeed)
[nodeB removeFromParent];
else {
nodeB.physicsBody.contactTestBitMask = 0;
[self performSelector:#selector(providingCollsion:) withObject:nodeB afterDelay:0.1];
}
}
}
-(void) providingCollsion:(SKSpriteNode *) node {
node.physicsBody.contactTestBitMask = racketHit;
}
-(void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
start = location;
startTime = touch.timestamp;
racketNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:racketNode.frame.size];
racketNode.physicsBody.categoryBitMask = racket;
racketNode.physicsBody.contactTestBitMask = HitIt;
racketNode.physicsBody.dynamic = NO;
racketNode.physicsBody.affectedByGravity = NO;
[racketNode runAction:[SKAction moveTo:location duration:0.01]];
}
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
racketNode.physicsBody = nil;
racketNode.speed = 0;
}
-(void) touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
CGFloat dx = location.x - start.x;
CGFloat dy = location.y - start.y;
CGFloat magnitude = sqrt(dx*dx+dy*dy);
// Determine time difference from start of the gesture
CGFloat dt = touch.timestamp - startTime;
// Determine gesture speed in points/sec
CGFloat speed = magnitude/dt;
racketNode.speed = speed;
[handNode runAction:[SKAction moveTo:[touch locationInNode:self] duration:0.01]];
}
Please tell me which part my code is wrong so as to make same object collide with high speed only not on slow speed and also no collision on stable state.
Instead of doing it manually, use UIPanGestureRecognizer to handle your swipes. With it, there is a velocity property that you can use to check if the speed is greater than a given value.
Here is a great tutorial to do it:
https://www.raywenderlich.com/76020/using-uigesturerecognizer-with-swift-tutorial

Spritekit - Using gestures to draw multiple lines with multiple fingers simultaneously

Using the below code, I can draw a line using the pan gesture using 1 finger. However what I am attempting to do is for 1 finger to draw 1 line and another finger to draw the second simultaneously. I've read somewhere about using a dictionary to store the touches but don't know how to write it in code. Can any one help?
-(void)didMoveToView:(SKView *)view {
UIPanGestureRecognizer *gestureRecognizerPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:gestureRecognizerPan];
}
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
pathToDraw = CGPathCreateMutable();
CGPathMoveToPoint(pathToDraw, NULL, touchLocation.x, touchLocation.y);
lineNode = [[SKShapeNode alloc] init];
lineNode.path = pathToDraw;
lineNode.name = lineNodeCategoryName;
[_gameLineNode addChild:lineNode];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
CGPathAddLineToPoint(pathToDraw, NULL, touchLocation.x, touchLocation.y);
lineNode.path = pathToDraw;
lineNode.name = lineNodeCategoryName;
lineNode.physicsBody = [SKPhysicsBody bodyWithEdgeChainFromPath:pathToDraw];
lineNode.physicsBody.dynamic = YES;
lineNode.strokeColor = [SKColor blackColor];
lineNode.glowWidth = 3.0;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPathRelease(pathToDraw);
}
}
Drawing multiple lines simultaneously is fairly straightforward. The key is to track each touch event independently. One way to do that is to maintain a dictionary that uses the touch event as the key and the shape node (used to draw the line) as the value.
#implementation GameScene {
NSMutableDictionary *lines;
}
-(void)didMoveToView:(SKView *)view {
self.scaleMode = SKSceneScaleModeResizeFill;
// Create a mutable dictionary
lines = [NSMutableDictionary dictionaryWithCapacity:10];
}
// Add each line node to the dictionary with the touch event as the key
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
// Create a mutable path
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:location];
// Create a shape node using the path
SKShapeNode *lineNode = [SKShapeNode shapeNodeWithPath:path.CGPath];
lineNode.strokeColor = [SKColor blackColor];
[self addChild:lineNode];
// Use touch pointer as the dictionary key. Since the dictionary key must conform to
// NSCopying, box the touch pointer in an NSValue
NSValue *key = [NSValue valueWithPointer:(void *)touch];
[lines setObject:lineNode forKey:key];
}
}
// Update the lines as needed
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
// Retrieve the shape node that corresponds to touch
NSValue *key = [NSValue valueWithPointer:(void *)touch];
SKShapeNode *lineNode = [lines objectForKey:key];
if (lineNode != NULL) {
// Create and initialize a mutable path with the lineNode's current path
UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:lineNode.path];
// Add a line to the current touch point
[path addLineToPoint:location];
// Update lineNode
lineNode.path = path.CGPath;
}
}
}
// Remove the line nodes from the dictionary when the touch ends
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
NSValue *key = [NSValue valueWithPointer:(void *)touch];
[lines removeObjectForKey:key];
}
}
#end

SKShapeNode Detect Two Lines Intersection

I am working on a application and i am drawing line based on user touch of user finger. Once the touch end event received the line is converted to last path. A new line is draw with name "Current path" node when a new touch began event received. I added a physics body for both the line with opposite contact bit mask but i am not able to receive collision event.
Following is my code:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
currentPath = CGPathCreateMutable();
currentPathNode = [self newLineNodeWithFillColor : CURRENT_LINE_COLOR];
CGPathMoveToPoint(currentPath, NULL, positionInScene.x, positionInScene.y);
currentPathNode.path = currentPath;
[self addChild:currentPathNode];
uint32_t contactBitMask = circleCategory | lastPathCategory;
[self addPhysicsBodyForLine:currentPathNode withCategoryBitMask:drawPathCategory withContactBitMask:contactBitMask];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPathAddLineToPoint(currentPath, NULL, positionInScene.x, positionInScene.y);
currentPathNode.path = currentPath;
uint32_t contactBitMask = lastPathCategory;
[self addPhysicsBodyForLine:currentPathNode withCategoryBitMask:drawPathCategory withContactBitMask:contactBitMask];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if(lastPath == nil){
lastPath = CGPathCreateMutable();
}
CGPathAddPath(lastPath, nil, currentPath);
[lastPathNode removeFromParent];
if(currentPathNode != nil){
[currentPathNode removeFromParent];
currentPathNode = nil;
}
lastPathNode = [self newLineNodeWithFillColor : LAST_LINE_COLOR];
lastPathNode.path = lastPath;
[self addChild:lastPathNode];
[self addPhysicsBodyForLine:lastPathNode withCategoryBitMask:lastPathCategory withContactBitMask:drawPathCategory];
CGPathRelease(currentPath);
}
- (void) addPhysicsBodyForLine:(SKShapeNode*)node withCategoryBitMask:(uint32_t)category withContactBitMask:(uint32_t)contactBitMask{
node.physicsBody = [SKPhysicsBody bodyWithEdgeChainFromPath:node.path];
node.physicsBody.categoryBitMask = category;
node.physicsBody.contactTestBitMask = contactBitMask;
node.physicsBody.collisionBitMask = contactBitMask;
node.physicsBody.dynamic = YES;
node.physicsBody.usesPreciseCollisionDetection = YES;
}
But collision is not Detected? Any Solution.
Collisions do not work that way. You will only register a collision if you use physics to move a node's position. Creating a new physics body over (or across) an already existing physics body will not register a collision.
You can use -(BOOL)intersectsNode:(SKNode *)node every time a new path is drawn to check if the new node intersects any other node.

How to remove a child SKSpritenode from SKNode?

i will explain a part of my code. I have Spritenodes (images) who are moving down on the screen.
SKTexture* Squaretexture = [SKTexture textureWithImageNamed:#"squaregreen"];
SquareTexture.filteringMode = SKTextureFilteringNearest;
Square = [SKSpriteNode spriteNodeWithTexture:SquareTexture];
Square.name = #"square";
.
.
.
[_objects addChild:Square];
_objects is a SKNode and Square is a SKSpriteNode. Now there is my code: every one second there is one square, who came from "over the screen" and is moving to the bottom. (Also there are more then one squares on the screen).
Now I want this: When I touch a square it should be "deleted" or hidden, but only the one who i touch. With my code, when i touch all squares are deleted or nothing. I tried with removefromparent and removechild, but i couldn't solve it.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode: self];
SKNode *node = [self nodeAtPoint:location];
NSLog(#"Point in myView: (%f,%f)", location.x, location.y);
if ([node.name isEqualToString:#"Square"]) {
[Square removeFromParent];
[Square removeAllChildren];
}
}
Do you have a suggestion how can I do it?
Thanks for Answers.
Mehmet
You almost had it right. The trick is that you need to have a unique identifier for each object (sprite) that you create and then store those objects in an array for later use.
The code below creates 5 sprites and gives them unique names: Sprite-1, Sprite-2, etc...
Whenever a touch is registered, it extracts the touched node's name, searches the array for the matching object, removes the object from the view and lastly removes the object from the array.
Note that my sample code is based on landscape view.
#import "MyScene.h"
#implementation MyScene
{
NSMutableArray *spriteArray;
int nextObjectID;
}
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
spriteArray = [[NSMutableArray alloc] init];
nextObjectID = 0;
// create 5 sprites
for (int i=0; i<5; i++)
{
SKSpriteNode *mySprite = [SKSpriteNode spriteNodeWithColor:[SKColor blueColor] size:CGSizeMake(30, 30)];
nextObjectID ++; // increase counter by 1
mySprite.name = [NSString stringWithFormat:#"Sprite-%i",nextObjectID]; // add unique name to new sprite
mySprite.position = CGPointMake(50+(i*70), 200);
[spriteArray addObject:mySprite];
[self addChild:mySprite];
}
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode: self];
SKNode *node = [self nodeAtPoint:location];
NSLog(#"touched node name: %#",node.name);
NSLog(#"objects in spriteArray: %lu",(unsigned long)[spriteArray count]);
NSMutableArray *discardedItems = [NSMutableArray array];
for(SKNode *object in spriteArray)
{
if([object.name isEqualToString:node.name])
{
[object removeFromParent];
[discardedItems addObject:object];
}
}
[spriteArray removeObjectsInArray:discardedItems];
NSLog(#"objects in spriteArray: %lu",(unsigned long)[spriteArray count]);
}
-(void)update:(CFTimeInterval)currentTime
{
//
}
#end

Getting location of section of sprite

I'm using IOS7's sprite kit and using NSSpriteNode with default anchor of (.5, .5) [center]. The sprite will be rotating around a lot, is there a way to get the location relative to the anchor point? Like, if I'm looking for the sprites top-center anchorpoint of (.5,1) or bottom-center (.5, 0)? This way I can always get the same location of a part of a sprite however it is rotated?
self.player = [SKSpriteNode spriteNodeWithImageNamed:#"ship.png"];
self.player.anchorPoint = CGPointMake(.5, 1);
CGPoint p = [self.player convertPoint:self.player.position toNode:self]];
NSLog(#"x=%f,y=%f", p.x, p.y);
self.player.anchorPoint = CGPointMake(.5, 0);
CGPoint p = [self.player convertPoint:self.player.position toNode:self]];
NSLog(#"x=%f,y=%f", p.x, p.y);
This ends up yielding the same point even though I'm changing my anchor point to be different parts. I know this is a bad idea to change the anchor point, but trying to give an idea of what I'm trying to do.
I'd like a method something like:
CGPoint imageTopLoc = [self.player getLocationUsingAnchorPoint:CGPointMake(.5, 1)];
CGPoint imageBottomLoc = [self.player getLocationUsingAnchorPoint:CGPointMake(.5, 0)];
// calculate vector of direction between of two points... (next)
Thanks,
Chris
This code returns the coordinates / touch point inside a node, regardless of the node's rotation. (It's based upon the SpriteKit Game template.)
Is that what you are looking for?
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
SKSpriteNode *ship = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
ship.name = #"ship";
ship.position = CGPointMake(100, 100);
ship.zRotation = M_PI * 0.5;
[self addChild:ship];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKNode *ship = [self childNodeWithName:#"ship"];
CGPoint p = [self convertPoint:location toNode:ship];
NSLog(#"x=%f,y=%f", p.x, p.y);
}
}

Resources