SwiftとSprite Kitでボイド

  • 52
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Swiftは6月に一度手を出したんですが、2ヶ月以上放置してたらすっかり忘れてしまったので再勉強中です。
今回は、以前から興味のあったBoidを作ってみました。

フレームワークはSprite Kitを利用。
(使ったことない方はSpriteKit.jpをどうぞ。)

完成イメージ

boid.gif

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

tnantoka/boid

プロジェクト作成

テンプレートは GameGame TechnologySpriteKit を選択します。

GameScene.sksSKNode inspectorColorBlack に変更し、SizeW640H1136 にしておきます。

また、GameScene.swiftを編集してdidMoveToViewの中身、touchesBeganを削除します。

この状態で実行すると、真っ黒の画面が表示されるようになります。

鳥(?)の表示

BridNodeを追加して、シーンに表示します。

三角形とかでもいいんですが、Sprite Kitはパーティクルを簡単に表示できるのが魅力なので、炎を使ってみます。

動かしてみると魚に見えなくもないと思います。

Resourceから SpriteKit Particle FileParticle templateFire を選択して、fire.sksを作ります。
Particle Emitter Editorを開くとXcodeが落ちることがあるので注意してください。

作成したパーティクルの角度やサイズなどを調整し、BirdNodeの子ノードとして追加します。

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で表示します。

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

ルールの実装

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

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

ベースクラス

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

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)

群れの中心に向かわせます。

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

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

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

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

プロパティ

BirdNode.swift
    let maxSpeed: CGFloat = 4.0
    let size: CGFloat = 20.0

    var velocity = CGPoint(x: 0.0, y: 0.0)
    var rules: [Rule]!

初期化

BirdNode.swift
    override init() {
        super.init()

        self.rules = [
            CohesionRule(weight: 1.0),
            SeparationRule(weight: 0.8),
            AlignmentRule(weight: 0.1)
        ]

        self.addFireNode()
    }

移動処理

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)
    }

仕上げ

BirdNodeupdateを、GameSceneupdateから実行します。
(このメソッドは毎フレーム呼び出されます。)

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

参考文献