【Unity/2D】Flappy Bird風ゲームを作ってみる話 #6 土管オブジェクトを一定間隔で生成する機能を作る

記事をご覧いただき、誠にありがとうございます。

投稿主の無能です。

前回は、オブジェクトの破棄エリアを作成し、プレイヤーが接触したら非アクティブ、プレイヤー以外が接触したら破棄する、という機能を作りました。

今回は、土管が一定間隔で生成される仕組みを実装したいと思います。

土管が一定間隔で生成される仕組み

まずは、「土管が一定間隔で生成される仕組み」というものについて考えてみましょう。

一定間隔と言っているので、時間の流れが関係していることは分かりますね。
そして、その機能が失われずに、ゲーム実行中の間はずっと働いてくれる、という事になります。

例えば、2秒毎に土管オブジェクトが生成される機能があれば、ゲームを開始してから2秒後に土管を生成します。

そして土管を生成した時点を0秒として、また2秒経過したら、土管を生成します。

この処理がゲームの実行中に、絶え間無く続いて欲しいのが今回実装する内容となります。

何となくでもいいので、処理内容を掴めたでしょうか。

では早速やっていきましょう。

土管を生成するオブジェクトを作る

まずは土管を生成するオブジェクトを作成します。

土管はこれから自動で生成される仕組みを作るので、変更を適用したらHierarchyビューから削除します。

削除する前に、OverridesがNo Overridesとなっていて、適用する変更が無いことを確認してください。

削除出来たらCreate Emptyで空のオブジェクトを作成します。

作成した空のオブジェクトは「PipeGenerator」という名前にします。

OverridesがNo Overridesとなり、適用する変更が無いことを確認してHierarchyビューの土管を削除する

土管を削除した

Create Emptyで空のオブジェクトを作成

名前を「PipeGenerator」にする

このPipeGeneratorから、Prefabにした土管を生成するようにしていきます。

生成する土管について考える

では生成する土管のPrefabについて考えてみましょう。

土管のPrefabのInspectorビューを見ると、土管を動かす機能であるPipeControllerスクリプトがアタッチされています。

という事は、土管が生成された段階で、既に土管には自分で動く機能がある、という事になります。

後はPipeGeneratorは、元となるPrefabをコピーして、そのコピーを生成すれば良いですね。

このように、元のオブジェクトを大量生産して利用するのがPrefabの特徴です。

PipeGeneratorについて考える

では生成元となるPipeGeneratorについても考えてみましょう。

生成する土管は自律して右から左へ一定の速度で動くようになっていますので、PipeGeneratorは画面外の右側に位置することが望ましいです。

土管を生成するX座標の位置をPipeGeneratorオブジェクトのX座標の位置にすればいいですね。

対してY座標は、画面の上下の範囲内で、ある程度の幅でランダムになって欲しいです。

ずっと同じ高さの土管が生成されても、ゲームとしては面白くありませんね。

土管が同じ高さ・同じ隙間で生成されてもゲームとしては面白味に欠ける

Y座標が一定の範囲でランダムになるという事は、土管が生成されるY座標が一定の範囲でランダムになるという事です。

つまり、土管を生成する高さが、生成される毎に毎回変化するという事です。

Y座標をランダムにした時のイメージ

こうなると、一気にゲームっぽくなりますね。

という事で、この方針で土管を生成するようにしましょう。

方針が決まったので、次はどうやって土管を生成するのか、具体的なところを考えてみましょう。

土管は上下の二つがあり、その隙間をプレイヤーが通過出来るようになっていないと、ゲームとして破綻してしまいます。

という事は、土管の上側を生成したら、ある程度の隙間を確保したうえで、土管の下側が生成されることになります。

土管生成時のイメージ

つまり、生成する位置をまとめると

・上側の土管
X座標:PipeGeneratorのX座標
Y座標:一定の範囲でランダム

・確保する隙間
X座標:PipeGeneratorのX座標
Y座標:上側の土管オブジェクトを生成した位置から決まった分の隙間

・下側の土管
X座標:PipeGeneratorのX座標
Y座標:上側の土管を生成した位置 + 確保した隙間 の下から生成

という事になりますね。

後はこの方法で、一定時間毎に土管が生成され続ければいいですね。

ではスクリプトで上記の方法を実装していきましょう。

スクリプトで土管を生成する機能を書く

ではスクリプトを書く前に、準備をしていきます。

先程考察した通り、土管を生成するPipeGeneratorオブジェクトを右側に配置します。

PipeGeneratorのTransformのXを10にします。

PipeGeneratorのTransformのXを10にする

Sceneビュー

もし視認できるようにする場合は、Sprite Rendererを追加してめたんの画像を設定を設定してください。
後でSprite Rendererを非アクティブにすることで視認できなくなります。

では新たに「PipeGenerator」スクリプトを作成し、PipeGeneratorオブジェクトにアタッチします。

「PipeGenerator」スクリプトを作成

PipeGeneratorのオブジェクトへアタッチ

ではこのPipeGeneratorから土管を生成する仕組みを書いていきます。

