Unityで簡単な2D脱出ゲームを作ってウェブサイトで公開してみよう 〜スクリプトに関するあれやこれ〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
補足・・・足りない点を補うこと

この記事を三行にまとめると

変数や関数が使用できる有効範囲
どのオブジェクトにセットするか
スクリプトの初期化のタイミング
今回は余談と言うか、C#のスクリプトに関するいくつかの補足事項についてお話します。「脱出ゲームの製作」という枠からは少しはずれてしまうんですが、良かったら少々おつき合いくださいな。

動画はこちらです。





変数や関数が使用できる有効範囲

変数や関数には「スコープ」と呼ばれる有効範囲があります。

ここでちょいと前回作成した室内移動のコードを見てみましょう。

public class Room : Monobehaviour {
  public GameObject[] rooms;
  private int now = 0;

  public void Move(string direction) {
    rooms[now].SetActive(false);

    if(direction == "right") {
      now += 1;
    } else {
      now -= 1;
    }

    if(now > 3) {
      now = 0;
    } else if(now < 0) {
      now = 3;
    }

    rooms[now].SetActive(true);
  }
}

変数や関数の先頭に「public」とか「private」とか書いてありますね。これがスコープです。

publicはこの変数や関数がどこからでも使用可能であることを意味します。日本語で言うと公開ですかね。公開状態なので誰でも好きに使ってくれと。Move関数も発動し放題だぞと、そういうことです。一方のprivateは非公開な状態を意味するのでどこでも誰でも使用可能というわけにはいきません。かなり範囲が限られます。簡単に言うとこのクラスの中(上記で言うとRoomクラス)でしか使えないというものなのですが、これについてはもっといろんなコードを書いたりスクリプトを作ってから改めて見てみましょう。たぶんそっちの方が分かりやすい。

インスペクターでセットするためにはpublicでなければなりません。rooms変数をpublicからprivateに変えるとインスペクターでゲームオブジェクトをセットできなくなります。関数についても同様で、Move関数をprivateにするとインスペクターのEvent Triggerでセットできなくなります。逆に言うと、インスペクターで扱う必要のない変数や関数はpublicにする必要がありません。now変数をprivateにしたのもそれが理由です。

あとこれは別の回でもう少し詳しくやりますが、他のクラスから関数を呼び出す場合などもスコープがpublicじゃないとダメです。上記のMove関数を他のクラスから呼び出したい時にスコープがprivateになっていると、呼び出し権限がありませんみたいなエラーが出ます。

他にも「protected」などのスコープがありますがこれも別の回でやりましょう。そのうちこのスコープを使う日が来るので、またその時に。

今はとりあえず「インスペクターで扱うものはpublic、そうじゃないものはprivate」くらいの認識でも大丈夫だと思います。



どのオブジェクトにセットするか

スクリプトを使用するにはいずれかのオブジェクトにセットする必要があるわけですが、必ずしもイベントを実行したいオブジェクトに対象のスクリプトをセットする必要はありません。どういうことかって言うと、例えば上記のMove関数は画面上部にある矢印をクリックした時に呼び出したいわけですが、そのためにはRoomスクリプトを矢印にセットしないといけないのかと言うとそんなことはなくて、早い話がどのオブジェクトについていても呼び出すことはできます。実際に本作でもRoomクラスは矢印オブジェクトではなく、別のオブジェクト(Floorオブジェクト)につけています。

スクリプトのセット

ようはドラッグ&ドロップで引っ張ってくるオブジェクトにそのスクリプトがついていれば関数をセットできます。この場合はFloorオブジェクトをドラッグ&ドロップすればどのオブジェクトでもMove関数を呼び出すイベントを作成できます。

ただし同じスクリプトを複数のオブジェクトで扱う場合、同じオブジェクトにセットされたものか別のオブジェクトにセットされたものかで挙動は異なります。

言葉だけだとちょっと分かりづらいですけど、本作の左右の矢印オブジェクトにセットしたMove関数は、どちらも共通のFloorオブジェクトにセットしたRoomスクリプトのMove関数を呼び出しています。この場合、どちらの矢印をクリックした場合でも同じスクリプトにアクセスすることになります。だから現在のnow変数の値が0だった場合、右矢印をクリックするとnowは1になります。その次に左矢印をクリックすると、nowは-1されて再び0になります。このようにnow変数の値は両者の矢印の間で共有されている状態です。

でもここでRoomスクリプトを左右の矢印それぞれにセットして、それぞれからMove関数を呼び出すようにしたとします。

Leftオブジェクト

Rightオブジェクト

こんな感じで一方はLeftオブエジェクトにセットしたRoomスクリプトのMove関数、もう一方はRightオブエジェクトにセットしたRoomスクリプトのMove関数を呼び出すようにした場合、それぞれ別々のスクリプトにアクセスすることになります。内部的には二つのRoomスクリプトが起動しているような状態です。なのでnow変数の値も共有されません。Rightのnowを+1してもLeftの方は0のままです。逆もまたしかり。

