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

Need serious help on creating a comet with animations in SpriteKit

我目前正在处理一个 SpriteKit 项目,需要创建一个带有渐变尾巴的彗星,该彗星在屏幕上进行动画处理。我在这方面遇到了 SpriteKit 的严重问题。

尝试 1。它:

  1. 绘制 CGPath 并根据路径创建 SKShapeNode
  2. 创建一个带渐变的正方形 SKShapeNode
  3. 创建一个 SKCropNode 并将其 maskNode 指定为线,并将正方形添加为 child
  4. 在屏幕上动画正方形,同时被 line/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 的错误。 SKShapeNode has 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)

其余代码基本相同。这会产生类似的效果,而且没有错误 performance/memory。然而,SKCropNode 发生了一些奇怪的事情,它看起来像这样

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

尝试 3. 经过一些研究,我发现我可以使用 SKEffectNode 而不是 SKCropNode 来使用剪裁效果并保留行 width/glow。

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

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

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

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

编辑:我考虑过发射器,但可能有数百个 comets/other 节点在几秒钟内进入,我认为它们不可行 performance-wise。在这个项目之前我没有使用过 SpriteKit,所以如果我错了请纠正我。

这看起来像是附加到彗星路径的自定义着色器的问题。如果你不熟悉 OpenGL Shading Language (GLSL) in SpriteKit it lets you jump right into the GPU fragment shader specifically to control the drawing behavior of the nodes it is attached to 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 class CometPathShape 并通过自定义动作设置动画。

class 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()

class 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 没有平滑地跨曲线段混合。不过,小心构建曲线应该可行。