Sprite Kit中使用SKShapeNode时性能较差

13

我正在使用Sprite Kit 制作“Achtung die kurve” 的克隆版。对于不断移动的线条/玩家,我使用了 CGMutablePathRef 和 SKShapeNode。在 update 方法中,我进行了以下操作:

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

为了将线添加到画布上,更新方法会不断更新_xPos和_yPos来使其增长。

我想问的是是否有另一种更有效的绘制线条的方式,因为我现在的方法在一段时间后(大约15-20秒)会导致帧率急剧下降。此时帧率会持续下降,直到游戏无法进行。时间分析器告诉我这行代码_lineNode.path = path是导致帧率下降的原因。

感谢任何帮助!非常感谢。

附加说明:我试图完全不使用SKShapeNode,因为它们似乎不能很好地绘制线条(曲线上有小洞/瑕疵等)。

屏幕截图:Line being constantly drawn drawn


3
  1. 不要在模拟器中测试性能,它与实际性能相差甚远。
  2. 不要期望 SKShapeNode 很快,尤其是当你有很多节点或者频繁改变它们的路径时。我的理解是它们主要用于调试绘图和裁剪节点,并不应该用作游戏的主要可视化节点。
- CodeSmile
我明白了。嗯,设备确实会像模拟器一样出现FPS下降的情况。你有什么建议可以用来解决这个问题吗?感谢你的回复 :) - oyvindhauge
可以使用从点到点延伸的精灵,或者使用cocos2d和自定义OpenGL绘图。 - CodeSmile
好的,谢谢!也许我会选择cocos2d这条路。 - oyvindhauge
根据您的项目复杂度,Sparrow 也拥有 OpenGL 绘制功能,这也是一个选择。但它不像 cocos2D 那样强大。 - prototypical
3个回答

23

很遗憾,SKShapeNode 不适合你想要做的事情。不过,有一种方法可以优化它,尽管有一些注意事项。

首先,最大的问题之一是 fps 很低,因为每添加一个线段就会增加一次绘制计数。如果在 SKView 实例上设置 showsDrawCount,你就会明白我的意思。

在这个答案中 Multiple skshapenode in one draw?,你可以了解到如何使用 SKEffectNodeshouldRasterize 属性来解决绘制一次的问题。如果你不这样做,处理器时间将花费在每一帧的众多绘制上。

所以可以看出,绘制是你无法获得想要性能的主要问题。但是,你似乎想要持续绘制,所以我要提出的建议可能是对你可行的解决方案。

我建议的解决方案的逻辑如下:

1-创建一个用作画布的 SKSpriteNode

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

3-将那个 SKShapeNode 作为画布的子节点。

4-通过 SKShapeNode 绘制新的线段。

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

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

回到 #4 循环,为下一个线段创建新路径。

如有需要,请重复此过程。

结果应该是你的绘制计数永远不会超过 2 次,这将解决高绘制计数的问题。

基本上,你正在保留以前绘制的内容并将其保存为纹理,因此只需要一次 SKShapeNode 绘制最新的线段和一次 SKTexture 绘制即可。

再次说明,我还没有尝试过这个过程,如果有任何滞后,那就是每帧 textureFromNode 调用的原因。如果有任何瓶颈,那就是它!

更新

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

基本节点元素为:

容器 -> 包含需要缓存的所有元素的 SKNode

画布 -> 显示所绘制段的缓存版本的 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];
    }
}

就像我所说的,这并不是包罗万象的代码,我假设你对这个概念有足够的了解,可以将其实现到你的应用程序中。 这些只是我基本实现的示例。


好的,我尝试了这种方法,总体来说帧率确实更好,特别是在你完成绘制后。然而,在你绘制时,textureFromNode调用确实像我们预想的那样成为了瓶颈。因此,也许在绘制x个线段之前创建一个阈值,然后再缓存图像,可以改善这种延迟。 - prototypical
好的,好消息,现在我可以设置缓存前使用的段数,这非常可行。现在是60fps!然而,这并没有解决你的线条质量问题,哈哈。随着这个方案在大多数情况下非常可行,我会更新答案。我还将SKShapeNodes池化,以避免SKShapeNodes的丑陋内存泄漏问题。 - prototypical
答案现已更新,包括一些涉及的示例代码。我使用5个段的池获得了60fps,但您可以根据需要增加/减少该数量。 - prototypical
感谢您的时间!我接受了您的答案,因为您找到了使用SKShapeNode和60 FPS的方法。干得好! :) 不过我还是更倾向于使用Cocos2d,因为它似乎会给我更多自由。而且通过使用它,我可能会学到更多关于游戏开发的技术方面的知识。 - oyvindhauge
@prototypical 还有,为什么我们需要那个池?为什么我不能在画布上绘制,在下一次绘制时从中获取纹理,删除除纹理外的所有元素,并重复该操作? - user1483208
显示剩余7条评论

2

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

curSegment.lineCap = 1;

2

我尝试将 Swift 2.2 中 prototypical 提出的好答案进行翻译:

自定义节点:

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

}

The 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仓库的链接


阅读了一些关于改进更好的SKShapeNode替代方案或强有力的重构的文章后,我发现了这个名为SKUShapeNode的项目,实际上被纳入了SKUtilities 2项目,其想法是创建一个SKSpriteNode的子类,使用CAShapeLayer进行渲染(一些文档),存在一些错误,并且始终需要从UIKit CALayer转换点到实际的Sprite Kit使用节点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接