用 SKScene 和 SKActions 替换 CALayer 和 CABasicAnimation

replacing CALayer and CABasicAnimation with SKScene and SKActions

我正在尝试在 Metaballs 上重组此 Github Swift project,以便圆圈由 SKShapeNodes 表示,这些圆圈由 SKActions 而不是 CABasicAnimation 移动。

我对 viewController 中出现的各种 Metaball 参数(handleLenRate、Spacing 等)不感兴趣。我基本上希望能够使用 SKActions 为动画指定开始和结束位置。

我不确定如何实现这一点,尤其是如何用 SKShapeNode 的 SKActions 替换下面的 startAnimation 函数:

    func startAnimation() {
    let loadingLayer = self.layer as! DBMetaballLoadingLayer
    loadingAnimation = CABasicAnimation(keyPath: "movingBallCenterX")
    loadingAnimation!.duration = 2.5
    loadingAnimation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    loadingAnimation!.fromValue = NSValue(CGPoint:fromPoint)
    loadingAnimation!.toValue = NSValue(CGPoint: toPoint)
    loadingAnimation!.repeatCount = Float.infinity
    loadingAnimation!.autoreverses = true
    loadingLayer.addAnimation(loadingAnimation!, forKey: "loading")
}

请在下方查看我能够执行的操作:

MBCircle class:

struct MBCircle {
var center: CGPoint = CGPointZero
var radius: CGFloat = 0.0
var frame: CGRect {
    get {
        return CGRect(x: center.x - radius, y: center.y - radius, width: 2 * radius, height: 2 * radius)
    }
  }
}

struct DefaultConfig {
    static let radius: CGFloat = 15.0
    static let mv: CGFloat = 0.6
    static let maxDistance: CGFloat = 10 * DefaultConfig.radius
    static let handleLenRate: CGFloat = 2.0
    static let spacing: CGFloat = 160.0
}

GameScene class(代表DBMetaballLoadingLayerDBMetaballLoadingView):

class GameScene: SKScene {

private let MOVE_BALL_SCALE_RATE: CGFloat = 0.75
private let ITEM_COUNT = 2
private let SCALE_RATE: CGFloat = 1.0//0.3
private var circlePaths = [MBCircle]()
var radius: CGFloat = DefaultConfig.radius
var maxLength: CGFloat {
    get {
        return (radius * 4 + spacing) * CGFloat(ITEM_COUNT) 
    }
}
var maxDistance: CGFloat = DefaultConfig.maxDistance
var mv: CGFloat = DefaultConfig.mv
var spacing: CGFloat = DefaultConfig.spacing {
    didSet {
        _adjustSpacing(spacing)
    }
}
var handleLenRate: CGFloat = DefaultConfig.handleLenRate
var movingBallCenterX : CGFloat = 0.0 {
    didSet {
        if (circlePaths.count > 0) {
            circlePaths[0].center = CGPoint(x: movingBallCenterX, y: circlePaths[0].center.y)
        }
    }
}

func _generalInit() {
    circlePaths = Array(0..<ITEM_COUNT).map { i in
        var circlePath = MBCircle()
        circlePath.center = CGPoint(x: (radius * 10 + spacing) * CGFloat(i), y: radius * (1.0 + SCALE_RATE))
        circlePath.radius = i == 0 ? radius * MOVE_BALL_SCALE_RATE : radius
        circlePath.sprite = SKShapeNode(circleOfRadius: circlePath.radius)
        circlePath.sprite?.position = circlePath.center
        circlePath.sprite?.fillColor = UIColor.blueColor()
        addChild(circlePath.sprite!)
        return circlePath

    }
}

func _adjustSpacing(spacing: CGFloat) {
    if (ITEM_COUNT > 1 && circlePaths.count > 1) {
        for i in 1..<ITEM_COUNT {
            var circlePath = circlePaths[i]
            circlePath.center = CGPoint(x: (radius*2 + spacing) * CGFloat(i), y: radius * (1.0 + SCALE_RATE))
        }
    }
}

func _renderPath(path: UIBezierPath) {
    var shapeNode = SKShapeNode()
    shapeNode.path = path.CGPath
    shapeNode.fillColor = UIColor.blueColor()
    addChild(shapeNode)
   }

func _metaball(j: Int, i: Int, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat) {
    let circle1 = circlePaths[i]
    let circle2 = circlePaths[j]

    let center1 = circle1.center
    let center2 = circle2.center

    let d = center1.distance(center2)

    var radius1 = circle1.radius
    var radius2 = circle2.radius

    if (d > maxDistance) {
        _renderPath(UIBezierPath(ovalInRect: circle2.frame))

    } else {
        let scale2 = 1 + SCALE_RATE * (1 - d / maxDistance)
        radius2 *= scale2
        _renderPath(UIBezierPath(ovalInRect: CGRect(x: circle2.center.x - radius2, y: circle2.center.y - radius2, width: 2 * radius2, height: 2 * radius2)))

    }

    if (radius1 == 0 || radius2 == 0) {
        return
    }

    var u1: CGFloat = 0.0
    var u2: CGFloat = 0.0
    if (d > maxDistance || d <= abs(radius1 - radius2)) {
        return
    } else if (d < radius1 + radius2) {
        u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
        u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
    } else {
        u1 = 0.0
        u2 = 0.0
    }

    let angle1 = center1.angleBetween(center2)
    let angle2 = acos((radius1 - radius2) / d)
    let angle1a = angle1 + u1 + (angle2 - u1) * v
    let angle1b = angle1 - u1 - (angle2 - u1) * v
    let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
    let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v

    let p1a = center1.point(radians: angle1a, withLength: radius1)
    let p1b = center1.point(radians: angle1b, withLength: radius1)
    let p2a = center2.point(radians: angle2a, withLength: radius2)
    let p2b = center2.point(radians: angle2b, withLength: radius2)

    let totalRadius = radius1 + radius2
    var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
    d2 *= min(1, d * 2 / totalRadius)
    radius1 *= d2
    radius2 *= d2

    let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
    let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
    let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
    let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)

    let pathJoinedCircles = UIBezierPath()
    pathJoinedCircles.moveToPoint(p1a)
    pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
    pathJoinedCircles.addLineToPoint(p2b)
    pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
    pathJoinedCircles.addLineToPoint(p1a)
    pathJoinedCircles.closePath()
    _renderPath(pathJoinedCircles)
}

func startAnimation() {

}
override func didMoveToView(view: SKView) {

    _generalInit()

}

    override func update(currentTime: CFTimeInterval) {
    /* Called before each frame is rendered */
   }
}

我没有对CGPointExtensionclass进行任何更改。

更新

我仍在努力获得变形球效果,这是我目前根据 Alessandro Ornano 的建议取得的进展:

import SpriteKit

extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
    let dx = point.x - self.x
    let dy = point.y - self.y
    return sqrt(dx * dx + dy * dy)
}

func angleBetween(point: CGPoint) -> CGFloat {
    return atan2(point.y - self.y, point.x - self.x)
}

func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint   {
    return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}

func minus(point: CGPoint) -> CGPoint {
    return CGPoint(x: self.x - point.x, y: self.y - point.y)
}

func length() -> CGFloat {
    return sqrt(self.x * self.x + self.y + self.y)
}

}

class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
var balls = [SKShapeNode]()
var distanceBtwBalls : CGFloat = 15

private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
    // Some parameters
    let strokeColor = SKColor.orangeColor()
    let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
    let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
    let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
    let dBWidth = dBStopX - dBStartX
    let totalBalls = 7 // first and last will be hidden
    let ballArea = dBWidth / CGFloat(totalBalls-1)
    distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

    // Create dbCircle
    dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
    dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
    dBCircle.strokeColor = strokeColor
    dBCircle.name = "dBCircle"
    dBCircle.fillColor = UIColor.clearColor()
    addChild(dBCircle)


    // Make static balls
    for i in 0..<totalBalls {
        let ball = SKShapeNode.init(circleOfRadius: radiusBall)
        ball.position =   CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
        ball.strokeColor = strokeColor
        ball.name = "ball"
        ball.fillColor = UIColor.clearColor()
        balls.append(ball)
        if i == 0 || i == totalBalls-1 {
            ball.hidden = true
        }
        addChild(ball)
    }
    mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
    let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
    actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

    let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
    actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

        node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
override func update(currentTime: NSTimeInterval) {
    var i = 0
    self.enumerateChildNodesWithName("ball") {
        node, stop in
        let ball = node as! SKShapeNode

        if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
            if (ball.actionForKey("zoom") == nil) {

                let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
                let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
                let seq = SKAction.sequence([zoomIn,zoomOut])
                ball.runAction(seq,withKey: "zoom")
            }
        }
        i += 1
    }
    movingBeziers()
}

func _renderPath(path: UIBezierPath) {
    let shapeNode = SKShapeNode(path: path.CGPath)
    shapeNode.fillColor = UIColor.blueColor()
    addChild(shapeNode)

}

func movingBeziers()    {
    _renderPath(UIBezierPath(ovalInRect: dBCircle.frame))
    for j in 1..<balls.count {
    self.latestTestMetaball(j, circleShape: dBCircle, v: 0.6, handleLenRate:  2.0, maxDistance: self.distanceBtwBalls)
    }
}

func latestTestMetaball (j: Int, circleShape: SKShapeNode, v: CGFloat, handleLenRate: CGFloat, maxDistance: CGFloat)    {
    let circle1 = circleShape
    let circle2 = balls[j]

    let center1 = circle1.position
    let center2 = circle2.position

    let d = center1.distance(center2)

    var radius1 = circle1.frame.width
    var radius2 = circle2.frame.width


    var u1: CGFloat = 0.0
    var u2: CGFloat = 0.0
    if (d > maxDistance || d <= abs(radius1 - radius2)) {
        return
    } else if (d < radius1 + radius2) {
        u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
        u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
    } else {
        u1 = 0.0
        u2 = 0.0
    }

    let angle1 = center1.angleBetween(center2)
    let angle2 = acos((radius1 - radius2) / d)
    let angle1a = angle1 + u1 + (angle2 - u1) * v
    let angle1b = angle1 - u1 - (angle2 - u1) * v
    let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
    let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v

    let p1a = center1.point(radians: angle1a, withLength: radius1)
    let p1b = center1.point(radians: angle1b, withLength: radius1)
    let p2a = center2.point(radians: angle2a, withLength: radius2)
    let p2b = center2.point(radians: angle2b, withLength: radius2)

    let totalRadius = radius1 + radius2
    var d2 = min(v * handleLenRate, p1a.minus(p2a).length() / totalRadius)
    d2 *= min(1, d * 2 / totalRadius)
    radius1 *= d2
    radius2 *= d2

    let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
    let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
    let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
    let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)

    let pathJoinedCircles = UIBezierPath()
    pathJoinedCircles.moveToPoint(p1a)
    pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
    pathJoinedCircles.addLineToPoint(p2b)
    pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
    pathJoinedCircles.addLineToPoint(p1a)
    pathJoinedCircles.closePath()
    let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
    shapeNode.fillColor = UIColor.blueColor()
    addChild(shapeNode)
  }
}

假设您的问题是如何使用节点和 SKAction 复制相同的行为,我相信这应该可以做到。

var shapeNode : SKNode?

func startAnimation(){

    if let node = self.shapeNode {
        //assume that we have a node initialized at some point and added to the scene at some x1,y1 coordinates

        /// we define parameters for the animation
        let positionToReach = CGPoint(x: 100, y: 100) /// some random position
        let currentPosition = node.position /// we need the current position to be able to reverse the "animation"
        let animationDuration = 2.5 //loadingAnimation!.duration = 2.5

        /// we define which actions will be run for the node
        let actionForward = SKAction.moveTo(positionToReach, duration: animationDuration)
        let actionBackwards = SKAction.moveTo(currentPosition, duration: animationDuration)

        // we needed two actions to simulate loadingAnimation!.autoreverses = true

        /// we wrap the actions in a sequence of actions
        let actionSequence = SKAction.sequence([actionForward, actionBackwards]) /// animations to repeat

        /// we want to repeat the animation forever
        let actionToRepeat = SKAction.repeatActionForever(actionSequence) ///loadingAnimation!.repeatCount = Float.infinity

        /// showtime
        node.runAction(actionToRepeat)

    }

}

如果我需要更新任何部分,请告诉我,因为我还没有测试过它。您仍然需要使用您的实际值和对象。

我在回复时提到了How would I repeat an action forever in Swift?

您可以使用 moveToXtimingMode parameter.

轻松实现这种动画

Swift 3 翻译在下面这个答案的末尾。

举个例子我用Xcode Sprite-Kit "Hello, World!"官方项目demo:

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
        let myLabel = SKLabelNode(fontNamed:"Chalkduster")
        myLabel.text = "Hello, World!"
        myLabel.fontSize = 15
        myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))

        self.addChild(myLabel)
        mediaTimingFunctionEaseInEaseOutEmulate(myLabel)
    }
    func mediaTimingFunctionEaseInEaseOutEmulate(node:SKLabelNode) {
        let actionMoveLeft = SKAction.moveToX(CGRectGetMidX(self.frame)-100, duration:1.5)
        actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

        let actionMoveRight = SKAction.moveToX(CGRectGetMidX(self.frame)+100, duration:1.5)
        actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

        node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
    }
}

