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

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

Related

How to draw a line in SpriteKit efficiently

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

Spritekit - Objective C - Add SKShapenodes to an array and then remove nodes one by one via button

Essentially I draw multiple lines on the screen by using a SKShapenode and adding it to a dictionary.
I want to now store the SKShapenodes in a array and remove the last item in the array via a button. Any idea anyone?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
SKLabelNode *touchedNode = (SKLabelNode *)[self nodeAtPoint:positionInScene];
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
lineNode = [SKShapeNode shapeNodeWithPath:path.CGPath];
lineNode.strokeColor = [SKColor blackColor];
lineNode.glowWidth = 3.0;
[_gameLineNode 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];
_lineNodeArray = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
[_lineNodeArray addObject:lineNode];
}
}
}
- (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];
lineNode = [lines objectForKey:key];
if (lineNode != NULL) {
// Create and initial a mutable path with the lineNode's path
UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:lineNode.path];
// Add a line to the current touch point
[path addLineToPoint:location];
// Update lineNode
lineNode.path = path.CGPath;
lineNode.physicsBody = [SKPhysicsBody bodyWithEdgeChainFromPath:path.CGPath];
lineNode.physicsBody.categoryBitMask = lineNodeCategory;
lineNode.physicsBody.contactTestBitMask = bubble1Category | bubble2Category| bubble3Category | bubble4Category | bubble5Category | bubble6Category;
lineNode.physicsBody.collisionBitMask = ballCategory | ballHalf1Category | ballHalf2Category;
lineNode.physicsBody.dynamic = YES;
lineNode.name = lineNodeCategoryName;
CGPoint _currentPoint = [[touches anyObject] locationInNode:self];
CGPoint _previousPoint = [[touches anyObject] previousLocationInNode:self];
_delta = CGPointMake(_currentPoint.x - _previousPoint.x, _currentPoint.y - _previousPoint.y);
}
}
}
Delete action - This will remove the line from the array but how do i remove the last line from the game view?
-(void)deleteLine{
_lineNodeArray = [NSMutableArray array];
[_lineNodeArray lastObject ];
[_lineNodeArray removeLastObject];
}

Spritekit - calculate distance the finger has travelled between two events to increment your a NSUInteger

I use the following code to draw a line from point Touch point A to Touch point B.
The code also allows me to draw multiple lines simultaneously.
My first attempt was to create a NSUInteger and increase it by 1 per pixel moved in touchesMoved. However the increment is not consistent. It appears that moving your finger fast will increase the number slowly whereas moving your finger slowly will increase the number fast.
I think I need to calculate the distance moved between two events and then increment the number. I don't know how to do this, can anyone help?
//Create a line at first point of touch
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
SKLabelNode *touchedNode = (SKLabelNode *)[self nodeAtPoint:positionInScene];
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
lineNode = [SKShapeNode shapeNodeWithPath:path.CGPath];
lineNode.strokeColor = [SKColor blackColor];
lineNode.glowWidth = 3.0;
[_gameLineNode 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];
}
}
// Draw the line to last touch point dynamically
- (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];
lineNode = [lines objectForKey:key];
if (lineNode != NULL) {
// Create and initial a mutable path with the lineNode's path
UIBezierPath *path = [UIBezierPath bezierPathWithCGPath:lineNode.path];
// Add a line to the current touch point
[path addLineToPoint:location];
// Update lineNode
lineNode.path = path.CGPath;
lineNode.physicsBody = [SKPhysicsBody bodyWithEdgeChainFromPath:path.CGPath];
lineNode.physicsBody.categoryBitMask = lineNodeCategory;
lineNode.physicsBody.contactTestBitMask = bubble1Category | bubble2Category| bubble3Category | bubble4Category | bubble5Category | bubble6Category;
lineNode.physicsBody.collisionBitMask = ballCategory | ballHalf1Category | ballHalf2Category;
lineNode.physicsBody.dynamic = YES;
lineNode.name = lineNodeCategoryName;
//ATTEMPT 1 - NSUINTEGER that increases per pixal moved
testNumber ++;
testLabel.text = [NSString stringWithFormat:#"%lu",(unsigned long)testNumber];
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
inkLockOrUnlock = [[NSUserDefaults standardUserDefaults] boolForKey:#"inkLockOrUnlock"];
if (inkLockOrUnlock == NO) {
SKAction *block0 = [SKAction runBlock:^{
for (UITouch *touch in touches) {
NSValue *key = [NSValue valueWithPointer:(void *)touch];
[lines removeObjectForKey:key];
}
[_gameLineNode removeAllChildren];
}];
SKAction *inkAnimation = [SKAction sequence:#[block0,]];
[self runAction:[SKAction repeatAction:inkAnimation count:1]];
} else {}
}
If I understand you, I think you might be misunderstanding a lot here. Every time touchMoved gets called, it gives you an xy coord for current position of that touch. That function can only be called max once per frame.
It sounds like you are thinking the function is called for every pixel the touch gets moved, which is not the case.
So store the initial point from touchBegan, then use math to determine distance between initial point and the current point inside touchMoved.
But also keep in mind that points don't necessarily equal pixels.

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.

UITouch coordinates and SKSpriteNode coordinates are different

I have an SKSpriteNode that is initialized with an image. I am using the -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event method to figure out if the user touch is within the bounds of the sprite. For some reason, even when I tap inside the sprite, the coordinates are not even similar. They seem to be on a different coordinate system.
#import "GameScene.h"
SKSpriteNode *card;
#implementation GameScene
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
self.backgroundColor = [SKColor whiteColor];
card = [SKSpriteNode spriteNodeWithImageNamed:#"card"];
card.position = CGPointMake(500, 500);
[self addChild:card];
}
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
int cardX = card.frame.origin.x;
int cardwidth = card.frame.size.width;
int cardY = card.frame.origin.y;
int cardHeight = card.frame.size.height;
if(touchLocation.x >= card.frame.origin.x &&
touchLocation.x <= (card.frame.origin.x + card.frame.size.width) &&
touchLocation.y <= card.frame.origin.y &&
touchLocation.y >= (card.frame.origin.y + card.frame.size.height))
{
self.backgroundColor = [SKColor blackColor];
}
else{
self.backgroundColor = [SKColor whiteColor];
}
}
#end
Node and View have different coordinate system.
If you need to know the location of the tapped point in the node coordinate you may want to replace the line :
CGPoint touchLocation = [touch locationInView:self.view];
by :
CGPoint touchLocation = [touch locationInNode:self];
For Swift 4 and above:
if playButton.frame.contains(touch.location(in: scene!)) {
print("Play button touched")
}

Resources