自己紹介
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
サブクラスか、SKScene
、GLKViewController
、その他自分で作ったカスタムクラスなど。 - そのオブジェクトの中に次のような変数を作る。
- このような時間経過とは、言い換えればゲーム内で直前のフレームからどのくらい時間が経ったかという事を示す。できれば、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("アプリが非アクティブになる (ゲームのポーズなどを行う)")
}
}
なお上に列挙した関数に限っていうと
- アプリ起動時には
applicationDidBecomeActive
- ホームボタンなどを押してアプリがバックグラウンドに行った時には
applicationWillResignActive
とapplciationDidEnterBackground
- アプリアイコンを押すなどしてアプリが前面に戻った時には
applicationWillEnterForeground
とapplicationDidBecomeActive
が呼ばれる。
このようなアプリのライフサイクルの詳細についてはこちらやこちら(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