見出し画像

【第一回】縦スクロールSTGの作り方【Unity】

今回はゲームの作り方第一回ということで、縦スクロールのシューティングゲームを作成していきたいと思います!縦スクロールシューティングを構成する要素は主に6個あります
・プレイヤーの移動、弾の発射
・敵機の段階攻撃(フェーズ攻撃)
・プレイヤー、敵の当たり判定
・残機システム、敵のHPバー
・勝敗システム
・その他シーンシステム
順を追って作り方を解説していきます!
*注意:Unityの操作がある程度(スクリプト、コンポーネントのアタッチ、その他UIなどの作成法やインスペクターウィンドウの見方)分かっていることが前提とさせていただきます。申し訳ありません。

1.プレイヤーの移動、弾の発射

プレイヤーの移動は主にInput.GetAxisという関数を用います。
これはunity搭載の関数で、引数には”Horizontal”と”Vertical”の二つがあり、これはそれぞれx軸y軸に対応しています。
使用方法は以下の通りです
①これらの入力情報をfloat型の変数、X,Yに代入します
➁X,Yをもとにベクトルを作成します
➂➁で作成したベクトルに速度をかけ、それらの速度をRigidBodyのvelocity    に代入します
RigidbodyとはUnity側が用意した物理演算用のコンポーネントです。
これらのスクリプトをプレイヤーオブジェクトにアタッチしたらWASDで移動可能になります
*RigidBody2Dの付け忘れに注意!
次に弾の発射です。
弾の発射にはInstantiate関数を使用します。
書式は Instantiate(生成するオブジェクト,位置,角度);
です、
生成するためにはそのオブジェクトがPrefab化されている必要があります。手順は以下の通りです。
④最初にGameObjectを宣言しInstantiate関数でクローンを作成します
⑤クローンが画面外に出た場合、
OnBecameInvisibleでイベントを取得し、クローンを削除します。
これでプレイヤーのスクリプトは完成です …( ´∀` )
そうです、弾は当然自発的に上へ進んでいかなければなりません。
よって弾のスクリプトを作成しUpdate関数内に transform.Translate(0,speed(任意の数or宣言したfloat),0)を入れてあげましょう!
*Time.deltatimeを変数に加算し時間制御をしないと弾が出過ぎてしまうので注意!(counter += Time.deltatime)
今回はif文を使用しています!
if文は
if(条件文){
  実行処理;
}
が書式です!
これで完成です!
以下プレイヤースクリプトと弾のスクリプトです!


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
  public float speed;
  public Rigidbody2D rb;
  public GameObject Bullet;
  private GameObject Bullet_C;
  private float timecounter;
 // Start is called before the first frame update
 void Start()
 {
 }

// Update is called once per frame
 void Update()
 {
    //1
    float X = Input.GetAxisRaw("Horizontal");
    float Y = Input.GetAxisRaw("Vertical");

    //2
    Vector2 direction = new Vector2(X, Y);

    //3
    rb.velocity = direction * speed;

    timecounter += Time.deltaTime;
    if(timecounter >= 0.5f)
    {
        //4
        Bullet_C = Instantiate(Bullet, this.gameObject.transform.position, Quaternion.identity);
        timecounter = 0f;
    }
 }

 private void OnBecameInvisible()
 {
    //5
    Destroy(Bullet_C);
 }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletMovement : MonoBehaviour
{
  public float speed;
  // Start is called before the first frame update
  void Start()
  {
  }

  // Update is called once per frame
  void Update()
  {
    this.transform.Translate(0, speed, 0);
  }
}

2.敵機の段階攻撃

さて、プレイヤーの移動と弾の発射ができたところで次は敵機の攻撃の作成です。
今回はプレイヤーの方向に弾を発射させるのを第一フェーズとし、30秒後に敵の周りに円盤を作り順々に弾が発射される第二フェーズに移行することにします。
敵機のスクリプトを作成するにあたり、まず最初に敵機と敵用の弾を用意しておきましょう。
まず敵のフェーズの作成です
今回はフェーズごとの動作を関数とし、その関数を秒数に応じて実行する形とします。やり方は以下の通りです
①プレイヤーをオブジェクトとして宣言したのち、位置を取得
➁プレイヤーのベクトルから自身(敵機)のベクトルを減算し、そのベクトルの値に速度をかけたものを生成した敵の弾に代入
フェーズ2に関しては今回は大量の空のゲームオブジェクトを円盤状に作成しその上にスポーンさせるという原始的な方法をとります。
やり方は以下の通りです
①GameObject(円盤状のもの)を配列として宣言
➁そこにコルーチンを使い0.5秒おきにforループで配置
➂配置した順番に一つ一つその時のプレイヤーの位置めがけて発射

配列…( ´∀` )?となると思います。ちゃんと解説させていただきます。
配列とは
public or private 型[] 宣言名;
で宣言される箱が横に並んだものをいいます。
宣言名 = new 型[数字];で新たに任意の個数の箱を生成できたり…
宣言名 = new 型[数字]{数字の数だけの要素を,で区切る};で代入できたりしちゃいます!最強です。
さてもういっちょ。

コルーチンってなんぞや?
ってなったと思います。コルーチンを簡単に解説しますと関数の最初のvoidのところをIEnumeratorに変えることでその関数に再生、停止の機能を持たせるっていう感じです!
さて、話を戻すと
今回、敵の弾はプレイヤーの方向に進んでいかなきゃいけませんよね。
しかし
今回はフェーズ1でそれを実装しているのでコピペしちゃいましょう!
今回のプログラムではforループを多用しています
forループは
for(変数;条件;変数に対する処理){}
と書きます。
そうすることで条件に達するまで変数に対する処理が行われ続けます。
特に
for(int i = 0;i < ○○.Length ;i++){}
という形式を使用しています。
このプログラムは配列の長さの数だけループを実行する処理なのですが…
これがまた超便利!!!
この書き方、覚えておくと短縮になります!では以下プログラムです!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMovement : MonoBehaviour
{
  public GameObject Player;
  public GameObject EnemyBullet;
  public float BulletSpeeed;
  public float timecounter,fazecounter;
  public GameObject[] EnemyBullet_C;
  public GameObject[] ShootPoint;
  public bool faze1, faze2;
  // Start is called before the first frame update
  void Start()
  {
    //配列の長さ指定
    int i  = ShootPoint.Length;
    EnemyBullet_C = new GameObject[i];
  }
  // Update is called once per frame
  void Update()
  {
      timecounter += Time.deltaTime;
      //開始してから何秒経ったか
      fazecounter += Time.deltaTime;
      if (fazecounter <= 3)
    {
        Faze1();
        
        faze1 = true;
    }
    if(fazecounter > 3 && faze2 == false)
    {
        StartCoroutine("Faze2");
    }

}
//第一フェーズ
void Faze1()
{
    if (timecounter >= 0.5f)
    {
        //初期値に時間を戻す
        timecounter = 0f;

        //自身(敵機)のベクトルを取得
        Vector2 mypos = this.transform.position;

        //プレイヤーのベクトルを取得
        Vector2 PlayerPos = Player.transform.position;

        GameObject Clone = Instantiate(EnemyBullet, this.transform.position, Quaternion.identity);

        //プレイヤーのベクトルから敵機のベクトルを減算
        Vector2 targetPos = PlayerPos - mypos;

        //弾のrighdbodyに速度を代入
        Clone.GetComponent<Rigidbody2D>().velocity = targetPos * BulletSpeeed * Time.deltaTime;
    }
}
//第二フェーズ
IEnumerator Faze2()
{
    Debug.Log("開始");
    faze2 = true;
    //iを0からはじめShootPointの配列の長さと同じまでiを1ずつ足し続ける
    for(int i = 0; i <ShootPoint.Length; i++)
    {
        //1つずつ弾を配列に代入
        EnemyBullet_C[i] = Instantiate(EnemyBullet, ShootPoint[i].transform.position, Quaternion.identity);
        yield return new WaitForSeconds(0.5f);
        Debug.Log("生成");
    }

    

    //弾のrighdbodyに速度を代入
    for (int i = 0; i < EnemyBullet_C.Length; i++)
    {
        //自身(敵機)のベクトルを取得
        Vector2 mypos = this.transform.position;

        //プレイヤーのベクトルを取得
        Vector2 PlayerPos = Player.transform.position;

        //プレイヤーのベクトルから敵機のベクトルを減算
        Vector2 targetPos = PlayerPos - mypos;
        //代入された順番に弾を発射
        EnemyBullet_C[i].GetComponent<Rigidbody2D>().velocity = targetPos * BulletSpeeed * Time.deltaTime;

        yield return new WaitForSeconds(0.5f);
    }

    fazecounter = 0;
    faze2 = false;
   
}
}

