Swiftは6月に一度手を出したんですが、2ヶ月以上放置してたらすっかり忘れてしまったので再勉強中です。
今回は、以前から興味のあったBoidを作ってみました。
フレームワークはSprite Kitを利用。
(使ったことない方はSpriteKit.jpをどうぞ。)
完成イメージ
この記事の元になったソースコードは以下で公開しています。
プロジェクト作成
テンプレートは Game、Game Technology
は SpriteKit を選択します。
GameScene.sks
のSKNode inspector
でColor
を Black に変更し、Size
はW
を 640、H
を 1136 にしておきます。
また、GameScene.swift
を編集してdidMoveToView
の中身、touchesBegan
を削除します。
この状態で実行すると、真っ黒の画面が表示されるようになります。
鳥(?)の表示
BridNode
を追加して、シーンに表示します。
三角形とかでもいいんですが、Sprite Kitはパーティクルを簡単に表示できるのが魅力なので、炎を使ってみます。
動かしてみると魚に見えなくもないと思います。
Resource
から SpriteKit Particle File、Particle template
は Fire を選択して、fire.sks
を作ります。
※ Particle Emitter Editor
を開くとXcodeが落ちることがあるので注意してください。
作成したパーティクルの角度やサイズなどを調整し、BirdNodeの子ノードとして追加します。
import SpriteKit
class BirdNode: SKNode {
override init() {
super.init()
self.addFireNode()
}
private func addFireNode() {
guard let fireNode = NSKeyedUnarchiver.unarchiveObjectWithFile(NSBundle.mainBundle().pathForResource("fire", ofType: "sks")!) as? SKEmitterNode else { return }
fireNode.particleScale = 0.15
fireNode.xScale = 0.7
fireNode.yScale = 0.9
fireNode.particleLifetime = 0.3
fireNode.emissionAngle = -CGFloat(90.0 * M_PI / 180.0)
fireNode.emissionAngleRange = 0.0
fireNode.particlePositionRange = CGVector(dx: 0.0, dy: 0.1)
fireNode.particleColor = SKColor.orangeColor()
self.addChild(fireNode)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
これをGameScene
で表示します。
let numberOfBirds = 10
var birdNodes = [BirdNode]()
override func didMoveToView(view: SKView) {
let degree: Double = 360.0 / Double(self.numberOfBirds)
let radius = 120.0
for i in 0..<self.numberOfBirds {
let birdNode = BirdNode()
let radian = degree * Double(i) * M_PI / 180.0
let x = Double(CGRectGetMidX(self.frame)) + cos(radian) * radius
let y = Double(CGRectGetMidY(self.frame)) + sin(radian) * radius
birdNode.position = CGPoint(x: x, y: y)
self.addChild(birdNode)
self.birdNodes.append(birdNode)
}
}
こんな感じになります。
ルールの実装
Boidsは簡単な3つのルールを実装することで、群れにみえるのがおもしろいところです。
人工 無 命といったところでしょうか。
それでは、ルールを実装します。
ベースクラス
各ルールに共通の処理を実装します。
import UIKit
class Rule: NSObject {
var velocity: CGPoint!
var weight: CGFloat
var weighted: CGPoint {
return CGPoint(x: self.velocity.x * self.weight, y: self.velocity.y * self.weight)
}
init(weight: CGFloat) {
self.weight = weight
super.init()
self.clear()
}
func clear() {
self.velocity = CGPoint(x: 0.0, y: 0.0)
}
func evaluate(targetNode targetNode: BirdNode, birdNodes: [BirdNode]) {
self.clear()
}
}
ルール1:結合(Cohesion)
群れの中心に向かわせます。
import UIKit
class CohesionRule: Rule {
let factor: CGFloat = 300.0
override func evaluate(targetNode targetNode: BirdNode, birdNodes: [BirdNode]) {
super.evaluate(targetNode: targetNode, birdNodes: birdNodes)
for birdNode in birdNodes {
if birdNode != targetNode {
self.velocity.x += birdNode.position.x
self.velocity.y += birdNode.position.y
}
}
self.velocity.x /= CGFloat(birdNodes.count - 1)
self.velocity.y /= CGFloat(birdNodes.count - 1)
self.velocity.x = (self.velocity.x - targetNode.position.x) / self.factor
self.velocity.y = (self.velocity.y - targetNode.position.y) / self.factor
}
}
ルール2:分離(Separation)
他のBirdNode
とぶつからないようにします。
import UIKit
class SeparationRule: Rule {
let threshold = 30.0
override func evaluate(targetNode targetNode: BirdNode, birdNodes: [BirdNode]) {
super.evaluate(targetNode: targetNode, birdNodes: birdNodes)
for birdNode in birdNodes {
if birdNode != targetNode {
if self.distanceBetween(targetNode.position, birdNode.position) < self.threshold {
self.velocity.x -= birdNode.position.x - targetNode.position.x
self.velocity.y -= birdNode.position.y - targetNode.position.y
}
}
}
}
private func distanceBetween(pointA: CGPoint, _ pointB: CGPoint) -> Double {
let x = Double(pointA.x - pointB.x)
let y = Double(pointA.y - pointB.y)
return sqrt(x * x + y * y)
}
}
ルール3:整列(Alignment)
他のBirdNode
と速度・方向を合わせます。
import UIKit
class AlignmentRule: Rule {
let factor: CGFloat = 2.0
override func evaluate(targetNode targetNode: BirdNode, birdNodes: [BirdNode]) {
super.evaluate(targetNode: targetNode, birdNodes: birdNodes)
for birdNode in birdNodes {
if birdNode != targetNode {
self.velocity.x += birdNode.velocity.x
self.velocity.y += birdNode.velocity.y
}
}
self.velocity.x /= CGFloat(birdNodes.count - 1)
self.velocity.y /= CGFloat(birdNodes.count - 1)
self.velocity.x = (self.velocity.x - targetNode.velocity.x) / self.factor
self.velocity.y = (self.velocity.y - targetNode.velocity.y) / self.factor
}
}
ルールの評価
BirdNode
から各ルールを呼び出して、移動させます。
なお、各ルールには重みをつけています。(ルール1、2、3の順に重くしてある)
また、ルールの評価が終わった後に、画面からはみ出していないか、maxSpeed
を超えていないかのチェックを行ないます。
そして最後に、移動方向に回転させる処理もします。
プロパティ
let maxSpeed: CGFloat = 4.0
let size: CGFloat = 20.0
var velocity = CGPoint(x: 0.0, y: 0.0)
var rules: [Rule]!
初期化
override init() {
super.init()
self.rules = [
CohesionRule(weight: 1.0),
SeparationRule(weight: 0.8),
AlignmentRule(weight: 0.1)
]
self.addFireNode()
}
移動処理
func update(birdNodes birdNodes: [BirdNode], frame: CGRect) {
for rule in self.rules {
rule.evaluate(targetNode: self, birdNodes: birdNodes)
}
self.move(frame)
self.rotate()
}
private func move(frame: CGRect) {
self.velocity.x += rules.reduce(0.0, combine: { sum, r in sum + r.weighted.x })
self.velocity.y += rules.reduce(0.0, combine: { sum, r in sum + r.weighted.y })
let vector = sqrt(self.velocity.x * self.velocity.x + self.velocity.y * self.velocity.y)
if (vector > self.maxSpeed) {
self.velocity.x = (self.velocity.x / vector) * self.maxSpeed
self.velocity.y = (self.velocity.y / vector) * self.maxSpeed
}
self.position.x += self.velocity.x
self.position.y += self.velocity.y
if (self.position.x - self.size <= 0) {
self.position.x = self.size
self.velocity.x *= -1
}
if (self.position.x + self.size >= CGRectGetWidth(frame)) {
self.position.x = CGRectGetWidth(frame) - self.size
self.velocity.x *= -1
}
if (self.position.y - self.size <= 0) {
self.position.y = self.size
self.velocity.y *= -1
}
if (self.position.y + self.size >= CGRectGetHeight(frame)) {
self.position.y = CGRectGetHeight(frame) - self.size
self.velocity.y *= -1
}
}
private func rotate() {
let radian = -atan2(Double(velocity.x), Double(velocity.y))
self.zRotation = CGFloat(radian)
}
仕上げ
BirdNode
のupdate
を、GameScene
のupdate
から実行します。
(このメソッドは毎フレーム呼び出されます。)
override func update(currentTime: CFTimeInterval) {
for birdNode in self.birdNodes {
birdNode.update(birdNodes: self.birdNodes, frame: self.frame)
}
}
}
これで完成です。
Swift × Sprite Kitの感想
CGFloatとDouble・FloatのConversionを忘れてよく怒られました。
Objective-Cの時は、CGFloatばっかり使っててあんまり意識してなかったです。
課題
このままだと、例えば エサに反応する のように、 他のBirdNode
以外に反応するルール が書きづらいので何とかしたいところです。