Unityに関連する記事です

 ベルトスクロールアクションでは各エリア単位で敵が生成されます。このロジックを設計していきます。



設計


 スクリプタブル・オブジェクトに登録する情報には、各エリアのデータとして AreaData クラスをListとして管理しています。
そのため、ステージごとにエリアの数を可変化することが可能です。また、AreaData クラスには出現する敵の情報を登録することができるような設計になっています。

 この情報を利用して、各エリアに出現する敵の種類と数を自動的に生成する仕組みを順番に作ります。

 この手順ではまず、敵の自動生成を繰り返すサイクルを実装します。


実装の手順


 1.敵管理用のEnemyクラスを作成する
 2.Enemy用ゲームオブジェクトにEnemyクラスをアタッチし、プレファブ化する。デバッグを行う
 3.Enemyクラスを修正する
 4.GameManagerクラスを修正する
 5.ゲームを実行して動作を確認する


新しく学習する内容


 ・if文におけるbool型の省略記法
 ・省略可能な引数を持つメソッドを定義する方法と使用方法
 ・メソッドの引数を利用してアサイン情報を取得する方法
 ・三項演算子による処理
 ・GameObject型ではない、自作クラスを用いたゲームオブジェクトのインスタンス方法


1.敵管理用のEnemyクラスを作成する

 
 敵役のゲームオブジェクトにアタッチするクラスを作成します。

Enemy.cs

 <= クリックすると開きます



<if文におけるbool型の省略記法>


 bool型ではif文の条件式に省略記法を用いることが出来ます。
変数名をそのまま条件式に記載した場合には true である確認を求め、否定演算子(!)を先頭につけて変数名を記載した場合には false である確認を求めます。

 if (isDebug) {  <= if (isDebug == true) と同じになります
    StartCoroutine(DestroyEnemy(3.0f));
  }

  if (!isDebug) {  <= if (isDebug == false) と同じになります
    StartCoroutine(DestroyEnemy(3.0f));
  }


<省略可能な引数を持つメソッドを定義する方法と使用方法>


 省略可能な引数とはデフォルト引数とも呼ばれます。メソッドに引数を用意する際に、あらかじめ初期値を設定(初期化)しておくことで
その引数を省略した場合には初期値を使用し、省略せずに記述した場合にはその値を使用する、という定義になっています。

 なお今回は戻り値のあるメソッドで使用していますが、戻り値のない void のメソッドでも利用できます。修飾子もprivate,publicなどに関係なく利用できます。

 <今回のケースの場合のメソッドの定義>

