Низкая производительность с SKShapeNode в Sprite Kit

Я делаю клон " Achtung die kurve " в Sprite Kit. Для постоянно движущихся линий / игроков я использую CGMutablePathRef вместе с SKShapeNode. В методе обновления я делаю это

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

добавить в строку. Метод update также постоянно обновляет _xPos и ​​_yPos, чтобы он рос.

Я предполагаю, что я действительно спрашиваю, есть ли другой, более эффективный способ рисования линий, так как способ, которым я это делаю, теперь слишком сильно снижает частоту кадров через некоторое время (около 15-20 секунд). В этот момент FPS постоянно падает до тех пор, пока игра не станет недоступной для игры. Time Profiler сообщает мне, что эта строка: _lineNode.path = path является причиной сброса FPS.

Спасибо за любую помощь! Это очень ценится.

PS. Я пытаюсь вообще не использовать SKShapeNode, так как кажется, что они не могут хорошо рисовать линии (маленькие дырки / артефакты на кривых и т. Д.)

Скриншот: Линия постоянно рисуется

3 ответа

Решение

К сожалению, SKShapeNode не так хорош для того, что вы пытаетесь сделать. Тем не менее, есть способ оптимизировать это, хотя и с некоторыми оговорками.

Во-первых, одна из самых больших проблем с fps заключается в том, что число ничьих становится чрезвычайно высоким, потому что каждый добавляемый вами отрезок линии - это еще одна ничья. Если вы установите showsDrawCount на ваше SKView Например, вы поймете, что я имею в виду.

В этом ответе несколько скшапеноде в одном розыгрыше?, вы можете получить больше информации о том, как вы можете использовать shouldRasterize свойство SKEffectNode решить проблему, если вы рисуете что-то один раз. Если вы этого не сделаете, вы будете тратить процессорное время на многочисленные розыгрыши каждого кадра.

Таким образом, вы можете видеть, что ничья - это главная проблема, когда вы не получаете желаемую производительность. Тем не менее, вы, кажется, хотите постоянно рисовать, поэтому то, что я собираюсь предложить, может оказаться для вас жизнеспособным решением.

Логика решения, которое я предлагаю, такова:

1 - Создать SKSpriteNode что мы можем использовать в качестве холста.

2 - Создать SKShapeNode это будет использоваться для рисования ТОЛЬКО текущего сегмента линии.

3 - Сделать это SKShapeNode дитя холста.

4 - Нарисуйте новый отрезок линии через SKShapeNode

