解説

スクリプトの全体像【Unityの教科書#4】

この記事では、C#スクリプトの「クラス」に関する解説とUnityと絡めたC#スクリプトの操作について説明していきます。

アクセス修飾子

まずはC#におけるアクセス修飾子について説明していきます。アクセス修飾子を理解することで、プログラマーとして大きく成長できます。ここで頑張って学習しましょう!

アクセス修飾子とは

アクセス修飾子とは、変数などの公開範囲を設定するためにつける設定値のことです。つまり、スクリプトの外からのアクセスを許可するかどうかに関わります。
今回は特に、publicprivateの二種類のアクセス修飾子について説明していきます。

public修飾子とprivate修飾子

public修飾子とprivate修飾子の違いは以下の通りです。

  • public修飾子:スクリプトの外からアクセス可能にする。
  • private修飾子:スクリプトの外からアクセス不可にする。

具体的な例を見てみましょう!
Unity上でスクリプトSampleを作成しましょう。
Create>C#Scriptと選び、ファイル名を”Sample”として以下のコードを記述してください。

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

public class Sample : MonoBehaviour
{
    public int pub; //publicなので外からアクセスできる

    private int pri; //privateなのでこのスクリプト中からしかアクセスできない

    int num; //修飾子を付けなかった場合はprivateとして扱われる

    void Start()
    {
        //何かしらの処理
    }
}

このスクリプトをオブジェクトにアタッチしてみましょう。
すると、インスペクタービューから変数pubの値のみ変更可能なことが確認できると思います。

その他の宣言時キーワード

publicとprivate以外にも宣言時につけるキーワードは存在します。ここではその一部を軽く紹介するだけにとどめておくので、気になる方は各自で調べてみてください。

  • const:その変数を書き換え不可な定数として宣言するときに使用する。
  • readonly:使用する場所によって、値が変わる可能性がある定数を宣言するときに使用する。

private変数をEditor上に表示する

public変数にすることでインスペクターから値を変更できるようになりましたが、実は、SerializeFieldを使うことでprivate変数も同様に編集できるようになります。

先程の例を以下のように変更してみます。

public int pub; //publicなので外からアクセスできる

//↓インスペクタービューからアクセスできるようにする
[SerializeField] private int pri; //privateなのでこのスクリプト中からしかアクセスできない

そうすると、インスペクタービューから変数priも変更できるようになっていることが確認できると思います。ただ注意点として、インスペクタービューから値の変更ができる場合、優先されるのはインスペクタービューの方になります。スクリプト内で値の宣言をしている場合は気を付けましょう。

ただインスペクタービューから変更できるようにするならpublicでいいのでは、と思う人もいるでしょう。
しかし、SerializeFieldで見えるようになった変数はprivate修飾子のままなので、スクリプトの外からアクセスされることがありません。そのため、勝手に値が書き換わってしまうなどの想定していない事故を防ぐことができるというメリットがあります。

他のクラスから参照する方法

今までコンポーネントをGetComponent<>()で、ゲームオブジェクトをgameObjectで取得してきましたが、これではスクリプトをアタッチしているオブジェクトしか参照することができません。ここからは自身のスクリプトのアタッチ先オブジェクト以外への参照方法を説明します。

コンポーネントを参照する

コンポーネントの値を操作したい際にはSerializeFieldから参照するのが楽なのですが、GetComponentを使用することでスクリプト内でコンポーネントの取得を完結することができます。

SerializeField

まず、以下のように変数を宣言します。

[SerializeField] Rigidbody2D rb2d;

Editorに行きます。参照したいRigidbody2Dが付いているゲームオブジェクトを、インスペクターに表示されたスクリプトのrb2dにドラッグ&ドロップして使います。

<手順の写真を載せてください>

GetComponent

GetComponentを使うことで、オブジェクトに付いたコンポーネントの参照を取得することができます。これは前のコンポーネント操作の記事で多く登場していたと思います。使用例は以下の通りです。今回はRigidbody2Dを取得しています。

Rigidbody2D rb2d = GetComponent<Rigidbody2D>();

使う際の注意点として、Update関数内で使わないようにしましょう。コンポーネントは実行中に何度も付け外しするようなものではないため、一度取得してしまえば基本問題ないからです。なので使用するときは、Start関数内で一つのコンポーネントに対して一度だけということを心がけましょう。