3.プレイヤー、敵の当たり判定

来ました難関。大体みんなここで詰まるといっても過言ではありません。
なのでがっつり解説していきます!
当たり判定に今回はOnTriggerEnter2Dを使用します
TriggerにはEnter,Stay,Exitとあります。
そのままの意味です。入ったときか触れてるときずっとか抜けた時。実行するタイミングが違うだけです。
Colliderと迷う方もいらっしゃいますが…
colliderは跳ね返り、Triggerはすり抜けるという認識で大丈夫です!
Triggerの関数を作ろうとしてvoid On…と入力すると勝手に変換に出てきてくれるのですが…
private void OnTriggerEnter2D(Collider2D collision)
いつも括弧の中には何も入っていなかったのに対し今回は入っています
これは簡単に言えば、ぶつかってきたオブジェクトのcollider2D(ex.BoxCollider2D,CircleCollider2D)
をcollisionという名前でプログラム内で使わせてくれるってことです!
使わない手はありません。
つまり…
collision.gameObject.tagと書くとぶつかってきたオブジェクトのタグを取得できます!なんて優れモノ!
それを利用し今回は
①ぶつかってきたオブジェクトのタグを判別
➁もしそのタグが”EnemyBullet”ならライフを1減らす
というスクリプトで残機制度を作成します!
以下追記スクリプト(プレイヤーの移動と同じスクリプトに追記してください)

private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "EnemyBullet")
{
life--;
}
}
//ただし、lifeはpublic int life;で宣言してあるものとする
//宣言してなかったらしてね!

collision.gameObject.tag == "EnemyBullet"は条件文で、もし当たってきたもののタグがEnemyBulletだったらライフを1減らすという意味です!
--と-1は同義です。
意外とあっけなく残機制度は通過しましたよね
…( ´∀` )
HPバーってどうやって作るんだ?
多くの方がそう思うと思います。
安心してください、難しく考える必要はありません。
実はHPバーというのは
現在の体力÷元(最大値)の体力の百分率でゲージを埋めているんです
じゃあゲージはどうやって作るのかと。
ゲージの代わりとして音量バー(unity標準搭載)を使用します。取っ手を消せばHPバーに近似できますからね。
create>>UI>>Sliderでバーを作成してください!
Sliderの構造は…
slider
   -BackGround
   -FillArea
     -Fill
   -HandleSlideArea
     -Handle
となっています
この中でいらないものは…( ´∀` )
そう、”HandleSliderArea”です、なので削除!
そのあとはFillAreaを選択し右側のインスペクターからRectTransformのLeftRightの値を0に、その子オブジェクトのFillを選択しRectTransformkaraWidthを0にしましょう!これで余分な空白が消えたはずです!
色に関してですが、基本的には緑→赤なため…( ´∀` )
Backgroundの色を緑に、Fillの色を赤にします。
まとめると
slider
   -BackGround:色を緑に
   -FillArea:LeftRightをそれぞれ0に
     -Fill:Width0と色を赤に
   -HandleSlideArea:消す
     -Handle:消す
です!ここからはプログラムに入ります!
まずSliderのコンポーネントにはValue(Max1,Min0)というものがあり、
ここに今の体力÷元の体力の百分率を常時代入すると
HPバーが再現できます。
手順としては
①元の体力、今の体力両方のInt型の変数を用意
➁最初に元の体力と今の体力を同値(イコールで結ぶ)にしておく。
➂プレイヤーの時と同様の手順で体力を減らす
④今の体力÷元の体力を計算しValueに代入
スクリプトとしては以下のものになります!(敵の動きのスクリプトに追記してください)

//一番上に追記
using UnitySystem.UI;

//宣言文追記
public int HP,MaxHP;
public Slider HPbar;

//Start関数内へ追記
//初期化
HPbar.value = 0;

//Update関数内への追記
//例の計算式
HPbar.value = 1.0f - (float)((float)HP / (float)MaxHP);
//新規関数として追記
private void OnTriggerEnter2D(Collider2D collision)
{
//Playerタグに触れたら
if(collision.gameObject.tag == "Player")
{
//HPから10引く
HP -= 10;
}
}

ここで(float)について補足
int型とfloat型では扱える桁数が違っていて
intは整数、floatは浮動小数点数です
そのため計算結果をintのまま放置してしまうと…( ´∀` )
0か1です。最悪です。HPbarとして欠陥とかいう次元ではありません。
なのでfloatに変換することによって0.○○…を出力できるようにしているわけです!

4.勝敗システム

ここまでくればあとはもう一息です!頑張りましょう!
ここでは敵のHPが0になったら勝ち、自分のHPが0になったら負けと画面に表示する機能を作っていきます!
今回もUIを使うためusing UnityEngine.UI;をお忘れずに!
まずUIからテキストを作成してください。
サイズや文字関連好きなようにを決めたら…
早速スクリプト製作開始です!
今回はText型でテキストを宣言してTextコンポーネントにあるtext(宣言名.textと書いて使用する、string型)を使用していきます。
プレイヤー、敵のスクリプトに下記のものを追記してください。

//プレイヤー
//一番上に追記
using UnityEngine.UI;
//宣言文
public Text Message;
//Update関数内に追記
if(HP <= 0){
     Message.text = "Loser";
}
//敵機
//宣言文
public Text Message;
//Update関数内に追記
//if(HP <= 0){
      Message.text = "Winner";
}

Textを書き換える際は
宣言名(今回でいうMessage).text = "表示させたい文章";
で書き換えることができます!
今回if文でHPのが0以下になったらテキストをそれぞれ変更しています!
実はこれで終わりなんです。簡単でしょ?

5.その他シーン遷移

また聞き慣れない単語の登場です。
シーンというのは映画のワンシーンなどと同じ意味です。
タイトルのシーン、ステージ選択のシーン、戦闘シーン…etc
ゲームというのは沢山のシーンの集合体なわけです。
今まで膨大な作業をしてきたシーンは戦闘シーンです。
なのでこれからタイトルシーンを作っていきます!
タイトルシーンは勝敗関わらず3秒後くらいに戻るようにしたいですよね。
なので今回はInvokeという関数を使ってその処理を実行していきます!
Invokeとは…
Invoke("実行させたい関数の名前",float型の秒数);
が書式です。関数をfloat型の秒数後に実行してくれます。
そして一番大事シーン遷移は…
UIの時と同様、using UnityEngine.SceneManagement;
を一番上に追記し、
SceneManager.LoadScene("ロードしたいシーン");でロードすることができます!
なので今回の構図は
using UnityEngine.SceneManagement;

負けた(勝った)時Invoke("シーンをロードする関数",3f);

void シーンをロードする関数(){
      SceneManager.LoadScene("ロードしたいシーン");
}
になるわけです!
早速これをプレイヤーと敵機のスクリプトに追記していきましょう!
スクリプトは下記の通りです!

//Player

//一番上に追記
using UnityEngine.SceneManagement

//勝ったテキスト表示の下に追記
Invoke("LoadTitle", 3f);

//新規関数として追記
void LoadTitle()
{
SceneManager.LoadScene("Title");
}
//敵機

//一番上に追記
using UnityEngine.SceneManagement

//負けたテキスト表示の下に追記
Invoke("LoadTitle", 3f);

//新規関数として追記
void LoadTitle()
{
SceneManager.LoadScene("Title");
}

これで実行すると…( ´∀` )
エラーが出ます。そりゃそうです。Titleって名前のシーンなんて作った覚えありませんから。ということでcreate>>Sceneで名前を必ずTitleにしてください。LoadSceneの括弧の中と同じ名前でなければいけません。
そうしたら次に左上のFile>>buildSettingでTitleシーンを開いた状態でAddOpenSceneをクリックしてください。

BuildSettingsの画像

これで追記完了です。SceneInbuildにSampleSceneとTitleがあることを確認してください(上記画像は未完了)
これで遷移するかと思います!
…( ´∀` )
遷移した後どうやって戻ればいいのかとなりますよね。
というわけでタイトルとボタンを追加します!
create >> UI >> Buttonと
create >> UI >> Textを作成してください。
Textは各々好きなように設定してください
Buttonのほうですが、押された際にシーン遷移しなければならないので
最後のスクリプトを作成していきます
今回はクリックされた際にそのスクリプトのシーンをロードする関数を動かしたいので
public void 関数名(){
        SceneManager.LoadScene("さっきまで作業していたシーンの名前")
}
と記述していきます。特に変更していない場合はSampleSceneだと思います
下記はそのスクリプトです

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ButtonScript : MonoBehaviour
{
 public void OnClick()
 {
   SceneManager.LoadScene("SampleScene");
 }
}

using UnityEngine.SceneManagement;を忘れずに!
これをボタンにアタッチ(コンポーネントとして付与)してください
そしたらOnClickというところのプラスマークを押してボタンをNoneのところにドラックアンドドロップしてください。
そしたら下記画像のように選択してください!
これはボタンにさっき付けたスクリプトのさっき作った関数をクリックされたときの処理として登録しているってことです!

Onclick
選択画面

これにて終了~!お疲れさまでした~!

最後に

ここまで読んでくださった方々大変ありがとうございます!
今回は一応遊べる形になるまでの過程を弾幕ゲームの作り方としてまとめてみました!これを最後ビルドすればもうあなたはゲーム開発者への一歩また前進したことになります!これからもぜひ、PORTERをよろしくお願いします!また、読んでくださった方々のゲーム開発が楽しくなることを心より願っております!下に今回のコードとUnityPackageを張り付けておきますのでご自由に使用してください!


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class PlayerMovement : MonoBehaviour
{ 
    public float speed;
    public Rigidbody2D rb;
    public GameObject Bullet;
    private GameObject Bullet_C;
    private float timecounter;
    public Text Message;

    public int life;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        //①
        float X = Input.GetAxisRaw("Horizontal");
        float Y = Input.GetAxisRaw("Vertical");

        //➁
        Vector2 direction = new Vector2(X, Y);

        //➂
        rb.velocity = direction * speed;

        timecounter += Time.deltaTime;
        if(timecounter >= 0.5f)
        {
            Bullet_C = Instantiate(Bullet, this.gameObject.transform.position, Quaternion.identity);
            timecounter = 0f;
        }

        if(life <= 0)
        {
            Message.text = "Loser";
            Invoke("LoadTitle", 3f);
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == "EnemyBullet")
        {
            life--;
            
        }
    }

    void LoadTitle()
    {
        SceneManager.LoadScene("Title");
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class EnemyMovement : MonoBehaviour
{
    public GameObject Player;
    public GameObject EnemyBullet;
    public float BulletSpeeed;
    public float timecounter,fazecounter;
    public GameObject[] EnemyBullet_C;
    public GameObject[] ShootPoint;
    public bool faze1, faze2;
    public Slider HPbar;

    public Text Message;

    public int HP,MaxHP;

   
    // Start is called before the first frame update
    void Start()
    {
        //最大HPと今の体力を同値に
        MaxHP = HP;
        //配列の長さ指定
        int i  = ShootPoint.Length;
        EnemyBullet_C = new GameObject[i];
        HPbar.value = 0;
    }

    // Update is called once per frame
    void Update()
    {
        timecounter += Time.deltaTime;
        //開始してから何秒経ったか
        fazecounter += Time.deltaTime;
        if (fazecounter <= 3)
        {
            Faze1();
            
            faze1 = true;
        }
        if(fazecounter > 3 && faze2 == false)
        {
            StartCoroutine("Faze2");
        }
        //例の計算式
        HPbar.value =1.0f - (float)((float)HP / (float)MaxHP);
        if(HP <= 0)
        {
            Message.text = "Winner";
            Invoke("LoadTitle", 3f);
        }
    }
    //第一フェーズ
    void Faze1()
    {
        if (timecounter >= 0.5f)
        {
            //初期値に時間を戻す
            timecounter = 0f;

            //自身(敵機)のベクトルを取得
            Vector2 mypos = this.transform.position;

            //プレイヤーのベクトルを取得
            Vector2 PlayerPos = Player.transform.position;

            GameObject Clone = Instantiate(EnemyBullet, this.transform.position, Quaternion.identity);

            //プレイヤーのベクトルから敵機のベクトルを減算
            Vector2 targetPos = PlayerPos - mypos;

            //弾のrighdbodyに速度を代入
            Clone.GetComponent<Rigidbody2D>().velocity = targetPos * BulletSpeeed * Time.deltaTime;
        }
    }
    //第二フェーズ
    IEnumerator Faze2()
    {
        Debug.Log("開始");
        faze2 = true;
        //iを0からはじめShootPointの配列の長さと同じまでiを1ずつ足し続ける
        for(int i = 0; i <ShootPoint.Length; i++)
        {
            //1つずつ弾を配列に代入
            EnemyBullet_C[i] = Instantiate(EnemyBullet, ShootPoint[i].transform.position, Quaternion.identity);
            yield return new WaitForSeconds(0.5f);
            Debug.Log("生成");
        }

        

        //弾のrighdbodyに速度を代入
        for (int i = 0; i < EnemyBullet_C.Length; i++)
        {
            //自身(敵機)のベクトルを取得
            Vector2 mypos = this.transform.position;

            //プレイヤーのベクトルを取得
            Vector2 PlayerPos = Player.transform.position;

            //プレイヤーのベクトルから敵機のベクトルを減算
            Vector2 targetPos = PlayerPos - mypos;
            //代入された順番に弾を発射
            EnemyBullet_C[i].GetComponent<Rigidbody2D>().velocity = targetPos * BulletSpeeed * Time.deltaTime;

            yield return new WaitForSeconds(0.5f);
        }

        fazecounter = 0;
        faze2 = false;
       
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        //Playerタグに触れたら
        if(collision.gameObject.tag == "Player")
        {
            //HPから10引く
            HP -= 10;
            Debug.Log((HP/MaxHP));
        }
    }

    void LoadTitle()
    {
        SceneManager.LoadScene("Title");
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletMovement : MonoBehaviour
{
    public float speed;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.Translate(0, speed * Time.deltaTime, 0);
    }

    private void OnBecameInvisible()
    {
        Destroy(this.gameObject);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyBullet : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private void OnBecameInvisible()
    {
        Destroy(this.gameObject);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ButtonScript : MonoBehaviour
{
    public void OnClick()
    {
        SceneManager.LoadScene("SampleScene");
    }
}


この記事が気に入ったらサポートをしてみませんか?