Unity 5で 2Dシューティングのチュートリアルを行う際の注意点(第12回)
Unity公式チュートリアル 2Dシューティング
第12回 Waveを5個にする、スコアの実装
上記チュートリアルで、私がつまずいたところ、初心者がつまずきそうなところをピックアップして解説します。
12.1 Waveの作成
Waveをさらに4つ増やします。第07回で作成したWaveプレハブを複製したものを改良していきます。Waveのエネミーの配置は同じにしなくても問題ありません。例として4つのWaveを紹介します。
まず、Wave.unitypackageをダウンロードして、インポートする方法を説明します。
Wave.unitypackageをダウンロードします。
Unity画面を起動し、プロジェクトを開いた状態にします。
エクスプローラの画面で、ダウンロードしたWave.unitypackageをダブルクリックします。
すると、Unity画面に下図のようなダイアログが出るので、Importをクリックします。
これで、Projectタブの Prefabs配下に Wave2 - 5 がインポートされます。
これだけだとつまらないので、自分で一つ作ってみましょう。
まずは、Waveプレハブを複製します。
Projectタブの Prefabs 配下にある Waveをクリックして選択後、メニューから Edit -> Duplicate を選択します。
すると、Waveの複製が Wave1として現われます。
この Wave1をいじっていきましょう。
まず、Wave1を編集するために、Sceneの中に一旦置きましょう。
メニューの Window -> Scene を選択し、Sceneウィンドウを表示させます。
Projectタブの Prefabs 配下にある Wave1 を Sceneへドラッグ&ドロップします。
位置を合わせやすいように親オブジェクトの位置を画面の中心としましょう。
Hierarchyタブの Wave1 を選択し、Inspectorで Position を x = 0, y = 0, z = 0 に修正します。
敵3体が画面中央に移動すると思います。
敵を大きくしたいので、Scaleをいじります。
Hierarchyタブの Wave1 を選択し、Inspectorで Scale を x = 3, y = 3, z = 1 に修正します。
敵の縦位置を修正します。
Hierarchyタブの Wave1 の下にぶら下がっている Enemyを選択し、Inspectorでそれぞれ以下のように位置を修正してください。
- 1つ目の Enemy:Positon x = 0, y = 0.6, z = 0
- 2つ目の Enemy:Positon x = -1, y = 0, z = 0
- 3つ目の Enemy:Positon x = 1, y = 0, z = 0
全ての Enemyが弾を撃つように修正します。
また、弾を撃つ間隔も調整します。
Hierarchyタブの Wave1 の下にぶら下がっている Enemyを選択し、Inspectorでそれぞれ以下のように情報を修正してください。
- すべての Enemy:Spaceship(Script)の CanShot にチェックを入れる
- 1つ目の Enemy:Spaceship(Script)の Shot Delay = 0.6
- 2つ目の Enemy:Spaceship(Script)の Shot Delay = 0.8
- 3つ目の Enemy:Spaceship(Script)の Shot Delay = 0.8
Enemyの移動速度も変えてみます。
中央のやつだけすこし速くします。
- 1つ目の Enemy:Spaceship(Script)の Speed = 0.8
ザコも2体、増やしてみましょう。
Hierarchyタブの Wave1 の下にぶら下がっている 一つ目の Enemyを右クリックし、Duplicateを選択して複製します。
これをもう一度繰り返し、ザコを2つ作ります。
Enemy(1)とEnemy(2)ができたはずです。
Hierarchyタブの Enemy(1)を選択し、Inspectorで以下のように情報を設定してください。
- Position: x = -0.6, y = 0.6, z = 0
- Rotation: x = 0, y = 0, z = 10
- Scale : x = 0.6, y = 0.6, z = 1
- Spaceship(Script): Speed = 0.6
- Spaceship(Script): Shot Delay = 0.5
Hierarchyタブの Enemy(2)を選択し、Inspectorで以下のように情報を設定してください。
- Position: x = 0.6, y = 0.6, z = 0
- Rotation: x = 0, y = 0, z = -10
- Scale : x = 0.6, y = 0.6, z = 1
- Spaceship(Script): Speed = 0.6
- Spaceship(Script): Shot Delay = 0.5
最後に大きい Enemyの HP を増やしましょう。
- すべての大きい Enemy:Enemy(Script)の HP = 30
下図のようになると思います。
もちろん、上記の通りにしなくても構いません。
好きなように配置し、パラメータをいじってみてください。
最後に、編集した内容をプレハブに反映させて、オブジェクトを削除しましょう。
Hierarchyタブの Wave 1を選択し、Inspectorの Prefab の横にある「Apply」をクリックします。
これで編集した内容が、Projectタブの Prefabs配下にある Wave1 に反映されます。
その後、Hierarchyタブの Wave 1を選択し、Deleteキーでオブジェクトを削除します。
作成できたらEmitterゲームオブジェクトをクリックし、インスペクターのWavesへドラッグ&ドロップしてWaveを追加します。
では、できあがった Waveを設定していきましょう。
Hierarchyタブの Emitterを選択し、Inpectorの Emitter(Script)にある Wavesの Size を 6にします。
すると、Element 0〜5 ができるので、その枠に対して Projectタブの Prefabs配下にある Wave, Wave2, Wave3, Wave4, Wave5, Wave1 をそれぞれドラッグ&ドロップします。
自分で作った Wave1 は、ラスボスとして最後に設定しました。
ゲームを再生してみましょう。Wave → Wave 2 → Wave 3 → Wave 4 → Wave 5 → Wave ... と延々とループしているはずです。
少し難しいと感じたら自分で難易度を調整してみましょう。エネミーのHPや向きや移動スピード、ShotPositionゲームオブジェクトの増減で調整可能です。
ゲームを再生してみましょう。
Waveが次々に流れてきたら成功です。
もし、Wave2以降が流れてこなかったら、インポートした Wave配下にぶら下がっている Enemyの中のスクリプト等のリンクが切れています。
この場合、スクリプトとオブジェクトのリンクを張り直す必要があります。
具体的には以下のようにします。
以下は、Wave2〜5の ダウンロードしてインポートしたデータ全てに対して行います。
- Wave2 配下のEnemyを全て選択する(ぶら下がっているもののうち、一番上を選択後、一番下をShiftを押しながら選択する)
- Inscpectorで、Animatorのところにある Controller 枠のリンクが切れているので(Missing Runtime Animator Controllerと書いてある)、Projectタブの Animation/Enemy 配下にある Enemy をドラッグ&ドロップする。
- Inscpectorに(Script)というものが2つあるので、上の方の枠(Missing (MonoScript)と書いてある空欄)に Projectタブの Scripts配下にある Spaceshipをドラッグ&ドロップする
- 下の方の枠に Projectタブの Scripts配下にある Enemy をドラッグ&ドロップする
- Spaceship(Script)の中の Bullet枠に、Projectタブの Prefabs配下にあるEnemy Bulletをドラッグ&ドロップ
- Spaceship(Script)の中の Explosion枠に、Projectタブの Prefabs配下にあるEnemy Explosionをドラッグ&ドロップ
- Spaceship(Script)と、Enemy(Script)にある各パラメータ(Speed,Shot Delay,Can Shot, HP)を以下のように設定し直す
上記を Wave3,4,5にも行います。
パラメータは、面倒だったら Speedだけ1にしておけば、とりあえず流れてくるのを確認できることでしょう。
流れてこなければ、どこかでエラーが起きています。上記の設定を確認してください。
ゲームが難しいと感じたら、パラメータをいじってみましょう。
配置を変更したい時は、一度プレハブからシーンへドロップし、Scene上(Herarchy配下)で可視化してから行うのが便利です。ルートのWaveの位置を(0,0,0)にしてから位置合わせするとやりやすいと思います。ただし、このときは最後にプレハブに適用することを忘れないようにしましょう。
単にHPやSpeedを変えるだけなら、プレハブ(Project/Prefabs配下)のものを直接いじっても構いません。
12.2 スコアの実装
最後の要素として「スコア」を実装します。
今回はゲーム中に獲得した現在のスコアと、今までのハイスコアを実装します。
スコアの表示
スコアを画面に表示するためにGUITextを配置します。
空のゲームオブジェクトを作成し、名前をScore GUIとしましょう。TransformのPositionは X 0 Y 0 Z 0にしてください。
メニューから、GameObject -> Create Empty で空のゲームオブジェクトを作ります。
Hierarchyタブに、GameObject ができるので、これを選択し、Inspector内で「Score GUI」にリネームします(Score GUIと入力後、TABキーを押すとリネームできます)。
HierarchyウィンドウのCreateボタンをクリックしてGUI Textを選択してください。GUI Textコンポーネントが付いたゲームオブジェクトが作成されます。
Hierarchyの Create からは GUI Textは選択できません。
GUI Text はコンポーネントになっているので、まず空のゲームオブジェクトを追加し、そこに GUI Text を追加するという形で「GUI Textコンポーネントが付いたゲームオブジェクト」を作成します。
まず、メニューから、GameObject -> Create Empty で空のゲームオブジェクトを作ります。
続いて、Hierarchyタブ内に追加された GameObjectを選択し、Inspector上の「Add Component」ボタンを押して、Rendering -> GUI Text を選択して追加します。
同じものをもう1つ作成し、Score GUIの子要素とします。そして、名前をScoreとHighScoreにしましょう。
もう一度同じ手順で「GUI Textコンポーネントが付いたゲームオブジェクト」を作成します。
作成した2つの「GUI Textコンポーネントが付いたゲームオブジェクト」を、Hierarchyタブの「Score GUI」にドラッグ&ドロップして、Score GUIの子要素にします(Score GUI の下に、Score と HighScore がぶら下がります)。
その後、それぞれを「Score」「HighScore」にリネームします(オブジェクトを選択し、Inspectorでリネームします)。
ScoreとHighScoreの設定を行います。
TransformのPosition、GUITextのText、Anchor、Font、Font Sizeを図12.1と図12.2を見ながら設定しましょう。
まず、Hierarchyタブの Score GUI を選択し、Inspectorで Positionを(0,0,0)にします。
ここは、チュートリアルの画像の通り、設定してください。
Hierarchyタブの Score と HighScore をそれぞれ選択後、Inspector内で値を編集します。
なお、GUI Text の Font のところには、Projectタブの Font 配下にある「SAM_5C_27TRG_」をドラッグ&ドロップして設定します。
正しく設定を行うと図12.3のようにゲームビューの右上と右下に0と表示が行われます。
うまくできましたか?
表示されない時は、Position、Text、Anchor、Font、Font Sizeなどの設定を見直してみましょう。
スクリプトの作成
Score.csをScriptsフォルダ配下に作成してください。
Projectタブの Scriptを右クリックし、Create -> C# Script でスクリプトを追加後、Score にリネームします。
今回、スコアの保存はPlayerPrefsクラスを使用して保存します。
PlayerPrefs というのは、Unityが提供しているデータ管理クラスのことです。
このクラスを使うと、ハイスコアなどのゲームを終了後も残しておきたいデータなどを簡単に保存することができます。
Projectタブの Script配下にある Score をダブルクリックし、MonoDevelopで開いて Score.csを編集します。
Score.cs のソースは、チュートリアルの内容でまるごと置き換えて問題ありません。
置換後、Ctrl+Sで保存し、Unity画面に戻りましょう。
作成したScore.csをScore GUIゲームオブジェクトにドラッグ&ドロップしてください。
Projectタブの Script配下にある Score を、Hierarchyタブの Score GUI へドラッグ&ドロップします。
インスペクターのScore GUITextとHigh Score GUITextにScoreゲームオブジェクトとHighScoreゲームオブジェクトをそれぞれドラッグ&ドロップします。
Hierarchyタブの Score GUI を選択します。
Inspectorに表示される Score GUI Text欄に Hierarchyタブの Scoreをドラッグ&ドロップします。
同様に、Inspector内の High Score GUI Text欄に Hierarchyタブの HighScore をドラッグ&ドロップします。
エネミーにポイントを持たせる
エネミーを倒したらポイントを追加していきます。そのためにEnemy.csにポイントを保持する変数を加えましょう。
そして、OnTriggerEnter2Dメソッド内で爆発する時にスコアにポイントを追加する処理を追加します。
Projectタブの Prefabs配下にある Enemy をダブルクリックし、MonoDevelopで開いて Enemy.csを編集します。
Enemy.cs のソースは、チュートリアルの内容でまるごと置き換えて問題ありません。
置換後、Ctrl+Sで保存し、Unity画面に戻りましょう。
ダイアログが出るので、「Go Ahead!」をクリックしてソースを自動修正させましょう。
変更点は以下の通りです。
- スコアのポイント用変数 point を追加
- ヒットポイントが0以下の場合の処理に、スコアコンポーネントを取得してポイントを追加する処理を追加
それぞれのWaveプレハブ内にあるEnemyでポイントを調整します。HPが高いほど高得点にしたり、難易度によって得点を変化させてみましょう。
Projectタブの Prefabs下の 各 Wave 配下にある Enemyを選択し、Inspectorを確認すると、Enemy(Script)のところに Point 欄が追加されているので、ここでポイントを設定します。
EnemyはWave毎に複数選択し、一気に設定すると楽です。
ハイスコアの保存
最後にゲームオーバーの時にハイスコアを保存するようにします。
Manager.csのGameOverメソッドに追記します。
Projectタブの Prefabs配下にある Manager をダブルクリックし、MonoDevelopで開いて Manager.csを編集します。
Manager.cs のソースは、チュートリアルの内容でまるごと置き換えて問題ありません。
置換後、Ctrl+Sで保存し、Unity画面に戻りましょう。
変更点は以下の通りです。
- GameOver時の処理に、ハイスコアの保存処理を追加
お疲れ様でした。ゲーム制作編は以上で終了となります。
ところで、現在の状態だと、ゲームオーバー時にスコアがクリアされてしまい、自分が何点取ったのかわからなくなっています。
これは、ゲームオーバー時に、Score.cs のセーブ処理が走るのですが、その中で Initialize()という初期化関数を読んでおり、そこでスコアを0にしているからです。
コレはゲームとしておかしいので、ゲームオーバーの時はスコアを0にせず、ゲーム開始の時に0にするように改造したいと思います。
まず、Score.cs (Projectタブの Script/Score)の Save()の中の最後の行、Initialize()の呼び出し行を、行のアタマに//をつけて、一行丸ごとコメントアウトします。
// ゲーム開始前の状態に戻す
//Initialize ();
次に、Score.cs (Projectタブの Script/Score)の Initialze()関数を private から public にします。
// ゲーム開始前の状態に戻す
public void Initialize ()
最後に、Manager.cs (Projectタブの Script/Manager)の GameStart()の末尾に、以下のInitialize()呼び出しを追加します。
//スコアを初期化する
FindObjectOfType().Initialize();
これでゲームオーバー時にスコアはクリアされず、ゲーム開始時にクリアされるようになると思います。
リスタート時に Wave と敵をリセットする
チュートリアルのままだと、ゲームオーバー後、Xキーでゲームをリスタートしても、敵はリセットされません。
これではどうかと思うので、普通のゲームっぽく、リスタートしたら、敵をクリアし、Waveも最初からにリセットするようにしたいと思います。
具体的には、Projectタブの Script配下にある Emitterを、以下のソースで置換します。
Emitter.cs
using UnityEngine; using System.Collections; public class Emitter : MonoBehaviour { // Waveプレハブを格納する public GameObject[] waves; // 現在のWave private int currentWave; // Managerコンポーネント private Manager manager; IEnumerator Start () { // Waveが存在しなければコルーチンを終了する if (waves.Length == 0) { yield break; } // Managerコンポーネントをシーン内から探して取得する manager = FindObjectOfType<Manager>(); bool flag_start = true; while (true) { // タイトル表示中は待機 while(manager.IsPlaying() == false) { currentWave = 0; yield return new WaitForEndOfFrame (); } flag_start = false; // Waveを作成する GameObject g = (GameObject)Instantiate (waves [currentWave], transform.position, Quaternion.identity); // WaveをEmitterの子要素にする g.transform.parent = transform; // Waveの子要素のEnemyが全て削除されるまで待機する while (g.transform.childCount != 0) { if(manager.IsPlaying() == false){ //ゲームオーバー時はここに来る currentWave = 0; flag_start = true; }else if(flag_start){ //ここに来た場合、敵が残っている状態で再スタートされた //もうループで敵が倒されるまで待つ必要はないので、ループを抜ける break; } yield return new WaitForEndOfFrame (); } // Waveの削除 Destroy (g); // 格納されているWaveを全て実行したらcurrentWaveを0にする(最初から -> ループ) if (manager.IsPlaying() == false || flag_start || waves.Length <= ++currentWave ) { currentWave = 0; } } } }
改造点は、以下の通りです。
- flag_startフラグ変数を追加
- 待機中は、currentWave を0にリセット
- ゲーム開始時は、flag_startをリセット
- ゲームオーバー時は、currentWave を0にリセットし、flag_startフラグを立てる
- 敵が残っている状態でリスタートされたら、敵全滅まで待つループを抜ける
- 敵全滅まで待つループを抜けた後、currentWave を0にリセットにする条件として、「非プレイ中」「flag_start が true」を追加
これで、よりゲームっぽくなったと思います。
これでチュートリアルは終了です。
お疲れ様でした。