我以此为契机
学习了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