Create Endless Cgpath Without Framedrops

Poor performance with SKShapeNode in Sprite Kit

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.

Method of creating a user created line with physics and strong performance

Here is an example of how to get it working efficiently. You can work on how to get it on and off of the scene for even more efficiency.

The basic premises is you use 1 SKShapeNode to draw your continuous line. Once you finished drawing the line, you convert it over to a texture to be used as an SKSpriteNode

//
// GameScene.swift
// physics
//
// Created by Anthony Randazzo on 7/28/17.
// Copyright © 2017 SychoGaming. All rights reserved.
//

import SpriteKit
import GameplayKit

class GameScene: SKScene {

private var shape = SKShapeNode()
private var path : CGMutablePath!

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

let position = touches.first!.positionOnScene
path = CGMutablePath()
path.move(to: CGPoint.zero)
shape.path = path
shape.position = position
self.addChild(shape)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let position = touches.first!.positionOnScene - shape.position
path.addLine(to: position)
shape.path = path
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let position = touches.first!.positionOnScene - shape.position
path.closeSubpath()
shape.removeFromParent()
autoreleasepool
{

let sprite = SKSpriteNode(texture: self.view?.texture(from: shape,crop:shape.frame))

sprite.position = CGPoint(x:shape.frame.midX,y:shape.frame.midY)
let physicsBody = SKPhysicsBody(polygonFrom: path)
physicsBody.isDynamic = false
sprite.physicsBody = physicsBody

addChild(sprite)
}
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let position = touches.first!.positionOnScene
}

override func update(_ currentTime: TimeInterval) {
}
}

extension UITouch
{
var positionOnScene : CGPoint
{
return self.location(in: (self.view as! SKView).scene!)
}
}
extension CGPoint
{
static public func -(lhs:CGPoint,rhs:CGPoint) -> CGPoint
{
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static public func -= (lhs:inout CGPoint,rhs:CGPoint)
{
lhs = lhs - rhs
}
}


Related Topics



Leave a reply



Submit