private IEnumerator DestroyEnemy(float waitTime = 0.0f) {

 通常の引数とは異なり、型、変数名の宣言に加えてイコールで初期化を行っています。
これが省略した引数の定義の方法になります。なお、複数の引数を持つメソッドの場合、この省略書式は、省略していない引数の宣言が終わってから書かないとエラーになります。

 <複数の引数を持つ場合のメソッドでの省略引数の定義>

 1.エラーにならない場合(省略していない引数を書いた後に省略引数を定義する)
 private IEnumerator DestroyEnemy(int x, GameObject obj, float waitTime = 0.0f) {

 private IEnumerator DestroyEnemy(int x, GameObject obj, float waitTime = 0.0f, bool isSwitch = true) {     // 2つ以上の省略も可能


 2.エラーになる場合(省略していない引数が省略引数よりも後ろに定義されている)
  private IEnumerator DestroyEnemy(int x, float waitTime = 0.0f, GameObject obj) {


 省略引数を持つメソッドを呼び出す命令には2つの方法があります。
省略引数を書いて(省略せずに)呼び出す方法と、省略引数を書かないで(省略して)呼び出す方法です。

  float waitTime = 3.0f;

 <省略引数を省略しない場合の命令>
 DestroyEnemy(waitTime);

 <省略引数を省略する場合の命令>
 DestroyEnemy();

 上記の場合には、3.0f が引数として渡されます。
 下記の場合には引数が省略されていますので、省略引数で初期化して宣言している 0.0f が引数として渡されます。

 省略引数の値を利用することがほとんどで、たまに引数を指定して渡すようなメソッドの場合には、
引数の値が必要な場合にだけメソッドの引数を記述すればよいことになりますので、その都度、引数を書く手間が省けます。


2.Enemy用ゲームオブジェクトにEnemyクラスをアタッチしてデバッグを行う


 Enemy用のゲームオブジェクトに、作成したEnemyクラスをアタッチします。
インスペクターを確認し、isDebug変数のスイッチにチェックを入れて true にします。
これでデバッグモードになり、生成された敵は3秒後に自動的に破棄されます。

Enemy用ゲームオブジェクトのインスペクター画像



 ゲームを起動し、実際に3秒後に敵がゲームから破棄されるかをデバッグして確認します。


検証動画
https://gyazo.com/3f9c09142c908d750ac95e5bbaa683f3


 挙動に問題がなければ、このEnemy用ゲームオブジェクトをプレファブ化しておきます。
プレファブにしたらヒエラルキー上からは削除してください。


3.Enemyクラスを修正する


Enemy.cs

 <= クリックすると開きます



<メソッドの引数を利用してアサイン情報を取得する方法>


 他のクラスの情報をクラス内で利用したい場合、その紐づけの方法にはいくつかの手段があります。
private変数内に代入したいのであれば、紐づけしたいクラスがアタッチされているゲームオブジェクトを探して(Findメソッドを利用して)、それからGetComponentする方法、
public変数であれば、インスペクターからゲームオブジェクトをドラッグアンドドロップして事前にアサインしておく方法などです。

 今回Enemyクラスでは、GameManagerクラスの情報を代入するための変数をprivateで用意しています。
こういった場合にはインスペクターによる事前のアサインができませんが、Findメソッドを利用せずにアサインを取得する方法の1つとして、
メソッドの引数の値にGameManagerクラスを届けてもらう方法があります。

<呼ばれるメソッド>
    private GameManager gameManager;                      // アサイン情報

    public void SetUpEnemy(GameManager gameManager) {     // <=  引数でGameManagerクラスが届いている   
        this.gameManager = gameManager;                   // それを変数に代入する
    }


<呼び出す側>
public class GameManager : MonoBehaviour

 Enemy enemy;
  enemy.SetUpEnemy(this);     // <= 引数としてthis(このケースではGameManager自身)を渡している

 このような手法を用いれば、処理の重いFindメソッドを利用することなく、private変数にアサイン情報を代入することが可能です。
クラス内で利用したい外部クラスの情報は、それをどのように取得するか、という部分で設計を検討することで、このようなやりとりによる値の代入ができます。


 

4.GameManagerクラスを修正する


 GameManagerクラスを修正して、先ほどプレファブにしたEnemy用ゲームオブジェクトをスクリプトから自動生成するようにします。
最初にAreaDataの情報は使わずに、時間の経過に合わせて連続で敵が自動的に生成されるように処理を実装します。

 このとき、生成した敵の数はカウントするように変数を用意して、敵が何体生成されたかを確認出来るようにします。
ただし生成数の上限値はまだ設けずに、まずは、どんどんと生成される処理にしておきます。

 また敵を生成する際のプレファブの型に、GameObject型ではなく、Emeny型を使用してインスタンスを行います。


GameManager.cs



<三項演算子による処理>

 
 三項演算子という処理は、if/else文の分岐処理を1行で簡潔に記述できる書式です。

今回の使用例は、次のような条件のif文を1行にまとめたものです。

 int direction = Random.Range(0, 2);
 charaPos.x = direction == 0 ? charaPos.x += 2.5f : charaPos.x -= 2.5f;
 ↓
    int direction = Random.Range(0, 2);
    
  // プレイヤーの左右どちら側に敵を生成するかランダムで決める(0 = 右方向、1 = 左方向)
    if(direction == 0)
    {
         charaPos.x += 2.5f
    } 
    else 
    {
         charaPos.x -= 2.5f
    }

 三項演算子はその名前の通りで、3つの項目があります。

【条件文】 ? 【trueだった場合返す値や処理】 : 【falseだった場合返す値や処理】

 右辺では、この条件式とtrueとfalseの3つの項目を用意して、その結果に合わせて左辺へ値を代入します。


参考記事
@crazy_traveler様
参考になる三項演算子
https://qiita.com/crazy_traveler/items/5fb5ec9568e...


<GameObject型ではない、自作クラスを用いたゲームオブジェクトのインスタンス方法>


 ゲームオブジェクトをスクリプトから生成する際にはGameObject型で宣言した変数にGameObjectであるプレファブをアサインして利用することが多いです。
このとき、親ゲームオブジェクトに自作したクラス(今回はEnemyクラス)がアタッチされている場合には、GameObject型ではなく、そのクラスを宣言して
同じようにプレファブをアサインしてインスタンスすることが出来ます。

 // まだエリアのデータは使わずに敵を生成。生成する際にEnemy型のプレファブを使用するので、左辺に用意する変数の型もEnemy型としている
  Enemy enemy = Instantiate(enemyPrefab, generatePos, Quaternion.identity);

 // Enemy型で生成して変数に代入しているので、すぐにメソッドの呼び出しができる
 enemy.SetUpEnemy(this); 

 利点はコメントにもあるように、GameObject型ではなく、Enemyクラスで生成をしているので、その変数を使うとすぐにメソッドの呼び出しができます。
つまり、GameObject型で生成していた際に、メソッドの呼び出しに必要になっていたクラス取得をするGetComponetの処理が不要になっています。

 // GameObject型で生成
 GameObject enemyObj = Instantiate(enemyPrefab, generatePos, Quaternion.identity);

 // Enemyクラスのメソッドを呼び出すには、GameObject型では呼び出せないので、Enemyクラスを取得する必要がある
 Emeny enemy = enemyObj.GetComponent<Enemy>()

 // メソッドの呼び出し
 enemy.SetUpEnemy(this);  

 GetComponentメソッドの処理はあまり軽い処理ではありません。またGameObject型の変数もGetComponent用にわざわざ用意している変数です。
敵をたくさん生成するような今回の場合には、敵を生成する際に、このように余分な変数やGetComponentをしないで済む方法で設計し、実装しておいた方がよいでしょう。


5.ゲームを実行して動作を確認する


 GameManagerゲームオブジェクトを選択し、インスペクターを確認します。
アサイン情報が追加されていますが、EnemyPrefab以外の情報はすべてゲームの実行と同時に代入されますので
Project内にあるEnemy用ゲームオブジェクトをEnemyPrefabにドラッグアンドドロップしてアサインします。

 Enemyクラスがアタッチされているゲームオブジェクトであれば、今までのGameObject型と同じようにアサイン出来ます。
EnemyPrefab変数のアサイン情報も、None(GameObject)ではなく、None(Enemy)と、型の指定がいつもと異なっていることが分かります。


GameManagerゲームオブジェクトのインスペクター画像


 
 プレファブのアサインが終了したらゲームを実行して動作を確認します。
AppearTimeの値が自動的にランダム値が設定されて、GenerateTimerがカウントアップします。
GenerateTimerの値がAppearTimeを超えたら敵が生成されて、GenerateCountが1つ増えます。

 また次の生成に向けてAppearTimeに新しい値が入り、GenerateTimerも0に戻されます。
そして再度カウントアップをして、という形のサイクルになります。

 この処理がゲーム実行中ずっと繰り返されれば成功です。適宜なタイミングでゲームを止めてください。


検証動画 GenerateTimerがカウントアップし、AppearTimeを超えたら敵が生成される。GenerateCountが 1 増える。
https://gyazo.com/1aeebc51b84311b13db69767a9c37520


 以上でこの手順は終了です。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



プログラムの基礎学習

コード練習

技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

3Dトップビューアクション(白猫風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます