LoginSignup
49
47

More than 5 years have passed since last update.

SwiftとSprite Kitでボイド

Last updated at Posted at 2014-09-23

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

参考文献

49
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
47