共通のVariablesスクリプトから見ていきます。

Variables.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Variables : MonoBehaviour
{
    #region const-float
    public const float zero = 0;                                // ゼロの定数
    #endregion

    #region Var-string
    public const string tag_Player = "Player";                  // タグ:プレイヤー
    #endregion

    #region Var-Vector2
    public static Vector2 screenMin, screenMax;                 // 画面サイズ取得用
    #endregion
}
では見ていきましょう。

変数にVector2のscreenMin、screenMaxを用意しました。

この変数は画面のサイズを取得しておくためのものになります。

Variablesスクリプトの変更点は変数の追加のみです。

では本題のPipeGeneratorスクリプトを見ていきましょう。

PipeGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PipeGenerator : MonoBehaviour
{
    #region Var-Pipe
    [Header("生成する土管:上")]
    [SerializeField] GameObject pipe_Top;
    [Header("生成する土管:下")]
    [SerializeField] GameObject pipe_Bottom;
    [Header("土管生成時に確保する隙間")]
    [SerializeField] float genPipeGap = 10f;
    [Header("土管生成時の下限:上")]
    [SerializeField] float topLowerLimit = 1f;
    #endregion

    #region Var-PipeGenerate
    [Header("土管の生成待機時間:初回")]
    [SerializeField] float waitTime_First = 1f;
    [Header("土管の生成待機時間:二回目以降")]
    [SerializeField] float waitTime_AfterSecond = 1f;
    #endregion

    #region Var-Internal
    Vector2 screenMin, screenMax;                                           // ゲーム画面の最小値・最大値の取得用
    const float pipeAngle_Top = 180f;                                       // 上側の土管の回転用
    #endregion

    #region Start
    void Start()
    {
        // ゲーム画面の大きさを取得
        GetScreenSize();
        // 土管を繰り返し一定間隔毎に生成
        InvokeRepeating(nameof(PipeGenerate), waitTime_First, waitTime_AfterSecond);
    }
    #endregion

    #region GetScreenSize
    // 画面の大きさを取得
    void GetScreenSize()
    {
        // 画面の大きさを取得
        screenMin = Camera.main.ViewportToWorldPoint(Vector2.zero);         // 最小値
        screenMax = Camera.main.ViewportToWorldPoint(Vector2.one);          // 最大値
        // 画面の大きさを共有の変数へ保存
        Variables.screenMin = screenMin;                                    // 最小値
        Variables.screenMax = screenMax;                                    // 最大値
    }
    #endregion

    #region PipeGenerate
    // 土管の生成
    void PipeGenerate()
    {
        // 土管の上側の生成ポイントを作成
        float _generatePoint = Random.Range(screenMax.y, Variables.zero + topLowerLimit);
        // 土管の上側をコピー
        Instantiate(pipe_Top,
                    // X座標:PipeGeneratorのX座標     // Y座標:作成した生成ポイント
                    new Vector2(transform.position.x, _generatePoint),
                    // 180°逆さまにする
                    Quaternion.Euler(Variables.zero, Variables.zero, pipeAngle_Top));
        // 土管の下側をコピー
        Instantiate(pipe_Bottom,
                    // X座標:PipeGeneratorのX座標     // Y座標:生成ポイント - 確保した隙間
                    new Vector2(transform.position.x, _generatePoint - genPipeGap),
                    // 回転無し
                    Quaternion.identity);
    }
    #endregion
}
では見ていきましょう。

メンバ変数

まずは上下の土管をコピーして複製するため、コピーするオリジナルのオブジェクトが必要になります。
そのためGameObjectのpipe_Topとpipe_Bottomを用意しました。

それから確保した隙間を空けておくため、floatのgenPipeGapになります。

そして上側の土管を生成する際に、生成範囲が画面サイズの下ギリギリまでだと、流石にゲームの障害物としての役割を全うできなくなります。

そのため生成の下限を決めておく必要があるので、floatのtopLowerLimitを用意しました。

残りの変数は、土管の生成の際に使用する変数となります。

floatのwaitTime_FirstとwaitTime_AfterSecondになります。

メンバ変数は以上になります。

Start関数

次はStart関数を見ていきましょう。

Start関数では、はじめに画面サイズの最小値・最大値を取得するGetScreenSize関数を呼んでいます。

次にInvokeRepeatingで一定時間ごとに関数の処理を繰り返しています。

無能の2Dシューティングをやって頂いた方にはもう既出のものですね。


詳細はUnityマニュアルにある通り、
InvokeRepeating(呼び出す関数名, 初回の関数を呼び出すまでの間隔, 二回目以降の関数を呼び出すまでの間隔);
となっています。

例えば初回の関数の呼び出しまでの間隔が5秒、二回目以降の関数の呼び出しの間隔が3秒だったとします。

その場合はゲームを開始してから5秒後に初回の関数呼び出しがなされ、二回目以降は3秒間隔で関数呼び出しが行われます。

InvokeRepeatingの処理のイメージ

詳しくはUnityマニュアルでご確認ください。

