iOS Sprite Kit SKShapeNode does not fill entire path - ios

Issue: A SKShapeNode sometimes doesn't draw the entire path. If it's moving each frame some frames it doesn't draw. In my real case scenario I'm using a very complex 10+ sided shape but for this demo I'm just doing a triangle. See screenshots for example (http://imgur.com/a/50mHA).
Code: See this in action by adding this to the basic template, or any scene, and move the mouse (finger) around. Some shapes show this glitching more than others but if you move around enough you'll see it on every shape.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint location = [[touches anyObject] locationInNode:self];
[self enumerateChildNodesWithName:#"path" usingBlock:^(SKNode *node, BOOL *stop) {
[node removeFromParent];
}];
CGMutablePathRef p = CGPathCreateMutable();
CGPathMoveToPoint(p, NULL, location.x-80, location.y-130);
CGPathAddLineToPoint(p, NULL, location.x+80, location.y);
CGPathAddLineToPoint(p, NULL, location.x-80, location.y+130);
CGPathCloseSubpath(p);
SKShapeNode *shape = [SKShapeNode node];
shape.name = #"path";
shape.path = p;
shape.fillColor = [UIColor redColor];
[self addChild:shape];
}
Any idea what's going on?
Edit: I wanted to add that the overall effect I'm trying to accomplish is dynamic lighting in a scene with physics bodies and a player (light source). I have used the information from http://ncase.me/sight-and-light/ to create a polygon. However as the player runs through the level, 99% of the time it works fine, except for that 1% where the shape glitches and doesn't draw completely. Which is what I'm showing in the example here.

OK, what you're doing here is very inefficient which is possibly why you're getting this effect.
What you should do is create a single shape node and store it in a property...
#property (nonatomic, strong) SKShapeNode *theShapeNode;
and when the scene loads you can create it once...
// in some method that runs once when the scene loads...
// path is relative to the shape node not the the scene it is in.
// by MOVING the shapeNode the path moves too.
CGMutablePathRef p = CGPathCreateMutable();
CGPathMoveToPoint(p, NULL, 0, -260);
CGPathAddLineToPoint(p, NULL, 160, 0);
CGPathAddLineToPoint(p, NULL, 0, 260);
CGPathCloseSubPath(p);
self.theShapeNode = [SKShapeNode node];
self.theShapeNode.path = p;
self.theShapeNode.fillColor = [UIColor redColor];
[self addChild:theShapeNode];
Then in the touches moved method...
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint location = [[touches anyObject] locationInNode:self];
self.theShapeNode.position = location;
}
This means that each time the touch moves you're just repositioning the node that already exists. You're not having to create the path each time.

Related

SKShapeNode position issues

I add a couple of nodes, like this:
SKShapeNode* node= [SKShapeNode shapeNodeWithRectOfSize:rect.size]; // always 32x32
node.fillColor= [UIColor darkGrayColor];
node.strokeColor= [UIColor lightGrayColor];
node.position= position;
node.physicsBody= [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(rect.size.width - 2.0, rect.size.height - 2.0)];
node.physicsBody.categoryBitMask= catSomeNode;
node.physicsBody.contactTestBitMask= catSomeOtherNode;
node.physicsBody.dynamic= YES;
node.physicsBody.usesPreciseCollisionDetection= YES;
node.physicsBody.affectedByGravity= NO;
node.physicsBody.collisionBitMask= 0;
[self addChild:node];
NSLog(#"Adding node at position %#", NSStringFromCGPoint(node.position));
e.g. for the first shape:
2015-11-14 16:17:25.162 MyApp[13479:632495] Adding node at position {16, 16}
Later, I will try to animate the shape when it is touched. To determine the direction of the animation, I need to find out the position of the node.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode* node = [self nodeAtPoint:location];
if (!(node.physicsBody.categoryBitMask & catSomeNode))
return;
if ([node isKindOfClass:[SKShapeNode class]])
{
// Here, the position is different
}
}
The changed position according to the debugger is:
(lldb) p position
(CGPoint) $0 = (x = 15.999996185302734, y = 15.999996185302734)
This will mess up my logic, because i cannot easily compare positions.
2 questions:
1) Positioning
Is it correct, that positioning is always based on the node center, unlike e.g. UIView? This means essentially when I want to place a node at 0/0, I will have to set the position to width/2 / height/2, right?
This feels a bit cumbersome, but well, I can work with that.
2) Touch detection
As you can see in my example, the position of the brick was changed. Why is that? After [self addChild:node]; the position will remain at 16/16, but after the touch detection it is changed to 15.999996185302734 / 15.999996185302734.
Whats wrong? I even saved the pointer of the node as class member, to check the position (in case nodeAtPoint returns a different object). But still the position is messed up.
[edit] Did some more tests. I saved the pointer to the SKShapeNode added above in my class, and I put a debug message in the scene's update: method, which prints the position of the node. This is what happens:
2015-11-14 18:39:07.560 MyApp[14414:696141] Node position {16, 16}
2015-11-14 18:39:07.701 MyApp[14414:696141] Node position {15.999999046325684, 15.999999046325684}
2015-11-14 18:39:07.866 MyApp[14414:696141] Node position {15.999998092651367, 15.999998092651367}
2015-11-14 18:39:07.951 MyApp[14414:696141] Node position {15.999997138977051, 15.999997138977051}
2015-11-14 18:39:08.019 MyApp[14414:696141] Node position {15.999996185302734, 15.999996185302734}
There is no code executed between the first update: and the second one. Also I did not tap, so no touch detection was done.
This leads me to believe I have some sort of initialization issue, so the scene is somewhat changed as soon as the app is loaded, or the scene becomes visible. I thought I already have this covered, because the node is added in the - (void)didMoveToView:(SKView *)view method.

SpriteKit - Scale in one direction

How do I make an object grow in one direction is SpriteKit?
Ultimately, I would like to have a node be inserted where the user touches and have each end of a line scale until it hits another object in the same category or hits the edge of the screen. I understand how to have it scale on an axis and I can then cancel the scaling after one collision is detected but I want both directions to scale independently until a collision is made for each. I am new to SpriteKit so my searches may be using the wrong terms but I have not been able to find anything relevant.
Code to add a "wall" where the user touches
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch * touch in touches) {
CGPoint location = [touch locationInNode:self];
[self addWallAtLocation:location];
}
}
-(void)addWallAtLocation:(CGPoint)location{
int odd_or_even = arc4random() % 2;
SKSpriteNode * wall = [SKSpriteNode spriteNodeWithColor:[UIColor whiteColor] size:CGSizeMake(2, 2)];
wall.position = location;
wall.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(2, 2)];
wall.physicsBody.dynamic = NO;
wall.physicsBody.categoryBitMask = wallCategory;
wall.physicsBody.contactTestBitMask = ballCategory;
[self addChild:wall];
SKAction * grow;
if (odd_or_even == 1) {
grow = [SKAction scaleYTo:300 duration:2];
} else {
grow = [SKAction scaleXTo:300 duration:2];
}
[wall runAction:grow];
}
How I want it to work:
Touch 1 makes the horizontal line from a point which shoots out in both directions on the x-axis until both ends hit the edge of the screen. Touch 2 would then create a point which shoots out in both directions on the y-axis until the lower end hits the wall created by Touch 1 and the upper end hits the edge of the screen. The lines (scaled points) are generated where the user touches. So I do not want the part of the vertical line that is highlighted by the red box to be there.
Scaling in one direction is just a solution I see to this problem, if there is a better way of going about it I will certainly try that other solution.
EDIT FOR UPDATED QUESTION:
It sounds like you might be asking "how to scale a thing in a single axis, but then stop the scaling of that single axis in one direction only when it collides with another thing"? There isn't a simple way to do that just using actions, but you you could do some slight of hand by manipulating the anchor point of a sprite. Or, probably more appropriately, you should use 2 sprites per touch, with one action sending one north, and another action sending the other one south. Maybe like this:
-(void)someMethod{
SKSpriteNode *touch2TopPart = [SKSpriteNode node];
SKSpriteNode *touch2BottomPart = [SKSpriteNode node];
touch2TopPart.anchorPoint = CGPointMake(.5, 0);
touch2BottomPart.anchorPoint = CGPointMake(.5, 1);
touch2TopPart.name = #"touch2TopPart";
touch2BottomPart.name = #"touch2BottomPart";
SKAction *sendTopPartUp = [SKAction scaleYTo:100 duration:2];
SKAction *sendBottomPartDown = [SKAction scaleYTo:100 duration:2];
[touch2TopPart runAction:sendTopPartUp withKey:#"sendTopPartUp"];
[touch2BottomPart runAction:sendBottomPartDown withKey:#"sendBottomPartDown"];
}
-(void)whateverTheCollisonMethodIsCalledBetweenThingAandThingB
{
SKSpriteNode *somePartMovingInASingleDirection = thingAOrWhatever;
[somePartMovingInASingleDirection removeAllActions];
}
---ORIGINAL ANSWER
I'm not exactly sure what you're asking, but you already have the actions for scaling in just X or Y, so here's the simple usage for removing one of those actions after physics is simulated. You might be looking for how to detect a specific collision between two entities, but I can't be sure.
-(void)someMethod{
SKSpriteNode *thisSprite = [SKSpriteNode node];
thisSprite.name = #"spriteRunningActions";
SKAction *growX = [SKAction scaleXTo:100 duration:2];
SKAction *growY = [SKAction scaleYTo:100 duration:2];
[thisSprite runAction:growX withKey:#"actionCurrentlyScalingX"];
[thisSprite runAction:growY withKey:#"actionCurrentlyScalingY"];
}
-(void)didSimulatePhysics
{
SKSpriteNode *thatSprite = (SKSpriteNode*)[self childNodeWithName:#"spriteRunningActions"];
//stop a given action
[thatSprite removeActionForKey:#"actionCurrentScalingX"];
[thatSprite removeActionForKey:#"actionCurrentScalingY"];
}
Setting the anchor point like below made the "lines" scale in one direction.
wall.anchorPoint = CGPointMake(0, 0);

Determining whether a point lies within the boundary of a physics body in Sprite Kit

I'm using Sprite Kit to create a game involving balloons. The balloon image is a long rectangle (dotted line) but I've defined a physics body around the balloon itself using a polygon (solid line). I'd like players to be able to "pop" the balloon. I'm using the following block of code to achieve this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
NSArray *nodes = [self nodesAtPoint:[touch locationInNode:self]];
for (SKNode *node in nodes) {
[node removeFromParent];
SKAction *pop = [SKAction playSoundFileNamed:#"pop.wav" waitForCompletion:NO];
[self runAction:pop];
}
}
Unfortunately, when there are several balloons in the bunch, this method can result in a single tap popping multiple balloons. This is because the sprite images overlap (even though the physics bodies do not). There is also the undesired effect that a tap on the string will have the same effect as a tap on the balloon.
I have access to the coordinates of the touch point, but is it even possible to detect whether a point lies within the area defined by the physics body (as opposed to the node)?
Use the physics world's bodyAtPoint: method:
SKPhysicsBody* body = [self.scene.physicsWorld bodyAtPoint:location];
bodyAtPoint is buggy but you have a working alternative.
You can create rays using baloon polygon path.
[self.physicsWorld enumerateBodiesAlongRayStart:rayStart end:rayEnd
usingBlock:^(SKPhysicsBody *body, CGPoint point,CGVector normal, BOOL *stop) {
if(body.categoryBitMask & baloonCategory){
SKPhysicsBody *contactBody=body;
CGPoint contactPoint=point;
*stop=YES;
}
}];
I looked but wasn't able to find a way of checking whether a touch is made within the physicsBody of a sprite. I would suggest separating the string and the ballon sprites.
You can try any the following:
Using separate sprite nodes for the balloon and the string and connecting them using a SKPhysicsJoint.
Subclass SKSpriteNode and add two sprites, i.e. the balloon and the string as children of the Sprite. Then u can check whether a touch made on the sprite is on the ballon or not.
Additionally for the multiple ballon pop problem, you can modify your code like this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
NSArray *nodes = [self nodesAtPoint:[touch locationInNode:self]];
//Instead of popping all nodes within the touch, pop only the first object.
if ([nodes count] > 0)
{
SKNode *node = [nodes firstObject];
[node removeFromParent];
SKAction *pop = [SKAction playSoundFileNamed:#"pop.wav" waitForCompletion:NO];
[self runAction:pop];
}
}

intersectsNode not detecting intersection

I'm building an app using spriteKit (objective-C), and I'm using intersectsNode to tell if a player-drawn line collides with letter tiles on the gameboard. I have another instance of intersectsNode in my program that is working fine, but for some reason this new scenario always returns false. I've checked the parameters, and the only thing spriteKit says is make sure they are both children of the same tree, which they definitely are (self). One is a SKSpriteNode, and the other is a SKShapeNode.
if (directionIsDecided == TRUE) {
for (letterTile in letterTileArray) {
if ([letterTile intersectsNode:selectorLine]) {
[lettersToBeCalculatedForPoints addObject:letterTile];
}
}
}
Hopefully I'm not overlooking something stupid... Any ideas what could cause this function to never return true?
*edit
Ok, I think it has something to do with how I'm drawing my line. I changed it up so it detects collision with the vowels that have been spawned on the board, each vowel being a child of letterTile, and now it tells me that ALL vowels are colliding with the selecorLine, even if it doesn't touch any of them.
This is how I created my selectorLine:
Implementation:
CGMutablePathRef pathToDraw;
SKShapeNode *selectorLine;
TouchesBegan:
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
pathToDraw = CGPathCreateMutable();
CGPathMoveToPoint(pathToDraw, NULL, positionInScene.x, positionInScene.y);
selectorLine = [SKShapeNode node];
selectorLine.path = pathToDraw;
selectorLine.strokeColor = [SKColor greenColor];
selectorLine.lineWidth = 10;
[self addChild:selectorLine];
selectorLine.zPosition = 101;
TouchesMoved:
UITouch* touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPathAddLineToPoint(pathToDraw, NULL, positionInScene.x, positionInScene.y);
selectorLine.path = pathToDraw;
Any and all ideas are appreciated, thanks!
I have been working around this in my code by using CGRectIntersectsRect for SKShapeNode bounding rectangle intersections.
Using CGRectIntersectsRect your code would look like:
if (directionIsDecided == TRUE) {
for (letterTile in letterTileArray) {
if (CGRectIntersectsRect(letterTile.frame, selectorLine.frame)) {
[lettersToBeCalculatedForPoints addObject:letterTile];
}
}
}
intersectsNode uses the calculateAccumulatedFrame method.
It's stated a few places deep deep in the SpriteKit documentation that you should override this method with your own in many cases. After reading that I ended up doing it all the time. SKShapeNodes are a prime example of this. But if you subclass a SKNode and fill it with other content you'll probably also not want all of it to "intersect" as per usual.
So override the calculateAccumulatedFrame method for your shapenode subclass, and it should intersect nicely again.
I had the same problem as you. I had an array undeclared so adding objects to it did nothing and there were no nodes to check against. Double check that your letterTileArray is not empty.
Similar problem, in my case one of the nodes I was comparing was simple SKNode grouping together other node types. An SKNode has a position but no width nor height so it does not 'intersect' anything.
For me a combination of #JoePasq and #Theis Egeberg's answers was needed:
if node.calculateAccumulatedFrame().intersects(sprite.frame) {...
(note if both nodes are simple SKNodes then use caluclateAccuulatedFrame in both positions, in my case sprite was an SKSpriteNode so a simple .frame is fine.)

Cocos2d ccDrawLine performance issue

I use cocos2d 2.0 and Xcode 4.5. I am trying to learn how to draw a line. I can draw a line but after I drew few lines a serious performance issue occurs on Simulator.
Simulator starts to freeze, draws lines very very slowly and worst of all ,I guess because of -(void)draw is called every frame, the label on the screen becomes bold
before lines :
after lines;
I use following code :
.m
-(id) init
{
if( (self=[super init])) {
CCLabelTTF *label = [CCLabelTTF labelWithString:#"Simple Line Demo" fontName:#"Marker Felt" fontSize:32];
label.position = ccp( 240, 300 );
[self addChild: label];
_naughtytoucharray =[[NSMutableArray alloc ] init];
self.isTouchEnabled = YES;
}
return self;
}
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
BOOL isTouching;
// determine if it's a touch you want, then return the result
return isTouching;
}
-(void) ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [ touches anyObject];
CGPoint new_location = [touch locationInView: [touch view]];
new_location = [[CCDirector sharedDirector] convertToGL:new_location];
CGPoint oldTouchLocation = [touch previousLocationInView:touch.view];
oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
oldTouchLocation = [self convertToNodeSpace:oldTouchLocation];
// add my touches to the naughty touch array
[_naughtytoucharray addObject:NSStringFromCGPoint(new_location)];
[_naughtytoucharray addObject:NSStringFromCGPoint(oldTouchLocation)];
}
-(void)draw
{
[super draw];
ccDrawColor4F(1.0f, 0.0f, 0.0f, 100.0f);
for(int i = 0; i < [_naughtytoucharray count]; i+=2)
{
CGPoint start = CGPointFromString([_naughtytoucharray objectAtIndex:i]);
CGPoint end = CGPointFromString([_naughtytoucharray objectAtIndex:i+1]);
ccDrawLine(start, end);
}
}
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
ManageTraffic *line = [ManageTraffic node];
[self addChild: line z:99 tag:999];
}
I saw few Air Traffic Control games such as Flight Control, ATC Mania works really well.
Does this performance issue occur because of CCDrawLine/UITouch *touch or it is a common issue?
What Flight Control, ATC Mania might be using for line drawing?
Thanks in advance.
EDIT::::
OK I guess problem is not ccDrawLine, problem is I call ManageTraffic *line = [ManageTraffic node]; every time touch ends it calls init of node so it overrides scene
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
ManageTraffic *line = [ManageTraffic node];
[self addChild: line z:99 tag:999];
}
There's three things going on:
You assess performance on the Simulator. Test it on a device as Ben says.
You store points as strings and convert strings back to CGPoint. That is terribly inefficient.
ccDrawLine is not exactly efficient. For a couple dozen line segments it's ok. In your case maybe not (see below).
For #2, create a point class with only a CGPoint property and use that to store points in the array. Removes the string conversion or packing into NSData.
For #3 make sure that new points are only added if the new point is at least n points away from the previous point. For example a distance of 10 should reduce the number of points while still allowing for relatively fine line details.
Also regarding #3, I notice you add both current and previous point to the array. Why? You only need to add the new point, and then draw points from index 0 to 1, from 1 to 2, and so on. You only have to test for the case where there is only 1 point. The previous touch event's location is always the next touch event's previousLocation. So you're storing twice as many points as you need to.

Resources