# #StackBounty: #swift #community-challenge #sprite-kit Swiftly turning wheels – The May 2017 Community Challenge

### Bounty: 50

This is my attempt at the May 2017 Community Challenge in Swift, with a chain consisting of

I took this as an opportunity
to learn SpriteKit, Apple’s
framework for 2D games. At least Xcode 8.3.2 with Swift 3 is required to compile
the code, it runs on both macOS and iOS (instructions below).

VectorUtils.swift – Some helper methods for vector calculations.

``````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 – The type describing a single sprocket.

``````import CoreGraphics

struct Sprocket {
let center: CGPoint
let teeth: Int

var clockwise: Bool!
var prevAngle: CGFloat!
var nextAngle: CGFloat!
var prevPoint: CGPoint!
var nextPoint: CGPoint!

self.center = center
}

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 – The type describing the complete chain drive
system. Also contains the code to compute rotation directions,
tangent angles/points, and the length of the various segments of the
chain.

``````import CoreGraphics

struct ChainDrive {

var sprockets: [Sprocket]

var length: CGFloat!
var period: CGFloat!
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 {
if !sprockets[i].clockwise {
phi = -phi
}
sprockets[i].nextAngle = a + phi
sprockets[j].prevAngle = a + phi
} else {
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
} else {
period = p2
}

}

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

return (coords, phases)
}

}
``````

SprocketNode.swift – Defines a `SKShapeNode` subclass for drawing
a single sprocket.

``````import SpriteKit

class SprocketNode: SKShapeNode {
let clockwise: Bool
let teeth: Int

init(sprocket: Sprocket) {
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)
startAngle: a1, endAngle: a2, clockwise: false)
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))
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
}
}

fatalError("init(coder:) has not been implemented")
}
}
``````

LinkNode.swift – Defines a `SKShapeNode` subclass for drawing

``````import SpriteKit

static let narrowWidth: CGFloat = 2
static let wideWidth : CGFloat = 6

let pitch: CGFloat

init(pitch: CGFloat) {
self.pitch = pitch
super.init()

let path = CGMutablePath()
startAngle: phi, endAngle: 2 * π - phi, clockwise: false)
startAngle: -π/2, endAngle: π/2, clockwise: false)
path.closeSubpath()
self.path = path
self.fillColor = .black
self.lineWidth = 0
self.strokeColor = .clear
}

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 – Defines a `SKScene` subclass for drawing
and animating the chain drive.

``````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] = []

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
}

fatalError("init(coder:) has not been implemented")
}

func setUpScene() {

backgroundColor = .white
sprocketNodes = chainDrive.sprockets.map(SprocketNode.init)
for node in sprocketNodes {
}

let (coords, _) = chainDrive.linkCoordinatesAndPhases(offset: 0)
for i in 0..<coords.count {
let j = (i + 1) % coords.count
node.moveTo(leftPin: coords[i], rightPin: coords[j])
}
}

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)
let p1 = coords[i % coords.count]
let p2 = coords[(i + 1) % coords.count]
}
for i in 0..<phases.count {
sprocketNodes[i].zRotation = phases[i]
}
}
}
``````

The complete project is
available on GitHub.
Alternatively:

• In Xcode 8.3.2 (or later), create a new project from the “Cross-platform SpriteKit Game” template.
• Select “Include iOS Application” and/or “Include macOS Application”.
• Add the above source files to the project.
• In the GameViewController.swift files, replace
``````let scene = GameScene.newGameScene()
``````

by

``````let scene = ChainDriveScene.newScene()
``````
• Compile and run!

The animation runs with approx 60 frames per second both on an
1.2 GHz MacBook and on an iPhone 6s.
To give you a rough impression of what it looks like, I took a screen
recording with QuickTime Player and converted it to an animated GIF
with ffmpeg and gifsicle:

All feedback is welcome, such as (but not limited to):

• Can the geometrical computations be simplified?
• Better type/variable/function names?
• There are several “implicitly unwrapped optional” properties
in `struct Sprocket`. The reason is that these are computed
(in `func computeSprocketData()`) after all sprockets have been
initialized. Any suggestions how to do this two-step initialization
more elegantly?
• Initially I used a `SKAction` for rotating the sprockets, but did not
find a way to animate the chain with `SKActions`. Therefore both
sprockets and chain links are now updated in the `update()` method
(which is called for each frame). Is there are better way to achieve
the same result?
• Another idea was to use `SKAction.followPath()` to animate the chain links.
That worked well for one link, but I could not figure out how to make the
other links follow the same path with a delay. Is that possible?
• This is my first SpriteKit project, therefore any advice on how to
make more idiomatic use of that framework is appreciated.

Get this bounty!!!

This site uses Akismet to reduce spam. Learn how your comment data is processed.