Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Poor performance with SKShapeNode in Sprite Kit

I'm making a "Achtung die kurve"-clone in Sprite Kit. For the constantly moving lines/players I'm using A CGMutablePathRef along with an SKShapeNode. In the update method I'm doing this

// _lineNode is an instance of SKShapeNode and path is CGMutablePathRef
CGPathAddLineToPoint(path, NULL, _xPos, _yPos);
_lineNode.path = path;

to add to the line. The update method is also updating the _xPos and _yPos constantly to make it grow.

I guess what I'm really asking is is there another, more efficient way of drawing the lines, since the way I'm doing it now drops the frame rate way too much after a while (about 15-20 seconds). At this point the FPS just drops constantly until the game is unplayable. The Time Profiler tells me that this line: _lineNode.path = path is the cause of the FPS drop.

Thanks for any help! It is greatly appreciated.

PS. I'm trying to not use SKShapeNode at all since they seem to not being able to draw the lines too good (Small holes/artifacts in the curves etc.)

Screenshot: Line being constantly drawn drawn

like image 503
oyvindhauge Avatar asked Jul 03 '14 12:07

oyvindhauge


1 Answers

Unfortunately, SKShapeNode is not that great for what you are trying to do. However, there is a way to optimize this, albeit with some caveats.

First one of the largest problems with the fps is that the draw count gets extremely high because each line segment you add is another draw. If you set showsDrawCount on your SKView instance, you will see what I mean.

In this answer Multiple skshapenode in one draw?, you can get more information about how you can use shouldRasterize property of a SKEffectNode to solve the problem if you are drawing something once. If you don't do this, you will have processor time spent on numerous draws each frame.

So you can see that the draws is the main issue with you not getting the performance you desire. However, you seem to want to be drawing consistently over time, so what I am going to suggest might be a viable solution for you.

The logic of the solution I am suggesting is as such :

1 - Create a SKSpriteNode that we can use as a canvas.

2 - Create one SKShapeNode that will be used to draw ONLY the current line segment.

3 - Make that SKShapeNode a child of the canvas.

4 - Draw a new line segment via SKShapeNode

5 - Use the SKView method `textureFromNode to save what has currently been drawn on the canvas.

6 - set the texture of the canvas to that texture.

Loop back to #4 and make a new path for your SKShapeNode for the next line segment.

Repeat as needed.

The result should be that your draw count will never be higher than 2 draws, which would solve the problem of a high draw count.

Basically, you are preserving what has previously been drawn in a texture, therefore only ever needing one SKShapeNode draw for the latest line segment and one draw for the SKTexture.

Again, I have not tried this process yet, and if there is any lag it would be in that textureFromNode call each frame. If anything would be your bottleneck, that would be it!

I might try this theory out some time today, as I need textureFromNode for another problem I am trying to solve, and so I'll definitely find out how fast/slow that method is! haha

UPDATE

This is not complete code, but is the important parts to achieve the desired drawing performance (60fps) :

The basic node elements are :

container -> SKNode that contains all elements that need to be cached

canvas -> SKSpriteNode that will display the cached version of drawn segments

pool of segments -> used to draw segments initially, and get reused as needed

First create a pool of SKShapeNodes :

pool = [[NSMutableArray alloc]init];

//populate the SKShapeNode pool
// the amount of segments in pool, dictates how many segments
// will be drawn before caching occurs.
for (int index = 0; index < 5; index++)
{
    SKShapeNode *segment = [[SKShapeNode alloc]init];
    segment.strokeColor = [SKColor whiteColor];
    segment.glowWidth = 1;
    [pool addObject:segment];
}

Next create method for getting a SKShapeNode from pool :

-(SKShapeNode *)getShapeNode
{
    if (pool.count == 0)
    {
        // if pool is empty, 
        // cache the current segment draws and return segments to pool
        [self cacheSegments];
    }

    SKShapeNode *segment = pool[0];
    [pool removeObjectAtIndex:0];

    return segment;
}

Next create a method for getting a segment from pool and drawing the line :

-(void)drawSegmentFromPoint:(CGPoint)fromPoint toPoint:(CGPoint)toPoint
{
    SKShapeNode *curSegment = [self getShapeNode];
    CGMutablePathRef path = CGPathCreateMutable();
    curSegment.lineWidth = 3;
    curSegment.strokeColor = [SKColor whiteColor];
    curSegment.glowWidth = 1;
    curSegment.name = @"segment";

    CGPathMoveToPoint(path, NULL, fromPoint.x, fromPoint.y);
    CGPathAddLineToPoint(path, NULL, toPoint.x, toPoint.y);
    curSegment.path = path;
    lastPoint = toPoint;
    [canvas addChild:curSegment];
}

Next is a method for creating a texture and returning existing segments to the pool :

-(void)cacheSegments
{
    SKTexture *cacheTexture =[ self.view textureFromNode:container];
    canvas.texture = cacheTexture;
    [canvas setSize:CGSizeMake(canvas.texture.size.width, canvas.texture.size.height)];
    canvas.anchorPoint = CGPointMake(0, 0);
    [canvas enumerateChildNodesWithName:@"segment" usingBlock:^(SKNode *node, BOOL *stop)
     {
         [node removeFromParent];
         [pool addObject:node];
     }];

}

Lastly the touch handlers :

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self cacheSegments];
    for (UITouch *touch in touches)
    {
        CGPoint location = [touch locationInNode:self];
        lastPoint = location;
        [self drawSegmentFromPoint:lastPoint toPoint:location];
    }
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches)
    {
        CGPoint location = [touch locationInNode:self];
        [self drawSegmentFromPoint:lastPoint toPoint:location];
    }
}

As I said, this is not all inclusive code, I assume you understand enough about the concept that you can implement into your application. These are just examples of my barebones implementation.

like image 87
prototypical Avatar answered Oct 07 '22 13:10

prototypical