Быстро поворачивающиеся колеса - май 2017 года.

Это моя попытка май 2017 года. Задача сообщества в Свифте, с цепочкой, состоящей из жесткие ссылки.

Я воспринял это как возможность чтобы узнать SpriteKit , Apple рамки для 2D игр. Для компиляции требуется, по крайней мере, Xcode 8.3.2 с Swift 3 код, он работает на и 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 { $0.center.x - $0.radius }.min()! - 15
        let miny = system.sprockets.map { $0.center.y - $0.radius }.min()! - 15
        let maxx = system.sprockets.map { $0.center.x + $0.radius }.max()! + 15
        let maxy = system.sprockets.map { $0.center.y + $0.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 (или более поздней версии) создайте новый проект из шаблона Cross-Platform SpriteKit Game.
  • Выберите «Включить приложение iOS» и /или «Включить приложение MacOS».
  • Добавьте исходные файлы в проект.
  • В файлах GameViewController.swift замените

    let scene = GameScene.newGameScene()
    

    let scene = ChainDriveScene.newScene()
    
  • Скомпилируйте и запустите!

Анимация выполняется со скоростью около 60 кадров в секунду как на 1,2 ГГц MacBook и на iPhone 6s. Чтобы дать вам грубое впечатление о том, как это выглядит, я взял экран запись с помощью QuickTime Player и преобразование его в анимированный GIF с помощью ffmpeg и gifsicle :

 введите описание изображения здесь>> </a>
<аhref = введите изображение здесь »> </a> </p>

<p> Вся обратная связь приветствуется, например (но не ограничиваясь этим): </p>

<ul>
<li> Можно ли упростить геометрические вычисления? </li>
<li> Улучшенные имена типов /переменных /функций <<li>
<li> Существует несколько «неявно развернутых необязательных» свойств
в <code>struct Sprocket</code>. Причина в том, что они вычисляются
(в <code>func computeSprocketData ()</code>) после того, как все звездочки были
инициализируется. Любые предложения, как сделать эту двухэтапную инициализацию
более элегантно? </li>
<li> Сначала я использовал <code>SKAction</code> для вращения звездочек, но не использовал
найти способ анимировать цепочку с помощью <code>SKActions</code>. Поэтому оба
звездочки и звенья цепи теперь обновляются в методе <code>update ()</code>
(который вызывается для каждого кадра). Есть ли лучший способ достичь
тот же результат? </li>
<li> Другая идея заключалась в том, чтобы использовать <code>SKAction.followPath ()</code> для анимации цепочечных ссылок.
Это хорошо работает для  one , но я не мог понять, как сделать
другие ссылки следуют по тому же пути с задержкой. Возможно ли это? </li>
<li> Это мой первый проект SpriteKit, поэтому любые советы о том, как
сделать более идиоматическое использование этой структуры оценено. </li>
</ul></body></html>

29 голосов | спросил Martin R 31 Mayam17 2017, 00:30:24

0 ответов


Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132