GetScreenSize関数

次はGetScreenSize関数になります。

こちらも2Dシューティングをやって頂いた方は既出のものです。

screenMinではメインカメラビューポート座標の最小値、ここではVector2.zeroをワールド座標にしています。

同様にscreenMaxもメインカメラのビューポート座標の最大値、ここではVector2.oneをワールド座標にしています。

ビューポート座標というのは、ゲーム画面に写っているオブジェクトの位置を、比率で表したものになります。

ちょっと説明だけだと分かり辛いと思うので、詳しい内容の記事のリンクを置いておきますので、そちらを参考になさってください。


こちらの記事でも書かれている通り、ゲーム画面の左下が最小値(0, 0)となり、右上が最大値(1, 1)になります。

そしてビューポート座標を普段使っているワールド座標に変換している、ということです。

そしてVariablesスクリプトのscreenMin・screenMaxに代入して共通のものにしています。

今のところ出番は先になるので気にしないでおいてください。

PipeGenerate関数

最後にPipeGenerate関数です。

この関数で土管をコピーして作成します。

はじめに、土管を生成する位置を_generatePointとして作成します。

変数の前に_(アンダーバー)が付いているのは、ローカル変数や一時的な変数になります。

ローカル変数というのは、メンバ変数とは異なり、その関数内や処理だけで動く変数になります。
つまり、このPipeGenerate関数内でしか有効でない変数ということになります。
ローカル変数は、その関数や処理が役目を果たすと(終了すると)、消えて無くなる関数だと覚えておいてください。

つまりは「一時的な変数」を作る時には、「変数の名前の前に_を付ける」という覚え方で問題無いです。

そして_generatePointとして、画面サイズの最大値~画面サイズの最小値 + 確保した隙間 の間を乱数の発生範囲としています。

そしてInstantiateで上下の土管をオリジナルからコピーして生成します。

Object.Instantiate - Unityマニュアル

ここも2Dシューティングを履修済みであれば既出のものです。

Instantiateは、オリジナルのオブジェクトを複製し、そのクローンを生成します。

また、渡される引数の数によって、その振る舞いが変わります。

カッコ内で渡している引数は三つで、
1:コピーするオリジナルのオブジェクト
2:クローンを生成する位置(座標)
3:クローンの回転の度合い
となります。

Unityマニュアルだと、四番目の関数の使い方に該当します。

まず上側の土管を複製します。

複製したクローンを出現させる位置(座標)は、先程の乱数の発生位置になります。

意図的にマイナスの数にしない限りはマイナスになりませんよね。

そしてクローンの回転ですが、上側の土管は逆さまになって欲しいので、180°の傾斜を付ける、つまり逆さまの状態になるように指定しています。

ここで注意が必要なのが、Quaternionという四元数というもので角度を指定しなければいけないことです。

Eulerというのは、かの有名な数学者のオイラーになります。

つまり、Quaternion.Eulerというのはオイラー角でx,y,zの角度を指定しています。

ここで必要なのはzの角度になるので、z以外の角度(z,yの角度)は0°で、zの角度だけが180°になるようにしています。

これで上側の土管を作成できました。

同様にInstantiateで下側の土管をコピーします。

位置はX座標は同じくPipeGeneratorオブジェクトの位置です。

Y座標は、上側の土管の位置と確定した隙間より下になります。

ですので、上側の土管の生成位置から確定した隙間の高さを引いてあげることで、下側の土管の生成位置が確定します。

そして回転ですが、下側の土管は回転しなくていいので、Quaternion.Identityで回転無し(クローンを作成した回転状態のまま)で生成されます。

またもう一つのやり方として、ゲーム内時間を足し合わせて蓄積して、閾値を超えたらPipeGenerate関数を呼び出す、というやり方も考えられます。

この場合はUpdate関数が必要になるので、ぜひチャレンジしてみてください。

これで土管を複製せる準備が出来ました。

動作確認前のUnityの設定

スクリプトで必要な機能を準備できたので、Unityに戻って諸々の準備をしていきます。

PipeGeneratorオブジェクトのInspectorビューで、追加した項目を設定します。

上下の土管のオブジェクトは、Prefabsフォルダ内のオブジェクトを設定します。

後の数値は画像通りにしました。

勿論自由に設定して構わないですが、当たり判定やその他の調整は適宜行ってください。

PipeGeneratorスクリプトの追加された項目を設定

それでは動作確認します。

動作確認

ゲームを実行して動作確認をしてみましょう。

ゲーム実行画面

土管が一定間隔で生成されて、DestroyAreaに接触すると消えていきますね。

土管の高さもランダムになっています。

ちょっとゲームっぽい雰囲気が出てきました。

まとめ

今回は
  • PipeGeneratorオブジェクトを作成した
  • 土管が生成される仕組み、PipeGeneratorの役割について考えた
  • PipeGeneratorスクリプトを作成し、土管を生成する機能を実装した
という事をやりました。

次回は、ゲームの一時中断とゲームオーバー後の処理についてやっていこうと思います。

では、また次回!

コメント