这是我在2017年5月的Swift社区挑战赛中尝试的,该挑战链由刚性链接组成。
我以此为契机
学习了Apple的SpriteKit框架, 2D游戏。至少需要具有Swift 3的Xcode 8.3.2才能编译代码,该代码必须同时在macOS和iOS上运行(以下说明)。
VectorUtils.swift –一些用于矢量计算的辅助方法。 >
 import CoreGraphics

let π = CGFloat.pi

extension CGVector {

    init(from: CGPoint, to: CGPoint) {
        self.init(dx: to.x - from.x, dy: to.y - from.y)
    }
    
    func cross(_ other: CGVector) -> CGFloat {
        return dx * other.dy - dy * other.dx
    }
    
    var length: CGFloat {
        return hypot(dx, dy)
    }
    
    var arg: CGFloat {
        return atan2(dy, dx)
    }
}
 

Sprocket.swift –描述单个链轮的类型。
 import CoreGraphics

struct Sprocket {
    let center: CGPoint
    let radius: CGFloat
    let teeth: Int
    
    var clockwise: Bool!
    var prevAngle: CGFloat!
    var nextAngle: CGFloat!
    var prevPoint: CGPoint!
    var nextPoint: CGPoint!
    
    init(center: CGPoint, radius: CGFloat) {
        self.center = center
        self.radius = radius
        self.teeth = Int((radius/2).rounded())
    }
    
    init(_ triplet: (x: CGFloat, y: CGFloat, r: CGFloat)) {
        self.init(center: CGPoint(x: triplet.x, y: triplet.y), radius: triplet.r)
    }
    
    // Normalize angles such that
    //     0 <= prevAngle < 2π
    // and
    //     prevAngle <= nextAngle < prevAngle + 2π  (if rotating counter-clockwise)
    //     prevAngle - 2π < nextAngle <= prevAngle  (if rotating clockwise)
    mutating func normalizeAngles() {
        prevAngle = prevAngle.truncatingRemainder(dividingBy: 2 * π)
        nextAngle = nextAngle.truncatingRemainder(dividingBy: 2 * π)
        while prevAngle < 0 {
            prevAngle = prevAngle + 2 * π
        }
        if clockwise {
            while nextAngle > prevAngle {
                nextAngle = nextAngle - 2 * π
            }
        } else {
            while nextAngle < prevAngle {
                nextAngle = nextAngle + 2 * π
            }
        }
    }
    
    mutating func computeTangentPoints() {
        prevPoint = CGPoint(x: center.x + radius * cos(prevAngle),
                            y: center.y + radius * sin(prevAngle))
        nextPoint = CGPoint(x: center.x + radius * cos(nextAngle),
                            y: center.y + radius * sin(nextAngle))
    }
}
 

ChainDrive.swift-描述完整链传动系统的类型。还包含用于计算旋转方向,
切线角度/点以及
 import CoreGraphics

struct ChainDrive {
    
    var sprockets: [Sprocket]
    
    var length: CGFloat!
    var period: CGFloat!
    var linkCount: Int!
    var accumLength: [(CGFloat, CGFloat)]!
    
    init(sprockets: [Sprocket]) {
        self.sprockets = sprockets
        
        computeSprocketData()
        computeChainLength()
    }
    
    init(_ triplets: [(CGFloat, CGFloat, CGFloat)]) {
        self.init(sprockets: triplets.map(Sprocket.init))
    }
    
