Плохая производительность с SKShapeNode в наборе Sprite

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

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

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

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

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

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

Снимок экрана: Line being constantly drawn drawn

Ответ 1

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

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

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

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

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

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

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

3 - Сделайте SKShapeNode дочерний элемент холста.

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

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

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

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

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

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

В принципе, вы сохраняете то, что ранее было нарисовано в текстуре, поэтому требуется только один SKShapeNode draw для последнего сегмента линии и один ничья для SKTexture.

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

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

UPDATE

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

Основными элементами node являются:

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

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

Ответ 2

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

curSegment.lineCap = 1;

Ответ 3

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

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

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")
    }

}

GameScene:

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 repo.


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