Help us understand the problem. What is going on with this article?

[iOS]ゲーム開発 初心者の学び

自己紹介

iOSで開発経験が2年程度ありますが、ゲームは全く作ったことがありません。趣味でゲームを作ろうと思っています。前提知識を随時まとめてみます。書いたコードはこちらです Github

アーキテクチャ

  • Entity-Component Architecture

    • ゲームで使うオブジェクト(Entity)に、様々な再利用可能な振る舞いを定義した部品(Component)を持たせる形で作っていく。GameplayKitの中のGKComponentを継承したクラスにより、Componentを作成していく。
    • 自前でこうした仕組みを作ることもできるが、GameplayKitを利用していくことで楽にできる。
  • GKEntity

    • 通常は特にサブクラス化せず、addComponent(component:)メソッドでコンポーネントを追加していく。
    • update(deltatime:): Entity-Componentアーキテクチャ では周期的に行う動作を定義する必要がある。このメソッドを呼ぶと、そのエンティティが持っているコンポーネントのupdate(deltaTime:)メソッドも全て呼ばれる。deltatimeとは、前回アップデートがされてからの秒数を指す。
  • GKComponentSystem

    • ある種類のコンポネント全てに対しupdateを行いたい時に使用できる。例えば、ゲーム内に存在する全オブジェクトに物理演算を行いたい時など。
let graphicsComponentSystem = GKComponentSystem(componentClass: GraphicsComponent.self)
let physicsComponentSystem = GKComponentSystem(componentClass: PhysicsComponent.self)
for entity in entities {
    //entity内に存在するGraphicComponentを取得
    graphicsComponentSystem.addComponent(foundIn: entity)
   //entity内に存在するPhysicsComponentを取得
    physicsComponentSystem.addComponent(foundIn: entity)
}
//全GraphicComponentをupdate
graphicsComponentSystem.update(deltaTime: 0.033)
//全PhysicsCOmponentをupdate
physicsComponentSystem.update(deltaTime: 0.033)
  • ゲーム全体の時間経過の管理
    • どのオブジェクトで時間経過を管理するかを決める。ViewControllerサブクラスか、SKSceneGLKViewController、その他自分で作ったカスタムクラスなど。
    • そのオブジェクトの中に次のような変数を作る。
    • このような時間経過とは、言い換えればゲーム内で直前のフレームからどのくらい時間が経ったかという事を示す。できれば、1秒間に60フレーム(すなわち、経過時間が0.0166秒 = 16ミリ秒程度)になるのが良いが、処理が重い場合は達成できないこともある。
TimeKeeper.swift
public class TimeKeeper {
    public var lastFrameTime: Double = 0.0

    public func update(currentTime: Double) {

        // このメソッドが最後に呼ばれた時からどのくらい時間が経ったか計算
        let deltaTime = currentTime - lastFrameTime

        // 1秒で3単位移動する
        let movementSpeed = 3.0

        // 速度と時間を掛け算し、このフレームでどのくらい移動するか決める
        someMovingObject.move(distance: movementSpeed * deltaTime)

        // lastFrameTime に currentTime を代入し、次回このメソッドが呼ばれた際、
        // 経過時間を計算できるようにしておく
        lastFrameTime = currentTime
    }
}
  • アプリがアクティブ・非アクティブになるタイミングを知る
    • ゲームがポーズできるようにアプリが非アクティブになるタイミングが知りたい時や、逆にアクティブになったタイミングが知りたい場合がある。NotificationCenterを用いてそのタイミングを知ることができる。
    • ターン制のゲームの場合、リアルタイム性がないので、ポーズなどはあまり気にしなくて良くなる。
    • アプリがバックグラウンドになった時には、あまりメモリを消費するような動きはせず最小限の動きのみにすること。消費が激しい場合、Appleからリジェクトされる場合がある。
GameViewController.swift
private extension GameViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        setObserver()
     }

    func setObserver() {

        let center = NotificationCenter.default

        let didBecomeActive = #selector(
            GameViewController.applicationDidBecomeActive(notification:)
        )

        let willEnterForeground = #selector(
            GameViewController.applicationWillEnterForeground(notification:)
        )

        let willResignActive = #selector(
            GameViewController.applicationWillResignActive(notification:)
        )

        let didEnterBackground = #selector(
            GameViewController.applciationDidEnterBackground(notification:)
        )

        center.addObserver(self,
                           selector: didBecomeActive,
                           name: UIApplication.didBecomeActiveNotification,
                           object: nil)

        center.addObserver(self,
                           selector: willEnterForeground,
                           name: UIApplication.willEnterForegroundNotification,
                           object: nil)

        center.addObserver(self,
                           selector: willResignActive,
                           name: UIApplication.willResignActiveNotification,
                           object: nil)

        center.addObserver(self,
                           selector: didEnterBackground,
                           name: UIApplication.didEnterBackgroundNotification,
                           object: nil)
    }

    @objc
    func applicationDidBecomeActive(notification: Notification) {
        print("アプリがアクティブになった")
    }

    @objc
    func applciationDidEnterBackground(notification: Notification) {
        print("アプリがバックグラウンドに入った (テクスチャのアンロードなどを行う)")
    }

    @objc
    func applicationWillEnterForeground(notification: Notification) {
        print("アプリがアクティブになる (アンロードしたテクスチャの再読み込みなどを行う)")
    }

    @objc
    func applicationWillResignActive(notification: Notification) {
        print("アプリが非アクティブになる (ゲームのポーズなどを行う)")
    }
}

なお上に列挙した関数に限っていうと

  1. アプリ起動時にはapplicationDidBecomeActive
  2. ホームボタンなどを押してアプリがバックグラウンドに行った時にはapplicationWillResignActiveapplciationDidEnterBackground
  3. アプリアイコンを押すなどしてアプリが前面に戻った時にはapplicationWillEnterForegroundapplicationDidBecomeActive

が呼ばれる。

このようなアプリのライフサイクルの詳細についてはこちらこちら(iOS13以降)など参照。

  • Timerクラスによるゲームの更新
    • 特定の秒数が経過した後(あるいは、経過する毎)に処理をしたいということがある。その場合、Timerクラスを使うと良い。
GameViewController.swift
    var timer: Timer? = nil 

    override func viewDidLoad() {
        super.viewDidLoad()

        // タイマーの設置
        timer = Timer.scheduledTimer(
            timeInterval: 0.5, // 0.5秒後に発火
            target: self,
            selector: #selector(updateWithTimer(timer:)),
            userInfo: nil,
            repeats: true // くり返しON
        )
    }

    @objc
    func updateWithTimer(timer: Timer) {
        //タイマー発火時に呼ばれるメソッド。ゲームの更新などを行う。

        print("Timer Went Off!")
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        // タイマーを止める
        timer?.invalidate()
        timer = nil
    }

なお、DispatchQueueを使い以下のように書くこともできる。

func placeBomb() { print("bomb placed.") }
func explodeBomb() { print("bomb!") } //"bomb placed"の10秒後に"bomb!"と表示したい。

let deadline = DispatchTime.now() + 10

placeBomb()

DispatchQueue.main.asyncAfter(deadline: deadline, execute: {
    // Time's up
    explodeBomb()
})
  • ゲームのポーズ

    • ゲームをポーズした際、一部のオブジェクトは動作を続け、一部のオブジェクトは停止するということが多い。例えば、ネットワークを介して通信する部分や、ユーザインターフェースを担当する部分はポーズで止めないのが普通だと考えられる。
    • その場合、Bool値でゲームのポーズを管理するとともに、ゲームオブジェクトをポーズし得るものとし得ないものの2タイプに分ければ良い。
 for gameObject in gameObjects {

    if paused == false || gameObject.canPause == false {
        gameObject.update(deltaTime: deltaTime)
    }
 }
  • ゲーム開始からの経過時間
    • ゲーム開始時刻、現在時刻の二つをDateクラスで持つ。その二つの間の経過時間を、timeIntervalSinceメソッドで計算すればよい。