ゲームオブジェクトを参照する

ここからは、スクリプト内でゲームオブジェクトを取得する方法を紹介します。ゲームオブジェクトを取得する方法はたくさん存在します。基本的にはSerializeFieldで参照して取得するのが楽なのでおすすめしますが、スクリプト内で参照を完結する方法として代表的なものもいくつか紹介します。

SerializeField

まず、以下のように変数を宣言します。

[SerializeField] GameObject obj;

Editorに行きます。参照したいゲームオブジェクトを、インスペクターに表示されたスクリプトのobjにドラッグ&ドロップして使います。

<画像を付けてください>

GameObject.Find

GameObject.Findを使うことで、シーン上にあるすべてのアクティブなゲームオブジェクトの中から指定した名前のものを一つ取得します。使用例は以下の通りです。

GameObject obj = GameObject.Find("探したい名前");

同じ名前のゲームオブジェクトは基本的に存在しません。そのため、上記のFindObjectOfTypeと異なりいつでも求めたものが得られます。
またFindObjectOfTypeと同様に、シーン上にあるすべてのゲームオブジェクトを検索するため非常に重たい処理になります。そのため、Update関数内で使わないようにしましょう

GameObject.FindGameObjectWithTag

GameObject.FindGameObjectWithTagを使うことで、シーン上にあるすべてのアクティブなゲームオブジェクトの中から指定したタグのものを一つ取得します。使用例は以下の通りです。

GameObject obj = GameObject.FindGameObjectWithTag("探したいタグ名");

ただし、同じタグが複数のゲームオブジェクトに設定されている場合はうまく動作しません。この場合は、GameObject.FindGameObjectsWithTagを使うことでそれらをすべて取得することができます。使用例は以下の通りです。

GameObject[] items = GameObject.FindGameObjectsWithTag("探したいタグ名");

使用方法はほぼ一緒ですが、代入する変数を配列で宣言することには注意してください。

他にもゲームオブジェクトを取得する方法があります(重要度低め)

FindObjectOfType

FindObjectOfTypeを使うことで、シーン上にあるすべてのゲームオブジェクトの中から特定のコンポーネントをアタッチされているものを一つ取得します。使用例は以下の通りです。

GameObject obj = FindObjectOfType<探したいコンポーネント名>();

ただし、シーン上に条件を満たすゲームオブジェクトが複数ある場合には求めたものが得られない可能性があります。その場合は、FindObjectsOfTypeを使うことで、それらをすべて取得することができます。使用例は以下の通りです。

GameObject[] items = FindObjectsOfType<探したいコンポーネント名>();

使用方法はほぼ一緒ですが、代入する変数を配列で宣言することには注意してください。

また、どちらもシーン上にあるすべてのゲームオブジェクトを検索するため非常に重たい処理になります。そのため、Update関数内で使わないようにしましょう

Transform.Find

ここではまず事前知識として、子オブジェクトについて説明していきます。まずは画像のようにChild1をParentにドラッグ&ドロップしてください。

すると、このような形になったと思います。この場合のParentが親オブジェクト、Child1が子オブジェクトになります。子オブジェクトの数に制限はありません。

親子関係が成立すると、親オブジェクトの変化が子オブジェクトにも反映されるようになります。実際に、Parentの座標を動かしたり大きさを変更したりしてみましょう。Child1も同様の挙動をすることが確認できます。

子オブジェクトについて分かったところで、Transform.Findの説明に戻ります。Transform.Findを使うことで、自身のすべての子オブジェクトの中から指定した名前のオブジェクトを一つ取得します。使用例は以下の通りです。

//親オブジェクトを探す
GameObject ParentObj = GameObject.Find("親オブジェクトの名前");

//子オブジェクトの中から探す
GameObject ChildObj = ParentObj.transform.Find("探したい名前").gameObject;

子オブジェクトの中から探す関係上、先に親オブジェクトを探す必要があります。今回は、先ほど紹介したGameObject.Findを使いました。Transform.Findの良い点は、非アクティブなオブジェクトでも取得可能なことです。

コンポーネントからゲームオブジェクトを取得する

実は、コンポーネントからゲームオブジェクトを取得することが可能です。使用例は以下の通りです。

Rigidbody2D rb2d = GetComponent<Rigidbody2D>();

GameObject obj = rb2d.gameObject;

