【Unity】2Dシューティングを作ってみる話 #3

記事をご覧いただき、誠にありがとうございます。
投稿主の無能です。

前回でプレイヤーの移動制限が出来るようになったので、今回は弾を発射出来るようにしたいと思います。

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

弾の発射を実装する

シューティングでは必須の機能、弾を発射する機能を実装していきます。

流れとしては、弾のプレハブを作成して、そのプレハブを生成し、一定の方向に向けて速度を持たせることで弾の射出という感じになります。

では、弾のプレハブを作っていきます。

弾のプレハブを作成する

弾のプレハブと言っても、まだ仮の段階なので丸いオブジェクトで代用しておきたいとおもいます。

GameObject > 2D Object > Sprites > Circle で丸を作ります。

名前は「PlayerBullet」にします。

GameObject > 2D Object > Sprites > Circle で丸を作る

名前は「PlayerBullet」とする

中央に丸(Circle)ができました。

Circleの登場

この丸の色を分かりやすく黄色にします。

黄色にする

黄色になったCircle

このままでは大きいので、TransoformのScaleで大きさを変更します。

Scaleのx・yを0.3にすると丁度良い大きさになると思います。

Scaleのx・yを0.3にする

丁度良い大きさになった

弾らしくなったので、次は本物の弾の様に当たり判定のColliderとRigidbodyを付けていきます。

Inspectorビューの下のAdd Componentから、Circle Collider 2DとRigidbody 2Dを検索してアタッチします。

Circle Collider 2Dをアタッチ

Rigidbody 2Dをアタッチ


そしてPlayerと同様に、重力を0に、Colliderのis Triggerにチェックを入れます。

Colliderのis Triggerにチェック・Gravity Scaleを0にする

あとはこれをプレハブにしてあげます。

Prefabsフォルダを作成して、PlayerBulletをドラッグすれば出来上がりです。

ProjectビューのAssetsフォルダ内にPrefabsフォルダを作成

Prefabsフォルダ内にPlayerBulletをドラッグしてPrefabを作成

そしてHierarchyビューのPlayerBulletを削除します。

これが結構忘れがちなんですが、きちんと削除しておきます。
後程このPlayerBulletをアタッチするのですが、Prefabsフォルダ内のものをアタッチします。

HierarchyビューのPlayerBulletを削除

これで弾の準備が出来ました。

このまま本体から弾を射出しても良いのですが、発射ポイントを作って、そこから弾が出るような仕組みにしたいと思います。

発射ポイントを作る

発射ポイントはGameObject > Create Empty で空のオブジェクトを作成して、名前を「PlayerFirePos」にします。

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

名前を「PlayerFirePos」にする

そしてPlayerFirePosをPlayerにドラッグして、Playerの子オブジェクトにします。

PlayerFirePosをPlayerにドラッグして、Playerの子オブジェクトにする

そうすると、Transoformのpositionのyが1になりました。

Transoformのpositionのyが1になった

Hierarchyビューで階層を変えただけなのにどうして?となりますが、一旦置いておきましょう。

次にPlayerFirePosは実体が無く視認するのが非常に困難なので、見えるようにしていきます。

PlayerFirePosにSprite Rendererというコンポーネントをアタッチします。

Sprite Rendererというコンポーネントをアタッチ


さて、アタッチしたのは良いけど何も変わりません。

ここで分かりやすいように、Spriteに適当な画像をアタッチします。
向きとPlayerFirePosの中心が分かれば何でも結構です。

今回はUnityの画像にします。

Spriteの右の〇をクリックして選択画面を開きます。
開いたら検索に「visibility」と入力して、白い目のマークを選択します。

Spriteの右の〇をクリックして選択画面を開き、検索に「visibility」と入力して、白い目のマークを選択

これでPlayerFirePosが視認出来るようになりました。

PlayerFirePosが視認出来るようになった

ワールド座標とローカル座標

さて、PlayerとPlayerFirePosは現在親子関係にあるのですが、座標の仕組みがちょっと違います。

親のPlayerは、変わらずワールド座標です。
以前お話した現実世界のGPSですね。

対して子のPlayerFirePosは、ローカル座標というものになります。

親オブジェクトの中心を0として、親オブジェクトからどれくらい離れたかを表しています。

簡単に言ってしまえば、家族で例えると親オブジェクトはそのまま親御さんで、子オブジェクトは元気一杯の子供になります。

公園などで親御さんが見守る中、元気一杯遊んでいる子供が、正にワールド座標とローカル座標の関係になります。

ワールド座標は親オブジェクトの座標で、Unityの世界でのGPSとなり、子オブジェクトのローカル座標は親オブジェクトが基準になる、という違いを覚えておきましょう。

試しにPlayerを動かすとどうなるのかを見てみます。

PlayerのRotationのzを変更すると、PlayerFirePosも追従して同じ分だけ動きます。

PlayerFirePosも追従して同じ分だけ動く

これは親オブジェクトからの距離に関わらず、親オブジェクトと全く同じ分の動きになります。

先程の家族で例えると、親と子で3m離れていたとします。
その状態で親がその場で回転すると、子も同じ分だけ回転します。

…傍目からはとてもシュールですね。
まあ何となく理解できたかと思います。

これがUnityの世界に於ける親子関係になります。
孫や曾孫も同じです。

では発射ポイントを整えていきます。

発射ポイントを整える

先程のPlayerの変化をCtrl+zキーで元に戻して、発射ポイントを整えます。

やることは、発射ポイントの中心をPlayerの機首の先端にすることと、発射する向きを決めてあげます。

ここは自由に決めて良いのですが、無能はTransoformのpositionのyを0.5、Rotationのzを0にしました。

Transoformのpositionのyを0.5、Rotationのzを0にした

良い感じの位置にできました。

あとはSprite Rendererを非アクティブにすれば発射ポイントの完成です。

Sprite Rendererを非アクティブにする

Remove Componentによる削除ではなく、非アクティブにするのは、まだPlayerが仮の段階で、後で画像を設定した時に再度発射ポイントの調整が出てくるからです。

では、弾が発射できるようにスクリプトに追記しましょう。

弾を発射する処理を追記

さてようやくお膳立てができたので、スクリプトに弾を発射する処理を追記します。

やることは、PlayerBulletを生成して、決まった方向に動けばいいわけです。

このようになりました。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    [Header("Playerの動く速さ")][SerializeField] float moveSpeed = 1f;
    [Header("移動制限の余白")]  [SerializeField] float marginX = 1f, marginY = 1f;
    [Header("弾のオブジェクト")][SerializeField] GameObject playerBullet;
    [Header("弾の速さ")]        [SerializeField] float bulletSpeed = 10f;
    [Header("発射ポイント")]    [SerializeField] Transform playerFirePos;
    [Header("弾の発射の閾値")]  [SerializeField] float bulletDelay = 1f;

    Rigidbody2D playerRB;                   // プレイヤーのRigidbody
    Vector2 moveDirection, min, max;        // プレイヤーのベクトル,画面サイズの最小ベクトル/最大ベクトル
    Vector2 playerPos;                      // プレイヤーの移動制限用
    float bulletInterval;                   // 弾の発射間隔管理用

    void Start()
    {
        // プレイヤーのRigidbodyを取得
        playerRB = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // 入力受付
        InputProcess();

        // 弾の発射 ---
        // deltaTimeを加算
        bulletInterval += Time.deltaTime;
        // 発射間隔が閾値未満の場合
        if (bulletInterval < bulletDelay)
        {
            // 処理を中断
            return;
        }
        // スペースキーが押された場合
        if (Input.GetKey(KeyCode.Space))
        {
            // 弾の発射関数を呼ぶ
            PlayerShot(playerFirePos);
        }
        // 発射間隔をリセット
        bulletInterval = 0;
        // --- ここまで

    }

    void FixedUpdate()
    {
        // プレイヤーを動かす
        PlayerMove();
    }

    // 入力受付
    void InputProcess()
    {
        // 入力を正規化して受け取る
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");
        // 入力を正規化する
        moveDirection = new Vector2(x, y).normalized;
    }

    // プレイヤーを動かす
    void PlayerMove()
    {
        // 移動制限
        MoveClamp();
        // プレイヤーを動かす
        playerRB.velocity = moveDirection * moveSpeed;
    }

    // プレイヤーの移動制限
    void MoveClamp()
    {
        // 最後の移動地点を取得
        playerPos = transform.position;
        // ビューポート座標からワールド座標を取得
        min = Camera.main.ViewportToWorldPoint(Vector2.zero);
        max = Camera.main.ViewportToWorldPoint(Vector2.one);
        // 移動を制限する
        playerPos.x = Mathf.Clamp(playerPos.x, min.x + marginX, max.x - marginX);
        playerPos.y = Mathf.Clamp(playerPos.y, min.y + marginY, max.y - marginY);
        // プレイヤーの位置を取得した最後の地点にする
        transform.position = playerPos;
    }

    // 弾の発射
    void PlayerShot(Transform firePos)
    {
        // 弾を生成
        GameObject bulletClone = Instantiate(playerBullet, firePos.position, Quaternion.identity);
        // 弾のRigidbodyに速度ベクトルをつける
        bulletClone.GetComponent<Rigidbody2D>().velocity = new Vector2(bulletSpeed, 0);
    }
}
説明の前に、Unityに戻ってスクリプトで追加した必要なものをアタッチしていきます。

HierarchyビューのPlayerを選択すると、スクリプトのパラメータが追加されています。

この中に弾のオブジェクトと発射ポイントがNoneになっているので、それぞれアタッチしていきます。

弾のオブジェクトにはPrefabsフォルダに入っているPlayerBulletを、発射ポイントにはPlayerの子オブジェクトにしたPlayerFirePosをアタッチしてください。

追加された未設定の状態のパラメータに必要なものをアタッチする

弾のオブジェクト:PlayerBullet
発射ポイント:PlayerFirePos
をアタッチする

アタッチする時の注意点ですが、PlayerBulletは必ずPrefabsフォルダ内にある方をアタッチしてください。

Hierarchyビューにもし残っているものをアタッチしてしまうと、PlayerBulletはHierarchyビューにある一発だけなので、その後は弾が生成されません。

ではゲームを実行して確認してみましょう。

スペースキーを押すと弾が発射された

スペースキーを押すと、PlayerBulletが発射されているのが確認出来ました。

弾もちゃんと発射ポイントから発射されていることが確認できます。

それでは追記分を説明していきます。

弾を生成して動かすPlayerShot関数

始めにPlayerShot関数について説明します。

PlayerShot関数は引数としてfirePosを受け取っています。

無能のコードだと44行目でPlayerShot関数を呼んでいるのですが、この時に先程アタッチしたPlayerFirePosを渡します。

PlayerShot関数の最初に弾を生成…というかコピーしてクローンを生成しています。

オブジェクトクローンの生成の際にInstantiateという関数で生成しています。
Instantiate関数はUnityで用意されている関数で、引数は(生成するオブジェクト, 生成する場所:position, 生成する角度:rotation)となります。

クローン生成するオブジェクトだけでも問題ありませんが、一般的には生成する場所と角度を指定します。
詳細はドキュメントをご覧ください。

Instantiate - Unity Documentation

生成したオブジェクトの場所は、引数で受け取ったTransoformのfirePosの位置になります。
ここではPlayerFirePosですね。

そして生成したオブジェクトをGameObjectのbulletCloneとして取得しています。

次は取得したbulletCloneのRigidbody2Dを取得し、そのvelocityに速度ベクトルを与えています。
今回は右に向かって進むベクトルです。

もし縦型シューティングであればVector2のyにbulletSpeedを設定します。

あまり無いと思いますが、下向きであれば-bulletSpeedにします。
左向きの時も同様です。

ここまでがPlayerShot関数の内容になります。

次はUpdate関数内の記述を見ていきます。

Update関数内に弾を発射する処理を作る

Update関数内では、流れとしてTime.delataTimeでカウントアップのタイマーを作成し、閾値を超えたら弾が発射出来るようにしています。

では具体的に見ていきます。

bulletInetrvalにTime.delataTimeを足し合わせて代入しています。

この+=は加算代入演算子という算術演算子の一つになります。

勿論四則演算の全てが〇算代入演算子として使用できます。
詳細はドキュメントをご覧ください。

加算演算子 - + と+= - Microsoft Learn

カウントアップしたbulletInetrvalは、次のif文の条件式で閾値となるbulletDelayと比較して、bulletDelay未満であればreturnで処理を中断しています。

returnは処理を中断して呼ばれたところへ戻るのですが、戻り先が無いのでただ中断するという処理になります。

ここで閾値未満で処理が中断されているので、閾値を超えないと次の処理に行けません。

閾値を超えた場合はどうなっているかと言うと、キーボードのスペースキーが押されたらPlayerShot関数をPlayerFirePosと言う引数を付けて呼びます。

これで先程のPlayerShot関数での処理になる、という感じです。

最後にカウントアップしたbulletIntervalを0に戻しています。

このカウントアップの処理のおかげで、スペースキーを押しっぱなしでも一定の間隔でしか弾が発射出来ない、というわけです。

弾の発射間隔をもっと早くしたい場合は、UnityでPlayerのInspectorビューのbulletDelayを
小さくしてください。
閾値が下がればカウントアップで閾値を超えるタイミングが早くなります。

スクリプトは読んでも書いても勉強になる

一見すると複雑そうなスクリプトでも、蓋を開けてみると実際はそんなに難しくなくて、今まで書いてきたスクリプトの処理の組み合わせなので、尻込みせずにどんどんスクリプトを書いたり読んだりしてみてください。

誰かの書いたコードは非常に勉強になります。
そして自分でコードを書く時は、大体想定外な事になって、工夫してコードがより良くなっていきます。
一発で想定通りに動くととても気持ちが良く、思わずガッツポーズまで出てしまいます(無能だけかな…)。

まあスクリプトを書く楽しみはそれぞれだと思うので、臆せずやっていきましょう。

まとめ

今回は
  • 弾のプレハブを作った
  • 発射ポイントを作成した
  • スペースキーを押して弾を発射出来る処理をスクリプトに追加した
という事をやりました。

次は、弾を発射しても遥か彼方に行ってしまうので、弾を消す仕組みを作りたいと思います。

では、また次回!

コメント