GameViewController.swift
private extension GameViewController {

    /// ゲームの開始時刻
    var gameStartDate: Date?

    override func viewDidLoad() {
        super.viewDidLoad()

        gameStartDate = Date()
    }

    /// ゲーム開始からの経過時間を取得する
    func getCurrentElapsedTime() {
        let now = Date()

        guard  let date = gameStartDate else { return }

        let timeSinceGameStart = now.timeIntervalSince(date)

        NSLog("The Game Started \(timeSinceGameStart) seconds ago.")

        //経過時間(秒数)を、何時間・何分・何秒という形に整形する
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .positional

        let formattedString = formatter.string(from: timeSinceGameStart)
        print("Time elapsed: \(formattedString ?? "")")
    }
}
  • 依存性(ある処理が終わってから別の処理をやる)

いろいろやり方はあると思うが、OperationクラスのaddDependency(_:)メソッドが利用できる。

let firstOperation = BlockOperation { () -> Void in
    print("First Operation")
}

let secondOperation = BlockOperation { () -> Void in
    print("Second Operation")
}

let thirdOperation = BlockOperation { () -> Void in
    print("Third Operation")
}

//2番目の`Operation`は、1番目と3番目が終わった後のみやるようにする。
secondOperation.addDependency(firstOperation)
secondOperation.addDependency(thirdOperation)

let operations = [firstOperation, secondOperation, thirdOperation]

//バックグラウンド にて実行開始
let backgroundQueue = OperationQueue()
backgroundQueue.qualityOfService = .background
backgroundQueue.addOperations(operations, waitUntilFinished: true)

こうすると、下記のように依存性を設定した2番目のOperationが最後に実行される。

Third Operation
First Operation
Second Operation
  • 画像などのアセットをゲーム実行中に読み込みたい
    • 先のOperationクラスで、バックグラウンドで実行するよう指定すれば良い。
    • 画像の全読み込みが終わってからやりたい処理(画面を更新するなど)があれば、依存性を設定すればよい。
let imagesToLoad = ["image1.jpg", "image2.jpg", "image3.jpg"]

let imageLoadingQueue = OperationQueue()

// バックグランドで実行するよう設定する
imageLoadingQueue.qualityOfService = .background

// 複数の画像の同時読み込みを許可する(10個まで)
imageLoadingQueue.maxConcurrentOperationCount = 10

// 全画像読み込み完了後にやりたい処理
let loadingComplete = BlockOperation { () -> Void in
    print("loading complete!")
}

var loadingOperations: [Operation] = []

for imageName in imagesToLoad {
    let loadOperation = BlockOperation { () -> Void in
        print("Loading \(imageName)")
    }

    loadingOperations.append(loadOperation)

    // 最後にやりたい処理に対して、依存性を設定していく。
    loadingComplete.addDependency(loadOperation)
}

imageLoadingQueue.addOperations(loadingOperations, waitUntilFinished: false)
imageLoadingQueue.addOperation(loadingComplete)

表示関係

SpriteKit

2Dグラフィックでのアニメーションを行うための公式ライブラリ。

SKView

UIViewのサブクラス。

  • presentScene(_:):シーン(SKSceneクラスオブジェクト)を呼び出すメソッド。シーンがビュー上に呼び出されると、シミュレーションしているだけの状態から実際に画面上にそれを表示させる。
  • isPaused: このプロパティをtrueにすると、SKView上のシーンが一時停止する。

参考

  • "iOS Swift Game Development Cookbook: Simple Solutions for Game Development Problems" 3rd Edition, Jonathon Manning & Paris Buttfield-Addison, O'Reilly Media, 2018
  • GameplayKit Programming Guide
iridge
O2OやFinTechソリューションの企画・開発・運用をしています。
https://iridge.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away