    mutating func computeSprocketData() {
        
        // Compute rotation directions:
        for i in 0..<sprockets.count {
            let j = (i + 1) % sprockets.count
            let k = (j + 1) % sprockets.count
            
            let v1 = CGVector(from: sprockets[j].center, to: sprockets[i].center)
            let v2 = CGVector(from: sprockets[j].center, to: sprockets[k].center)
            sprockets[j].clockwise = v1.cross(v2) > 0
        }
        if !sprockets[0].clockwise {
            sprockets[1..<sprockets.count].reverse()
            for i in 0..<sprockets.count {
                sprockets[i].clockwise = !sprockets[i].clockwise
            }
        }
        
        // Compute tangent angles:
        for i in 0..<sprockets.count {
            let j = (i + 1) % sprockets.count
            
            let v = CGVector(from: sprockets[i].center, to: sprockets[j].center)
            let d = v.length
            let a = v.arg
            if sprockets[i].clockwise == sprockets[j].clockwise {
                var phi = acos((sprockets[i].radius - sprockets[j].radius)/d)
                if !sprockets[i].clockwise {
                    phi = -phi
                }
                sprockets[i].nextAngle = a + phi
                sprockets[j].prevAngle = a + phi
            } else {
                var phi = acos((sprockets[i].radius + sprockets[j].radius)/d)
                if !sprockets[i].clockwise {
                    phi = -phi
                }
                sprockets[i].nextAngle = a + phi
                sprockets[j].prevAngle = a + phi - π
            }
        }
        
        // Normalize angles and compute tangent points:
        for i in 0..<sprockets.count {
            sprockets[i].normalizeAngles()
            sprockets[i].computeTangentPoints()
        }
    }
    
    mutating func computeChainLength() {
        accumLength = []
        length = 0
        for i in 0..<sprockets.count {
            let j = (i + 1) % sprockets.count
            let l1 = length + abs(sprockets[i].nextAngle - sprockets[i].prevAngle) * sprockets[i].radius
            let l2 = l1 + CGVector(from: sprockets[i].nextPoint, to: sprockets[j].prevPoint).length
            accumLength.append((l1, l2))
            length = l2
        }
        
        let count = Int(length / (4 * π))
        let p1 = length / CGFloat(count)
        let p2 = length / CGFloat(count + 1)
        if abs(p1 - 4 * π) <= abs(p2 - 4 * π) {
            period = p1
            linkCount = count
        } else {
            period = p2
            linkCount = count + 1
        }
        
    }
    
    func linkCoordinatesAndPhases(offset: CGFloat) -> ([CGPoint], [CGFloat]) {
        var coords: [CGPoint] = []
        var phases: [CGFloat] = []
        var offset = offset
        var total = offset
        var i = 0

        repeat {
            let j = (i + 1) % sprockets.count
            let s: CGFloat = sprockets[i].clockwise ? -1 : 1
            
            var phi = sprockets[i].prevAngle + s*offset / sprockets[i].radius
            phases.append(phi)
            while total <= accumLength[i].0 && coords.count < linkCount {
                coords.append(CGPoint(x: sprockets[i].center.x + cos(phi) * sprockets[i].radius,
                                      y: sprockets[i].center.y + sin(phi) * sprockets[i].radius))
                phi += s * period / sprockets[i].radius
                total += period
            }
            
            var d = total - accumLength[i].0
            let v = CGVector(from: sprockets[i].nextPoint, to: sprockets[j].prevPoint)
            while total <= accumLength[i].1 && coords.count < linkCount {
                coords.append(CGPoint(x: sprockets[i].nextPoint.x + d * v.dx / v.length,
                                      y: sprockets[i].nextPoint.y + d * v.dy / v.length))
                d += period
                total += period
            }
            
            offset = total - accumLength[i].1
            i = j
        } while coords.count < linkCount

        return (coords, phases)
    }
    
}
 

SprocketNode.swift –为绘图定义一个SKShapeNode子类
单个链轮。
 import SpriteKit

class SprocketNode: SKShapeNode {
    let radius: CGFloat
    let clockwise: Bool
    let teeth: Int
    
    init(sprocket: Sprocket) {
        self.radius = sprocket.radius
        self.clockwise = sprocket.clockwise
        self.teeth = sprocket.teeth
        super.init()
        
        let path = CGMutablePath()
        path.move(to: CGPoint(x: radius - 2, y: 0))
        for i in 0..<teeth {
            let a1 = π * CGFloat(4 * i - 1)/CGFloat(2 * teeth)
            let a2 = π * CGFloat(4 * i + 1)/CGFloat(2 * teeth)
            let a3 = π * CGFloat(4 * i + 3)/CGFloat(2 * teeth)
            path.addArc(center: CGPoint.zero, radius: radius - 2,
                        startAngle: a1, endAngle: a2, clockwise: false)
            path.addArc(center: CGPoint.zero, radius: radius + 2,
                        startAngle: a2, endAngle: a3, clockwise: false)
        }
        path.closeSubpath()
        self.path = path
        
        self.lineWidth = 0
        self.fillColor = SKColor(red: 0x86/255, green: 0x84/255, blue: 0x81/255, alpha: 1) // #868481
        self.strokeColor = .clear
        self.position = sprocket.center
        
        do {
            let path = CGMutablePath()
            path.addEllipse(in: CGRect(x: -3, y: -3, width: 6, height: 6))
            path.addEllipse(in: CGRect(x: -radius + 4.5, y: -radius + 4.5,
                                       width: 2 * radius - 9, height: 2 * radius - 9))
            let node = SKShapeNode(path: path)
            node.fillColor = SKColor(red: 0x64/255, green: 0x63/255, blue: 0x61/255, alpha: 1) // #646361
            node.lineWidth = 0
            node.strokeColor = .clear
            self.addChild(node)
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
 

LinkNode.swift –定义用于绘图的SKShapeNode子类。
 import SpriteKit

class LinkNode: SKShapeNode {
    static let narrowWidth: CGFloat = 2
    static let wideWidth : CGFloat = 6
    
    let pitch: CGFloat
    
    init(pitch: CGFloat) {
        self.pitch = pitch
        super.init()
        
        let phi = asin(LinkNode.narrowWidth / LinkNode.wideWidth)
        let path = CGMutablePath()
        path.addArc(center: CGPoint(x: -pitch/2, y: 0), radius: LinkNode.wideWidth/2,
                    startAngle: phi, endAngle: 2 * π - phi, clockwise: false)
        path.addLine(to: CGPoint(x: pitch/2, y: -LinkNode.narrowWidth/2))
        path.addArc(center: CGPoint(x: pitch/2, y: 0), radius: LinkNode.narrowWidth/2,
                    startAngle: -π/2, endAngle: π/2, clockwise: false)
        path.closeSubpath()
        self.path = path
        self.fillColor = .black
        self.lineWidth = 0
        self.strokeColor = .clear
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func moveTo(leftPin: CGPoint, rightPin: CGPoint) {
        position = CGPoint(x: (leftPin.x + rightPin.x)/2,
                           y: (leftPin.y + rightPin.y)/2)
        zRotation = CGVector(from: leftPin, to: rightPin).arg
    }

}
 

ChainDriveScene.swift –定义了一个SKScene子类,用于绘制
并为链传动设置动画。
 import SpriteKit

typealias Triples = [(CGFloat, CGFloat, CGFloat)]

// The system from the challenge: https://codereview.meta.stackexchange.com/a/7264 :
let system0: Triples = [(0, 0, 16), (100, 0, 16), (100, 100, 12), (50, 50, 24), (0, 100, 12)]

// Other systems from https://codegolf.stackexchange.com/q/64764:
let system1: Triples = [(0, 0, 26), (120, 0, 26)]
let system2: Triples = [(100, 100, 60), (220, 100, 14)]
let system3: Triples = [(100, 100, 16), (100, 0, 24), (0, 100, 24), (0, 0, 16)]
let system4: Triples = [(0, 0, 60), (44, 140, 16), (-204, 140, 16), (-160, 0, 60), (-112, 188, 12),
                      (-190, 300, 30), (30, 300, 30), (-48, 188, 12)]
let system5: Triples = [(0, 128, 14), (46.17, 63.55, 10), (121.74, 39.55, 14), (74.71, -24.28, 10),
                      (75.24, -103.55, 14), (0, -78.56, 10), (-75.24, -103.55, 14),
                      (-74.71, -24.28, 10), (-121.74, 39.55, 14), (-46.17, 63.55, 10)]
let system6: Triples = [(367, 151, 12), (210, 75, 36), (57, 286, 38), (14, 181, 32), (91, 124, 18),
                      (298, 366, 38), (141, 3, 52), (80, 179, 26), (313, 32, 26), (146, 280, 10),
                      (126, 253, 8), (220, 184, 24), (135, 332, 8), (365, 296, 50), (248, 217, 8),
                      (218, 392, 30)]

class ChainDriveScene: SKScene {
    
    let chainDrive: ChainDrive
    let chainSpeed = 16 * π // speed (points/sec)
    
    var initialTime: TimeInterval!
    var sprocketNodes: [SprocketNode] = []
    var linkNodes: [LinkNode] = []
    
    class func newScene() -> ChainDriveScene {
        let system = ChainDrive(system0)
        return ChainDriveScene(system: system)
    }
    
    init(system: ChainDrive) {
        self.chainDrive = system
        
        let minx = system.sprockets.map {   let scene = GameScene.newGameScene()
.center.x -     let scene = ChainDriveScene.newScene()
.radius }.min()! - 15
        let miny = system.sprockets.map { struct Sprocket.center.y - func computeSprocketData().radius }.min()! - 15
        let maxx = system.sprockets.map { SKAction.center.x + SKActions.radius }.max()! + 15
        let maxy = system.sprockets.map { update().center.y + SKAction.followPath().radius }.max()! + 15
        
        super.init(size: CGSize(width: maxx - minx, height: maxy - miny))
        self.anchorPoint = CGPoint(x: -minx/(maxx - minx), y: -miny/(maxy - miny))
        self.scaleMode = .aspectFit
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setUpScene() {
        
        backgroundColor = .white
        sprocketNodes = chainDrive.sprockets.map(SprocketNode.init)
        for node in sprocketNodes {
            self.addChild(node)
        }
        
        let (coords, _) = chainDrive.linkCoordinatesAndPhases(offset: 0)
        for i in 0..<coords.count {
            let j = (i + 1) % coords.count
            let node = LinkNode(pitch: chainDrive.period)
            node.moveTo(leftPin: coords[i], rightPin: coords[j])
            self.addChild(node)
            linkNodes.append(node)
        }
    }
    
    override func didMove(to view: SKView) {
        self.setUpScene()
    }
    
    override func update(_ currentTime: TimeInterval) {
        if initialTime == nil {
            initialTime = currentTime
        }
        
        let distance = CGFloat(currentTime - initialTime) * chainSpeed * speed
        let k = Int(distance/chainDrive.period) % linkNodes.count
        let offset = distance.truncatingRemainder(dividingBy: chainDrive.period)
        
        let (coords, phases) = chainDrive.linkCoordinatesAndPhases(offset: offset)
        for i in 0..<linkNodes.count {
            let p1 = coords[i % coords.count]
            let p2 = coords[(i + 1) % coords.count]
            linkNodes[(i + linkNodes.count - k) % linkNodes.count].moveTo(leftPin: p1, rightPin: p2)
        }
        for i in 0..<phases.count {
            sprocketNodes[i].zRotation = phases[i]
        }
    }
}
 

完整的项目是
可在GitHub上使用。
或者:


在Xcode 8.3.2(或更高版本)中,从“跨平台SpriteKit Game”模板创建一个新项目。 />

选择“包括iOS应用程序”和/或“包括macOS应用程序”。


添加将上述源文件添加到项目中。


在GameViewController.swift文件中,替换
 q4312079q 




 q4312079q 


编译并运行!


1.2 GHz MacBook和iPhone 6s上,动画以每秒约60帧的速度运行。
为了给您大致的印象,我拍摄了一个屏幕
使用QuickTime Player并将其转换为带有ffmpeg和gifsicle的GIF动画。


欢迎所有反馈,例如(但不限于):

可以简化几何计算吗?
是否有更好的类型/变量/函数名称?
q4312079q中有几个“隐式展开的可选”属性。原因是在所有链轮均已初始化后
(在q4312079q中)进行计算。关于如何更优雅地进行两步初始化的任何建议?
最初,我使用q4312079q来旋转链轮,但没有找到使用q4312079q为链进行动画处理的方法。因此,
链轮和链节现在都可以通过q4312079q方法
(每帧调用)进行更新。有没有更好的方法来实现相同的结果?
另一个想法是使用q4312079q为链节设置动画。
对于一个链接来说效果很好,但是我不知道如何使
其他链接延迟相同的路径。
这是我的第一个SpriteKit项目,因此,任何有关如何使该框架更惯用的建议都值得赞赏。

评论

@Linny:很高兴向您提供这个问题的悬赏。不幸的是,这与前两个赏金一样失败。

一个必须尝试:)。

我想知道...要么我们没有足够的经验丰富的Swift人,要么每个人都发现您的代码完美。

没有代码是完美的。我看不到/运算符周围的空格,因为一个:P