输出:

更新(这部分开始模拟静态球和动态球左右移动但没有metaball动画)

class GameScene: SKScene {
    var dBCircle : SKShapeNode!
    let radiusDBCircle: CGFloat = 10
    let radiusBall: CGFloat = 15
    private let SCALE_RATE: CGFloat = 0.3
    override func didMoveToView(view: SKView) {
        // Some parameters
        let strokeColor = SKColor.orangeColor()
        let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
        let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
        let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
        let dBWidth = dBStopX - dBStartX
        let totalBalls = 7 // first and last will be hidden
        let ballArea = dBWidth / CGFloat(totalBalls-1)
        let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

        // Create dbCircle
        dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
        dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
        dBCircle.strokeColor = strokeColor
        dBCircle.name = "dBCircle"
        dBCircle.fillColor = UIColor.clearColor()
        addChild(dBCircle)
        // Make static balls
        for i in 0..<totalBalls {
            let ball = SKShapeNode.init(circleOfRadius: radiusBall)
            ball.position =   CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
            ball.strokeColor = strokeColor
            ball.name = "ball"
            ball.fillColor = UIColor.clearColor()
            if i == 0 || i == totalBalls-1 {
                ball.hidden = true
            }
            addChild(ball)
        }
        mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
    }
    func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
        let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
        actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

        let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
        actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

        node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
    }
    override func update(currentTime: NSTimeInterval) {
        var i = 0
        self.enumerateChildNodesWithName("ball") {
            node, stop in
            let ball = node as! SKShapeNode

            if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
                if (ball.actionForKey("zoom") == nil) {
                    let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
                    let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
                    let seq = SKAction.sequence([zoomIn,zoomOut])
                    ball.runAction(seq,withKey: "zoom")
                }
            }
            i += 1
        }
    }
}

新的元球动画更新:

终于实现了这个结果,我的目标是让它和原来的非常相似:

是否可以对时间进行一些变化(例如 zoomIn 或 zoomOut 时间值或 actionMoveLeft、actionMoveRight 时间值),这是代码:

import SpriteKit
class GameScene: SKScene {
    var dBCircle : SKShapeNode!
    let radiusDBCircle: CGFloat = 10
    let radiusBall: CGFloat = 15
    private let SCALE_RATE: CGFloat = 0.3
    override func didMoveToView(view: SKView) {
        // Some parameters
        let strokeColor = SKColor.orangeColor()
        let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
        let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
        let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
        let dBWidth = dBStopX - dBStartX
        let totalBalls = 7 // first and last will be hidden
        let ballArea = dBWidth / CGFloat(totalBalls-1)
        let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

        // Create dbCircle
        dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
        dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
        dBCircle.strokeColor = strokeColor
        dBCircle.name = "dBCircle"
        dBCircle.fillColor = UIColor.clearColor()
        addChild(dBCircle)
        // Make static balls
        for i in 0..<totalBalls {
            let ball = SKShapeNode.init(circleOfRadius: radiusBall)
            ball.position =   CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
            ball.strokeColor = strokeColor
            ball.name = "ball"
            ball.fillColor = UIColor.clearColor()
            if i == 0 || i == totalBalls-1 {
                ball.hidden = true
            }
            addChild(ball)
        }
        mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
    }
    func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
        let actionMoveLeft = SKAction.moveToX(dBStartX, duration:2.5)
        actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut

        let actionMoveRight = SKAction.moveToX(dBStopX, duration:2.5)
        actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut

        node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
    }
    //MARK: - _metaball original function
    func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : NSTimeInterval = 0.015) {
        let center1 = circle1.position
        let center2 = circle2.position
        let d = center1.distance(center2)
        var radius1 = radiusDBCircle
        var radius2 = radiusBall
        if (radius1 == 0 || radius2 == 0) {
            return
        }
        var u1: CGFloat = 0.0
        var u2: CGFloat = 0.0
        if (d > maxDistance || d <= abs(radius1 - radius2)) {
            return
        } else if (d < radius1 + radius2) {
            u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
            u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
        } else {
            u1 = 0.0
            u2 = 0.0
        }
        let angle1 = center1.angleBetween(center2)
        let angle2 = acos((radius1 - radius2) / d)
        let angle1a = angle1 + u1 + (angle2 - u1) * v
        let angle1b = angle1 - u1 - (angle2 - u1) * v
        let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
        let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
        let p1a = center1.point(radians: angle1a, withLength: radius1)
        let p1b = center1.point(radians: angle1b, withLength: radius1)
        let p2a = center2.point(radians: angle2a, withLength: radius2)
        let p2b = center2.point(radians: angle2b, withLength: radius2)
        let totalRadius = radius1 + radius2
        var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
        d2 *= min(1, d * 2 / totalRadius)
        radius1 *= d2
        radius2 *= d2
        let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
        let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
        let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
        let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
        let pathJoinedCircles = UIBezierPath()
        pathJoinedCircles.moveToPoint(p1a)
        pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
        pathJoinedCircles.addLineToPoint(p2b)
        pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
        pathJoinedCircles.addLineToPoint(p1a)
        pathJoinedCircles.closePath()
        let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
        shapeNode.strokeColor = SKColor.orangeColor()
        shapeNode.fillColor = UIColor.clearColor()
        addChild(shapeNode)
        let wait = SKAction.waitForDuration(vanishingTime)
        self.runAction(wait,completion: {
            shapeNode.removeFromParent()
        })
    }
    override func update(currentTime: NSTimeInterval) {
        var i = 0
        self.enumerateChildNodesWithName("ball") {
            node, stop in
            let ball = node as! SKShapeNode
            let enlargeFrame = CGRectMake(ball.frame.origin.x-self.radiusBall*3,ball.frame.origin.y,ball.frame.width+(self.radiusBall*6),ball.frame.height)
            if CGRectContainsRect(enlargeFrame, self.dBCircle.frame) {
                if (ball.actionForKey("zoom") == nil) {
                    let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
                    zoomIn.timingMode = SKActionTimingMode.EaseInEaseOut
                    let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
                    let wait = SKAction.waitForDuration(0.8)
                    let seq = SKAction.sequence([zoomIn,zoomOut,wait])
                    ball.runAction(seq,withKey: "zoom")
                }
            }
            self._metaball(ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 4 * self.radiusBall)
            i += 1
        }
    }
}
//MARK: - Extensions
extension CGPoint {
    func distance(point: CGPoint) -> CGFloat {
        let dx = point.x - self.x
        let dy = point.y - self.y
        return sqrt(dx * dx + dy * dy)
    }
    func angleBetween(point: CGPoint) -> CGFloat {
        return atan2(point.y - self.y, point.x - self.x)
    }
    func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint   {
        return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
    }
    func minus(point: CGPoint) -> CGPoint {
        return CGPoint(x: self.x - point.x, y: self.y - point.y)
    }
    func length() -> CGFloat {
        return sqrt(self.x * self.x + self.y + self.y)
    }
}

Swift 3:

(我用 maxDistance: 5 * self.radiusBallmaxDistance: 4 * self.radiusBall 做了一点改动,使其与原始版本更相似,但您可以根据需要进行更改)

import SpriteKit
class GameScene: SKScene {
    var dBCircle : SKShapeNode!
    let radiusDBCircle: CGFloat = 10
    let radiusBall: CGFloat = 15
    private let SCALE_RATE: CGFloat = 0.3
    override func didMove(to view: SKView) {
        let label = self.childNode(withName: "//helloLabel") as? SKLabelNode
        label?.removeFromParent()
        self.anchorPoint = CGPoint.zero
        // Some parameters
        let strokeColor = SKColor.orange
        let dBHeight = self.frame.midY
        let dBStartX = self.frame.midX-260 // extreme left
        let dBStopX = self.frame.midX+260 // extreme right
        let dBWidth = dBStopX - dBStartX
        let totalBalls = 7 // first and last will be hidden
        let ballArea = dBWidth / CGFloat(totalBalls-1)
        let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)

        // Create dbCircle
        dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
        dBCircle.position = CGPoint(x:self.frame.midX, y:dBHeight)
        dBCircle.strokeColor = strokeColor
        dBCircle.name = "dBCircle"
        dBCircle.fillColor = UIColor.clear
        addChild(dBCircle)
        // Make static balls
        for i in 0..<totalBalls {
            let ball = SKShapeNode.init(circleOfRadius: radiusBall)
            ball.position =   CGPoint(x:dBStartX+(distanceBtwBalls*CGFloat(i)), y:dBHeight)
            ball.strokeColor = strokeColor
            ball.name = "ball"
            ball.fillColor = UIColor.clear
            if i == 0 || i == totalBalls-1 {
                ball.isHidden = true
            }
            addChild(ball)
        }
        mediaTimingFunctionEaseInEaseOutEmulate(node: dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
    }
    func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
        let actionMoveLeft = SKAction.moveTo(x: dBStartX, duration:2.5)
        actionMoveLeft.timingMode = SKActionTimingMode.easeInEaseOut

