サイトアイコン 上尾市のWEBプログラマーによるブログ

「たった1日で基本が身に付く! Swift アプリ開発 超入門」の感想・備忘録3

「たった1日で基本が身に付く! Swift アプリ開発 超入門」の感想・忘備録2の続き

SpriteKitとは

シューティングゲームの作成

プロジェクト作成〜初期画面の表示まで

  1. プロジェクト作成⇒Game
  2. LanchScreen.storyboardを選択⇒Labelなどでスプラッシュ画面を作成
  3. Main.storyboardを選択⇒既にある画面の左側にViewControllerを追加
  4. 矢印を左に移動し、追加したViewを最初の画面にする
  5. 追加したViewにButtonを追加
  6. Controlを押しながらButtonを右側のViewにドロップしShow Detailを選択
  7. Gimpなどで画像を作成。背景は透過色にする。
  8. Assets.xcassetsをクリックし、AppIconに画像をドロップ
  9. 1xから2xにドラッグ&ドロップ
  10. シーン(GameScene.sks)をクリックし、デフォルトのLabel(Hello, World!)を削除
  11. GameScene.swiftをクリックし、didMove以外のメソッドを削除
  12. 以下のように、メンバ変数とdidMoveメソッドを追加(didMoveは画面が表示された時に最初に実行される)
// ======================
// メンバ変数を追加
// ======================
var node1 = SKSpriteNode()

// 〜省略〜

// ==========================
// didMoveメソッドを追加
// ==========================
override func didMove(to view: SKView) {
    var sizeRate : CGFloat = 0.0
    var node1Size = CGSize(width: 0.0, height: 0.0)
    let offsetY = frame.height / 20
    
    // 画像ファイルの読み込み
    self.node1 = SKSpriteNode(imageNamed: "画像名")
    // 自機を幅の1/5にするための倍率を求める
    sizeRate = (frame.width / 5) / self.node1.size.width
    // 自機のサイズを計算する
    node1Size = CGSize(width: self.node1.size.width * sizeRate,
                        height: self.node1.size.height * sizeRate)
    // 自機のサイズを設定する
    self.node1.scale(to: node1Size)
    // 自機の表示位置を設定する
    self.node1.position = CGPoint(x: 0, y: (-frame.height / 2) + offsetY + node1Size.height / 2)
    // シーンに自機を追加(表示)する
    addChild(self.node1)
}

※Y座標計算は以下のような処理になっている。

敵の表示

  1. メンバ変数とdidMoveメソッドの処理を追加
// ==========================
// メンバ変数を追加
// ==========================
var enemyRate : CGFloat = 0.0  // 敵の表示倍率用変数の追加
var enemySize = CGSize(width: 0.0, height: 0.0)  // 敵の表示サイズ用変数の追加
var timer: Timer?

// 〜省略〜

// ==========================
// didMoveメソッドに処理を追加
// ==========================
// 敵の画像ファイルの読み込み
let tempEnemy = SKSpriteNode(imageNamed: "画像名")
// 敵を幅の1/5にするための倍率を求める
enemyRate = (frame.width / 10) / tempEnemy.size.width
// 敵のサイズを計算する
enemySize = CGSize(width: tempEnemy.size.width * enemyRate, height: tempEnemy.size.height * enemyRate)

// 敵を表示するメソッドmoveEnemyを1秒ごとに呼び出し
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
    self.moveEnemy()
})

Timer.scheduledTimerは引数blockの処理を繰り返し呼び出す

  1. moveEnemyメソッドを追加
// ==========================
// moveEnemyメソッドを追加
// ==========================
// 敵を表示するメソッド
func moveEnemy() {
    let enemyNames = ["画像名", "画像名", "画像名"]
    let idx = Int.random(in: 0 ..< 3)
    let selectedEnemy = enemyNames[idx]
    let enemy = SKSpriteNode(imageNamed: selectedEnemy)
    
    // 敵のサイズを設定する
    enemy.scale(to: enemySize)
    // 敵のx方向の位置を生成する
    let xPos = (frame.width / CGFloat.random(in: 1...5)) - frame.width / 2
    // 敵の位置を設定する
    enemy.position = CGPoint(x: xPos, y: frame.height / 2)
    // シーンに敵を表示する
    addChild(enemy)
    
    // 指定した位置まで2.0秒で移動させる
    let move = SKAction.moveTo(y: -frame.height / 2, duration: 2.0)
    // 親からノードを削除する
    let remove = SKAction.removeFromParent()
    // アクションを連続して実行する
    enemy.run(SKAction.sequence([move, remove]))
}

加速度センサーの追加

  1. import文、メンバ変数、didMoveの処理、を追加
// ==========================
// import文を追加
// ==========================
import CoreMotion

// 〜省略〜

// ==========================
// メンバ変数を追加
// ==========================
let motionMgr = CMMotionManager()
var accelarationX: CGFloat = 0.0

// 〜省略〜

// ==========================
// didMoveメソッドに処理を追加
// ==========================
// 加速度センサーの取得間隔を設定取得処理
motionMgr.accelerometerUpdateInterval = 0.05
// 加速度センサーの変更値取得
motionMgr.startAccelerometerUpdates(to: OperationQueue.current!) { (val, _) in
    guard let unwrapVal = val else {
        return
    }
    let acc = unwrapVal.acceleration
    self.accelarationX = CGFloat(acc.x)
}
  1. didSimulatePhysicsメソッドを追加
// ===============================
// didSimulatePhysicsメソッドを追加
// ===============================
// シーンの更新
override func didSimulatePhysics() {
    let pos = self.node1.position.x + self.accelarationX * 30
    if pos > frame.width / 2 - self.node1.frame.width / 2 { return }
    if pos < -frame.width / 2 + self.node1.frame.width / 2 { return }
    self.node1.position.x = pos
}

ミサイル発射処理を追加

  1. touchesBeganメソッドを追加
// ===============================
// touchesBeganメソッドを追加
// ===============================
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    // 画像ファイルの読み込み
    let missile = SKSpriteNode(imageNamed: "missile")
    // ミサイルの発射位置の作成
    let missilePos = CGPoint(x: self.node1.position.x,
                             y: self.node1.position.y +
                                (self.node1.size.height / 2) -
                                (missile.size.height / 2))
    // ミサイル発射位置の設定
    missile.position = missilePos
    // シーンにミサイルを表示する
    addChild(missile)
    
    // 指定した位置まで0.5秒で移動する
    let move = SKAction.moveTo(y: frame.height / 2 - missile.size.height, duration: 0.5)
    // 親からノードを削除する
    let remove = SKAction.removeFromParent()
    // アクションを連続して実行する
    missile.run(SKAction.sequence([move, remove]))
}

衝突判定を追加

  1. メンバ変数、didMoveの処理、moveEnemyの処理、touchesBeganの処理、を追加
// ===============================
// メンバ変数を追加
// ===============================
// カテゴリビットマスクの定義
let myCategory : UInt32 = 0b0001
let missileCategory : UInt32 = 0b0010
let enemyCategory : UInt32 = 0b0100

// 〜省略〜

// ===============================
// didMoveメソッドに処理を追加
// ===============================
// 画面への重力設定
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
// 自機への物理ボディ、カテゴリビットマスク、衝突ビットマスクの設定
self.node1.physicsBody = SKPhysicsBody(rectangleOf: self.node1.size)
self.node1.physicsBody?.categoryBitMask = self.myCategory
self.node1.physicsBody?.collisionBitMask = self.enemyCategory
self.node1.physicsBody?.isDynamic = true

// 〜省略〜

// ===============================
// moveEnemyメソッドに処理を追加
// ===============================
// 敵への物理ボディ、カテゴリビットマスクの設定
enemy.physicsBody = SKPhysicsBody(rectangleOf: enemy.size)
enemy.physicsBody?.categoryBitMask = self.enemyCategory
enemy.physicsBody?.isDynamic = true

// 〜省略〜

// ===============================
// touchesBeganメソッドに処理を追加
// ===============================
// ミサイルの物理ボディ、カテゴリビットマスク、衝突ビットマスクの設定
missile.physicsBody = SKPhysicsBody(rectangleOf: missile.size)
missile.physicsBody?.categoryBitMask = self.missileCategory
missile.physicsBody?.collisionBitMask = self.enemyCategory
missile.physicsBody?.isDynamic = true

衝突時の処理を追加

  1. SKPhysicsContactDelegateを継承し、didMoveの処理、touchesBeganの処理、didBeginメソッド、を追加
// ===============================
// SKPhysicsContactDelegateを継承
// ===============================
class GameScene: SKScene, SKPhysicsContactDelegate {

// 〜省略〜

// ===============================
// didMoveメソッドに処理を追加
// ===============================
// 以下のコードを追加することでdidBeginメソッドをGameSceneクラス内で呼び出せるようになる
physicsWorld.contactDelegate = self

// 〜省略〜

self.node1.physicsBody?.contactTestBitMask = self.enemyCategory

// 〜省略〜

// ===============================
// touchesBeganメソッドに処理を追加
// ===============================
missile.physicsBody?.contactTestBitMask = self.enemyCategory

// 〜省略〜

// ===============================
// didBeginメソッドを追加
// ===============================
/// 衝突時のメソッド
func didBegin(_ contact: SKPhysicsContact) {
    
    // 炎のパーティクルの読み込みと表示
    let explosion = SKEmitterNode(fileNamed: "パーティクルの名前")
    explosion?.position = contact.bodyA.node?.position ?? CGPoint(x: 0, y: 0)
    addChild(explosion!)
    
    // 炎のパーティクルアニメーションを0.5秒表示して削除
    self.run(SKAction.wait(forDuration: 0.5)) {
        explosion?.removeFromParent()
    }
    
    // 衝突したノードを削除する
    contact.bodyA.node?.removeFromParent()
    contact.bodyB.node?.removeFromParent()
}

ライフとスコアを表示する

  1. メンバ変数、didMoveの処理、を追加
// ===============================
// メンバ変数を追加
// ===============================
var lifeLabelNode = SKLabelNode()   // LIFE表示用ラベル
var scoreLabelNode = SKLabelNode()   // SCORE表示用ラベル
// LIFE用プロパティ
var life : Int = 0 {
    didSet {
        self.lifeLabelNode.text = "LIFE : \(life)"
    }
}
// SCORE用プロパティ
var score : Int = 0 {
    didSet {
        self.scoreLabelNode.text = "SCORE : \(score)"
    }
}

// 〜省略〜

// ===============================
// didMoveメソッドに処理を追加
// ===============================
// ライフの作成
self.life = 3
self.lifeLabelNode.fontName = "HelveticaNeue-Bold"
self.lifeLabelNode.fontColor = UIColor.white
self.lifeLabelNode.fontSize = 30
self.lifeLabelNode.position = CGPoint(
    x: frame.width / 2  - (self.lifeLabelNode.frame.width + 20),
    y: frame.height / 2 - self.lifeLabelNode.frame.height * 3)
addChild(self.lifeLabelNode)

// スコアの表示
self.score = 0
self.scoreLabelNode.fontName = "HelveticaNeue-Bold"
self.scoreLabelNode.fontColor = UIColor.white
self.scoreLabelNode.fontSize = 30
self.scoreLabelNode.position = CGPoint(
    x: -frame.width / 2  + self.scoreLabelNode.frame.width ,
    y: frame.height / 2 - self.scoreLabelNode.frame.height * 3)
addChild(self.scoreLabelNode)

ライフとスコアの増減処理を追加する

  1. メンバ変数、didBeginの処理、restartメソッド、を追加する
// ===============================
// メンバ変数を追加
// ===============================
var vc: GameViewController!

// 〜省略〜

// ===============================
// didBeginメソッドに処理を追加
// ===============================
// ミサイルが敵に当たった時の処理
if contact.bodyA.categoryBitMask == missileCategory ||
    contact.bodyB.categoryBitMask == missileCategory {
    self.score += 10
}

// 自機が爆発した時の処理
if contact.bodyA.categoryBitMask == myShipCategory ||
    contact.bodyB.categoryBitMask == myShipCategory {
    // ライフを1つ減らす
    self.life -= 1
    
    // 1秒後に restart を実行
    self.run(SKAction.wait(forDuration: 1)) {
        self.restart()
    }
}

// 〜省略〜

// ===============================
// restartメソッドを追加
// ===============================
// リスタート処理
func restart() {
    // ライフが0以下の場合
    if self.life <= 0 {
        // START画面に戻る
        vc.dismiss(animated: true, completion: nil)
    }
    
    // ライフが1以上なら自機を再表示
    addChild(self.node1)
}
  1. GameViewController.swiftのviewDidLoadメソッドに処理を追加する
(scene as! GameScene).vc = self

画面の向きを縦固定にする

プロジェクト名をクリック⇒Target⇒General⇒Device Orientation⇒Portraitのみチェック

アプリのアイコンを設定する

180×180の画像を用意し、Assets.xcassetsのAppIconの中のiOS 7-13 60ptのx3にドロップする。
AppStoreに出す場合は1024×1024が必要。

モバイルバージョンを終了