SpriteKitを使用したボールの衝突シミュレーション(Swift3.1対応版)

動作環境

Xcode 8.3.2(8E2002)

資料

手順

  1. ビューを生成 (舞台)
  2. シーンを追加 (場面)
  3. ノードを追加 (登場人物)
SKSpriteNode/SKLabelNode/SKShapeNode/SKEmitterNode 
テクスチャ画像/テキスト/図形/パーティクルのノードを表示するクラス
SKScene
Sprite Kitのコンテンツのシーンを表示するクラス
SKView
Sprite Kitのコンテンツのビューを表示するクラス

シーンの更新

1フレーム
-update
シーンの更新においてシーンのアクションが評価される前に発生する。
SKScene evaluates actions
-didEvaluateActions
シーンの更新においてシーンのアクションが評価された後に発生する。
SKScene simulates physics
-didSimulatePhysics
シーンの更新において物理シミュレーションが行われた後に発生する。
SKScene applies constraints
-didApplyConstraints
シーンの更新において制約を適合させた後に発生する。
-didFinishUpdate
シーンがアニメーションに必要なすべての処理を終わらせた後に呼ぶ。
SKView renders the scene

*60fpsなら1秒間に60回フレームの更新が発生する。

処理

1.不要な記述を削除する。
GameScene.swift
import SpriteKitclass GameScene: SKScene {
    override func didMove(to view: SKView) {
    }    override func update(_ currentTime: CFTimeInterval) {
    }
}
- didMove
シーンがビューによって提供された直後に呼び出される。
GameViewController.swift
import UIKit
import SpriteKitclass GameViewController: UIViewController {
   override func viewDidLoad() {
       super.viewDidLoad()
   }   override var shouldAutorotate: Bool {
       return true
   }   override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
       if UIDevice.current.userInterfaceIdiom == .phone {
           return UIInterfaceOrientationMask.allButUpsideDown
       } else {
           return UIInterfaceOrientationMask.all
       }
   }   override func didReceiveMemoryWarning() {
       super.didReceiveMemoryWarning()
   }   override var prefersStatusBarHidden: Bool {
       return true
   }
}
2.ビュー上にシーンを表示する。
GameViewController.swift
import UIKit
import SpriteKitclass GameViewController: UIViewController {
   override func viewDidLoad() {
       super.viewDidLoad()
       // シーンの作成
       let scene = GameScene()
       // View ControllerのViewをSKView型として取り出す
       let view = self.view as! SKView
       // FPSの表示
       view.showsFPS = true
       // ノード数の表示
       view.showsNodeCount = true
       // シーンのサイズをビューに合わせる
       scene.size = view.frame.size
       // ビュー上にシーンを表示
       view.presentScene(scene)
   }   override var shouldAutorotate: Bool {
       return true
   }   override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
       if UIDevice.current.userInterfaceIdiom == .phone {
           return UIInterfaceOrientationMask.allButUpsideDown
       } else {
           return UIInterfaceOrientationMask.all
       }
   }   override func didReceiveMemoryWarning() {
       super.didReceiveMemoryWarning()
   }   override var prefersStatusBarHidden: Bool {
       return true
   }
}
3.ボールを配置する。
GameScene.swift
import SpriteKitclass GameScene: SKScene {
    var ballColor: [CGFloat] = [0, 0, 0] // ボールの色
    var ballCollection: [SKShapeNode] = []    func initObjects(){
        for _ in 0..<10 {
            var radius: CGFloat = 20
            var ball = SKShapeNode(circleOfRadius: radius)
            ball.fillColor = UIColor(red: self.ballColor[0], green: self.ballColor[1], blue: self.ballColor[2], alpha: 1)
            // ランダムに配置
            var randIntX = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.width - radius*2)))
            var randIntY = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.height - radius*2)))
            ball.position = CGPoint(x:randIntX, y:randIntY)
            self.addChild(ball)
            self.ballCollection.append(ball)
        }
    }    override func didMove(to view: SKView) {
       // 初期化処理
       self.initObjects()
    }    override func update(_ currentTime: CFTimeInterval) {
    }
}
4.シーンとボールに重力を設定する。
GameScene.swift
import SpriteKitclass GameScene: SKScene, SKPhysicsContactDelegate {
    var ballColor: [CGFloat] = [0, 0, 0] // ボールの色
    var ballCollection: [SKShapeNode] = []    func initObjects(){
        for _ in 0..<10 {
            var radius: CGFloat = 20
            var ball = SKShapeNode(circleOfRadius: radius)
            ball.fillColor = UIColor(red: self.ballColor[0], green: self.ballColor[1], blue: self.ballColor[2], alpha: 1)
            // ランダムに配置
            var randIntX = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.width - radius*2)))
            var randIntY = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.height - radius*2)))
            ball.position = CGPoint(x:randIntX, y:randIntY)
           // ボールに重力を設定
           ball.physicsBody = SKPhysicsBody(circleOfRadius: radius)
           ball.physicsBody?.restitution = 1.0 // 反発係数
           ball.physicsBody?.linearDamping = 0.0 // 空気抵抗
           ball.physicsBody?.mass = 1.0 // 質量
           ball.physicsBody?.friction = 0.0 // 摩擦
           ball.physicsBody?.contactTestBitMask = 1
            self.addChild(ball)
            self.ballCollection.append(ball)
        }
    }    override func didMove(to view: SKView) {
       // 初期化処理
       self.initObjects()
       // シーンに重力を設定
       self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -3.0)
       self.physicsWorld.contactDelegate = self
       self.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
       self.physicsBody?.restitution = 1.0 // 反発係数
       self.physicsBody?.linearDamping = 0.0 // 空気抵抗
       self.physicsBody?.friction = 0.0 // 摩擦
       self.name = "frame"
    }    override func update(_ currentTime: CFTimeInterval) {
    }
}
5.パーティクルを設定する。
ConflictParticle.sksを追加
GameScene.swift
import SpriteKitclass GameScene: SKScene, SKPhysicsContactDelegate {
    var ballColor: [CGFloat] = [0, 0, 0] // ボールの色
    var ballCollection: [SKShapeNode] = []    func initObjects(){
        for _ in 0..<10 {
            var radius: CGFloat = 20
            var ball = SKShapeNode(circleOfRadius: radius)
            ball.fillColor = UIColor(red: self.ballColor[0], green: self.ballColor[1], blue: self.ballColor[2], alpha: 1)
            // ランダムに配置
            var randIntX = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.width - radius*2)))
            var randIntY = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.height - radius*2)))
            ball.position = CGPoint(x:randIntX, y:randIntY)
           // ボールに重力を設定
           ball.physicsBody = SKPhysicsBody(circleOfRadius: radius)
           ball.physicsBody?.restitution = 1.0 // 反発係数
           ball.physicsBody?.linearDamping = 0.0 // 空気抵抗
           ball.physicsBody?.mass = 1.0 // 質量
           ball.physicsBody?.friction = 0.0 // 摩擦
           ball.physicsBody?.contactTestBitMask = 1
            self.addChild(ball)
            self.ballCollection.append(ball)
        }
    }    func didBegin(_ contact: SKPhysicsContact) {
        if let nodeA = contact.bodyA.node {
            if let nodeB = contact.bodyB.node {
                if nodeA.name == "frame" || nodeB.name == "frame" {
                    // 壁との衝突
                    return
                }else {
                    // ボール同士の衝突
                    // パーティクル生成
                    let particle = SKEmitterNode(fileNamed: "ConflictParticle.sks")
                    self.addChild(particle!)
                    // ぶつかるたびにパーティクルが増えて処理が重くなるため
                    // パーティクルを表示してから1秒後に削除する
                    var removeAction = SKAction.removeFromParent()
                    var durationAction = SKAction.wait(forDuration: 1)
                    var sequenceAction = SKAction.sequence([durationAction, removeAction])
                    particle.run(sequenceAction)
                    // ボールの位置にパーティクルを移動
                    particle.position = CGPoint(x: nodeA.position.x, y: nodeA.position.y)
                    particle.alpha = 1
                    var fadeAction = SKAction.fadeAlpha(by: 0, duration: 0.5)
                    particle.run(fadeAction)
                }
            }
        }
    }    override func didMove(to view: SKView) {
       // 初期化処理
       self.initObjects()
       // シーンに重力を設定
       self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -3.0)
       self.physicsWorld.contactDelegate = self
       self.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
       self.physicsBody?.restitution = 1.0 // 反発係数
       self.physicsBody?.linearDamping = 0.0 // 空気抵抗
       self.physicsBody?.friction = 0.0 // 摩擦
       self.name = "frame"
    }    override func update(_ currentTime: CFTimeInterval) {
    }
}
6.端末の加速度を取得してボールの色を変化させる。
GameScene.swift
import SpriteKit
import CoreMotionclass GameScene: SKScene, SKPhysicsContactDelegate {
    var motionManager: CMMotionManager!  // CMMotionManagerを格納する変数
    var ballColor: [CGFloat] = [0, 0, 0] // ボールの色
    var ballCollection: [SKShapeNode] = [];    func initObjects(){
        for _ in 0..<10 {
            var radius: CGFloat = 20
            var ball = SKShapeNode(circleOfRadius: radius)
            ball.fillColor = UIColor(red: self.ballColor[0], green: self.ballColor[1], blue: self.ballColor[2], alpha: 1)
            // ランダムに配置
            var randIntX = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.width - radius*2)))
            var randIntY = radius + (CGFloat)(arc4random_uniform((UInt32)(self.frame.height - radius*2)))
            ball.position = CGPoint(x:randIntX, y:randIntY)
           // ボールに重力を設定
           ball.physicsBody = SKPhysicsBody(circleOfRadius: radius)
           ball.physicsBody?.restitution = 1.0 // 反発係数
           ball.physicsBody?.linearDamping = 0.0 // 空気抵抗
           ball.physicsBody?.mass = 1.0 // 質量
           ball.physicsBody?.friction = 0.0 // 摩擦
           ball.physicsBody?.contactTestBitMask = 1
            self.addChild(ball)
            self.ballCollection.append(ball)
        }
    }    func didBegin(_ contact: SKPhysicsContact) {
        if let nodeA = contact.bodyA.node {
            if let nodeB = contact.bodyB.node {
                if nodeA.name == "frame" || nodeB.name == "frame" {
                    // 壁との衝突
                    return
                }else {
                    // ボール同士の衝突
                    // パーティクル生成
                    let particle = SKEmitterNode(fileNamed: "ConflictParticle.sks")
                    self.addChild(particle)
                    // ぶつかるたびにパーティクルが増えて処理が重くなるため
                    // パーティクルを表示してから1秒後に削除する
                    var removeAction = SKAction.removeFromParent()
                    var durationAction = SKAction.wait(forDuration: 1)
                    var sequenceAction = SKAction.sequence([durationAction, removeAction])
                    particle.run(sequenceAction)
                    // ボールの位置にパーティクルを移動
                    particle.position = CGPoint(x: nodeA.position.x, y: nodeA.position.y)
                    particle.alpha = 1
                    var fadeAction = SKAction.fadeAlpha(by: 0, duration: 0.5)
                    particle.run(fadeAction)
                }
            }
        }
    }    override func didMove(to view: SKView) {
       // 初期化処理
       self.initObjects()
       // シーンに重力を設定
       self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -3.0)
       self.physicsWorld.contactDelegate = self
       self.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
       self.physicsBody?.restitution = 1.0 // 反発係数
       self.physicsBody?.linearDamping = 0.0 // 空気抵抗
       self.physicsBody?.friction = 0.0 // 摩擦
       self.name = "frame"       // CMMotionManagerを生成
       motionManager = CMMotionManager()
       // 加速度の値の取得間隔を設定する
       motionManager.accelerometerUpdateInterval = 0.1
        motionManager.startAccelerometerUpdates(to: OperationQueue.current!, withHandler: {(data, error) in
          print("x:\(data!.acceleration.x) y:\(data!.acceleration.y) z:\(data!.acceleration.z)")
           self.ballColor = [(CGFloat)(abs(data!.acceleration.x)), (CGFloat)(abs(data!.acceleration.y)), (CGFloat)(abs(data!.acceleration.z))]
        })
    }    override func update(_ currentTime: CFTimeInterval) {
      for ball in ballCollection {
          ball.fillColor = UIColor(red: self.ballColor[0], green: self.ballColor[1], blue: self.ballColor[2], alpha: 1)
      }
    }
}