now変数はどの画面を表示するか判定するための変数なので、nowの値がバラバラだとRightの方でRoom2をアクティブにした後、次にLeftを押したらRoom2は非アクティブにならずRoom4がアクティブになる、みたいな不具合が発生します。だからこういう場合は同じオブジェクトにセットしたスクリプトの関数を呼び出すようにした方が良いです。別々のオブジェクトにセットした場合にスクリプト間の値を共有する方法もあるにはあるんですが、それはまた別の機会にやりましょう。一度にあまりたくさんのことを覚えるのもアレだしね。

逆に共有すると困る場合もあると思います。変数の値を独立させたいのに別のオブジェクトをクリックしたら値が上書きされちゃった、みたいなことを避けたい場合は別々のオブジェクトにスクリプトをセットする必要があります。



スクリプトの初期化のタイミング

スクリプトには初期化という概念があります。英語だと「initialize」とか言うのかな。

Unityでスクリプトファイルを作成すると、最初に「Start」という関数が書かれていると思います。Roomスクリプトなどでは不要だったので削除していましたが、このStart関数はスクリプトが初期化される時に自動的に発動する関数です。

public class Sample : Monobehaviour {
  void Start() {
    Debug.Log("ゲームスタート");
  }
}

例えばこんなコードを作成して、このスクリプトをオブエジェクトにセットすると、初期化時にコンソール画面に「ゲームスタート」と表示されます。

この初期化のタイミングなのですが、アクティブなオブジェクトにスクリプトをセットした場合はゲーム開始と同時です。本作だとRoom1はゲーム開始時にアクティブな状態なので、Room1にスクリプトをセットしておくと、ゲーム開始と同時にゲームスタートの文字が出ます。

非アクティブなオブジェクトにセットした場合は、そのオブジェクトが最初にアクティブになったタイミングで初期化されます。本作の場合、Room2やRoom3はゲーム開始時には非アクティブなので、ここに上記のスクリプトをセットしておくと、室内を移動してRoom2やRoom3がアクティブになった時にコンソール画面にゲームスタートの文字が出ます。

それからもう一つ、アクティブなオブジェクトにセットした場合でもその親オブジェクトが非アクティブな場合は、親がアクティブになるまで初期化されません。ちょっとややこしいパターンですね。

この初期化のタイミングの何が重要かって言うと、Start関数は初期化されるまで発動しないのにイベントにセットした関数は初期化前でも動くってとこなんですよ。

動画とは違う例になりますけど、例えばこんなスクリプトがあったとします。

public class Sample : Monobehaviour {
  public string text; 

  void Start() {
    text = "Let's Unity!!";
  }

  public void Message() {
    console.log(text);
  }
}

Start関数の中でtextという変数に適当な文字を突っ込んで、Message関数が発動した時にtext変数の内容をコンソール画面に出そうというコードです。

このスクリプトを非アクティブなオブジェクトにセットして、別のアクティブなオブジェクトのクリックイベントでMessage関数を呼び出すようにした場合。クリックでMessage関数は呼び出せますが、スクリプトの初期化はまだなのでコンソール画面に「Let's Unity!!」の文字は出てきません。

インスペクターではなくスクリプトの中で変数にゲームオブジェクトをセットすることもできるのですが、それをStart関数の中でやっていた場合、初期化されるまで変数の中身は空なので、その変数に対してSetActiveなどを実行しようとすると「オブジェクトがセットされてねーよ」っていうエラーが出ます。Null何ちゃらエクセプションエラーとかそんな感じのやつ。

ゲーム作りが進んでいくとStart関数を使う機会も増えると思うのですが、その中でこちょこちょやる場合は一応初期化のタイミングは気にしといた方が良いのかなあと思います。

僕の中では「初期化=そのスクリプトが読み込まれて有効になる」みたいな感覚なので、なぜ初期化されてなくても関数だけは使えるのかってのがいまいち腑に落ちなかったりもするのですが……でもそういう仕様なんだよね。僕が何かを勘違いしているだけの可能性もあるけど、その辺りはこの記事や動画を見て補足してくれる人がいることを待ちたいと思います。他人任せですみませんけど、なにぶん座右の銘が他力本願なもんですから……。






こんなとこでしょうか。

正直、知らないとゲーム作りが先に進まないというような話ではないんですがね。「スコープだかスコップだか知らねーけどとりあえず全部publicって書いときゃ動くんだろ? 今そんなこと掘り下げられても興味ねーよ」とか言われたら、確かにそうっすねーって感じです。まあこういう話もあるんだよーくらいの感じで、酢こんぶでもかじりながら気楽に読んでもらえたらと思います。

次回はまたゲーム製作の方に戻りまして、オブジェクトを移動させる処理をスクリプトで書いてみたいと思いますので、よろしくお願いしまんとがわ。



本シリーズの記事の一覧はこちら
Unityで簡単な2D脱出ゲームを作ってウェブサイトで公開してみよう 〜エピローグ〜
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください