【问题标题】:Poor performance with SKShapeNode in Sprite KitSprite Kit 中的 SKShapeNode 性能不佳
【发布时间】:2014-08-24 13:12:37
【问题描述】:

我正在 Sprite Kit 中制作“Achtung die kurve”-克隆。对于不断移动的线/玩家,我使用 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 = path 是导致 FPS 下降的原因。

感谢您的帮助!非常感谢。

附言。 我试图完全不使用 SKShapeNode,因为它们似乎无法将线条画得很好(曲线中的小孔/伪影等)

截图:

【问题讨论】:

  • 1.不要在模拟器中测试性能。在性能方面,它不是最有代表性的。 2. 不要期望 SKShapeNode 很快,至少当你有 a) 很多或 b) 经常改变它们的路径时不会。我的理解是它们主要用于调试绘图和裁剪节点,不应该用作游戏的主要视觉节点。
  • 我明白了。好吧,设备确实会获得与模拟器相同的 FPS 下降。你对我应该用什么线有什么建议吗?谢谢回复:)
  • 无论是从点到点拉伸的精灵,还是 cocos2d 和自定义 OpenGL 绘图。
  • 好的,谢谢!我可能会改用 cocos2d 路径
  • 根据您项目的复杂程度,我相信 Sparrow 也有 OpenGL 绘图,所以这也是一个选项。它只是不如 cocos2D 强大。

标签: sprite-kit frame-rate skshapenode


【解决方案1】:

不幸的是,SKShapeNode 对于您正在尝试做的事情并不是那么好。但是,有一种方法可以优化这一点,尽管有一些注意事项。

fps 的第一个最大问题是绘制计数变得非常高,因为您添加的每个线段都是另一个绘制。如果你在你的SKView 实例上设置showsDrawCount,你就会明白我的意思。

在这个答案Multiple skshapenode in one draw? 中,您可以获得有关如何使用SKEffectNode 的shouldRasterize 属性来解决问题的更多信息,如果您正在绘制一次。如果您不这样做,您将花费处理器时间在每帧进行多次绘制。

因此,您可以看到,平局是您无法获得所需性能的主要问题。但是,您似乎希望随着时间的推移始终如一地绘制,所以我要建议的可能对您来说是一个可行的解决方案。

我建议的解决方案的逻辑是这样的:

1 - 创建一个我们可以用作画布的SKSpriteNode。

2 - 创建一个SKShapeNode,仅用于绘制当前线段。

3 - 将 SKShapeNode 设为画布的子级。

4 - 通过SKShapeNode绘制一条新线段

5 - 使用SKView 方法`textureFromNode 保存当前在画布上绘制的内容。

6 - 将画布的纹理设置为该纹理。

循环回到 #4 并为您的 SKShapeNode 为下一条线段创建一个新路径。

根据需要重复。

结果应该是您的抽奖次数永远不会超过 2 次,这将解决抽奖次数过多的问题。

基本上,您会保留之前在纹理中绘制的内容,因此只需要为最新的线段绘制一次 SKShapeNode,为SKTexture 绘制一次。

同样,我还没有尝试过这个过程,如果有任何延迟,那就是textureFromNode 调用每一帧。如果有什么是你的瓶颈,那就是它!

今天我可能会尝试这个理论,因为我需要textureFromNode 来解决我正在尝试解决的另一个问题,所以我一定会找出该方法的快/慢!哈哈

更新

这不是完整的代码,但却是实现所需绘图性能(60fps)的重要部分:

基本节点元素有:

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

Next 是一种创建纹理并将现有段返回到池中的方法:

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

正如我所说,这不是包含所有代码的代码,我假设您对可以在应用程序中实现的概念有足够的了解。这些只是我的准系统实现的示例。

【讨论】:

  • 感谢(非常详细的)建议!我可以试试这个,但希望使用 SKShapeNode 以外的东西,因为它在曲线上形成了丑陋的“洞”:)
  • 好的,我尝试了这种方法,总体而言它的帧率肯定更好,尤其是在您完成绘图时。但是,当您绘制 textureFromNode 调用确实是怀疑的瓶颈时。因此,也许在缓存图像之前创建一个阈值,通过该阈值绘制 x 数量的片段会改善延迟。
  • 我可能会尝试添加代码来更新,每次分段达到 5 个分段时,它都会通过 textureFromNode 缓存并杀死现有分段。那里可能有一个平衡。但总的来说,SpriteKit 缺少用于形状绘制的 OpenGL 解决方案,这很糟糕:/
  • 好的,好消息,现在这非常可行,我可以设置缓存前使用的段数量。现在60帧!但是,你的线路质量不是这样解决的,哈哈。将更新答案,因为这在大多数情况下都非常可行。我还汇集了 SKShapeNodes 以避免来自 SKShapeNodes 的丑陋内存泄漏。
  • Answer 现在更新了一些涉及部分的示例代码。我得到 60fps 的 5 段池,但是您可以根据需要增加/减少该数量。
【解决方案2】:

要修复曲线中的“洞”,只需将 lineCap 设置为非零值:

curSegment.lineCap = 1;

【讨论】:

    【解决方案3】:

    我尝试将prototypical 提出的好答案翻译成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 repo的链接。


    阅读了一些关于改进SKShapeNode 替代方案或强大重构的可能性的文章,我发现这个名为SKUShapeNode 的项目实际上已包含在SKUtilities 2 项目中,这是一个想法创建一个使用CAShapeLayer (some documentation) 渲染的SKSpriteNode 的子类,存在一些错误,并且总是使用节点将UIKit CALayer 转换为实际的Sprite Kit。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-15
      • 1970-01-01
      • 2014-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多