N.N. LLC. ロゴ - 千葉県船橋市のIT企業N.N. LLC.
技術ブログ

SwiftでiOSゲームを開発する — SpriteKitとGameplayKitの活用法

22分で読めます
SwiftでiOSゲームを開発する — SpriteKitとGameplayKitの活用法

はじめに — SwiftでiOSゲームを作るという選択

iOSゲーム開発と聞くと、UnityやUnreal Engineを思い浮かべる方が多いかもしれません。しかし、Apple純正のSpriteKitとGameplayKitを使えば、追加のエンジンなしにSwiftだけで本格的な2Dゲームを開発できます。

N.N. LLC.では、「クレイジーバルーン」「ドボンっ」「AI育成〜リバースボード〜」「賞金稼ぎチェス」など、複数のiOSゲームをSpriteKitで開発し、App Storeでリリースしています。本記事では、これらの実プロジェクトで培ったノウハウを包括的に解説します。

Swift 5.9+でのiOSゲーム開発の全体像

Swift 5.9では、マクロシステムや改良されたパラメータパック機能が追加され、ゲーム開発にも恩恵があります。Appleが提供するゲーム関連のフレームワーク群は以下の通りです。

  • SpriteKit: 2Dゲームのレンダリング、物理演算、アニメーション
  • GameplayKit: AI、パスファインディング、ルールシステム、ランダム生成
  • GameController: MFiゲームコントローラー対応
  • Game Center: ランキング、実績、マルチプレイヤー
  • AVFoundation: サウンド再生・管理
  • Core Haptics: 触覚フィードバック

UnityやUnreal Engineとの最大の違いは、iOSネイティブのフレームワークであるため、アプリサイズが小さく、起動が高速で、OSとの統合が完璧である点です。カジュアルゲームやボードゲームなど、2Dゲームの開発に特に適しています。

SpriteKitの基本 — ゲーム画面を構成する4つの要素

SKScene — ゲームのシーン管理

SKSceneは1つのゲーム画面を表すクラスです。タイトル画面、ゲームプレイ画面、リザルト画面など、画面ごとにシーンを作成します。

class GameScene: SKScene {
    // シーンがビューに表示された時に呼ばれる
    override func didMove(to view: SKView) {
        // 背景色の設定
        backgroundColor = .black

        // 物理演算のワールド設定
        physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
        physicsWorld.contactDelegate = self

        // ゲームオブジェクトの初期配置
        setupPlayer()
        setupEnemies()
        setupUI()
    }

    // 毎フレーム呼ばれるアップデート関数(60fps)
    override func update(_ currentTime: TimeInterval) {
        // ゲームロジックの更新
        updatePlayerPosition()
        checkBoundaries()
        updateScore()
    }
}

SKSpriteNode — キャラクターやオブジェクトの表示

SKSpriteNodeはゲーム内のあらゆるビジュアル要素を表現します。画像テクスチャの表示、色付き矩形、アニメーションスプライトなどに対応しています。

// プレイヤーキャラクターの作成
func setupPlayer() {
    let player = SKSpriteNode(imageNamed: "player")
    player.position = CGPoint(x: frame.midX, y: frame.midY)
    player.size = CGSize(width: 64, height: 64)
    player.name = "player"

    // 物理ボディの設定(衝突判定用)
    player.physicsBody = SKPhysicsBody(circleOfRadius: 30)
    player.physicsBody?.categoryBitMask = PhysicsCategory.player
    player.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
    player.physicsBody?.collisionBitMask = PhysicsCategory.ground
    player.physicsBody?.allowsRotation = false

    addChild(player)
}

SKAction — アニメーションと動きの制御

SKActionはSpriteKitのアニメーションシステムです。移動、回転、拡大縮小、フェード、色変更、サウンド再生など、あらゆるアクションをチェーンや並列で組み合わせられます。

// アニメーションの組み合わせ例
let moveUp = SKAction.moveBy(x: 0, y: 200, duration: 0.5)
let moveDown = SKAction.moveBy(x: 0, y: -200, duration: 0.3)
let scale = SKAction.scale(to: 1.2, duration: 0.2)
let scaleBack = SKAction.scale(to: 1.0, duration: 0.2)
let sound = SKAction.playSoundFileNamed("jump.wav", waitForCompletion: false)

// アクションを順番に実行
let jumpSequence = SKAction.sequence([
    SKAction.group([moveUp, scale, sound]),  // 同時実行
    SKAction.group([moveDown, scaleBack])
])

// 永続的に繰り返す
player.run(SKAction.repeatForever(jumpSequence))

SKPhysicsBody — 物理演算と衝突判定

SpriteKitには物理エンジンが内蔵されており、重力、摩擦、反発、衝突判定をシンプルなAPIで実装できます。「クレイジーバルーン」では、風船の浮遊感を物理演算で表現しています。

// 物理カテゴリの定義(ビットマスク)
struct PhysicsCategory {
    static let none:    UInt32 = 0
    static let player:  UInt32 = 0b0001  // 1
    static let enemy:   UInt32 = 0b0010  // 2
    static let ground:  UInt32 = 0b0100  // 4
    static let item:    UInt32 = 0b1000  // 8
}

// 衝突検知のデリゲート実装
extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        let collision = contact.bodyA.categoryBitMask
                      | contact.bodyB.categoryBitMask

        switch collision {
        case PhysicsCategory.player | PhysicsCategory.enemy:
            // プレイヤーと敵が接触 → ゲームオーバー処理
            handleGameOver()

        case PhysicsCategory.player | PhysicsCategory.item:
            // プレイヤーとアイテムが接触 → アイテム取得処理
            handleItemPickup(contact)

        default:
            break
        }
    }
}

GameplayKitのAI — 思考するゲームキャラクターの実装

GKMinmaxStrategist — ボードゲームAI

「AI育成〜リバースボード〜」と「賞金稼ぎチェス」では、GameplayKitのMinmax法AIを採用しています。GKMinmaxStrategistは、与えられたゲーム状態から最適な手を探索するAIアルゴリズムです。

// ゲームモデルのプロトコル実装
class BoardGameModel: NSObject, GKGameModel {
    var players: [GKGameModelPlayer]?
    var activePlayer: GKGameModelPlayer?

    // 現在の局面で可能な手をすべて返す
    func gameModelUpdates(for player: GKGameModelPlayer) -> [GKGameModelUpdate]? {
        guard let currentPlayer = player as? Player else { return nil }
        return generateValidMoves(for: currentPlayer)
    }

    // 手を適用して盤面を更新
    func apply(_ gameModelUpdate: GKGameModelUpdate) {
        guard let move = gameModelUpdate as? Move else { return }
        applyMove(move)
    }

    // 局面の評価値を返す(AIの判断基準)
    func score(for player: GKGameModelPlayer) -> Int {
        return evaluateBoard(for: player as! Player)
    }
}

// AIストラテジストの初期化と手の探索
let strategist = GKMinmaxStrategist()
strategist.gameModel = boardModel
strategist.maxLookAheadDepth = 5  // 5手先まで読む
strategist.randomSource = GKRandomSource.sharedRandom()

// 最善手を取得(バックグラウンドスレッドで実行推奨)
if let bestMove = strategist.bestMove(for: currentPlayer) as? Move {
    applyMove(bestMove)
}

GKDecisionTree — ルールベースAI

アクションゲームの敵キャラクターには、デシジョンツリー(決定木)ベースのAIが適しています。プレイヤーとの距離、残りHP、ステージの状況に基づいて行動を決定します。

パーティクルエフェクトの実装

SKEmitterNodeを使えば、爆発、炎、雪、煙、キラキラといったパーティクルエフェクトを実装できます。Xcodeのパーティクルエディタでビジュアルにパラメータを調整可能です。

// パーティクルエフェクトの表示
func showExplosion(at position: CGPoint) {
    guard let emitter = SKEmitterNode(fileNamed: "Explosion.sks") else { return }
    emitter.position = position
    emitter.zPosition = 10

    addChild(emitter)

    // 2秒後に自動的に削除
    let wait = SKAction.wait(forDuration: 2.0)
    let remove = SKAction.removeFromParent()
    emitter.run(SKAction.sequence([wait, remove]))
}

タッチ操作とジェスチャー認識

iOSゲームでは、タッチ操作が主要な入力手段です。SKSceneのタッチイベントメソッドをオーバーライドして実装します。

// タッチ操作の実装
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)

    // タッチ位置にあるノードを取得
    let touchedNodes = nodes(at: location)

    for node in touchedNodes {
        if node.name == "playButton" {
            startGame()
        } else if node.name == "player" {
            isPlayerDragging = true
        }
    }
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard isPlayerDragging, let touch = touches.first else { return }
    let location = touch.location(in: self)
    player.position = location
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    isPlayerDragging = false
}

スワイプ、ピンチ、ロングプレスなどの複雑なジェスチャーには、UIGestureRecognizerをSKViewに追加して対応します。

サウンド管理 — AVAudioEngineの活用

ゲームのサウンド管理は、BGM(ループ再生)とSE(効果音、単発再生)に分かれます。簡易的なSE再生にはSKAction.playSoundFileNamedを使えますが、BGMの制御やボリューム調整にはAVAudioEngineを使用します。

class SoundManager {
    static let shared = SoundManager()

    private var audioEngine = AVAudioEngine()
    private var bgmPlayer: AVAudioPlayerNode?
    private var bgmVolume: Float = 0.7

    // BGMの再生
    func playBGM(named filename: String) {
        guard let url = Bundle.main.url(
            forResource: filename, withExtension: "mp3"
        ) else { return }

        do {
            let file = try AVAudioFile(forReading: url)
            let player = AVAudioPlayerNode()

            audioEngine.attach(player)
            audioEngine.connect(player,
                to: audioEngine.mainMixerNode,
                format: file.processingFormat)

            try audioEngine.start()
            player.scheduleFile(file, at: nil) {
                // ループ再生
                self.playBGM(named: filename)
            }
            player.volume = bgmVolume
            player.play()
            bgmPlayer = player
        } catch {
            print("BGM再生エラー: \(error)")
        }
    }

    // BGMの停止
    func stopBGM() {
        bgmPlayer?.stop()
    }
}

Game Centerとの連携

Game Centerを使えば、ランキング(Leaderboard)と実績(Achievement)をAppleの標準UIで提供できます。

ランキングの送信

// スコアをGame Centerに送信
func submitScore(_ score: Int) {
    GKLeaderboard.submitScore(
        score,
        context: 0,
        player: GKLocalPlayer.local,
        leaderboardIDs: ["com.nn.crazballoon.highscore"]
    ) { error in
        if let error = error {
            print("スコア送信エラー: \(error)")
        }
    }
}

実績のアンロック

// 実績を解除
func unlockAchievement(id: String, percentComplete: Double = 100.0) {
    let achievement = GKAchievement(identifier: id)
    achievement.percentComplete = percentComplete
    achievement.showsCompletionBanner = true

    GKAchievement.report([achievement]) { error in
        if let error = error {
            print("実績送信エラー: \(error)")
        }
    }
}

パフォーマンス最適化 — 60fps維持のための実践テクニック

ゲームのパフォーマンスは、ユーザー体験に直結します。60fps(1フレームあたり約16.6ms)を安定して維持するための最適化テクニックを紹介します。

テクスチャアトラスの活用

個別の画像ファイルではなく、テクスチャアトラス(スプライトシート)を使用することで、GPU描画コールの回数を削減できます。Xcodeの.atlasフォルダに画像を入れるだけで自動的にアトラスが生成されます。

ノードの再利用(オブジェクトプール)

シューティングゲームの弾のように、大量に生成・破棄されるオブジェクトは、オブジェクトプールパターンで再利用します。removeFromParentとaddChildの頻繁な呼び出しは、メモリアロケーションのコストが高くなります。

シーン遷移時のメモリ管理

シーン遷移時にはremoveAllChildrenを呼び出し、不要なノードを確実に解放します。SKTextureのプリロードとキャッシュ管理も重要です。

// テクスチャのプリロード
SKTexture.preload([
    SKTexture(imageNamed: "enemy1"),
    SKTexture(imageNamed: "enemy2"),
    SKTexture(imageNamed: "background")
]) {
    // プリロード完了後にシーン遷移
    let gameScene = GameScene(size: self.size)
    self.view?.presentScene(gameScene,
        transition: SKTransition.fade(withDuration: 0.5))
}

Instrumentsによるプロファイリング

XcodeのInstrumentsを使って、CPU使用率、メモリ使用量、GPUフレームタイムを計測します。SKViewのshowsFPSshowsNodeCountshowsDrawCountプロパティを有効にすると、デバッグ中にリアルタイムでパフォーマンス情報を確認できます。

まとめ — Apple純正フレームワークで広がるゲーム開発の可能性

SpriteKitとGameplayKitの組み合わせは、iOS向け2Dゲーム開発において最も軽量かつ高効率な選択肢です。Unityと比較してアプリサイズが大幅に小さく(数MB〜数十MB)、起動も高速です。Apple純正フレームワークであるため、最新のiOS機能(Core Haptics、ARKit連携等)との統合もスムーズです。

カジュアルゲーム、パズルゲーム、ボードゲーム、教育ゲームなど、2Dの領域であればSpriteKitで十分に本格的な作品を開発できます。ゲーム開発に興味のある方は、Xcodeの「Game」テンプレートからプロジェクトを作成し、まずはSKSpriteNodeを1つ動かすところから始めてみてください。

Swift
SpriteKit
GameplayKit
iOSゲーム
AI

関連記事