SwiftとSprite Kitでボイド


Swiftは6月に一度手を出したんですが、2ヶ月以上放置してたらすっかり忘れてしまったので再勉強中です。
今回は、以前から興味のあった[Boid](http://ja.wikipedia.org/wiki/%E3%83%9C%E3%82%A4%E3%83%89_(%E4%BA%BA%E5%B7%A5%E7%94%9F%E5%91%BD))を作ってみました。

フレームワークはSprite Kitを利用。
（使ったことない方は[SpriteKit.jp](http://spritekit.jp/)をどうぞ。）

# 完成イメージ

![boid.gif](https://qiita-image-store.s3.amazonaws.com/0/2191/1760acf6-6d79-9394-edc5-8ab3e1e26079.gif "boid.gif")

この記事の元になったソースコードは以下で公開しています。

[tnantoka/boid](https://github.com/tnantoka/boid)

# プロジェクト作成

テンプレートは **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の子ノードとして追加します。

```swift:BirdNode.swift
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`で表示します。

```swift:GameScene.swift
    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)
        }
    }
```

こんな感じになります。

![boid.png](https://qiita-image-store.s3.amazonaws.com/0/2191/ca00f701-0f98-956a-dc65-d124241150b7.png "boid.png")


# ルールの実装

Boidsは簡単な3つのルールを実装することで、群れにみえるのがおもしろいところです。
人工 **無** 命といったところでしょうか。

それでは、ルールを実装します。

## ベースクラス

各ルールに共通の処理を実装します。

```swift:Rule.swift
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）
 
群れの中心に向かわせます。

```swift:CohesionRule.swift
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`とぶつからないようにします。

```swift:SeparationRule.swift
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`と速度・方向を合わせます。

```swift:AlignmentRule.swift
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`を超えていないかのチェックを行ないます。

そして最後に、移動方向に回転させる処理もします。

## プロパティ

```swift:BirdNode.swift
    let maxSpeed: CGFloat = 4.0
    let size: CGFloat = 20.0
    
    var velocity = CGPoint(x: 0.0, y: 0.0)
    var rules: [Rule]!
```

## 初期化

```swift:BirdNode.swift
    override init() {
        super.init()
    
        self.rules = [
            CohesionRule(weight: 1.0),
            SeparationRule(weight: 0.8),
            AlignmentRule(weight: 0.1)
        ]
        
        self.addFireNode()
    }
```

## 移動処理

```swift:BirdNode.swift
    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`から実行します。
（このメソッドは毎フレーム呼び出されます。）

```swift:GameScene.swift
    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`以外に反応するルール** が書きづらいので何とかしたいところです。

# 参考文献

* [ほぼ日刊イトイ新聞 - がんばれ森川くんの遺伝子くん](https://www.1101.com/morikawa/2001-06-25.html)
* [Processing.js基礎最速入門 - catch.jp-wiki](http://www.catch.jp/wiki/?Processing.js%B4%F0%C1%C3%BA%C7%C2%AE%C6%FE%CC%E7#l538b340)
* [【ボイド】JavaScriptとHTML5で『群れ』をシミュレーションしてみよう【プログラミング】 - あのねノート。](http://ottati.hatenablog.com/entry/2013/09/28/125953)
* [快技庵の電子書籍 - Swift離陸ガイド](http://kaigian.co.jp/books/)