Swiftでシューティングゲームを作ろう

# はじめに

こんにちは、東京大学 工学部 電気電子工学科というところに通っている学部3年生です。
インターンなどの機会を利用させていただきながらプログラミングの学習に励んでいます。
大学でのプログラミングに関する授業はアルゴリズムや計算量、CPUなどの低レイヤーな分野を中心的に扱うことが多いです。
そのため、Webサービスを利用しているユーザに近い部分を扱うことが少なかったりします。
この記事がそのあたりに興味を持つきっかけになればとても嬉しいです。

# この記事の目的

次の理由から、SwiftによるiOSゲーム開発を行うチュートリアルを作りました。
よりユーザに近い部分を開発することに興味を持っていただければ嬉しいです。

- 自分が持っているiPhoneで作ったものが動くと楽しい
- Macを持っていればXcodeをダウンロードするだけで開発構築が完了する
- メモアプリとかを作るよりはゲームを作ったほうが楽しいんじゃね？

# この記事の内容

- Swiftというプログラミング言語を使ってiPhoneのシューティングゲームを作ります
- フレームワークにはSpriteKitというものを使います
- iOSなのでMacを持っている人が対象です
- 当方、Swiftでのサービス開発の経験はありますが、ゲーム開発の経験はないので何かおかしいところがあるかもしれません
- Xcode 9.2(Swift 4.0.3)を利用するので[ここ](https://developer.apple.com/download/more/)から「Xcode 9.2」をダウンロードしておいてください
- Swiftの文法(変数定義やif文、for文など)について軽く知っておくと理解がスムーズになると思います(軽く検索すると出てくるのでこの記事では割愛します)
- Xcodeのダウンロードには時間がかかるので、その間にSwiftの文法をちょっと見てみるのはいかがでしょうか！

# シューティングゲームを作ろう

## 完成品

本チュートリアルで作成するゲームの完成品コードは[ここ](https://github.com/snowman-mh/shooting-game)に置いています。
リンク先に今回作るゲームのGIF画像が置いてあるので、どんなものを作るか知りたい人は見てみてください。

## プロジェクトの作成

ゲームアプリを開発するためにプロジェクトを作成します。

1. Xcodeを開く → `Create a new Xcode project`を選択 → `Game`を選択して`Next`
2. Product Nameには「shooting-game」と入力
3. Teamは「None」のままでOK(なんでもOK)
4. Organization Nameは自分の名前とかにする(なんでもOK)
5. Organization Identifierには「com.自分の名前」とかにする(なんでもOK)
6. Languageは「Swift」に
7. Game Technologyは「SpriteKit」に
8. チェックボックスは全部外しておいてOK

![スクリーンショット 2017-12-16 23.19.51.png](https://qiita-image-store.s3.amazonaws.com/0/149003/5e1c960f-d65a-4cd1-6714-c0d769956722.png)

## ビルドできるか確認

プロジェクトが作成できたら次のような画面が表示されているはずです。
まずは何もせずに左上の「三角ボタン」を押してください。
デフォルトで生成されているコードがビルドされて「Hello, World」と表示されたiPhoneシミュレータが立ち上がるはずです。
試しに画面をタップしたりスワイプしたりしてみましょう。なんか四角のリングみたいなのがフワフワ出てくるゲーム(？)で遊べます。
ちなみに、三角ボタンの横の「iPhone 8 Plus」を変更すると好きなiPhoneでビルドすることができます。

![スクリーンショット 2017-12-16 23.38.25.png](https://qiita-image-store.s3.amazonaws.com/0/149003/485c9b50-30ab-de6c-23e7-600a7a470087.png)

## コーディングの準備

コーディングを始める前に、デフォルトで記載されているコードを削除して、ゲームに使用する素材画像をインポートします。

1. `GameScene.sks`というファイルを開いて「Hello, World」というラベルを選択してDelete
2. `GameScene.swift`を開いていらないコードを消す
    - [ここ](https://github.com/snowman-mh/shooting-game/pull/1/files)を参考にコードを削除してください
    - この記事ではこんな感じでGitHubの「Pull Request」を参考に実装を進めていきます
    - GitHubもPull Requestもよく分からなくても大丈夫です。**背景が赤い部分は消すコード、緑の部分は追加するコード**ということを覚えておけば問題ありません。**ファイル名も書いてある**のでどのファイルのコードを触ればいいかも分かります。
3. [ここ](https://www.dropbox.com/sh/6avjmrl84uaplmz/AAAsRTExJrhASO4oylIPMVH1a?dl=0)の画像をすべてダウンロードする(すべてフリー素材です)
4. `Assets.xcassets`を開いて、先程ダウンロードした画像をドラッグ&ドロップする

## メニュー画面とゲーム画面

アプリ起動時に表示されるメニュー画面からゲーム画面に遷移するところまでを作ります。
ちょっとこのあたりはXcodeの使い方を知らないと分かりにくいところなので、画像多めで解説していきます。

1. `Main.storyboard`を開くと次のような画面が表示される
![スクリーンショット 2017-12-17 0.15.53.png](https://qiita-image-store.s3.amazonaws.com/0/149003/738c1ddb-b693-8b9b-b387-6cac2771b026.png)
2. 黒い画面の横あたりにに右下の「View Controller」をドラッグ&ドロップする
![スクリーンショット 2017-12-17 0.16.24.png](https://qiita-image-store.s3.amazonaws.com/0/149003/02a60a91-3ffd-e645-b79e-4c7ff3992a60.png)
3. 黒い画面の左側に付いている矢印マークを先程追加した白い画面に持っていく
![スクリーンショット 2017-12-17 0.17.13.png](https://qiita-image-store.s3.amazonaws.com/0/149003/f29c83b7-0c52-da85-eb3d-6dc440e0e69e.png)
4. 右下の検索ボックスに「button」と入力して出てくる「Button」を白い画面の中央に持ってきて、そのボタンをダブルクリックして「Start」というテキストに変更する
![スクリーンショット 2017-12-17 0.17.55.png](https://qiita-image-store.s3.amazonaws.com/0/149003/599e5ecf-dcbb-d14b-b7dc-1494f154d138.png)
5. 先程追加したボタンを**「Ctrlキー」を押しながら**右側の黒い画面にドラッグ&ドロップする。2つの画面の間に矢印が表示されれば成功です。
 ![スクリーンショット 2017-12-17 0.28.52.png](https://qiita-image-store.s3.amazonaws.com/0/149003/2a06283a-ccce-edf1-41c8-4da77744eb4d.png)
6. 左上の三角ボタンを押してRunしてみましょう。Startボタンをタップすると黒い画面に遷移するようになっているはずです。(ちなみに、`Cmd + R`でRunすることもできます)

## 地球の追加

ゲーム画面の下の方にずっと表示される地球を追加します。
[ここ](https://github.com/snowman-mh/shooting-game/pull/2/files)を参考に`GameScene`クラスの`didMove(to:)`メソッドにコードを追加しましょう。

- `didMove(to:)`メソッドはゲーム画面に遷移したときに自動的に実行される
- 地球を表示する処理はこのメソッド内に書けば良い
- `SKSpriteNode`はゲーム画面に表示するオブジェクトのクラスです
- `CGPoint`は画面内の座標(原点は画面中央)を表すクラスで、そのインスタンスを`earth`の`position`プロパティに代入します
- `earth`のスケールや位置を調整して、最後に`addChild(_:)`を呼んで画面に地球を表示します

## 宇宙船の追加

ゲーム画面に「端末の傾きで横方向に移動し、タップでミサイルが発射される宇宙船」を追加します。

1. [ここ](https://github.com/snowman-mh/shooting-game/pull/3/commits/966eec52a74fa01b446b9d52c19b1d650380ca38)を参考に宇宙船を画面に追加する
2. [ここ](https://github.com/snowman-mh/shooting-game/pull/3/commits/d131bb487d18c0b7643d636af8e6e4b99165548a)を参考に、端末の傾きによって宇宙船が動くようにする
    - `CoreMotion`というフレームワークをimportし、`CMMotionManager`というクラスを利用すると端末の傾きを取得できます
    - ここからは端末の傾きを取得する必要があるため、iPhone Simulatorではなく実機でビルドする必要があります。[ここ](https://i-app-tec.com/ios/device-test.html)などを参考にお持ちのiPhoneでビルドしてみてください。iPhoneを持っていないなら、少し難しいかもしれませんが、画面タップで宇宙船が左右に移動するように変更してみましょう。
3. [ここ](https://github.com/snowman-mh/shooting-game/pull/3/commits/039e67c77edd0b0877262009c759e6c9f097de0c)を参考に、画面をタップすると宇宙船からミサイルが発射されるようにする
    - `SKAction`クラスの機能を使って`SKSpriteNode`を動かすことができます
    - `SKSpriteNode`クラスのメソッド`run(_:)`を利用して実際に指定した`SKAction`を実行させることができます

## 小惑星を追加

ゲーム画面に「1秒ごとにランダムな位置に出現して迫ってくる小惑星」を追加します。
[ここ](https://github.com/snowman-mh/shooting-game/pull/4/files)を参考に一定時間ごとに小惑星を出現させましょう。

- `Timer`クラスの`scheduledTimer(withTimeInterval:repeats:block:)`メソッドを使うと一定時間後に`block`に渡した関数を実行してくれます
- `repeats`を`true`にすると一定時間ごとに同じ処理を行ってくれます
- `arc4random_uniform(_:)`という0から引数の間のランダム値を生成してくれる関数を利用して出現する小惑星を3種類から選択しています

## 衝突処理

「ミサイルと小惑星」、「小惑星と宇宙船」、「小惑星と地球」の衝突処理を実装します。

1. `File` → `New` → `File` → `SpriteKit Particle File`を選択して`Next`
2. Particle temprateが`Fire`になっていることを確認して`Next`
3. `Explosion.sks`という名前にして保存
4. [ここ](https://github.com/snowman-mh/shooting-game/pull/6/commits/122bf262113415d0bfa6e2840fc270449fbb576c)を参考に衝突処理を行うための準備を行う
    - `SKSpriteNode`には`physicsBody`というプロパティがある
    - そこに`SKPhysicsBody`のインスタンスを代入すると`SKSpriteNode`が当たり判定を持つようになる
    - `SKPhysicsBody`には`categoryBitMask`、`contactTestBitMask`、`collisionBitMask`という3つの衝突に関するプロパティがある
    - このあたりは説明すると少し長くなるので、今回は`categoryBitMask`にはそのノードのIDに相当するもの(2進数)、`contactTestBitMask`にはそのノードが衝突する相手のID(2進数)を代入すると良いということだけ覚えておけば十分です。
5. [ここ](https://github.com/snowman-mh/shooting-game/pull/6/commits/147f43be945c906ea233d262c7289915a453aafb)を参考に衝突時に小惑星が爆発するようにする
    - 衝突すると`didBegin(_:)`が自動的に呼ばれる
    - `didBegin(_:)`に渡ってくる`contact`は`bodyA`と`bodyB`を持っていて、それぞれ衝突した2つのノードの`SKPhysicsBody`になっている
    - しかし、例えば小惑星と宇宙船が衝突したとき、`bodyA`と`bodyB`のどちらが小惑星でどちらが宇宙船かどうかは分からないため、`categoryBitMask`を利用して自分で判別してあげる必要がある
    - 爆発のエフェクトには`SKEmitterNode`を使用する。特に`SKEmitterNode`に特有の処理は行わないのであまり気にする必要はない

## スコアとライフ

「小惑星をミサイルで破壊したときにスコアが増える処理」と「宇宙船と地球が小惑星に衝突したときにライフが減る処理」を実装します。

1. [ここ](https://github.com/snowman-mh/shooting-game/pull/7/commits/ea299f4869605554ce555f4dfb5ab271879fdc0e)を参考にライフを導入する
    - `didMove(to:)`の中で`SKSpriteNode`の配列である`hearts`を初期化する
    - 小惑星が宇宙船か地球と衝突したときに`hearts`を1つ減らす処理を行う
2. [ここ](https://github.com/snowman-mh/shooting-game/pull/7/commits/7398244c36bf498e115b7bcb7101fabd8fc3e9fd)を参考にスコアを導入する
    - スコアの表示にはテキストを表示するためのノードである`SKLabelNode`を利用する
    - スコアを管理する`score`プロパティに`didSet`を持たせることで、`score`に代入する度に`scoreLabel`の`text`プロパティを変更する処理を行うことができる
    - ミサイルが小惑星と衝突したときにスコアを加算するのを忘れずに

## ゲームオーバー

ライフが0になるとゲームが終了するようにします。

1. [ここ](https://github.com/snowman-mh/shooting-game/pull/8/commits/abf0739cd3b96ecd01802035bf2c91cf5864b89f)を参考に、ライフが0になったときにゲームを一時停止するようにする
    - `isPaused`プロパティに`true`を代入するとゲームを一時停止できる
    - そのときに`timer`プロパティの`invalidate()`を呼んでおく
2. [ここ](https://github.com/snowman-mh/shooting-game/pull/8/commits/dc8763791c79e12fd5d2f2130119fc0d6b5b764d)を参考に一時停止してから1秒後にメニュー画面に戻るようにする
    - `GameViewController`クラスの`dismiss(animated:completion:)`メソッドを呼ぶと最初のメニュー画面に戻ることができるが、`GameScene`から`GameViewController`のメソッドを直接呼ぶことができない
    - よって`GameViewController`から`GameScene`のインスタンスを作るときに`self`を`GameScene`に渡すようにして、`gameOver()`メソッドの中で渡ってきた`gameVC`(型は`GameViewController`)の`dismiss(animated:completion:)`を呼ぶ

## ベストスコアの保存と表示

[ここ](https://github.com/snowman-mh/shooting-game/pull/9/commits/71e5c812dfe3172036e899d599d3191acf59ca2e)を参考にゲーム画面にベストスコアを表示するようにしましょう。

- 値を保存するのには様々な方法がありますが、今回は一番簡単な`UserDefaults`を使う方法で行う
- `AppDelegate.swift`の`application(_:didFinishLaunchWithOptions:)`に書かれた処理は初めてアプリが起動されたときにのみ実行される
- `UserDefaults`では辞書型で値を保持していて、キーに対する値を`AppDelegate`で登録する
- ベストスコアが更新されていたら`UserDefaults`に保持している値を更新する

## 難易度を上げていく

このままでは無限にプレイできてしまうので、小惑星が迫ってくるスピードを時間が経つにつれて早くなるように変更します。
[ここ](https://github.com/snowman-mh/shooting-game/pull/10/commits/6d994ba005bcdd00f82505d0b172c9ac78414640)を参考に、時間とともに小惑星のスピードを上げていくようにします。
ここは今までの知識を使えばできるので答えを見ずに実装してみてもいいかもしれません。

## 完成です！

これでとりあえずは完成です！
お疲れ様でした！
ちなみにこのゲームの僕のベストスコアは390点です。
皆さんはどうでしたか？

## アレンジする

しかし、このアプリにはまだまだ改善点があります。
例えば以下のようなものがあるのでできそうなものから改善に挑戦してみましょう。
もちろん自分でオリジナルの機能を考えて実装してみるのも素晴らしいです！

- 一時停止機能を追加する
- 最初から始める機能を追加する
- 途中でやめる機能を追加する
- 難易度の上げ方(今は単純に小惑星のスピードを上げているだけ)を工夫する
- 左右だけでなく上下にも動けるようにする
- ときどきアイテムが落ちてきてミサイルが強くなったりする
- `didMove(to:)`にいっぱいコードを書いていて可読性が悪いのでコードを整理(リファクタリング)する

# おわりに

最後まで読んでいただきありがとうございました！

今回はSwiftでiOSゲームを開発しましたが、ゲームに限らずユーザに近い部分の開発に興味を持っていただければ嬉しいです。
もちろんSwiftを使ったiOSアプリ開発もオススメです。
まずは今回作ったゲームを改良してもいいですし、オリジナルのアプリを作ってみてもいいですね。
ゲーム以外のアプリを作るなら、ちょっと内容が古い記事になってしまっていますが、[僕が以前書いた記事](https://qiita.com/snowman_mh/items/0d27f4abf7b14c72107f)が参考になるかもしれません。