        let actionMoveRight = SKAction.moveTo(x: dBStopX, duration:2.5)
        actionMoveRight.timingMode = SKActionTimingMode.easeInEaseOut

        node.run(SKAction.repeatForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
    }
    //MARK: - _metaball original function
    func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : TimeInterval = 0.015) {
        let center1 = circle1.position
        let center2 = circle2.position
        let d = center1.distance(point: center2)
        var radius1 = radiusDBCircle
        var radius2 = radiusBall
        if (radius1 == 0 || radius2 == 0) {
            return
        }
        var u1: CGFloat = 0.0
        var u2: CGFloat = 0.0
        if (d > maxDistance || d <= abs(radius1 - radius2)) {
            return
        } else if (d < radius1 + radius2) {
            u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
            u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
        } else {
            u1 = 0.0
            u2 = 0.0
        }
        let angle1 = center1.angleBetween(point: center2)
        let angle2 = acos((radius1 - radius2) / d)
        let angle1a = angle1 + u1 + (angle2 - u1) * v
        let angle1b = angle1 - u1 - (angle2 - u1) * v
        let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
        let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
        let p1a = center1.point(radians: angle1a, withLength: radius1)
        let p1b = center1.point(radians: angle1b, withLength: radius1)
        let p2a = center2.point(radians: angle2a, withLength: radius2)
        let p2b = center2.point(radians: angle2b, withLength: radius2)
        let totalRadius = radius1 + radius2
        var d2 = min(v * handeLenRate, p1a.minus(point: p2a).length() / totalRadius)
        d2 *= min(1, d * 2 / totalRadius)
        radius1 *= d2
        radius2 *= d2
        let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
        let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
        let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
        let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
        let pathJoinedCircles = UIBezierPath()
        pathJoinedCircles.move(to: p1a)
        pathJoinedCircles.addCurve(to: p2a, controlPoint1: cp1a, controlPoint2: cp2a)
        pathJoinedCircles.addLine(to: p2b)
        pathJoinedCircles.addCurve(to: p1b, controlPoint1: cp2b, controlPoint2: cp1b)
        pathJoinedCircles.addLine(to: p1a)
        pathJoinedCircles.close()
        let shapeNode = SKShapeNode(path: pathJoinedCircles.cgPath)
        shapeNode.strokeColor = SKColor.orange
        shapeNode.fillColor = UIColor.clear
        addChild(shapeNode)
        let wait = SKAction.wait(forDuration: vanishingTime)
        self.run(wait,completion: {
            shapeNode.removeFromParent()
        })
    }
    override func update(_ currentTime: TimeInterval) {
        var i = 0
        self.enumerateChildNodes(withName: "ball") {
            node, stop in
            let ball = node as! SKShapeNode
            let enlargeFrame = CGRect(x:ball.frame.origin.x-self.radiusBall*3,y:ball.frame.origin.y,width:ball.frame.width+(self.radiusBall*6),height:ball.frame.height)
            if enlargeFrame.contains(self.dBCircle.frame) {
                if (ball.action(forKey: "zoom") == nil) {
                    let zoomIn = SKAction.scale(to: 1.5, duration: 0.25)
                    zoomIn.timingMode = SKActionTimingMode.easeInEaseOut
                    let zoomOut = SKAction.scale(to: 1.0, duration: 0.25)
                    let wait = SKAction.wait(forDuration: 0.7)
                    let seq = SKAction.sequence([zoomIn,zoomOut,wait])
                    ball.run(seq,withKey: "zoom")
                }
            }
            self._metaball(circle2: ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 5 * self.radiusBall)
            i += 1
        }
    }
}
//MARK: - Extensions
extension CGPoint {
    func distance(point: CGPoint) -> CGFloat {
        let dx = point.x - self.x
        let dy = point.y - self.y
        return sqrt(dx * dx + dy * dy)
    }
    func angleBetween(point: CGPoint) -> CGFloat {
        return atan2(point.y - self.y, point.x - self.x)
    }
    func point(radians: CGFloat, withLength length: CGFloat) -> CGPoint   {
        return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
    }
    func minus(point: CGPoint) -> CGPoint {
        return CGPoint(x: self.x - point.x, y: self.y - point.y)
    }
    func length() -> CGFloat {
        return sqrt(self.x * self.x + self.y + self.y)
    }
}