1. はじめに
Unityでゲーム制作をしてる時、左側にあるヒエラルキーを右クリックしてCreateからオブジェクトなりUIなりテキストなりを生成してシーン上に出すことがあると思います。
シューティングゲームとかパズルゲームとか作る人ならわかると思うんですけど、同じオブジェクトを使いまわすっていうのに何個も何個もヒエラルキー上に同じオブジェクトを置いていったりしたらキリがないですよね……
そんなときに使えるのが今回紹介する「Prefab」!
ゲーム制作をしているときに新たな発見をしたので残しておきたいなと思い、今回はこいつに関しての記事を作るに至ったというわけです!
では早速説明していきましょう!☟
2. Prefabとは何なのか
結論から申してしまうと、
まぁもうほんとにこれだけです。
「どれぐらいの大きさ(Scale)」「どれぐらいの回転(Rotation)」「どの位置(Position)」だけでなく、そのオブジェクトが持っているコンポーネントやスクリプト、マテリアルなどに関しての情報も保持している完璧な設計図というわけです。
まぁもっと分かりやすく言うなら、
同じオブジェクトの完全なるコピーを生み出すために必要な要素をすべて持っている設計図
ですね。
料理などで例えるなら、「ある人が作ったカレーをそっくりそのまま作りたい!」ってなったら「具材」「調味料」だけでなく「具材を入れる順序」「加熱の加減」「使う調理器具」と言った部分も完全にマネしないと全く同じものは作れないですよね。そんな感じです(適当)
そんな設計図としての役割を持つPrefabですが、設計図の元となるオブジェクトがないと設計図は作れません。
設計図を作るために必要な最初のオブジェクトを仮に”オリジナル”と呼ぶとき、その設計図を作ることを「オリジナルをPrefab化する」と言います。汎用的に言うなら「オブジェクトのPrefab化」ですかね。
そのやり方については3章で説明しますが、とりあえずここで覚えておいてほしいのは"Prefabは元になるオブジェクトの状態(情報)を保持した設計図"ということです。
ですが、ここでこんな疑問を持つ人もいるのではないでしょうか?
「設計図を作って何をするの?」
素晴らしい疑問です(上から目線)。
設計図を作っただけでは、その設計図に価値などありませんよね。
つまり設計図をもとに何かを作るからこそ設計図に価値が生まれるわけです。
ではUnity上でどうやってこのPrefabを使うことができるのか、それは……
「Instantiate()によるオブジェクトの複製」です!!!
Instantiate() = 設計図(Prefab)を参照してオブジェクトを複製する関数
例えばシューティンゲームの弾やパズルゲームのブロック、タワーディフェンスの敵などなど……同じオブジェクトを使い回したい場面は結構ありますよね。
最初の章でも触れましたが、それらをすべてヒエラルキーに並べるときりがないし、ましてや「時間差をおいて出現」や「別のオブジェクトが消えたら生成」などといったことをするとなると各オブジェクトにスクリプトをアタッチなんてことにもなりかねません。
しかしInstantiateを使えば「設計図1つ」で「無限に複製」できてしまうんです!
あとはInstantiateを呼び出すタイミングを変えれば時間差や条件に応じて複製ができてしまうわけです!
なんて簡単でなんてステキな関数……だからこそ、気を付けなければなりません。
このInstantiateもそうなのですが、なにより元となるPrefabの性質を勘違いすると大変な目に合います。
3. Prefab化の仕方と注意点
2章でも説明したように、Prefabを作るには元になるオブジェクトが必要になります。
今回は試しに"Enemy"というオブジェクトをPrefab化してみましょう。
①Unityのエディター画面の左側にあるヒエラルキーを右クリック。
②(2Dなら)「2D Object」➡「Sprites」➡「Square」で正方形を生成し、名前をEnemy
にする。
(3Dなら)「3D Object」➡「Cube」で立方体を生成し、名前をEnemyにする。
③座標をインスペクター上で(x, y, z) = (0, 0, 0)に設定。
④ヒエラルキーにいるEnemyをエディター画面の下にあるフォルダーとかがある場所に
ドラッグ&ドロップする。
すると、下のフォルダーとかがある場所に新しく「Enemy」という白い箱マークがついたものが追加され、ヒエラルキー上にあるEnemyには青い箱マークがついているのが分かると思います。
これがオブジェクトをPrefab化したときに起きる現象です。
つまり、この一連の流れでオブジェクトのPrefab化が完了するということでもあります。
あとはこれをInstantiate()を持つスクリプトなどで参照して複製すれば完了です。
(Instantiateの使い方に関しては別の記事などで調べてみてください)
なーんだ、めっちゃ簡単じゃん!
痛い目見ますよ、あなた(預言)
確かに、オブジェクトのPrefab化自体はめちゃくちゃ簡単です。
しかしそれゆえにこのPrefabが持つ性質に頭を悩ませてしまう場合があります。
以下が声を大にして伝えたいPrefabの性質です。
つまりどういうことか?
例えば、今回作ったEnemyの座標は(0, 0, 0)でしたが、"Prefab化した後で"このように思ったとしましょう。
「敵が出てくる位置を(10, 5, 10)に変更したいなぁ」
こうした時、いつも通りであればシーン上にいるEnemyを(10, 5, 10)に移動させますよね?
実際にこれでテストプレイをしてみると、確かにEnemyは(10, 5, 10)の位置にいます。
しかしながら、これを「これでこの後に複製する敵も全部(10, 5, 10)から出てくるよね」と考えた瞬間”オワリ”です。
例えばEnemySpawnerというEnemyを複製して出現させるためのオブジェクトを作り、そこにInstantiate()を用いて「Prefab化されたEnemyを参照し、複製する」といった処理を行わせるとします。
この状態で実行してみると、最初の一体に関しては(10, 5, 10)にいるのですが、Enemyの複製体はなぜか(0, 0, 0)からどんどん生成されていきます。
「シーン上でちゃんとEnemyの位置を変えたのに、 何で複製体は位置が変わらないんだ !」
そんなあなたに一言。
「設計図、ちゃんと書き直しましたか……?」
そう、いくらPrefab化したオブジェクトをシーン上で動かしたとしてもそれを設計図に反映させるには "手動で" 更新しなければならないのです。
つまりフォルダーとかがある所に入っている古い設計図を破棄し、新しい設計図の元となるオブジェクトを再度入れなおさなくてはならないということです。
上の状況を細かく書き出すと次のようになっています。
①Enemyの設計図が座標(0, 0, 0)として保存される。(Prefab化)
②シーン上の ”実体である” Enemyの座標を(10, 5 ,10)に変更。(設計図の変更は "なし")
③ "設計図通りの" Enemyの複製が成される(つまり座標は(0, 0, 0)のまま)
設計図(Prefab)というのはあくまで「シーン上に存在していない」ものであり、複製体はそうしたシーン上に存在していない設計図を参照して複製された瞬間に「シーン上に存在する実体」になります。
ヒエラルキー上にあるものは初めから「実体をもつもの」であるため、リアルタイムでの変更を行うことができますが、設計図(Prefab)ようなヒエラルキー上に「シーン上で実体を持たないもの」は必要に応じて変更を更新する必要があります。
また、参照するオブジェクトに「シーン上の実体であるオブジェクト」をアタッチしたとしても、それがPrefab化されている場合は最終更新時の設計図(Prefab)の情報を参照してしまいます。(ここが結構分かりづらい)
こうしたことがあるため、Prefab化は簡単でありながら詰まりやすいポイントでもあるよという紹介でした。
4. まとめ
記事を投稿するものとしてこんなこと言っちゃいけないとは思いますが、書くの疲れたのでサッと今回のポイントをまとめて本記事を終了したいと思います。
Prefabのポイント!
Prefab = オブジェクトの設計図!
Instantiateと一緒に活用して、オブジェクトを複製できる!
Prefab化すると設計図が保存されるけど、設計図の変更とシーン上の変更は同期されない!
Instantiateなどでオブジェクトの情報を参照するとき、そのオブジェクトがPrefab化されているならPrefabの方の情報を優先して取得する。
もし「Prefab化したくないけど、別シーンのオブジェクトが持つスクリプトにアタッチしたい」って場合は、アタッチしたい(アタッチする側の)オブジェクトをDontDestroyOnLoad();でシーン間を移動できるようにするかAdditiveでシーンを重ね掛けしてそもそも破棄させないようにする形を取り、その後アタッチされる側となるオブジェクトに次のようなコードを書きこめば、自分でわざわざアタッチしなくてもスクリプトが勝手に処理してくれるよ☆
public class BeAttachedObject : MonoBehaviour //アタッチされる側のオブジェクトのスクリプト
{
public AttachObject attachObject; //アタッチする側のオブジェクト(名前はシーン上のものをそのまま使おう)
void Awake()
{
attachObject = FindAnyObjectByType<AttachObject>(); // アタッチするオブジェクトを現在開いているシーン内から探す
}
...
では、これにて。
さらばだー。