在 SpriteKit 中创建带有动画的彗星需要认真的帮助

Posted

技术标签:

【中文标题】在 SpriteKit 中创建带有动画的彗星需要认真的帮助【英文标题】:Need serious help on creating a comet with animations in SpriteKit 【发布时间】:2016-05-20 17:39:14 【问题描述】:

我目前正在处理一个 SpriteKit 项目,需要创建一个彗星,它的尾巴会在屏幕上显示动画。在这方面,我对 SpriteKit 有严重的问题。

尝试 1。它:

    绘制一个 CGPath 并从路径创建一个 SKShapeNode 使用渐变创建方形 SKShapeNode 创建一个 SKCropNode 并将它的 maskNode 指定为 line,并添加 square 作为子节点

    在屏幕上为正方形设置动画,同时被线条/SKCropNode 剪裁

    func makeCometInPosition(from: CGPoint, to: CGPoint, color: UIColor, timeInterval: NSTimeInterval) 
            ... (...s are (definitely) irrelevant lines of code)
            let path = CGPathCreateMutable()
            ...
            let line = SKShapeNode(path:path)
            line.lineWidth = 1.0
            line.glowWidth = 1.0
    
            var squareFrame = line.frame
            ...
            let square = SKShapeNode(rect: squareFrame)
            //Custom SKTexture Extension. I've tried adding a normal image and the leak happens either way. The extension is not the problem
            square.fillTexture = SKTexture(color1: UIColor.clearColor(), color2: color, from: from, to: to, frame: line.frame)
    
            square.fillColor = color
            square.strokeColor = UIColor.clearColor()
            square.zPosition = 1.0
    
            let maskNode = SKCropNode()
            maskNode.zPosition = 1.0
            maskNode.maskNode = line
            maskNode.addChild(square)
    
            //self is an SKScene, background is an SKSpriteNode
            self.background?.addChild(maskNode)
    
            let lineSequence = SKAction.sequence([SKAction.waitForDuration(timeInterval), SKAction.removeFromParent()])
    
            let squareSequence = SKAction.sequence([SKAction.waitForDuration(1), SKAction.moveBy(CoreGraphics.CGVectorMake(deltaX * 2, deltaY * 2), duration: timeInterval), SKAction.removeFromParent()])
    
            square.runAction(SKAction.repeatActionForever(squareSequence))
            maskNode.runAction(lineSequence)
            line.runAction(lineSequence)
        
    

    有效,如下图所示。

问题是在屏幕上出现 20-40 个其他节点后,就会发生奇怪的事情。屏幕上的一些节点消失了,一些保留了下来。此外,fps 和节点数(在 SKView 中切换并且从未改变)

self.showsFPS = true
self.showsNodeCount = true

从屏幕上消失。这让我认为这是 SpriteKit 的一个错误。 SKShapeNodehas been known to cause issues。

尝试 2. 我尝试将 square 从 SKShapeNode 更改为 SKSpriteNode(根据需要添加和删除与两者相关的行)

let tex = SKTexture(color1: UIColor.clearColor(), color2: color, from:    from, to: to, frame: line.frame)
let square = SKSpriteNode(texture: tex)

其余代码基本相同。这会产生类似的效果,但在性能/内存方面没有错误。但是,SKCropNode 发生了一些奇怪的事情,看起来像这样

它没有抗锯齿,线条更粗。我尝试过更改抗锯齿、发光宽度和线宽。有一个最小宽度由于某种原因不能改变,将发光宽度设置得更大可以做到这一点 .根据其他***问题,掩码节点在alpha中是1或0。这是令人困惑的,因为 SKShapeNode 可以有不同的线条/发光宽度。

尝试 3. 经过一番研究,我发现我可以使用 SKEffectNode 而不是 SKCropNode 来使用剪切效果并保持线宽/发光。

    //Not the exact code to what I tried, but very similar
    let maskNode = SKEffectNode()
    maskNode.filter = customLinearImageFilter
    maskNode.addChild(line)

这产生了(字面上)与尝试 1 完全相同的效果。它创建了相同的线条和动画,但发生了与其他节点/fps/nodeCount 相同的错误。所以这似乎是 SKEffectNode 的错误,而不是 SKShapeNode。

我不知道如何通过尝试 1/3 或 2 绕过错误。

有谁知道我是否做错了什么,是否有绕过此问题的方法,或针对我的问题的完全不同的解决方案?

编辑:我考虑过发射器,但可能有数百个彗星/其他节点在几秒钟内进入,并且认为它们在性能方面不可行。在这个项目之前我没有使用过 SpriteKit,如果我错了,请纠正我。

【问题讨论】:

我会使用发射器来实现这种效果 不要认为他们会在性能方面发挥作用(请参阅帖子底部的编辑) 我同意如果有很多彗星发射器不是一个好的选择。 【参考方案1】:

这对于附加到彗星路径的自定义着色器来说似乎是个问题。如果您不熟悉 SpriteKit 中的 OpenGL Shading Language (GLSL),它可以让您直接跳转到 GPU 片段着色器,专门控制它附加到 via SKShader 的节点的绘制行为。

SKShapeNode 有一个 strokeShader 属性,用于连接 SKShader 以绘制路径。当连接到这个属性时,除了该点的颜色值之外,着色器还会传递路径的长度和当前正在绘制的路径上的点。*