5 - Используйте SKView метод `textureFromNode, чтобы сохранить то, что в данный момент нарисовано на холсте.

6 - установить текстуру холста на эту текстуру.

Вернитесь к #4 и проложите новый путь для вашего SKShapeNode для следующего отрезка.

Повторите по мере необходимости.

Результатом должно быть то, что ваш счет ничьей никогда не будет больше 2 ничьих, что решило бы проблему большого количества ничьих.

По сути, вы сохраняете то, что ранее было нарисовано в текстуре, поэтому вам нужна только одна SKShapeNode ничья для последнего отрезка и одна ничья для SKTexture,

Опять же, я еще не пробовал этот процесс, и если бы было какое-то отставание, это было бы в этом textureFromNode Назовите каждый кадр. Если что-нибудь и станет вашим узким местом, так оно и будет!

Я мог бы попробовать эту теорию сегодня, когда мне нужно textureFromNode для другой проблемы, которую я пытаюсь решить, и поэтому я определенно выясню, насколько быстрый / медленный этот метод! ха-ха

ОБНОВИТЬ

Это не полный код, но важная часть для достижения желаемой производительности при рисовании (60 кадров в секунду):

Основные элементы узла:

container -> SKNode, который содержит все элементы, которые необходимо кэшировать

canvas -> SKSpriteNode, который будет отображать кэшированную версию нарисованных сегментов

пул сегментов -> используется для начального рисования сегментов и используется повторно при необходимости

Сначала создайте пул 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];
}

Затем создайте метод для получения SKShapeNode из пула:

-(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;
}

Затем создайте метод для получения сегмента из пула и рисования линии:

-(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];
}

Далее следует метод создания текстуры и возврата существующих сегментов в пул:

-(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];
     }];

}

Наконец сенсорные обработчики:

-(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];
    }
}

Как я уже сказал, это не полный код, я предполагаю, что вы достаточно понимаете концепцию, которую вы можете внедрить в свое приложение. Это всего лишь примеры моей базовой реализации.

Чтобы исправить "дыры" в кривых, просто установите lineCap в ненулевое значение:

curSegment.lineCap = 1;

Я пытаюсь перевести на Swift 2.2 хороший ответ, предложенный прототипом:

Пользовательский узел:

class AOShapeNode: SKNode {
    var currentScene: SKScene!
    var canvas = SKShapeNode()
    var segment = SKShapeNode()
    var pool:[SKShapeNode]!
    var poolSize: Int = 50 // big number to improve performance
    var segmentLineWidth: CGFloat = 3

    init(currentScene scene:SKScene,nodeSize size: CGSize) {

        super.init()
        print("---");
        print("∙ \(self.dynamicType)")
        print("---")
        self.userInteractionEnabled = true
        self.currentScene = scene
        self.addChild(canvas)
        pool = [SKShapeNode]()
        for _ in 0..<poolSize
        {
            let segment = SKShapeNode()
            segment.strokeColor = UIColor.blackColor()
            segment.glowWidth = 1
            segment.lineCap = CGLineCap(rawValue: 1)!
            pool.append(segment)
        }
    }

    func getShapeNode() -> SKShapeNode {
        if(pool.count == 0)
        {
            self.cacheSegments()
        }
        let segment = pool.first
        pool.removeFirst()
        return segment!
    }

    func drawSegmentFromPoint(fromPoint:CGPoint, toPoint:CGPoint)->CGPoint {
        let curSegment = self.getShapeNode()
        let path = CGPathCreateMutable()
        curSegment.lineWidth = segmentLineWidth
        curSegment.strokeColor = SKColor.blackColor()
        curSegment.glowWidth = 1
        curSegment.lineCap = CGLineCap(rawValue: 1)!
        curSegment.name = "segment"
        CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y)
        CGPathAddLineToPoint(path, nil, toPoint.x, toPoint.y)
        curSegment.path = path
        canvas.addChild(curSegment)
        return toPoint
    }

    func cacheSegments() {
        if let cacheTexture = self.currentScene.view?.textureFromNode(self) {
            let resizeAction = SKAction.setTexture(cacheTexture, resize: true)
            canvas.runAction(resizeAction)
        }
        canvas.enumerateChildNodesWithName("segment", usingBlock: {
            (node: SKNode!, stop: UnsafeMutablePointer <ObjCBool>) -> Void in
            self.pool.append(node as! SKShapeNode)
            node.removeFromParent()
        })

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Сцена игры:

class GameScene: SKScene {
    var line : AOShapeNode!
    var lastPoint :CGPoint = CGPointZero

    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
        line = AOShapeNode.init(currentScene: self, nodeSize: self.size)
        self.addChild(line)
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
       /* Called when a touch begins */
        line.cacheSegments()

        for touch in touches {
            let location = touch.locationInNode(self)
            lastPoint = location
            lastPoint = line.drawSegmentFromPoint(lastPoint, toPoint: location)
        }
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        /* Called when a touch moved */
        for touch in touches {
            let location = touch.locationInNode(self)
            lastPoint = line.drawSegmentFromPoint(lastPoint, toPoint: location)
        }
    }
}

Это выше, это весь код, но если вы хотите попробовать, это ссылка на репозиторий github.


Чтение некоторых статей о возможностях улучшения SKShapeNode альтернатив или сильного ре-факторинга, я нашел этот проект под названием SKUShapeNode, фактически включенный в проект SKUtilities 2, идея сделать подкласс SKSpriteNode что делает с использованием CAShapeLayer ( некоторая документация), есть некоторые ошибки, и всегда есть UIKitCALayer к актуальному Sprite Kit, используя узел.

Другие вопросы по тегам