Edited at

SwiftとSprite Kitでボイド

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


参考文献