controlFadePath.fsh

void main() 

  //uniforms and varyings
  vec4  inColor = v_color_mix;
  float length = u_path_length;
  float distance = v_path_distance;
  float start = u_start;
  float end = u_end;

  float mult;

  mult = smoothstep(end,start,distance/length);
  if(distance/length > start) discard;

  gl_FragColor = vec4(inColor.r, inColor.g, inColor.b, inColor.a) * mult;

要控制沿路径的淡入淡出,使用两个名为 u_startu_end 的 SKUniform 对象将起点和终点传递到自定义着色器中,这些对象在自定义 SKShapeNode 类 CometPathShape 初始化期间添加到自定义着色器中,并通过自定义操作。

类 CometPathShape:SKShapeNode

class CometPathShape:SKShapeNode 

  //custom shader for fading
  let pathShader:SKShader
  let fadeStartU = SKUniform(name: "u_start",float:0.0)
  let fadeEndU = SKUniform(name: "u_end",float: 0.0)
  let fadeAction:SKAction

  override init() 
    pathShader = SKShader(fileNamed: "controlFadePath.fsh")

    let fadeDuration:NSTimeInterval = 1.52
    fadeAction = SKAction.customActionWithDuration(fadeDuration, actionBlock:
       (node:SKNode, time:CGFloat)->Void in
            let D = CGFloat(fadeDuration)
            let t = time/D
            var Ps:CGFloat = 0.0
            var Pe:CGFloat = 0.0

            Ps = 0.25 + (t*1.55)
            Pe = (t*1.5)-0.25

            let comet:CometPathShape = node as! CometPathShape
            comet.fadeRange(Ps,to: Pe) )

    super.init()

    path = makeComet...(...)   //custom method that creates path for comet shape 

    strokeShader = pathShader
    pathShader.addUniform(fadeStartU)
    pathShader.addUniform(fadeEndU)
    hidden = true
    //set up for path shape, eg. strokeColor, strokeWidth...
    ...
  

  func fadeRange(from:CGFloat, to:CGFloat) 
    fadeStartU.floatValue = Float(from)
    fadeEndU.floatValue = Float(to)     
  

  func launch() 
    hidden = false
    runAction(fadeAction, completion:  ()->Void in self.hidden = true;)
  

...

SKScene 初始化 CometPathShape 对象,缓存它们并将它们添加到场景中。在 update: 期间,场景只需在所选 CometPathShapes 上调用 .launch()

类 GameScene:SKScene

...
  override func didMoveToView(view: SKView) 
    /* Setup your scene here */
    self.name = "theScene"
    ...

    //create a big bunch of paths with custom shaders
    print("making cache of path shape nodes")
    for i in 0...shapeCount 
      let shape = CometPathShape()
      let ext = String(i)
      shape.name = "comet_".stringByAppendingString(ext)
      comets.append(shape)
      shape.position.y = CGFloat(i * 3)
      print(shape.name)
      self.addChild(shape)
    

  override func update(currentTime: CFTimeInterval) 
    //pull from cache and launch comets, skip busy ones
    for _ in 1...launchCount 
        let shape = self.comets[Int(arc4random_uniform(UInt32(shapeCount)))]
        if shape.hasActions()  continue 
        shape.launch()
    

这将每个彗星的 SKNode 数量从 3 个减少到 1 个,从而简化了您的代码和运行时环境,并为通过着色器实现更复杂的效果打开了大门。我能看到的唯一缺点是必须学习一些 GLSL。**

*在设备模拟器中并不总是正确的。模拟器没有将距离和长度值传递给自定义着色器。

**这和 CGPath glsl 行为中的一些特质。路径构造正在影响淡入淡出的执行方式。看起来v_path_distance 没有在曲线段之间平滑混合。不过,小心构建曲线,这应该可行。

【讨论】:

有趣。这使用的节点更少,但它每秒能处理多达 50 颗以上的彗星吗? 只有一种方法可以找出...但由于它直接使用 GL 绘制命令,它应该是最快的方法。 我仍然在使用 swift 和场景工具包,但使用上面的片段着色器将一些东西打在一起,每帧添加 15 条动画 SKShapeNode 线,结果是在旧版本上以约 11.5 fps 的速度渲染约 330 个节点迷你 iPad 和 iPhone 4S ......似乎它应该每秒处理 50 颗彗星就好了。 您是如何设置 GLSL 代码的?我们尝试了很多不同的组合,但它们看起来都很糟糕。看起来真的不可靠,彗星从两端和东西都在生长(这与代码的意图相去甚远,即使在最简单的情况下也是如此) 我现在知道动画应该做什么了...我在想象一条褪色的路径在移动,但您希望褪色沿路径移动(我认为)...让我稍微处理一下.

以上是关于在 SpriteKit 中创建带有动画的彗星需要认真的帮助的主要内容,如果未能解决你的问题,请参考以下文章

在 Sprite Kit 场景中创建带有重复图像的栏 - iOS

SpriteKit 动画意外停止

如何在 SpriteKit 中创建有限的、可滚动的背景?

带有 SpriteKit 的 UITableView

如何在 SpriteKit 中创建图像节点?斯威夫特 4

SpriteKit 自定义 UIScrollView 带加速