この例では、GetComponentで取得したコンポーネントを使用して、そのコンポーネントが付いているゲームオブジェクトを取得しています。

今まで紹介したものの他にシングルトン(Singleton)というものもありますが、ここでは名前だけの紹介とさせていただきます。詳しくは、発展編で紹介しているのでそちらを見てください。

クラスについて

クラスについて

C#におけるクラスについて、軽く説明したいと思います。
クラスとは、簡単に言ってしまえば設計図のようなものです。
と、いきなり言われてもあまりわからないと思うので、最初は、スクリプトを作成することがクラスを作成することと考えてもらって構いません。今までさんざんやってきたことですね。
基本的に、クラスとは、今まで扱ってきたこと(変数やメソッド等を記述する)を持つ概念のようなものです。

ここではとりあえず、機能と要素を列挙していきたいと思います。
これらのいずれも、今の時点ではそこまで理解しなくてもよいです。こんなものがあるんだな、程度でよいです。実際にやってみて、気になったら発展編を覗いてみてください。

フィールド(変数)

クラス内でデータを保持するための変数です。例えば、犬のクラスならば、名前や年齢が属性として考えられます。フィールドはクラスの中で定義され、アクセス修飾子 (public、private、protectedなど) を使用してアクセス制御が可能です。一般的に、フィールドはprivateで宣言され、外部からはアクセスできないようにします。変数のことと思ってもらってよいです。

メソッド(関数)

クラスが持つ振る舞いや操作を表します。例えば、犬のクラスならば、吠える・走る・食べるなどがメソッドとして考えられます。
もう少し具体的に言うと、何かしらの値(プレイヤーの速度や攻撃力)を変化させる(増やしたり減らしたり)する操作などのことです。

インスタンス

クラスの実体化したものを指します。クラスは、データ(属性)と操作(メソッド)をまとめた設計図のようなものであり、実際のデータを持つオブジェクトを生成するためには、そのクラスのインスタンスを作成する必要があります。

インスタンスの作成には、new演算子を使用します。これについてはVector2の項目で解説します。

継承

他のクラスから属性やメソッドを引き継ぐことができます。これにより、コードの再利用性が向上し、階層的な関係を表現できます。
簡単に言ってしまえば、クラス名の横にある、クラス名:MonoBehaviourの:MonoBehaviourの部分です。これはMonoBehaviourというクラスを継承していることを示します。これも詳しくは発展を参照してください。

Vector2

では、次はおそらく皆さんが頻繁に使うことになるであろう、Vector2について説明していきます。「C#の基礎 #2」でも変数として軽く説明しましたが、ここではより詳しく解説します。

Vector2は、2次元のベクトルを表すための構造体です。主に2次元空間内で位置や方向を表現するために使用されます。主に移動、衝突判定、描画などの操作に使用されます。

構造体については、クラスに似たようなもの、と思ってもらって構いません。大体はクラスと使い方も同じなので、クラスに続けてVector2を説明します。
構造体については、発展にて解説します。

では、これらを用いてコードを書いてみましょう。
Unity上でスクリプトTestを作成して、以下のコードを記述してください。

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

public class Test : MonoBehaviour
{

    void Update()
    {
        // 移動方向を設定
        Vector2 movevector = new Vector2(1,1);

        // 現在の位置から移動先を計算
        transform.Translate(movevector);
    }
}

簡単にコードの説明をします。

Vector2 movevector = new Vector2(1, 1);

ここでは、Vector2型の変数speedに(1,1)のベクトルを代入しています。この時のnewについては、クラスや構造体の新しいインスタンスを作成するために使われるものです。

構造体Vector2をnew演算子で実体化しているということです。

new演算子は少し難易度が高いので、ここではいわゆるおまじないのようなものと理解してください。

transform.Translate(movevector);

ここでは、オブジェクトのトランスフォームを変更することで、オブジェクトの位置を変更しています。

では、さっそくコードを実行してみましょう。Unity上でスクリプトをオブジェクトにアタッチしてみてください。

実行すると、右斜め上方向に進んでいくのがわかると思います。ベクトルの方向として(1,1)なので、マイフレームごとにxに1,yに1進んでいるということです。

Vector3という構造体もあります。これは、Vector2が2次元のベクトルだったのに対して、Vector3は3次元のベクトルを表します。これらの使い分けは、主に3Dゲームか2Dゲームかどうかで分けられています。

