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

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

投稿主の無能です。

今回は、2Dのシューティングゲームを作ってみます。
あくまでも一例となりますが、工夫次第でいくらでも面白いゲームになると思いますので、参考程度にしていただけると嬉しいです。

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

前提条件

必須ではないですが、予めやっておくと理解がスムーズになるので、「Roll a Ball」などのチュートリアルをやって、UnityやVisual Studioなどの操作に慣れておくことをお勧めします。

そして以下が無能の環境となりますので、良ければ参考にしてください。
  • PC:Windows Surface Laptop 2(Corei7-8650U RAM:8GB)
  • Unity Editor:2021.3.19f1
  • Visual Studio:Visual Studio 2022 17.5.0
この記事は、無能の以前の「Roll a Ball」のチュートリアルを終えたという前提でお話したいと思います。

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

プロジェクトの新規作成

まずは新規プロジェクトの作成をします。
今回作るゲームは2Dなので、プロジェクトは2DURPを選択し、名前を決めて任意の場所を保存場所に指定します。

プロジェクト名は「2DShootingSample」とします。

2D(URP)を新規プロジェクトとして作成

プロジェクトが作成されたら、UnityEditorのレイアウトを変更します。
レイアウトは「Roll a Ball」で使用したレイアウトにします。

「Roll a Ball」で使用したレイアウト

まず3Dとの比較として、奥行きとなるz軸が無いのでx軸とy軸の2次元になります。
ですので、見た感じSceneビューやGameビューは、画像のような平面になります。

でも3Dが全く使えないかと言えば違っていて、3Dも勿論扱えるようになっています。
簡単に言ってしまうと「見え方が2Dっぽい」というだけなので、そこだけ留意しておいてください。

これからオブジェクトを作成して手を加えるのですが、2Dとか3Dとかで区別してしまうと、少し理解が追い付かない部分が出て来てしまうこともあるので、あくまでも「見え方が2Dっぽいんだなぁ」という位で認識すると良いかも知れません。

では、次に背景を黒にしてみます。

背景を黒にする

背景を黒にして、ゲームオブジェクトが見やすいようにしていきます。

シーン上にあるMain Camera > Camera > EnvironmentのBack Groundの色相環で黒を選択すると色が変更できます。

Main Camera > Camera > EnvironmentのBack Groundの色相環で色の変更が可能

背景色を黒に変更

ではオブジェクトを追加していきます。

三角形を追加する

プレイヤーとなるオブジェクトを追加してみます。
今の段階では仮の状態になるので、Game Object > 2D Object > Sprites > Triangleで三角形を追加します。

Game Object > 2D Object > Sprites > Triangleで三角形を追加

追加された三角形の名前を「Player」とします。

三角形の名前を「Player」とする

今回は横スクロールのシューティングにしたいと思うので、三角形の先端が自機の前方方向になるように回転させます。

ここでちょっとこんがらがってしまうかもしれませんが、平面上のオブジェクトを時計回り・反時計回りに回転させる時はz軸を使って回転させます。

「ん?」と思われたかも知れませんが、x軸・y軸を基準にしても回転できます。

「z軸は使わないんじゃないの?」と思われた方は、無能が「2Dっぽく見えているだけ」というお話をしたことを思い出してみてください。

あくまで「2Dっぽい」と言うだけで、実際は3次元でオブジェクトは描画されている、という感じです。
実際に、Rotationのxを変化させてみると、三角形が手前や奥に倒れるような感じに回転することが見て取れます。

Rotationのxを変化させる:奥や手前に倒れ込むような回転をする

Rotationのyを変化させると、風見鶏の様に真ん中を軸にくるくる回転します。

Rotationのyを変化させる:真ん中を中心に回転をする

このように、実際に動かしてみると「2Dっぽく見えるだけ」というのが何となく理解できるかと思います。

要は「回転させる時はどの軸を基準にするか」という事を念頭に置けば、自ずと方向が分かってくるかと思います。

どうしても基準の軸が分からないという場合は、フレミングの左手の法則の左手を作ってみてください。
右手でも大丈夫です。

そして人差し指をオブジェクトの進む向きに合わせると、どの軸で回転すればいいかが分かります。
人差し指がx軸、中指がy軸、親指がz軸となるので、3本の指のいずれかを固定させて手を回転させてみてください。
するとオブジェクトの回転と基準となる軸がわかると思います。

…え、分からない?
すみません。自己流です。
ただ、自分で回転方向を簡単に表現できないかと思っていたら、意外にもフレミングの左手の形がマッチしたので、ご紹介させていただきました(汗)。

ともあれ、基準の軸に合わせて回転させるという事と、あくまで2Dっぽく見えるということだけ抑えておいてください。
回転はこの後もでてくるので、ゲーム作りに於いては結構重要な要素なんだな、と頭の片隅においておければ大丈夫です。

余談が過ぎましたが、z軸を-90°に設定すると、進行方向が左になった三角形になります。

z軸を-90°に設定

次は、Playerに必要なコンポーネントをアタッチしていきます。

Playerに必要なコンポーネントをアタッチする

Playerに必要になるコンポーネントは、
  • 動きを与えるためのRigidbody
  • 敵や障害物からダメージを受けるための当たり判定
が、今のPlayerには必須のコンポーネントになります。
なので上記の二つをアタッチしていきます。

PlayerをHieraruchyビューで選択し、Inspectorビューの最下にある「Add Component」ボタンを押下して追加します。

初めにRigidbodyから追加していきます。

Playerを選択して、Inspectorビューの最下にある「Add Component」ボタンを押下したら、そのまま「Rigidbody」と検索します。

HierarchyビューでPlayerを選択 > Add Componentを押下 > 「Rigidbody」で検索

ここで忘れがちになってしまうのですが、今作っているゲームは2Dになります。

ですので、ここで選択しなければならないのは2D用の「Rigidbody 2D」であることです。

このように、いくら前述した「2Dっぽい」という環境下でも、2Dとして動かすためには相応の設定が必要になります。
このコンポーネントの設定も意外に間違えやすい設定なので、注意して下さい。

後で設定するオブジェクトに設定するColliderを2D用のColliderではなくて3D用のものをアタッチして、ハマって30分程度浪費する…なんてこともあったりします…(経験者は語る)。

ということで、「Rigidbody 2D」をアタッチします。

Rigidbody 2Dをアタッチ

次に当たり判定となるColliderを設定します。

ColliderはPlayerの周りを覆う形でアタッチ出来ればいいのですが、三角形のColliderはありません。

そこで今回アタッチするColliderが「Polygon Collider」になります。

Polygon Colliderは線でオブジェクトを囲う形のColliderになっていて、複雑な形状のものでも覆う事が可能なColliderです。

現状だと三角形を覆う形のColliderですが、後程プレイヤーには戦闘機の形をあてがうので、戦闘機のような複雑な形状でも覆う事が可能なColliderです。

では「Add Component」ボタンを押下して、Polygon Colliderを検索してアタッチしてください。

Polygon Colliderを検索

Polygon Colliderをアタッチ

これで必要なコンポーネントをアタッチ出来たので、次にコンポーネントの設定をします。

まずは重力は不要なので、Rigidbody 2DのGravity Scaleを0にします。

Rigidbody 2DのGravity Scaleを0にする

これで重力が反映されなくなりました。

次にPolygon Collider 2Dのis Triggerにチェックを入れます。

Polygon Collider 2Dのis Triggerにチェックを入れる

これでColliderに当たっても、当たったという判定は残しつつ、Colliderを通過出来るようになりました。

では次に、スクリプトを書いていきます。

PlayerControllerスクリプトを記述する

今回の2Dシューティングのプロジェクトは、スクリプトによる制御が前回の「Roll a Ball」に比べて結構多めになっています。
それだけ細部にもスクリプトでの制御が必要と言うことなのですが、理解してしまえば「Roll a Ball」で書いたスクリプトと大差無いと感じると思います。

ですので、頑張ってみてください。

まずはProjectビューのAssetsフォルダ内に「Scripts」フォルダを作成して、Scriptsフォルダ内に「PlayerController」スクリプトを作成して、Playerにアタッチします。

ここの流れは「Roll a Ball」と同じですね。

Scriptsフォルダを作成

Scriptsフォルダ内に「PlayerController」スクリプトを作成

PlayerControllerスクリプトをPlayerにアタッチ

スクリプトを記述する準備が出来たので、Visual Studioを起動してスクリプトを書いていきます。

Playerの動かすスクリプトを記述する

何はともあれ、まずはPlayerが動かないと始まらないので、キーボードの十字キーまたはWASDキーから入力を受け取って、Playerが動く仕組みを作りたいと思います。

スクリプトの日本語の文字化けを修正する

ここで一つ、Unity Editor上でスクリプトのプレビューの日本語が文字化けしている問題について、修正する方法をご紹介します。

そもそも何故文字化けが起こるかなんですが、単純にエンコードの問題です。
Unity EditorがUTF-8、Visual StudioがShift-JISのエンコードなので、文字化けが起こっているというのが理由です。

ですので、Visual Studioの保存時にエンコードをUTF-8に指定して保存すれば文字化けが起こらなくなります。
その方法は以下の手順です。

スクリプトファイルの保存時に、ファイル > 名前を付けて○○.csを保存(○○はファイル名)を選択します。

ファイル > 名前を付けて○○.csを保存を選択

すると保存先を指定するダイアログが出るので、右下の「上書き保存(S)」の横にある三角形を押下します。

「上書き保存(S)」の横にある三角形を押下

選択項目に「エンコード付きで保存(V)」があるので選択します。

エンコード付きで保存(V)を選択

「保存オプションの詳細設定」ウィンドウが表示されるので、エンコードのプルダウンから「Unicode(UTF-8 シグネチャ付き) - コード 65001」を選択してOKボタンを押下します。

「Unicode(UTF-8 シグネチャ付き) - コード 65001」を選択してOKボタンを押下

するとエンコードが無事UTF-8になって、Unity Editorのプレビューでも日本語が正しく表示されるようになりました。

Unity Editorのプレビューでも日本語が正しく表示されるようになった

入力の受け取りはRoll a Ballと同じ

キーボードの入力の受付は、Roll a Ballで既にやっています。
水平方向をx、垂直方向をyとしたInput.GetAxisですね。

今回はGetAxisを正規化したGetAxisRawを使って入力を取得します。

何はともあれ記述しましょう。
このようになります。

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

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    [Header("Playerの動く速さ")][SerializeField] float moveSpeed = 1f;

    Rigidbody2D playerRB;       // プレイヤーのRigidbody
    Vector2 moveDirection;      // プレイヤーのベクトル

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

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

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

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

    // プレイヤーを動かす
    void PlayerMove()
    {
        // プレイヤーを動かす
        playerRB.velocity = moveDirection * moveSpeed;
    }
}
何やら初めて見る記述が多いですね。

順番に説明していきます。

RequireComponent

先ず目につくのは、クラスの宣言の前にあるRequireComponentではないでしょうか。

この一文では、コンポーネントの要求をしています。
要求しているのはtypeofで指定したコンポーネントになります。
このスクリプトの場合はRigidbody2Dですね。

この記述があると何が変わるのか?というところですが、もしtypeofで要求しているコンポーネントがPlayerにアタッチされていない場合、ここではRigidbody2Dを要求しているので、自動でRigidbody2Dがアタッチされます。

これだけでは無く、RequireComponentで要求されたコンポーネントは、Remove Componentで外せなくなります。

Remove Componentで削除しようとするとダイアログが出て削除できない

つまりRequireComponentの記述が(コメントアウトや削除によって)無くならない限り、自動的にアタッチされて外せないコンポーネントになります。

これの何が良いかと言うと、人為的なミス(ヒューマンエラー)が起こる確率がグッと減ることです。
アタッチし忘れでスクリプトがエラーを吐く…なんてことが防止出来るようになります。

書き方は
[RequireComponent(typeof(目的のコンポーネントの種類))]
となっているので、このまままるっと暗記してしまいましょう。

Header

次に目につくのがHeaderです。

これは見た方が早いですね。
Unity EditorのInspectorビューの項目に見出しが付いています。

Inspectorビューの項目に見出しが付いている

このように今までスクリプトのコメントにしていたものが、Inspectorビュー上で表示出来るようになるので、コメント量の削減やパラメータの明確化にも役に立ちます。

書き方は[Header("表示したい文字列")]となっていますので、こちらも暗記してしまいましょう。

SerializeField

次はSerializeFieldです。

シリアライズって何だ?という話になってきますが、シリアライズはシリアル化する、という意味合いになります。

シリアル化というのは、プログラム全般での意味からすると、様々な要素を組み合わせた複合的なデータや、コンピュータ(PC)上で実行中のプログラムで展開しているオブジェクト等を、一定のデータ形式変換規則に従い、文字列やバイト形式に変換して保存や送受信可能にすることを指すみたいです。

うーん…分からん!
ですよね。

この辺りは追々理解するとして、UnityではSerializeFieldだとどのような挙動になるのかを簡単に説明すると、publicの変数のようにInspectorビューに表示されるようになるけど、SerializeFieldは外部からのアクセスが出来ない、という状態になります。

まとめるとInspectorビューに表示されるが、他のスクリプトからアクセスできない、というものになります。

前述したスクリプトで例にすると、moveSpeedというfloat型の変数は、アクセス修飾子を省略しているのでprivateになっています。

ところがUnityEditorのInspectorビューに表示されて、パラメータを変更することが可能になっていますね。

これがSerializeFieldになります。

めっちゃ便利!…なんですが、勿論シリアライズするためのルールが存在します。
Unityのドキュメントに詳細が記載されているので、もしSerializeFieldでシリアル化出来ない場合は、一度目を通すと良いでしょう。

スクリプトのシリアル化 - Unity Documentation

SerializeFieldの書き方は[SerializeField] 変数となります。

これまでに記述してきたpublicとprivateに含めて上手く使い分けることで、より柔軟で安全なコードが書けるようになると思います。

Input.GetAxisRawとは

さて、ここまでは見慣れないものたちの説明でしたが、Start関数以降は馴染みがある記述ですね。

その中で説明が必要になるのは、Input.GetAxisRawですね。
Input.GetAxisとどう違うのかを説明していきます。

Input.GetAxisでは、方向キーで入力された値を0から1または0から-1の間の小数で表現していました。
対して正規化するInput.GetAxisRawでは、方向キーが押されたと判断すると即座に1または-1を返すようになります。

要は中間が無くなって、押されたか押されていないか、と言った判断になります。
イメージで言えば、押されたら手を挙げる、押されない場合は手を下げたまま、といったイメージになるでしょうか。

このようにInput.GetAxisRawでは正規化された入力値となって、その値をベクトルとして使っている、という事になります。

normalizedでベクトルを正規化

Input.GetAxisRawで正規化した入力値をmoveDirectionというベクトルにすることは分かりました。

moveDirectionの右辺の最後には.normalizedというものがくっついているのですが、これは何だ?という事で説明します。

このnormalizedというのも正規化するものとなります。

正規化したものをまた正規化するの?と疑問に感じてしまうかもしれませんが、normalizedでの正規化とは、ベクトルの長さを1にするという正規化になります。

ベクトルの長さを正規化すると何が良いのかというと、ベクトルの方向は変わらずに長さが1になる、ということです。

例えば、今のスクリプトでx・y共に1ずつ入力されたとします。
すると斜めの長さはどうなるでしょうか?

答えは三平方の定理で出すことができます。
x・yが共に1なので、縦横の線を引いてみると、斜めの線は直角二等辺三角形の斜辺になります。
三平方の定理で直角二等辺三角形の斜辺は1:1:√2なので、斜めに移動した時は√2の長さ、つまり1÷1.414…の0.7021…になります。

実際にスクリプトで正規化したベクトルと正規化していないベクトルの数値を表示してみます。
表示させる数値はmoveDirectionです。

正規化していないmoveDirection

正規化したmoveDirection

正規化したものは、先程求めた近似値になっていることが分かります。

これで斜めの移動だけが突出して速い、ということも無くなります。

自作の関数で処理を分けている理由

さて、分からない部分は大分解消されたと思いますが、どうして自作の関数で処理を分けているのか、という疑問を感じた方もいると思います。

理由は単純で、この後もスクリプトを追記していくので、どうしてもUpdate関数内がごちゃついたりしてきます。

なので関数を作成して処理をなるべく可視化する、つまりは見やすく流れが分かりやすいようにしたくて、関数を作って分けています。

今回はプレイヤーが方向キーで動くだけのスクリプトですが、弾を発射したり敵とぶつかったり…と幾つもの処理が必要になってきます。
それを追記していくと、可読性の高いコードにしておかないとどの処理がどこのスクリプトにあるのかが把握出来なくなって、他人も自分も理解不能なコードが出来てしまうのを防ぐために、関数で処理を分けるという方法をとっています。

あくまでも一つの方法なので、皆さんの方法で問題ありません。
無能はこうした、というだけのことです。

可読性の高いコードになるように頑張りましょう!

Unity Editorで実行して確認

では実際に動かして確認してみます。

このままだと遅くてイライラするので、InspectorビューでmoveSpeedを5にします。

moveSpeedを5にする

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

プレイヤーが方向キーで動けるようになりました。

プレイヤーを動かせるようになった

ただ動かせると言っても、制限が無い状態なので、カメラの画角から外れて縦横無尽に動けてしまいます。

カメラの画角から外れて動けてしまう

これだとカメラの画角内でしか敵のオブジェクトが出せない場合に、プレイヤーが画角外に出てしまえば絶対無敵になってしまいます。

次回はこのカメラの画角から外れてしまう現象を解決したいと思います。

まとめ

今回は
  • 新規プロジェクトを作成した
  • プレイヤーとなる三角形のオブジェクトを作った
  • プレイヤーにコンポーネントをアタッチし、スクリプトで動けるようにした
という事をやりました。

次回はカメラの画角から外れてしまう現象を解決していこうと思います。

では、また次回!

コメント