第二回にしてあまり基礎知識でないことに気づいてしまった今日この頃。 まあ、Unityの基礎(土台部分)ということで・・・
第一回が「プロジェクト」だったので、第二回は『シーン』です。
シーン(Scene)とは?
シーンという言葉は日本語だと物語上の一場面というイメージが強いでしょうか。 タイトルシーンやゲームシーンというように頭に言葉を加えると、 ゲーム上での意味がわかりやすいかもしれません。 あるいはタイトル画面、ゲーム画面といったように画面という言葉で置き換えるのもいいかもしれません *1。
Unity上でのシーンとは関係の深いオブジェクト同士の大きなまとまりであり、区切りです。 といっても最初はよくわからないと思うので、 タイトル画面やゲーム画面といった暗転で区切られるようなひとつのまとまりだと考えるのがいいと思います。 シーンのメリット・デメリットを理解するうちにシーンの役割はなんとなくわかってきます。
ひとつのプロジェクトは複数のシーンを含みます。 プロジェクトはゲームそのものだったので、この関係はわかりやすいと思います。 また逆からみれば複数のシーンはひとつのプロジェクトを共有します。 特にプロジェクトのアセット(Assets)は複数のシーンで共有なので注意しましょう。
シーンの編集
Unityエディタを開くと複数の区画(ビュー)が出てきますが、 このうちシーンの内容を最もよく表しているのはどれでしょうか? そりゃシーン(Scene)ビューだろう、と言いたくなりますが、 どちらかというとヒエラルキー(Hierarchy, 階層)ビュー *2 のほうが正確に表しています。
シーンというのはオブジェクトの集合です。 シーンに含まれる情報(の大部分)は シーン開始時にどのようなオブジェクトがどのような状態で存在しているかです。 この情報を最も簡潔に表しているのはヒエラルキービューで、 そのうち視覚化できるものを3D空間上に視覚的に表示したものがシーンビュー、 詳細を表示および編集するためのものがインスペクタ(Inspector, 調査)になります。
シーンの編集はヒエラルキービューにゲームオブジェクト(GameObject)を追加し、 そのゲームオブジェクトにインスペクタで様々なコンポーネント(Component)を追加していくことで行います。 ゲームオブジェクトとコンポーネントについては次回以降の記事で詳細に説明していきます。
シーンの実体
シーンの実体は.unityという拡張子のファイルです。
このファイルはデフォルトではバイナリファイル(文字以外の方法で表現されたファイル)ですが、 Edit > Project Settings > EditorからAsset SerializationのModeを"Force Text"にするとテキストファイルで出力できます。 少し中身をのぞいてみましょう。
適当なフォルダをつくり、それをプロジェクトとして読み込ませ、 デフォルトのシーンをアセットとして保存してみました。 これについてみていきます(すべて載せると長いので適宜省略してあります)。
%YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!29 &1 SceneSettings: ... --- !u!104 &2 RenderSettings: ... --- !u!127 &3 LevelGameManager: ... --- !u!157 &4 LightmapSettings: ... --- !u!196 &5 NavMeshSettings: ... --- !u!1 &285478055 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 285478060} - 20: {fileID: 285478059} - 92: {fileID: 285478058} - 124: {fileID: 285478057} - 81: {fileID: 285478056} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!81 &285478056 AudioListener: ... --- !u!124 &285478057 Behaviour: ... --- !u!92 &285478058 Behaviour: ... --- !u!20 &285478059 Camera: ... --- !u!4 &285478060 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 285478055} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 --- !u!1 &2118523913 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 2118523915} - 108: {fileID: 2118523914} m_Layer: 0 m_Name: Directional Light m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 --- !u!108 &2118523914 Light: ... --- !u!4 &2118523915 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 2118523913} m_LocalRotation: {x: .408217937, y: -.234569728, z: .109381676, w: .875426054} m_LocalPosition: {x: 0, y: 3, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0} m_RootOrder: 1
まずは最初の二行。
%YAML 1.1 %TAG !u! tag:unity3d.com,2011:
これはUnityがシーンファイルと認識するために必要なヘッダです。
YAMLの意味としては一行目がYAMLのバージョン指定、二行目がタグ(スキーマ)の略記の指定らしいです。
続けていくつかシーン固有の設定が記録されています。
Window > LightingやWindow > Occlusion Culling、Window > Navigationから設定できる項目が主です。
オブジェクト名 | 内容 |
---|---|
SceneSettings | オクルージョンの設定など |
RenderSettings | フォグやスカイボックスの設定など |
LevelGameManager | 謎。情報求む |
LightmapSettings | ライトマップの設定 |
NavMeshSettings | ナビメッシュの設定 |
その後にシーンに含まれるオブジェクトに関する情報が列挙されています。
まずは最初のGameObjectについてみてみましょう。
--- !u!1 &285478055 GameObject: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} serializedVersion: 4 m_Component: - 4: {fileID: 285478060} - 20: {fileID: 285478059} - 92: {fileID: 285478058} - 124: {fileID: 285478057} - 81: {fileID: 285478056} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1
一行目になにやら謎の暗号のような文字列があります。 詳細は省きますが、最後の数字がこのオブジェクトのファイル内におけるIDになります。
二行目にオブジェクトの種類が書かれており、その後の行にそのオブジェクトの状態が記録されています。 わかりやすいのは名前を表すm_Nameでしょうか。 エディタの画像と見比べればこれがヒエラルキービューに表示されているMain Cameraの情報だということがわかります。
続けてその後に出てくるTransformの内容を読んでいきます。
--- !u!4 &285478060 Transform: m_ObjectHideFlags: 0 m_PrefabParentObject: {fileID: 0} m_PrefabInternal: {fileID: 0} m_GameObject: {fileID: 285478055} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 1, z: -10} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0
ここで注目すべきはm_GameObjectの欄です。 この欄のfileIDに記述された値と先ほどのGameObjectのファイル内IDの値を比較してみると一致していることがわかります。 これはこのTransformコンポーネントが先ほどのGameObjectにアタッチ(関連付け)されていることを示しています。 また逆に、先ほどのGameObjectのm_Componentの最初の行のfileIDがこのTransformのIDと一致していることも確認できると思います。 エディタの画像と比べてみても、Main CameraのPositionの値がこのTransformのm_LocalPositionの値と一致しているのが見て取れます。
今回はほとんどオブジェクトを置いていないので数が少ないですが、 通常のシーンではシーンファイルの大部分がこのようなオブジェクトの記述になります。 シーンがオブジェクトの集合だといった意味が少し理解できたのではないかと思います。
シーンの区切り
シーンは関係の深いオブジェクトのまとまりだと話しました。 しかし、関係の深いとはなんでしょうか? ゲーム内のすべてのオブジェクトは他のオブジェクトと何かしら関係を持っているはずであり (でなければゲームに組み込まれている意味がありません)、 関係をもつオブジェクト同士のまとまりならばそれはゲームを構成するすべてのオブジェクトになってしまいます。 ゲームをいくつかのシーンに分けるには何を基準とすればよいでしょうか?
シーンの読み込み(LoadLevel)
ここで、シーンの読み込み処理に注目して境目について考えてみます。 Unityでシーンを読み込むときの処理は大まかに以下の手順で行われます。
- 新しく必要となったすべてのアセットの読み込み
- 次のシーンのゲームオブジェクトの読み込み
- 前のシーンのゲームオブジェクトの破棄(OnDestroy)
- 次のシーンのゲームオブジェクトの初期化(Awake)
- 不要になったアセットの破棄(UnloadUnusedAssets)
重要なのは前のシーンのゲームオブジェクトがすべて破棄されるということです。 結局前のシーンとの関係切れてるじゃん、と言いたくなりますがそこは心配なく、 この破棄対象には例外を設定することができます。 しかし例外は例外、各シーン間はこの例外をできる限り少なく保つ必要があります。 そして、これが少なくて済む場所こそがシーンの区切りとして適切です。
シーンの区切りの例
とはいえ、頭の中でシーンカンノカンケイガガガとかやっていてもわからないと思うので例を挙げておきます。
ステージ選択画面とゲーム画面を考えましょう。 この間で受け渡される情報とはなんでしょうか? シンプルに考えれば「どのステージが選択されたか」でしょう。 難易度などの追加情報があったとしてもその量はあまり多くないはずです。 よってこれらをシーンとして分割することは適切です。
一方、ポーズ画面とゲーム画面ならどうでしょうか? 一見ポーズ画面を出すために受け渡す情報なんてないように思えるかもしれませんが、 ポーズ画面は再開時に直前のゲームの状態に戻らなければなりません。 戻るためにはその時のゲームの進行状態すべてを受け取る必要があります。 よってこれらをシーンとして分割することは適切ではありません。
シーンの追加読み込み(LoadLevelAdditive)
しかし、「ポーズ画面とゲーム画面を一緒に作るのってわかりづらくない?」と思う方もいると思います。 実はこのような場合に編集の効率化を目的としてシーンを分けることがあります。 しかし前述の通りこれらに通常の読み込み処理を適用すると情報の受け渡しが面倒になります。
そこでUnityにはシーンの追加読み込みの仕組みがあります。 追加読み込みでは元のシーンのゲームオブジェクトを破棄しないため、 ゲーム画面のシーンに別々に作成したポーズ画面のシーンを結合することができます。 しかし考え方としては複数のシーンファイルでひとつのシーンを構成しているとしたほうがいいと思います。
このようなシーンの分割は多人数開発の際によく用いられるようです。 多人数開発では誰かが編集しているファイルを他の人が編集しないのが基本です (のちにそれぞれの編集結果をひとつにまとめる作業が必要なため)。 しかし、シーンファイルというのは扱う内容が広すぎるために同時編集できないと非常に効率が悪いのです。 そのため、うまくシーンファイルを分割してそれぞれのファイル毎に編集できるようにした上で、 あとからそれを追加読み込みで結合するという手法がとられることがあります。
余談:ひとつのシーンでのゲーム開発
シーンを分けようみたいな内容を書いておいてなんですが、 実はひとつのシーンでゲームを開発するという手法が存在します。 何故そんなことをするかというとひとえに読み込み速度を上げるためです。
シーンの読み込みの手順をよくみると、実はけっこう大雑把な処理をしています。 シーンに必要なものは一気にすべて読み込み、そのとき使っていないものはすべて破棄しています。 しかし、必要なものは必要になったときに読み込めばその分早く画面を表示できますし、 もしかしたら必要だと思っていたものはその文脈では必要ないかもしれません。 また、そのときは使っていないかもしれませんが、その直後にまた必要になることがわかっている場合もあるでしょう。
こういった読み込みの無駄を排除するために、 シーンの読み込み機能を一切用いずにすべての読み込みと破棄を自前で実装する手法があるそうです。 どうしても読み込みが遅くて我慢できないんだ、という場合には有効かもしれません。
しかし、基本的に実行速度と開発効率はトレードオフ(あちらを立てればこちらが立たず)の関係にあります。 Unityの機能を無視して自前の処理を書くのは相当な技術力と綿密な設計が必要です。 またそのゲームにおいて本当に読み込みに無駄が存在しているのかどうか確認することが重要です。 高速化というのは無駄の排除であり、その無駄がないのであればやる意味もありません。 さらには本当に高速化する必要があるのかという問いもすべきでしょう。 「高速化」という言葉は時間を捨てる罠になりがちだと思います(体験談)。
おわりに
プロジェクトに続いてシーンまでもこんなに長くなってしまった。
改めて調べてみると、シーンってなかなか奥が深いなという感じ。 自分の知識は所詮、独学かつ小規模な個人開発に限ったものなので視野が狭いのかもしれません。 かなりてきとーなこと書いているので、これはねーだろってところがあったら連絡もらえるとうれしいです。
参考
https://docs.unity3d.com/ja/current/Manual/TextualSceneFormat.html
https://forum.unity3d.com/threads/how-does-application-loadlevel-work-under-the-hood.325611/
執筆時のUnityのバージョン:5.1.2