usingディレクティブ

最後に、今まで書いてきたスクリプトの一番上のほうに合ったコードについて説明したいと思います。

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

これらは今までは特に触れてきませんでしたが、最後に軽く説明したいと思います。
これらはusingディレクティブというものです。
これは言ってみれば事前準備のようなもので、ここに追加することで様々な便利機能を追加することができます。上3行は基本的なもので、Unity側で勝手に用意してくれているものです。

今後、ゲーム開発を進めていくうえで様々な機能が必要になります。UI(ユーザーインターフェイス)を簡単に表示させたり、楽にアニメーションを動かしたり、テキストをきれいに表示させたり。

少し先取りになりますが、上に挙げた例を追加する場合には、それぞれ
using UnityEngine.UI;
using DG.Tweening;
using TMPro;

の追加が必要になります。

これらの機能は、Unity標準の機能でもできなくはないですが、とても大変です。
なので、ここに様々なものを前準備させておくことで、簡単に使える機能を追加できるようになります。

具体例を見ていきましょう。

以下のコードを書いてください。

このコードは簡単に言うと、テキストを表示させるコードです。

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

public class Test : MonoBehaviour
{
    public Text text;

    public string message = "Hello, World!";

    void Start()
    {
        text.text = message;
    }
}

しかし、このコードではエラーが発生していると思います。エラー部分(波線が引かれていると思います。)にカーソルを当ててみると、以下のようなエラーが出ると思います。

型または名前空間の名前 'Text' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)

これまたわからない文章が出てきました。ですが見たことがある文章(using ディレクティブなど)もあると思います。
このエラー文を簡単に説明すると、「知らない、準備されていないものが使用されていますよ、きちんと準備してあるか確認してね」ということです。
なので上の部分に以下のように変更してください。

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

これでエラーを吐かなくなったと思います。Textという型を使用するには、using UnityEngine.UI;という前準備を追加することが必要だったんですね。

これは、よく慣れてきたころにうっかりで忘れがちです。慣れてきた時こそ注意していきましょう。

なんでusingが必要になるのか気になる人へ

C#プログラミングにおいて、コードを整理し、区別しやすくするためにnamespaceという仕組みが使われます。イメージとしては、書類を整理するフォルダや引き出しのようなものです。これにより、大きなプロジェクトを作る際に別の名前空間であれば同じクラス名が重複しても大丈夫になり、命名管理がしやすくなります。usingディレクティブは、C#で他のnamespaceを参照するための仕組みです。usingで追加しているのは名前空間のことなんだな、程度に考えておいてください。

コルーチン

ゲーム開発において、何かを待って実行する処理を書きたいことがよくあります。例えば、数秒ごとに弾を発射する、会話イベントで選択肢を選ぶまで待ってから処理をする、シーンのロードが終わってから表示するなどです。

このような時に便利なUnityの機能がコルーチン(Coroutine)です。コルーチンを使用すると、このような非同期処理を簡単に実装できます。

非同期処理とは、あるタスクが完了するのを待っている間に、他のタスクを進めることができる手法です。例えば、レストランで注文後に料理が出来上がるのを待つ間に、それと関係なくゲームのことを考えているようなものです。料理の到着(あるタスク)を待っている間にも、他のこと(他のタスク)を進めることができます。逆に、今まで見てきたものはほとんどが同期処理で、今の処理が完了してから次の処理に進みます。同期処理の待ち時間が長いと全体の処理速度が遅くなる可能性があります。

コルーチンを開始する

Unityで非同期処理を簡単に実現させるためのコルーチンの基本的な使い方を見てみましょう。以下は、最初にコンソールにログを出力し、その後3秒待ってから再びログを出力するシンプルな例です。

using System.Collections;
using UnityEngine;

public class : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(WaitAndLog());
    }

    IEnumerator WaitAndLog()
    {
        Debug.Log("Start");
        // 3秒待つ
        yield return new WaitForSeconds(3f);
        // 3秒後に実行する処理
        Debug.Log($"3秒経過(Time:{Time.time})");
    }
}

UnityのコルーチンはIEnumeratorを返すメソッドとして定義します。このメソッド内ではyield return文を使用して処理を待機させることができます。コルーチンを開始するにはStartCoroutineメソッドを呼び出し、その引数としてコルーチンを定義したメソッドの返り値を渡します。ここでは動作原理には踏み込まないので、まずは書き方がわかれば大丈夫です。

$""の書き方については文字列補完を参照してください。

非推奨のコルーチンの呼び出し方

コルーチンの使い方を調べると

StartCoroutine("WaitAndLog");

というように、StartCoroutineの引数にメソッドの名前を文字列として渡す例があります。これでも実行はできますが、文字列として渡すことでバグの要因になりうることやパフォーマンスの問題等があるため、使わないようにしてください

コルーチンを停止する

コルーチンを外部から停止する方法としてStopCoroutineメソッドを紹介します。以下は、x座標に一定速度で移動するコルーチンを開始し、Sキーを押すとそのコルーチンを停止し、Rキーを押すと再開する例です。

using UnityEngine;
using System.Collections;

public class MoveWithConstantSpeedExample : MonoBehaviour
{
    float speed = 5.0f; // 移動速度
    Coroutine moveCoroutine;

    void Start()
    {
        // 移動のコルーチンを開始
        StartMoving();
    }

    void Update()
    {
        // Sキーを押すとコルーチンを停止
        if (Input.GetKeyDown(KeyCode.S))
        {
            StopMoving();
        }

        // Rキーを押すとコルーチンを再開
        if (Input.GetKeyDown(KeyCode.R))
        {
            StartMoving();
        }
    }

    void StartMoving()
    {
        if (moveCoroutine == null)
        {
            moveCoroutine = StartCoroutine(MoveWithConstantSpeed());
            Debug.Log("コルーチンを開始しました。");
        }
    }

    void StopMoving()
    {
        if (moveCoroutine != null)
        {
            StopCoroutine(moveCoroutine); // コルーチンを停止
            moveCoroutine = null; // コルーチン参照をクリア
            Debug.Log("コルーチンを停止しました。");
        }
    }

    IEnumerator MoveWithConstantSpeed()
    {
        // 無限ループで移動させ続ける
        while (true)
        {
            float step = speed * Time.deltaTime; // 次のフレームまでの移動距離
            transform.position = new Vector3(transform.position.x + step, transform.position.y + step, transform.position.z);
            yield return null; // 次のフレームまで待機
        }
    }
}

このようにStopCoroutineを使用することでコルーチンを停止させることができます。なお、コルーチンを開始したゲームオブジェクトが非アクテイブ(SetActive(false))になったりDestroyされるとコルーチンも止まります。(注:コルーチンを管理しているMonoBehaviourのenableをfalseにしているときにDestroyしても止まりません。)

コルーチンを使わずUpdateメソッドでも書ける?

上で挙げた非同期処理は、コルーチンを使用せずにUpdateメソッドで書くこともできますが、その場合、状態管理が複雑になり、コードが冗長になる可能性があります。例えば、特定の時間が経過したかどうかを判断するために、自分でタイマーを実装し、Updateメソッド内でそのタイマーを更新する必要があります。これに対して、コルーチンを使用すると、時間の経過を直感的に表現でき、コードもシンプルになります。

コルーチンは、特定のイベントが発生するまでの待機、時間に基づく処理の遅延、継続的な操作の実行など、多様な場面で利用可能です。コルーチンでよく使う文を簡単に紹介しておきます。ここでは詳細は省くので使うときに調べるとよいでしょう。

使用例説明
yield return new WaitForSeconds(2.5f);指定した秒中断する。
yield return null;1フレームだけ中断する。
yield return new WaitForEndOfFrame();1フレームだけ中断する。yield return null;より中断されるタイミングが遅い。
yield return new WaitForFixedUpdate();FixedUpdateが呼び出されるまで中断する。
yield return new WaitUntil(() => isJumping);指定した条件がtrueになるまで中断する。
yield return new WaitWhile(() => !isJumping);指定した条件がfalseになるまで中断する。
yield break;コルーチンを終了する。

コルーチン以外の非同期処理の構文async/await

Unityでコルーチン以外に非同期処理を扱う方法として、async/awaitという構文があります。これはコルーチンと比較して返り値を返せるなどのメリットがあります。Unityでasync/awaitを使う際はUniTaskライブラリを使用するかAwaitableを使用するのが良いです。

終わりに

次回は、今まで学習してきたことのまとめとして、「実際にゲームを作ってビルドしてみる」で実際にゲーム制作に挑戦してみましょう!
それでは!

タイトルとURLをコピーしました