エフアンダーバー

個人開発の記録

【Unity】 2Dアクション用のステージギミック

こちらの動画で2DアクションならではのギミックをUnity上で作成する様子をみて、 もう少しなんとかならないかなと思ったので自分でもつくってみました。

おしながき

今回作成したのは動画上にて紹介されていた以下の二つです。

  • 一方向からのみ乗れる床
  • 上下あるいは左右に動き、何かを押しつぶしそうになると移動方向が逆になるブロック

一方通行の床はまさに2Dアクションならではというギミックで、 キャラクターの移動方向によって床と衝突するか否かが変わります。 床とキャラクターとは一対多の関係なので単純に床の判定を消すだけでは実装できません。

方向転換するブロックはキャラクターを押しつぶさないという点でゲームとしては良心的ですが、 この押しつぶさないという処理がプログラミング上の大きな障害となります。 キャラクターの後ろに空間があるならばキャラクターを押し出し、 ないならば押すのをやめて反転するという仕組みとなるため、 空間があるかどうかをいかに判定するかがカギとなってきます。

注意事項

この記事での実装はギミックをつくる上で都合のいい仮定を置いています。 どんな構造のゲームにも適用できるわけではないので、 自身の環境で動かないようであれば改造するなりすっぱりあきらめるなりしてください。

記事中のコードは自由に使ってもらって構いませんが自己責任でお願いします。

一方通行の床

f:id:fspace:20151121021920g:plain

一方通行の床に関しては実はUnityにそのためのコンポーネントがあります。 ですので、実装というより機能の紹介になります。

PlatformEffector2D

Unity5になって2Dの物理演算にXXXEffector2Dという名前のコンポーネントがいくつか追加されました。 PlatformEffector2Dはそのうちのひとつで、一方向の衝突と側面の摩擦の無効化をサポートします。 XXXEffector2DはCollider2Dと組み合わせて使用します。

f:id:fspace:20151121023129p:plain

まずPlatformEffector2DをCollider2D系のコンポーネントがアタッチされたゲームオブジェクトにアタッチします。 その後、Collider2Dの"Used By Effector"にチェックを入れればPlatformEffector2Dが有効になります。

PlatformEffector2D自体の設定は"Use One Way"にチェックを入れて一方向の衝突を有効にし、 必要に応じて"Surface Arc"の値を変更します。 "Surface Arc"はどのくらいの角度までを面とみなすか、 つまりはキャラクターが接近してきた面がどれくらいの角度のときまでを衝突とするかを表します。 ここではBoxCollider2Dで真上以外は必要ないので0を設定しています。

方向転換するブロック

f:id:fspace:20151121024934g:plain

動画では質量を調整するなどして物理的な挙動を保ちながら実装をしていましたが、 こちらではRigidbody2Dの"Is Kinematic"のチェックを入れて非物理的な挙動で実装してみたいと思います。

仕組み

f:id:fspace:20151121025700p:plain

仕組みとしては適当な量のレイをブロックの前方に飛ばして、 当たったオブジェクトの一覧から前方にあとどれくらいの空間があるのかを把握するという方法になります。 空間が移動量以上にあれば移動しても問題ないと判断して移動し、移動量以下であればこれ以上は移動できないと判断し反転します。

ソースコード

少々長いですが、実装コードです。

using System.Diagnostics;
using UnityEngine;

[RequireComponent(typeof(BoxCollider2D), typeof(Rigidbody2D))]
public class MovingBlockControl : MonoBehaviour
{
   #region Constants

    private const int MaxRaycastHit = 8;

   #endregion

   #region Fields

    [SerializeField]
    private bool m_IsVertical;

    [SerializeField]
    private float m_Speed;

    [SerializeField]
    private int m_RaySize;

    [SerializeField]
    private float m_MaxRayDistance;

    [SerializeField]
    private LayerMask m_Collidable;

    [SerializeField]
    private LayerMask m_StaticCollidable;

    private BoxCollider2D m_BoxCollider;

    private Rigidbody2D m_Rigidbody;

    private readonly RaycastHit2D[] m_Results = new RaycastHit2D[MaxRaycastHit];

   #endregion

   #region Properties

    public bool IsVertical
    {
        get { return m_IsVertical; }
        set { m_IsVertical = value; }
    }

    public float Speed
    {
        get { return m_Speed; }
        set { m_Speed = value; }
    }

    public int RaySize
    {
        get { return m_RaySize; }
        set { m_RaySize = value; }
    }

    public float MaxRayDistance
    {
        get { return m_MaxRayDistance; }
        set { m_MaxRayDistance = value; }
    }

    public LayerMask Collidable
    {
        get { return m_Collidable; }
        set { m_Collidable = value; }
    }

    public LayerMask StaticCollidable
    {
        get { return m_StaticCollidable; }
        set { m_StaticCollidable = value; }
    }

   #endregion

   #region Messages

    private void Awake()
    {
        this.m_BoxCollider = GetComponent<BoxCollider2D>();
        this.m_Rigidbody = GetComponent<Rigidbody2D>();

        m_Rigidbody.isKinematic = true;
        m_Rigidbody.sleepMode = RigidbodySleepMode2D.NeverSleep;
    }

    private void FixedUpdate()
    {
        Move();
    }

    private void OnValidate()
    {
        m_RaySize = Mathf.Max(m_RaySize, 1);
        m_MaxRayDistance = Mathf.Max(m_MaxRayDistance, 0.0f);

        Rigidbody2D rigidbody = GetComponent<Rigidbody2D>();
        rigidbody.isKinematic = true;
        rigidbody.sleepMode = RigidbodySleepMode2D.NeverSleep;
    }

   #endregion

   #region Methods

    private void Move()
    {
        Vector2 start, direction, range;

        CollectRayInfo(out start, out range, out direction);

        Vector2 spacing = (m_RaySize > 1 ? range / (m_RaySize - 1) : Vector2.zero);
        for (int i = 0; i < m_RaySize; i++)
        {
            Vector2 origin = start + spacing * i;

            if (!CheckSpace(origin, direction))
            {
                m_Speed = -m_Speed;

                return;
            }
        }

        Vector2 nextPosition = m_Rigidbody.position + direction * Mathf.Abs(m_Speed) * Time.fixedDeltaTime;

        m_Rigidbody.MovePosition(nextPosition);
    }


    private void CollectRayInfo(out Vector2 start, out Vector2 range, out Vector2 direction)
    {
        Bounds bounds = m_BoxCollider.bounds;
        Rect rect = new Rect(bounds.min, bounds.size);
        if (m_IsVertical)
        {
            if (m_Speed >= 0.0f)
            {
                start = new Vector2(rect.xMin, rect.yMax + Vector2.kEpsilon) + Vector2.right * Vector2.kEpsilon;
                range = Vector2.right * (rect.width - Vector2.kEpsilon * 2.0f);
                direction = Vector2.up;
            }
            else
            {
                start = new Vector2(rect.xMin, rect.yMin - Vector2.kEpsilon) + Vector2.right * Vector2.kEpsilon;
                range = Vector2.right * (rect.width - Vector2.kEpsilon * 2.0f);
                direction = Vector2.down;
            }
        }
        else
        {
            if (m_Speed >= 0.0f)
            {
                start = new Vector2(rect.xMax + Vector2.kEpsilon, rect.yMin) + Vector2.up * Vector2.kEpsilon;
                range = Vector2.up * (rect.height - Vector2.kEpsilon * 2.0f);
                direction = Vector2.right;
            }
            else
            {
                start = new Vector2(rect.xMin - Vector2.kEpsilon, rect.yMin) + Vector2.up * Vector2.kEpsilon;
                range = Vector2.up * (rect.height - Vector2.kEpsilon * 2.0f);
                direction = Vector2.left;
            }
        }

        if (m_RaySize == 1)
        {
            start += range * 0.5f;
            range = Vector2.zero;
        }
    }

    private bool CheckSpace(Vector2 origin, Vector2 direction)
    {
        int layerMask = m_Collidable.value | m_StaticCollidable.value;
        int size = Physics2D.RaycastNonAlloc(origin, direction, m_Results, m_MaxRayDistance, layerMask);

        DrawRay(origin, direction, m_MaxRayDistance, Color.green);

        float space = 0.0f;
        float offset = 0.0f;
        for (int i = 0; i < size; i++)
        {
            RaycastHit2D result = m_Results[i];
            Collider2D collider = result.collider;

            if (collider == m_BoxCollider) continue;

            float diff = result.distance - offset;
            if (diff > Mathf.Epsilon)
            {
                space += diff;
            }

            int layer = collider.gameObject.layer;
            if (((1 << layer) & m_StaticCollidable.value) != 0)
            {
                DrawRay(origin, direction, result.distance, Color.red);

                return (space > Mathf.Abs(m_Speed * Time.fixedDeltaTime));
            }

            offset = GetNextOffset(origin, collider);
        }

        return (size != MaxRaycastHit);
    }

    private float GetNextOffset(Vector2 origin, Collider2D collider)
    {
        float offset;

        if (m_IsVertical)
        {
            if (m_Speed >= 0.0f)
            {
                offset = collider.bounds.max.y - origin.y;
            }
            else
            {
                offset = origin.y - collider.bounds.min.y;
            }
        }
        else
        {
            if (m_Speed >= 0.0f)
            {
                offset = collider.bounds.max.x - origin.x;
            }
            else
            {
                offset = origin.x - collider.bounds.min.x;
            }
        }

        return offset;
    }

    [Conditional("UNITY_EDITOR")]
    private void DrawRay(Vector2 origin, Vector2 direction, float distance, Color color)
    {
        UnityEngine.Debug.DrawLine(origin, origin + direction * distance, color);
    }

   #endregion
}

手法の比較

最初に書いたように方向転換するブロックの実装には移動できる空間があるかどうかの把握が重要になります。 動画の方法では物理エンジンの力を借りて押し返す力の検知により空間があるかどうかを確認しています。 一方でこの記事の方法では原始的に空間を計算しています。

動画の方法の問題点は非物理的な挙動を物理的な挙動に押し込んでいることです。 これにより他の物理的なオブジェクトとの関係が密になり、影響を与えやすくかつ受けやすくなります。 例えば、十分に大きい質量と仮定したオブジェクト同士が衝突しておかしな動作をしたり、 何か大きな力を押し返す力と勘違いして方向転換することがあるかもしれません。 最悪でも物理的な挙動をするはずなので仕様だと割り切ってしまうことはできるかもしれませんが・・・

一方でこの記事の方法の問題点は正確さに欠けることです。 現在は衝突対象が十分に大きくかつ矩形判定をもっているため概ね正確に検知できていますが、 衝突対象が小さければレイの隙間を抜けてしまうかもしれませんし、 複雑な形状を持っていれば空間の計算の精度が落ちます。 また応用のしづらさも問題になります。 現在ブロックは縦横にしか動きませんが、これを斜めに動かそうと思うとレイの飛ばし方を変える必要があります。 形状によっても異なるのであらゆる場合に対処するのはなかなかに骨が折れます。

・・・と、まあ、ようはどちらも万能な方法ではないということです。 用いている手法の弱点を把握しつつ、状況に適したものを利用するのがいいと思います。 あるいは確立されたもっと素晴らしい手法がどこかにあるのかもしれません。

おわりに

一方通行の床については標準機能でさくさくでしたが、 方向転換するブロックについては思いのほか苦戦しました。 もともとはKinematicで移動した方がいいんじゃないかと思っただけだったんだけど・・・

余談ですが、 実は自分も次の作品を自動生成ダンジョンの2Dアクションゲームにしようと思っていたので、 最初に動画見たときはけっこうショックでした。 ただ、動画を見ていくとかなり面白いものができそうなので今は自分のゲームの参考にしつつ期待して見てます。

ちなみに最終的なゲームデザインはけっこう異なりそうなので、一応まだつくる気でいます。 しばらくは忙しくて手がつけられそうにないですが・・・

ユニティちゃんライセンス

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています

Unityの基礎 その4 『コンポーネント』

前回からずいぶんと時間が空いてしまいましたが第四回です。 Unityのバージョンも5.2.3になってしまいましたが、 この記事自体はだいぶ前に書いたため5.1.2時点のものです。 次回からは最新バージョンになる・・・はず。

第四回は流れからわかったかもしれませんが『コンポーネント』です。 プロジェクト、シーン、ゲームオブジェクトときて、 今回のコンポーネントでUnityの構成要素の概要については完結です。

コンポーネント(Component)とは?

コンポーネントは一般人には聞き慣れない言葉かもしれませんが、 プログラマにとってはなじみ深い言葉かと思います。 「部品」という意味です。

前回述べたとおり、Unityにおいて「機能」を有しているのは主にコンポーネントになります。 Unityにおけるゲーム開発とは、様々なコンポーネントを追加し、それらを調整、制御していくことを指します。

コンポーネントは必ずひとつのゲームオブジェクトにくっつけて使用します。 コンポーネント単体で存在することはできません。 このくっつける操作のことをアタッチ(attach)するといい、 ひきはがす操作のことをデタッチ(detach)するといいます。 ちなみに、これはプログラミングで一般的に使用される用語なのでコンポーネントに限った話ではありません (例えば、ゲームオブジェクトに子を設定する操作もアタッチ、分離させる操作はデタッチです)。 また、コンポーネントを複数のゲームオブジェクトで共有することはできません。 同じものが欲しい場合にはコピーして使います。

コンポーネントの編集

コンポーネントは必ずゲームオブジェクトにアタッチしなければならないため、 コンポーネントの編集の際にはまずゲームオブジェクトを選択します。 このとき、複数のゲームオブジェクトを選択すると共通部分に関して同時編集ができます。

編集は主にインスペクタで行います。 Add Componentでコンポーネントを追加した後に、 インスペクタに表示される項目を入力していきます。 通常インスペクタに表示されるのは編集用インターフェースであり、 実際に内部で保存されているものとは違う場合があります。 編集用インターフェースでは物足りない場合には、 インスペクタ右上のメニューからDebugを選択することで保存される値を直接変更できます。 ただし、ここに表示される値を意味も知らずに変更するとゲームが盛大に崩壊する可能性があるので注意しましょう。

f:id:fspace:20150904015509p:plain
Local Rotationは実際にはQuaternionで保存されています。

コンポーネントの編集はシーンビューからもできる場合があります。 インスペクタで編集しづらい場合や値のあたりをつけたい場合にはこちらを利用しましょう。

コンポーネントの実体

コンポーネントの内容はゲームオブジェクト同様、 シーンファイルやプレハブファイルに保存されています。

前々回のシーンファイルの中身です。(再掲)

--- !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

前節のDebugモードのインスペクタの画像に写っているものと同じコンポーネントなので見比べてみるといいかもしれません。 インスペクタに表示されていないいくつかの情報は他のオブジェクトとの関係を示すもので、(多分)Unityとしてはいじってほしくない部分です。 逆にインスペクタにしか表示されていないInstance IDは実行時にのみ使用される情報です。

主なコンポーネント

各コンポーネントの詳細は今後の記事にて順次説明していくつもりですが、 ここでは大まかにコンポーネントの種類について軽く説明します。

レンダリング(描画)関係

カメラ(Camera)

レンダリングで最も重要な要素はカメラです。 あらゆる描画はカメラごとに行われます。

CGの世界になじみのない人にとってはコンピュータの中なのに何故カメラが必要なのかと疑問に思うかもしれませんが、 カメラは3D空間上に配置したモノをどの場所からどの角度でどれくらいの視野で見ているかという情報の表現です。 3DCGとは言いますが最終的に得たいのは二次元の画像なので、平面に変換するための情報が必要であり、それを持っているのがカメラなのです。

ちなみにカメラは複数作成することもできます。 といってもテレビのように複数の角度からの映像を切り替えるためではなく、 特殊なエフェクトをかけたりする場合に使います。

ライト(Light)

光の色や当たり方によってモノはいろいろな見え方をします。 また、そもそも光が無ければなにも見えません。

ライトは光を表現しますが、光そのものを描画するための機能ではありません。 光がどの位置からどのように発せられているかを表現することで、 モノの見え方をコントロールするためにあります。 光が強く当たっているところは鮮やかな色に、あまり当たっていないところは暗い色に見えますよね。

レンダラ(Renderer)

CGの世界では3D空間上に配置されたモノを特定の形式(主に2Dの画像)に変換することをレンダリング(Render)するといいます (基本的には「描画」=「レンダリング」)。 そして、Unityにおいてレンダリングする主体となるのがレンダラ(Render-er)です (日本語だと何故か「レンダーする」ではなく「レンダリングする」というので少しややこしいですが)。

Unityにおいて目に見えるゲームオブジェクトにはXXXRendererと名の付くコンポーネントがアタッチされているはずです。

物理演算関係

コライダ(Collider)

コライダは衝突する(Collide)もの(-er)という意味で当たり判定を表現します。 ただし、本当に当たっているかどうかを判定するためだけのものなので、 当たったからどうする(例えば、めり込まないようにする)といったところまでは面倒をみてくれません。

基本的に一切動かない場合にはコライダ単体で、 そうでない場合にはリジッドボディと組み合わせて利用します。

Unityには3Dの物理演算と2Dの物理演算があり、 それに対応してコライダも3D用と2D用の二種類があります。 それぞれは独立しているため、3Dのコライダと2Dのコライダの衝突を判定することはできません。

リジッドボディ(Rigidbody)

リジッドボディは剛体を意味します。 剛体というのは高校物理なんかでよく使ったであろう質点に形や大きさの概念が加わったものです。 ただし、どんなに力を加えても変形しないという条件付きです。

といっても、Unityのリジッドボディには形も大きさもありません。 これらはすべてコライダに依存しているためです。 リジッドボディはコライダと組み合わせて使うことでゲームオブジェクトに物理的な挙動をさせることができます。 例えば、衝突時の跳ね返りや回転、空気抵抗や重力の影響などの計算を自動でしてくれます。

コライダ同様、3D用と2D用の二種類があります。

ジョイント(Joint)

ジョイントは関節といった意味で、物理挙動の制約条件を表します。 リジッドボディ同士、あるいはリジッドボディと空間上の点を結びつける役割を果たします。

例えば、球を糸で吊った振り子のようなものを考えてみます。 球は重力に従って落ちますし、叩けば衝撃で飛んでいくといった物理挙動を見せますが、 端点から糸の長さ以上には離れません。 こういった制約条件をリジッドボディに与えるものがジョイントになります。

コライダ同様、3D用と2D用の二種類があります。

UI関係

Unityには新旧二つのGUIがあります(さらには有名アセットとしてNGUIというものもあります)。 古いものはなにかと性能がよろしくないので、今後は何かつくるときには新しい方を使うべきです。

新しいUnityのGUIはマニュアルにはNew UIと書かれていますが、 わかりにくいので一般的にはuGUIと呼ばれています。

キャンバス(Canvas)

キャンバスはUIを配置する場所を設定します。 uGUIでUIを構築する際にはまずキャンバスを作成し、 その下に様々なUI要素を配置していきます。

テキスト(Text), イメージ(Image), ボタン(Button), etc.

文字や画像の表示、ボタン等の基本的なUI要素はデフォルトで用意されています。 uGUIではこれらを組み合わせることでGUIを構成していきます。

レイアウトグループ(LayoutGroup)

UI要素の配置位置を決めるにはいくつかの方法があります。 一番シンプルなのは座標値を与える方法ですが、 これはUI要素や画面の大きさが変わったりするといとも簡単にレイアウトが崩れてしまいます。 また親要素との位置関係から決定する方法もありますが、 ひとつのUI要素との関係しか考慮できないため複数要素を整列するようなレイアウトは困難です。

複数のUI要素間でうまく調整しつつ配置を行うためにはレイアウトグループを利用します。 レイアウトグループはUI要素をどのように配置するかという方法を設定します。

基本的なレイアウトはデフォルトで用意されていますが、 複雑かつ柔軟なレイアウトを実現したい場合には自作もできます。

オーディオ関係

オーディオソース(AudioSource)

オーディオソースは音源を意味します。 音を発するゲームオブジェクトにはこのコンポーネントをアタッチし、これを通して音を鳴らします。

オーディオリスナーとの組み合わせにより、 音の発生源と観測点を表現することで音に現実感を出すエフェクト(例えば、遠くの音が小さく聞こえる等)をかけることができます。

オーディオリスナー(AudioListener)

オーディオリスナーは音の聞き手、観測者を意味します。 シーンにひとつだけ設定することで、オーディオソースから鳴った音をキャッチできます。 通常はメインカメラかプレイヤーに対してアタッチします。

特殊なもの

トランスフォーム(Transform)

TransformはUnityにおいて最も重要なコンポーネントです。 Transformはゲームオブジェクトがどの位置にどの向きでどれくらいの大きさで存在しているかを表します。 この情報がなければゲームオブジェクトは動くことができませんし、 そもそもどこにあるのかわからないので描画もできません。

またTransformには親子関係を設定する機能があります。 ゲームオブジェクトがこの機能を借りているのは前回の記事で書いた通りです。

余談ですがTransformは日本語でも滅多に「トランスフォーム」とは書きません。 日本語の「トランスフォーム」はロボットなんかの変形のイメージが強すぎるためかと思います。 「変形情報」というのも少し堅苦しいですしね。

スクリプト

スクリプトはプログラムの一部分を制御するためのちょっとしたプログラム *1 という意味のプログラミング用語で、英語としての意味は台本です。 ゲームオブジェクトがどのように動くべきか指示するものというわけです。

Unityにおいてスクリプトは他のコンポーネントでは表現できない各ゲーム固有の動作、ゲームロジックを表現します。 そしてそれゆえに開発者が唯一プログラミングできるコンポーネントになります。 Unityでの開発はこのスクリプトのプログラミング作業が大半を占めます。

その他

ここで紹介したもの以外にもイベントシステムやキャラクターのナビゲーション、パーティクルシステムにネットワーク関係と様々なコンポーネントがあります。 概要だけでも一度目を通しておくといいかもしれません。

おわりに

なんとなく使用方法は知っているものの「このコンポーネントとはなんだ?」と言われるとかなり難しいですね・・・
なんだかんだでここまでで一番書きづらかったです。

しばらくまとまった時間がとれなさそうなので、続きはしばらく先になるかもしれません。 大したこと書いてないように見えるかもしれませんが、 まとめ系の記事は調べまわることが非常に多いので時間を取られるのです・・・



執筆時のUnityのバージョン:5.1.2

*1:この表現は正確ではないと思いますが、 「スクリプト」という言葉はかなり曖昧でいい加減な使い方をされているため明確な定義を与えることは難しい気がします。 この意味での「スクリプト」と似た意味の言葉に「プラグイン」がありますが、 「プラグイン」は機能の追加、「スクリプト」は動作の制御というイメージです。

Unityの基礎 その3 『ゲームオブジェクト』

まだ説明していない部分をぼかしながら書くのがつらい・・・(特にスクリプト関係)
いっそもうUnity再入門くらいにしたほうがいいんだろうか。

プロジェクト、シーンときたので第三回はゲームオブジェクトです。

ゲームオブジェクト(GameObject)とは?

プロジェクトやシーンと比べるとゲームオブジェクトは何を指しているのかよくわからない言葉です。 しかし、ヒエラルキービューに表示されているやつと言われればだいたい理解できるでしょう。

ゲームオブジェクトはその名前からしてゲームの根幹をなすオブジェクトのように思えますが、実際には単なる整理用のコンテナ *1 です。 ゲームオブジェクト自体がもつ機能はほとんどありません。 ファイルシステムでいうところのフォルダに近い役割であり、 機能本体はファイルにあたるコンポーネント(次回以降に説明)が持っています。 ただし、ゲームオブジェクトは操作の対象として非常に便利なのでコンテナと思って侮るべからず。

ひとつのシーンは複数のゲームオブジェクトを含みます。 そしてそれぞれのシーンは基本的にはゲームオブジェクトを共有しません。 シーン間でゲームオブジェクトを共有したい場合には例外として設定する必要があります。

ゲームオブジェクトの編集

ゲームを構成する機能はコンポーネントにあるといいましたが、 コンポーネントはゲームオブジェクトにくっつける形でしか使用できません。 よってシーンの編集はまずゲームオブジェクトをつくるところから始まります。

基本的にはメニューからCreate Emptyを選択して空のゲームオブジェクトを作成します。 そのほかにもCubeやCameraなど様々な選択肢が表示されるとは思いますが、 これらはゲームオブジェクトにあらかじめコンポーネントが設定されている単なるプリセットであることに注意しましょう。 例えば、Cubeから作成したゲームオブジェクトのBox Colliderコンポーネントを削除しても何ら問題ありませんし、 Create Emptyで作成した空のゲームオブジェクトにCameraコンポーネントなどを追加すればCameraで作成したものと同じものをつくれます。

Create Emptyによって作成された空のゲームオブジェクトには、 ”空”と言っておきながらTransform(あるいはRectTransform)コンポーネントがついています。 これは特殊なコンポーネントで、ゲームオブジェクトとは切り離せない関係にあるため削除できません *2。 ただし、決してこれはゲームオブジェクトの機能などではなく、ひとつのコンポーネントなので注意(多くの点で優遇されてはいますが)。

「じゃあゲームオブジェクトの機能ってどこよ?」というと、インスペクタの最上部にある部分になります。

f:id:fspace:20150902173559p:plain
この細い部分

何度も言いますがゲームオブジェクトは単なるコンテナであって、 その主たる目的はコンポーネントを整理するためにあるのです。

ゲームオブジェクトの実体

ゲームオブジェクトにはそれぞれを表す実体となるファイルはありません。 というか、すでにどこに記録されているかみています。

前回のシーンファイルの中身です(再掲)

--- !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

すべて書いてありますね。

シーンファイルの他にはプレハブファイルの中に記録されることもあります。 形式はシーンファイルとほぼ変わりません。 プレハブについては次回以降の記事で説明します *3

ゲームオブジェクトの親子関係

ゲームオブジェクトはヒエラルキービューに表示されているものだと言いました。 どうしてヒエラルキー(階層)ビューに表示されているかといえば親子関係が設定できるためです。

というのは、実は若干ウソです。
エディタ上では確かにゲームオブジェクトが他のゲームオブジェクトを含んでいるように見えますが、 実際にはこの機能を持っているのはTransformコンポーネントになります。 ゲームオブジェクトは必ずTransformコンポーネントを持たなければならないという制約をつけることでこの機能を借りているわけです (さらには他のコンポーネントがゲームオブジェクトを介してこの機能を又借りしていたりもします)。

さて、借り物とはいえゲームオブジェクトには親子関係があります。 そしてゲームオブジェクトの機能はこの親子関係を考慮して動作したりしなかったりします。 この辺が非常にややこしいので、それぞれの機能ごとに親の設定が子にどう影響するのか確認するようにしましょう。

ゲームオブジェクトの親子関係はゲームの状態を整理する上で非常に重要になるのですが、 それ以外にもゲームオブジェクトの検索の際に重要になります (エディタ上での検索ではなくプログラムコードからの検索です)。 検索は対象となる数(いくつのものの中から探すか)によって時間のかかり方に大きな差がでます。 そのシーンに含まれるゲームオブジェクト全体から検索するのと、 あるゲームオブジェクトの子の中から検索するのでは速度がまるで違います。 適切な親子関係をつけた上で適切な検索手法を選んで検索するようにしましょう。

ゲームオブジェクトの機能

ゲームオブジェクトにも多少ながら機能があります。
これらについて簡単に説明していきます。

名前(name)

ゲームオブジェクトの名前です。 基本的に一目見てそのゲームオブジェクトの役割がわかる名前をつけておけば問題ありません。

名前はゲームオブジェクトの検索に利用することができますが、これはあまりオススメしません。 何故かといえば名前を変えるだけでゲームが動かなくなることがあるからです。 普通は名前の変更がゲームの進行に影響するとは考えないため、 何が原因で動かなくなったのか突き止めづらくなります。 またタイプミスにより無駄にデバッグの時間を割くことにもなりがちです。

もしも名前を検索に使いたいのであれば、 今後絶対に変更しないかつ単純な名前にするか、 一時的なコードのみに限定するべきです。

アクティブ(active)状態

ゲームオブジェクトが有効かどうかです。 有効ならばアクティブ(active)、無効なら非アクティブ(inactive)と呼びます。

非アクティブなゲームオブジェクトのコンポーネントは機能が無効になります。 Unityの提供するイベントシステムのイベントにも反応しないので注意。 また、非アクティブなゲームオブジェクトは一部の検索にも引っかかりません。

非アクティブ状態は親の状態から影響を受けるので注意してください。 親(あるいは祖先)が非アクティブならばその子はすべて非アクティブ状態になります。

タグ(tag)

タグは主にゲームオブジェクトの検索のために使用できます。 シーン全体から検索する場合にはタグによる検索が最速になります (おそらくタグごとのゲームオブジェクトリストを持っていると思われる)。

Unityではデフォルトでいくつかのタグが定義されています。

タグ名 意味
Respawn 復活地点
Finish ゴール地点
EditorOnly エディタ専用
MainCamera メインカメラ
Player 操作キャラクター
GameController ゲームの進行管理

このうちEditorOnlyとMainCameraは特殊なタグでそれぞれ効果があります。 EditorOnlyのタグがつけられたゲームオブジェクトはエディタ上でのみ存在し、ビルド時には含まれません。 MainCameraのタグがつけられたゲームオブジェクトのCameraコンポーネントはスクリプトからCamera.mainによって静的にアクセスできます。

正直この二つ以外はサンプルプロジェクト(あるいは標準アセット)に必要だから入っているのではないかと思っています。 PlayerとGameControllerはともかくRespawnやFinishは特定のゲームでしか使わないので標準で入れるには不適切です。 アセットをインポートするのに使うunitypackageはタグやレイヤーに関してあまり便利でないようなのでその辺の問題じゃないかと推測してます。

レイヤー(layer)

レイヤーは主にUnityの標準機能の対象を選択(フィルタリング)するのに使用します。 今後の記事で順次説明していきますが、カメラに映す対象やライトを当てる対象、物理演算で衝突する対象などの指定に用います。

Unityではデフォルトでいくつかのレイヤーが定義されています。

レイヤー名 意味
Default デフォルト
TransparentFX 透明効果
IgnoreRaycast レイキャスト無視
Water
UI UI

このうちTransparentFXとIgnoreRaycastは特殊なレイヤーでそれぞれ効果があります。 TransparentFXレイヤーのゲームオブジェクトはレンズフレアの遮蔽物にならなくなります。 IgnoreRaycastレイヤーのゲームオブジェクトはレイキャストにヒットしないようになります。

タグ同様WaterとUIはサンプルプロジェクトや標準アセット周りの関係じゃないかと思います。 もしかしたら特別な効果があるのかもしれませんが・・・

静的(static)かどうか

静的という言葉はこの業界ではいろいろな意味で使われるのでわかりづらいですが、 ここでは読んで字のごとく静止した(移動しない)ゲームオブジェクトであるかどうかです。 これは開発者が最適化のためにUnityに対して与えるヒントになります。 移動しないようにする設定ではないので注意。

ゲームオブジェクトの座標が変化しない場合、あらゆる事象を事前計算できる可能性があります。 しかし、この「座標が変化しない」ということを正確に解析することは困難なため、 開発者がUnityに教えてあげる必要があります。

どうしても実行速度が足りない場合に事前計算のメリット・デメリットを理解した上で使用するものなので、 よくわからないのであれば(たとえ、移動しないことがわかっていたとしても)触らないのが無難です。

おわりに

やっぱり内容的には長くなったけど、プロジェクトやシーンに比べれば、 淡々と機能の説明するだけなので楽ですね。

SendMessageとかhideFlagsとかも説明しようか迷ったけど、 スクリプトについてまだ説明していないのでやめました。 きっとそのうち、いや、いつか説明するかもしれません。

参考

https://docs.unity3d.com/Manual/GameObjects.html

https://docs.unity3d.com/ScriptReference/GameObject.html

https://answers.unity3d.com/questions/44112/default-tags-and-layers-in-unity.html



執筆時のUnityのバージョン:5.1.2

*1:マニュアルでは「容器」と表現されています。

*2:GameObjectのスクリプトリファレンスをみるとtransformプロパティの欄に「もしアタッチされていない場合はnull」と書いてあります。 アタッチされていない場合がどういう場合なのかは不明です。

*3:忘れなければ。今のところの予定には入っていません。

Unityの基礎 その2 『シーン』

第二回にしてあまり基礎知識でないことに気づいてしまった今日この頃。 まあ、Unityの基礎(土台部分)ということで・・・

第一回が「プロジェクト」だったので、第二回は『シーン』です。

シーン(Scene)とは?

シーンという言葉は日本語だと物語上の一場面というイメージが強いでしょうか。 タイトルシーンやゲームシーンというように頭に言葉を加えると、 ゲーム上での意味がわかりやすいかもしれません。 あるいはタイトル画面、ゲーム画面といったように画面という言葉で置き換えるのもいいかもしれません *1

Unity上でのシーンとは関係の深いオブジェクト同士の大きなまとまりであり、区切りです。 といっても最初はよくわからないと思うので、 タイトル画面やゲーム画面といった暗転で区切られるようなひとつのまとまりだと考えるのがいいと思います。 シーンのメリット・デメリットを理解するうちにシーンの役割はなんとなくわかってきます。

ひとつのプロジェクトは複数のシーンを含みます。 プロジェクトはゲームそのものだったので、この関係はわかりやすいと思います。 また逆からみれば複数のシーンはひとつのプロジェクトを共有します。 特にプロジェクトのアセット(Assets)は複数のシーンで共有なので注意しましょう。

シーンの編集

Unityエディタを開くと複数の区画(ビュー)が出てきますが、 このうちシーンの内容を最もよく表しているのはどれでしょうか? そりゃシーン(Scene)ビューだろう、と言いたくなりますが、 どちらかというとヒエラルキー(Hierarchy, 階層)ビュー *2 のほうが正確に表しています。

シーンというのはオブジェクトの集合です。 シーンに含まれる情報(の大部分)は シーン開始時にどのようなオブジェクトがどのような状態で存在しているかです。 この情報を最も簡潔に表しているのはヒエラルキービューで、 そのうち視覚化できるものを3D空間上に視覚的に表示したものがシーンビュー、 詳細を表示および編集するためのものがインスペクタ(Inspector, 調査)になります。

シーンの編集はヒエラルキービューにゲームオブジェクト(GameObject)を追加し、 そのゲームオブジェクトにインスペクタで様々なコンポーネント(Component)を追加していくことで行います。 ゲームオブジェクトとコンポーネントについては次回以降の記事で詳細に説明していきます。

シーンの実体

シーンの実体は.unityという拡張子のファイルです。

このファイルはデフォルトではバイナリファイル(文字以外の方法で表現されたファイル)ですが、 Edit > Project Settings > EditorからAsset SerializationのModeを"Force Text"にするとテキストファイルで出力できます。 少し中身をのぞいてみましょう。

適当なフォルダをつくり、それをプロジェクトとして読み込ませ、 デフォルトのシーンをアセットとして保存してみました。 これについてみていきます(すべて載せると長いので適宜省略してあります)。

f:id:fspace:20150830190703p:plain

%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でシーンを読み込むときの処理は大まかに以下の手順で行われます。

  1. 新しく必要となったすべてのアセットの読み込み
  2. 次のシーンのゲームオブジェクトの読み込み
  3. 前のシーンのゲームオブジェクトの破棄(OnDestroy)
  4. 次のシーンのゲームオブジェクトの初期化(Awake)
  5. 不要になったアセットの破棄(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

*1:Sceneは「場面」ではなく「舞台」の意味で使われているという話も。

*2:マニュアルではシンプルに hierarchy になっていますが一般的には hierarchy window や hierarchy view と呼ばれることが多いです。

Unityの基礎 その1 『プロジェクト』

ブログネタに困ってきたので、 不定期にUnityに関することをまとめていこうかなと思ってます。

第一回は、やっぱりここからかなと思い『プロジェクト』についてです。

プロジェクト(Project)とは?

プロジェクト、日本語としても一般的な単語となっていますが、 「計画」や「企画」といった意味です。

Unityにおいても想像通りの意味でひとつのゲーム開発プロジェクトを表します。 といっても、文書管理なんかは含まないのであくまでゲームプログラム的な意味でですが。

基本的にひとつのゲームに対してひとつのプロジェクトを作成します。

プロジェクトの実体

Unityを起動し"NEW PROJECT"を選択すると、 プロジェクトの新規作成画面が表示され、 適当に項目を選択するとプロジェクトが作成されます。 また、一度作成したプロジェクトは起動画面のショートカットや"OPEN OTHER"から開くことができます。

しかし、Unityは一体何をもってプロジェクトをプロジェクトとして認識しているのでしょうか?

試しに空のフォルダをつくってUnityにプロジェクトとして読み込ませてみます。 すると、Unityはこれをプロジェクトとして認識し、編集画面を表示します。 Unityにとってプロジェクトとは単なるフォルダに過ぎません。 プロジェクトの名前はフォルダの名前になります。

プロジェクトの構造(Structure)

プロジェクトは単なるフォルダではありますが、 Unityがその内容を認識するために内部の構造に関してある程度のルールがあります。 これは「プロジェクト直下のAssetsフォルダをアセットの保存場所とする」というようなものです。 Unityが認識する特殊なフォルダはプロジェクトの作成時、 あるいはそのフォルダが存在しないと判断されたときなどに自動的に作成されます。 特殊なフォルダ(Assetsなど)をリネームしてしまうと存在しないと判断されてしまうため気を付けましょう。

Unityで開発をしていると様々なファイルやフォルダがプロジェクトフォルダ内に作成されますが、 これらはゲームプロジェクトとして必要不可欠な(失われると再生できない)一次的なものと、 高速化や他のツールからの利用のために一次的なファイルから作成された(失われても再生できる)二次的なものに分けられます。

一次的なファイル・フォルダ

一次的な(失われると再生できない)フォルダはAssetsとProjectSettingsのみです。

Assetsフォルダ

Assetsフォルダはプロジェクトの核となるフォルダです。

開発者がゲームに組み込みたいソースコード、画像、音楽、テキスト等はすべてこのフォルダの中に入れます。 また一般的な利用法において、 開発者が唯一Unityエディタを介さずに内部に変更を加えてもよい場所になります。

Assetsフォルダは内部にさらに構造を持っており、 それによってプラグインなどが実現できます。

ProjectSettingsフォルダ

ProjectSettingsフォルダはプロジェクト全体に対する設定を保存しているフォルダです。

内部に含まれるファイルはUnityエディタメニューの「Edit > Project Settings」にて編集された情報です。 またプロジェクトに対応するUnityエディタのバージョンもここに記録されています。

二次的なファイル・フォルダ

二次的な(失われても再生できる)ファイルやフォルダは上記の一次的なもの以外ということになります。

代表的なものを挙げておきますがこれ以外にもあるかもしれません。 またプロジェクト直下にあったからといってUnityが作成したものかどうかはわかりません。 Unityではプロジェクトフォルダがカレントディレクトリとして扱われるため、 例えばプラグインがAssetsフォルダ内には入れたくない情報を保存しているかもしれません。

Libraryフォルダ

Libraryフォルダは円滑な開発を行うためのキャッシュを保存しているフォルダです。

AssetsやProjectSettingsはゲームの情報すべてを内包していますが、 それらの情報はゲーム内にてそのまま使われるとは限りません。 多くの場合、複数の情報を組み合わせてゲームプログラムとして使用するのに最適な情報へと変換されたのちに使用されます。 こういった情報をその都度作成していると非常に時間がかかるため、 一度作成した情報は保存しておいて変更がなければ再利用されます。 この再利用情報(キャッシュ)を保存しているのがLibraryフォルダです。

Libraryフォルダの内容は再生可能だといいましたが、 実はビルドの設定などほんの少しだけ再生できない情報が含まれています。 ゲームに関する内容ではなくエディタに関する内容、 かつプロジェクト固有な設定についてはLibraryフォルダに保存されているのかもしれません。

過去のバージョンのUnityではデフォルトでLibraryフォルダに アセットの依存関係などの一次的な情報が含まれていたようですが、 現在はAssetsフォルダ内の.metaという拡張子のファイル(デフォルトで隠しファイル設定)を用いて保存しているようです。

objフォルダ

プログラムをビルドする際にMonoDevelopやVisual Studioによって作成される一時的なファイルを保存しているフォルダです。

Unityが作成しているものではないためかこれだけ命名規則が違います。

Tempフォルダ

プログラムをビルドする際にUnityによって作成される一時的なファイルを保存しているフォルダです。

Assembly-(CSharpまたはUnityScript)[-vs].(csprojまたはunityproj)

Assetsフォルダ内のスクリプトを編集するためのプロジェクトファイルです。 MonoDevelopやVisual Studioにて読み込むために作成されます。

"CSharp"や"UnityScript"(JavaScript)といった文字列が対象としている言語を表しており、 "-vs"がVisual Studio用であることを示しています。

Assetsフォルダ内にエディタ用スクリプトやプラグインを含めていると、 "Editor"や"Plugins"といった文字列が追加されることもあります。 またVisual Studio Tools for Unity (UnityVS)を利用していると、 さらに名前が変わることがあります。

(プロジェクト名)[-csharp].sln

プロジェクトファイルを管理するためのソリューションファイルです。 プロジェクトファイル同様、MonoDevelopやVisual Studioで読み込むために作成されます。

"-csharp"はVisual Studio用のC#プロジェクト(.csproj)のみを扱うファイルになっています。

(プロジェクト名)[-csharp].userprefs

MonoDevelopが編集状態を保存するために作成するファイルです。

余談1:プロジェクトのバージョン管理システム設定

これまで見てきた通り、 Unityプロジェクトのバージョン管理を行う際、 必要となるのはAssetsフォルダとProjectSettingsフォルダ内のみです。 その他の部分は頻繁に書き換わるためバージョン管理の対象に含めると管理がしづらくなります。

またUnityではバージョン管理システムを用いる際に以下の設定が推奨されています。
Edit > Project Settings > Editor 内の項目
Version Control > Mode : "Visible Meta Files"
Asset Serialization > Mode : "Force Text"

これによりAssetsフォルダ内の.metaファイルから隠しファイル属性が消え、 シーンファイルなどがテキスト形式で出力されるようになります (diffが読めるようになる)。

余談2:プロジェクト名の変更

ここまで読んでいるのであれば蛇足かもしれませんが、 Unityのプロジェクト名はフォルダ名と同一なので、 プロジェクト名を変更したいときにはフォルダ名を変えればそれで済みます。 プロジェクト名に依存している項目も再生成されるので問題ありません (項目を削除せずに変更するとゴミが残る可能性はあります)。 ただし、プラグイン等がプロジェクト名に依存していないとも限らないので十分注意してください。

プロジェクト名を変更するとプロジェクトのパスが変わるため、 起動画面のショートカットは自動的に削除されます。 "OPEN OTHER"から再度プロジェクトを指定すると 変更された名前でショートカットが作成されます。

おわりに

最初なのでサクッと終わらせるつもりで「プロジェクト」にしたのですが、 調べているうちにものすごく長くなってしまった・・・。

ちなみに「一次的」とか「二次的」とかけっこうてきとーに言葉使っているので、 人に話すと伝わらないかもしれません、注意。

参考

https://unity3d.com/jp/learn/tutorials/modules/beginner/architecture/folders-in-version-control

https://docs.unity3d.com/Manual/ExternalVersionControlSystemSupport.html



執筆時のUnityのバージョン:5.1.2

UnityAdsVideoCacheについて

どうも「UnityAdsVideoCache」という検索ワードで以前書いたUnity Adsの記事にたどり着く人がいるみたいです。

www.f-sp.com

しかしこのワードで検索するということはおそらくUnity Adsで稼ぎたい開発者ではなく、 自分の端末に見知らぬフォルダを見つけて不安になった人だと思います。
そこで非開発者用にこれがなんなのか簡単に書いておきます。

UnityAdsVideoCacheとは?

不安だと思うので最初に言っておきますが、このフォルダは特に害のあるものではありませんし、 邪魔なら消してもかまいません(ただし、消すことにあまり意味はありません)。

UnityAdsVideoCacheは「Unity Ads」という広告を表示するサービスを利用した際に自動的につくられるフォルダです。 もちろん利用したといっても、 開発者がアプリの中に組み込んでおくことで自動的に広告を取得して表示するものであって、 アプリのユーザに義務云々は発生しません。 契約は開発者(あるいは広告主)とサービス提供者の間で行われています。 簡潔にいえば、開発者はUnity Adsを使ってユーザに広告を見せることで稼いでるわけです。

Unity AdsはUnityという超有名ゲームエンジン(簡単に言えばゲーム開発ツール)と同じ会社が提供しており、 ゲーム系の広告を配信しているため、組み込まれるアプリもゲームが多いです。 最も有名なのはクロッシーロード(Crossy Road)でしょうか。

とにかく、もしこのフォルダが作成されているのであれば、 動画広告を表示するゲームアプリをインストールしたはずです。

なんのためにあるの?

Unity Adsは広告の中でも動画広告を主に採用しています。 動画というのは文字や画像と比べると格段にデータ量が大きなものです。 そして、データ量が大きいということは取得するための通信に時間がかかるということです。 そうなると、いざ動画広告を表示しようとなったときに通信が終わるまで待たなくてはいけません。 特にそのときに通信環境が悪かったりすると最悪です。

このようなことを防止するためにキャッシュ(Cache)という方法があります。 これは事前に広告のデータを取得して端末内に保存し、必要になったときには端末内からデータを取得して再生するという方法です *1。 通信速度というのは端末内のデータへのアクセス速度と比べて格段に遅いので、これだけで非常に高速化されます。

もうお分かりかと思いますが、 UnityAdsVideoCacheというのはこの事前取得した広告のデータを保存しておくための場所なのです。

消してもいいの?

前述のとおり、UnityAdsVideoCacheというのは事前取得したデータを後の再生のために一時的に保管しているだけの場所です。 つまり、消しても特に問題ありません。

しかし、当然消した後に対象アプリを開けば動画データのダウンロードが始まり、フォルダが再作成されます。 なのでフォルダを消すことに特に意味はありません。 むしろ通信料の無駄になる可能性があります。

余談:WRITE_EXTERNAL_STORAGE

ちなみに開発者がAndroidにUnity Adsを組み込む場合にはアプリに WRITE_EXTERNAL_STORAGEというパーミッション(許可属性)をつけなくてはいけません。 なんのこっちゃという感じかもしれませんが、 Google Playからダウンロードしたのであればアプリの権限の表示を見たことがあると思います。 WRITE_EXTERNAL_STORAGEというのは「外部ストレージへの書き込み」の許可をユーザからもらう必要がありますよ、 という表示であり、実際ダウンロード時にユーザに確認されています(あまり気にしていないかもしれませんが)。 つまり、形式上Unity Ads(の組み込まれたアプリ)はユーザからフォルダとかつくっていいよ、 という許可をもらったうえでつくっているのです。

おわりに

「簡単に」とか言いつつひとつひとつ書いていたら長くなってしまった・・・
まあとにかく、怪しいもんじゃないので気にしないのが一番、ということでまとめておきます。

*1:一般的にキャッシュというと一度使ったデータを破棄せずに再利用するような場合が多いですが、ここでは事前取得しています。

【Unity】Sphereは動的バッチングができない!?

Unityでちょっとしたテストをしていたのですが、 なぜかDraw Callの値が非常に高くなっていたので調べてみたところ球の描画に問題があったようです。

UnityのSphereメッシュ(球)は動的バッチングをすることができず、 追加するとその分だけDraw Callの数が増えるということらしいです。

個人的に衝撃だったので記事にしてみました。

前提知識

Draw Call(ドローコール)とは?

Draw CallとはGPUに対する描画命令呼び出しのことで、一般的に重たい処理とされています (最新のアーキテクチャではこの常識も崩れつつあるとかないとか)。

重たい理由はGPUが描画に使うための情報をCPUで準備していることにあるそうです (そのためDraw Callが増えるとGPUではなくCPUが重くなります)。 この手の話は「Draw Call」で検索すると日本語でも多くの資料がヒットするはずなのでここでは省略します。

Draw Callの数はUnityエディタのゲームウィンドウ右上部にあるStats(統計データ)から確認できます。 Unity 5ではDraw Callの値はBatchesで表されます (横のSaved by batchingが後述のバッチングにより減少した数です)。

Draw Callは以前からよく速度の指標として用いられていますが、 Unity 5ではシェーダの準備をするSetPass Callの方が指標として適切とされているようです。 この値もStatsから確認できます。

動的バッチングとは?

あらかじめ定められた一連の処理を一度に実行することをコンピュータの世界ではバッチ処理といいます。

Unityにおける(ドローコール)バッチングとは複数の描画命令(Draw Call)をひとつの描画命令にまとめあげることをいいます。 バッチングがうまくいくとDraw Callが減少するため多くの場合に実行速度が改善されます。 これを行うための最も重要な条件はマテリアル(ここではUnityにおけるMaterialオブジェクトではなく単純に材質を表現するデータ群)が同じであることです。 UnityのMaterialオブジェクトは概ねマテリアルを表現しているため、 これを共有しているゲームオブジェクト同士は同時に描画できる最低条件を満たしていることになります。

動的バッチングとは動的(実行時、ランタイム)に行われるバッチングのことです。

頂点属性 (vertex attribute)とは?

頂点属性という訳が正しいかはわかりませんが、 vertex attributeとは頂点ごとに与えられる値のことをいいます(例えば位置や法線、UV等)。

前節にてマテリアルが一致していることがバッチングの条件だといいましたが、 じゃあマテリアル以外の描画用情報ってのはなんなのかというと形状データになります。 UnityにおけるMeshオブジェクトがこれにあたります。 この形状データが持っている情報が頂点と面の情報であり、さらにその頂点が持っている情報が頂点属性になります。

Sphereは動的バッチングができない!?

さて、前置きが長くなりましたが本題に戻ります。

Unity標準のSphereメッシュでは動的バッチングが効きません。 もちろんこれはマテリアルの不一致が原因ではありません。 メッシュ自体に問題があります。

Unityの動的バッチングにはマテリアルの一致以外にいくつかの条件があります。 詳細についてはマニュアルを参照してください。 今回はこれらの条件のうち頂点属性の数が原因となったようです。

マニュアルを見ると、 動的バッチングには頂点ごとにオーバーヘッドがかかるため、 メッシュに含まれる頂点属性の数が900未満のときにのみバッチングされるよと書いてあります *1。 つまりは、動的バッチングの準備にかかる時間の方が稼げる時間よりも長くなるかもしれないから、 複雑なメッシュに対してはバッチングしないよということみたいです。

Sphereメッシュには526もの頂点があるらしく、 例えばこれらが位置と法線の二つの情報を持っていると考えると、 この二倍の頂点属性数をもつことになり1052で900を超えてしまうため動的バッチングの対象外となります。

おわりに

モバイルアプリによって単純なゲームの面白さが再発見されている今日このごろ、 球のモデルというのはテスト以外でも頻繁に使われそうな気がしますが、 大量に生成する場合には自分でローポリの球モデルをインポートしたほうがいいようです。

*1:マニュアルの日本語訳ではこの部分が間違っていて、頂点属性数が頂点数になっています。

Unityのロードマップ (Ver. 5.2)

最近になってUnityがロードマップを公開していることを知りました。

Roadmap - Unity

せっかくなので次のUnity 5.2の内容を確認しておきます。

Unity 5.2で追加予定の機能

全容はリンク先をみてもらうとして、自分に関係ありそうな機能だけ。

  • Playables API (experimental)
  • Pref: UI Batching
  • Platform: Windows 10 Universal Apps Support
  • Services: Ads Integration
  • UI: Dropdown Control, 2D Clipping etc.

Playables API (experimental)

実験的な導入のようですが、 実行時にアニメーションのブレンドやら時間やらの状態を制御できたりするようです。 いままでできなかったアニメーションの細かい制御がスクリプトから可能になるっぽい。

APIの目的としては木構造を利用したデータソースの構造化と評価(合成とか)のようで、 将来的にはアニメーション以外のものにも適用していくようですが詳細は不明。

Pref: UI Batching

uGUIのバッチング方法の見直しでしょうか。

現在の実装だとなぜかバッチングされない構造があったのでその辺が直ることに期待。

Platform: Windows 10 Universal Apps Support

Windowsのアプリはやたら種類が多くてどれのことを指しているのかよくわからないのですが、 UWP(Universal Windows Platform)をサポートするってことでいいのかな? まあ、なんにせよWindows 10のストア用に出力できるようになりそうです。

Services: Ads Integration

詳細が書いていないので何が変わるのかはよくわからないのですが、 デフォルトで使えるようになるとか、 Unity Editor上で管理ができるようになるとかでしょうか。

昨日の記事に書いた音声ミュートの問題が解決してるといいな、という淡い期待。

UI: Dropdown Control, 2D Clipping etc.

uGUIに関するさまざまな改善。

ドロップダウンコントロールってまだなかったんですね・・・
あまり複雑な設定項目をつくらないから気づかなかった。

ステンシルバッファを使わない矩形のクリッピングは使い勝手がよさそうですね。 いままでは重いという噂のMaskコンポーネントを使わざるを得なかったので。

"CanvasRenderer API improvements"はなんだろ?

おわりに

高度なグラフィックにあまり興味のない自分としては、 あまり革新的な機能はない印象ですが、 細々と使い勝手がよくなっている模様。

それよりも気になったのはResearch欄の".NET Profile Upgrade"だったり。 まだ当分はアップグレードされないってことかな・・・




無理やり記事にしてみたけどやっぱり内容スカスカだなぁ・・・

Unity Adsのミュート機能

昨日に引き続きUnity Adsの話。

「しろたま」はタイトル画面で音声ミュートの設定ができるようになっているのですが、 現状この設定はUnity Adsの広告には効きません。 できればUnity Adsの広告もミュートで再生したい、 ということでUnity Adsで音声ミュートが可能かどうか調べてみました。

f:id:fspace:20150817180818p:plain

「しろたま」タイトル画面

Unity Adsのオプション

「しろたま」ではアセットストア版のUnity Adsを使用しているので、 これのオプション設定について調べてみます。

公式ドキュメント

まずは当然 公式のドキュメント から。

最下部のShowOptions Classの項目をみると、 設定できる項目は表示後のコールバックとユーザSIDのみらしい。

スクリプト

でも、どっかでミュート云々の話をみた気がする・・・
だいたい広告表示時に左下にミュートボタンあるわけだし・・・
ということでスクリプトを少し読んでみます。

それっぽい単語で検索をかけてみると、 UnityAds.csのparseOptionsDictionaryメソッドに"muteVideoSounds"の文字が。 引数のoptionsに"muteVideoSounds"が設定されているかどうか調べて、 オプション用文字列を生成してるっぽい。

これを設定すればいけるのでは、 と思い呼び出し元を辿るも ShowOptions -> Dictionary<string,string> -> string の順にオプションを変換してるっぽい。 ShowOptionsにミュート設定のフィールドがないから設定できないじゃん・・・。

とはいえ、内部をいじればミュート設定は可能かもしれない。
となると今度はこれが許されているかどうかです。

Unity Adsの利用規約

スクリプトを変更すればミュート設定が可能っぽいことがわかったので、 Unity Adsの利用規約 でこれが許可されているかどうか確認してみます。

最も該当しそうなのはProhibited Usesの項目。

4.3 Prohibited Uses. Under no circumstances shall the Publisher be permitted to run video advertisements in placements that i) are below-the-fold and autostart; or ii) by default (i.e., without user interaction) do not play the sound track of the video advertisement; or iii) play any other soundtrack than of the video advertisements during the playback of the video advertisement; or iv) autostart and are presented or located in such a manner that would make it unlikely that the ads would be viewed by humans, whether or not the sound is defaulted to off; or v) are located in pop-up, pop-under or similar windows other than provided by Applifier. ...

苦手な英語なのであっているかどうかわかりませんが多分こんな感じ(信じるならば自己責任で)。


パブリッシャは以下の場所でビデオ広告を表示する場合には許可が必要である。

  1. below-the-fold(初期状態で画面外)にて自動再生
  2. デフォルトで(ユーザの対話的操作なしに)音声を再生しない
  3. 動画広告再生中に他の音声を再生
  4. 音声の有無にかかわらず、人間に見えない方法で提供または配置して自動再生
  5. ポップアップやポップアンダーあるいは提供された方法以外の類似したウィンドウ上への配置

...


ひっかかりそうなのは2番ですね・・・。
タイトル画面でユーザがミュートを選択しているからという言い訳はできますが、 こういうのは自分に都合のいい方には倒さない方がいい気がする。

公式の回答?

・・・とここまではずいぶん前に確かめた内容なのですが、 そのときに同じ質問をフォーラムにしている方がいたので、 記事を書くにあたり確認してみると直接問い合わせて回答をもらっていました。

http://forum.unity3d.com/threads/mute-ad-volume.334469/

どうやらUnityに組み込む場合にはミュートする方法はないらしいです。
ただし、常にミュートでいいのであれば Unity AdsのAdminパネルからゲームごとに設定可能とのこと。

結論

いろいろ調べましたが、下手にいじらない方がよさそうです。
常時ミュートでよければAdminパネルから、 そうでないのならばあきらめるのが最善のよう。

Unity Adsとプライバシーポリシー

「しろたま」をアンドロイダーのアプリ審査に出した際、 必要なプライバシーポリシーがないことを指摘されたというおはなし。

Unity Adsの送信する情報

指摘されたのはUnity Adsが個人情報を外部に送信するにもかかわらず、 そのことが示されたプライバシーポリシーがGoogle Playに登録されていないということ。

Unity Adsは Unity Technologies社の提供する広告モジュールで、 「しろたま」では広告収入を得るために利用していました。

Unity Adsでは配信広告の選定のために 端末にインストールされているアプリの情報を送信するらしく、 このことがユーザに示されていないため基準を満たしていないとのことでした。 Unityのプライバシーポリシー を読むとアプリ情報が送信されることがちゃんと書いてありました。

Information that is passively collected by Unity (often automatically), including:

...

  • App list: After Players receive notice and do not opt-out, Unity may collect the names and application identifiers ("app id") of some or all of the applications installed on your mobile device, when Players receive ads delivered by Unity Ads.

...

Unity Adsにより追加されるパーミッション

プライバシーポリシーに必須ではないと思いますが、 書かれているとありがたい情報としてパーミッションの使用目的があります (アンドロイダーのアプリ審査時にも要求されます)。

Unity Ads(のアセットストア版)によって追加されるパーミッションは 以下の三つです。

  • ACCESS_NETWORK_STATE
  • INTERNET
  • WRITE_EXTERNAL_STORAGE

ACCESS_NETWORK_STATEとINTERNETの目的は自明でしょう。 広告を取得するのだから当然通信が必要になります (前述のアプリ情報の送信などもあるため取得のみが目的と言い切るのはまずいですが)。

WRITE_EXTERNAL_STORAGEは広告のプリキャッシュに必要なようです。 実際アプリがインストールされた端末のストレージを見てみると UnityAdsVideoCacheというフォルダが作成されており、 内部に動画が保存されているのが確認できます。

これらのパーミッションはUnity Adsによって追加されますが、 もちろんその他の要因によっても追加されうるものです。 パーミッションの目的を示す場合には要因すべてを列挙する必要があるためご注意を。

(おまけ)その他のパーミッション

「しろたま」ではUnity Adsの追加するパーミッション以外に READ_PHONE_STATEを要求しています。 これはSystemInfo.deviceUniqueIdentifierを参照した際に 自動的に追加されるようです。

アプリ審査の結果

アンドロイダーからのメールに、 親切にもプライバシーポリシー作成のための参考文献を載せてくれていたため、 それらを見ながら自分なりにプライバシーポリシーを作成し、 再度アプリ審査を要求したところ無事審査を通過しました。

androider.jp

作成したプライバシーポリシー

最後に作成したプライバシーポリシーへのリンクを貼っておきます。

しろたま プライバシーポリシー

【Unity】カスタムイメージエフェクトのつくりかた

Unity5でフリー版でのイメージエフェクト(ImageEffect)が解放されたので、 カスタムイメージエフェクトのつくりかたを書こうかな、と思ったんですが、 すでにけっこうあるんですね・・・
まあ、需要があるかはともかく自己流でも書いてみます。

まずはともあれサンプルを。
f:id:fspace:20150811163101g:plain

イメージエフェクトとは?

イメージエフェクトとは、 様々なオブジェクトをレンダリングした結果としてできる一枚の画像に対してかけるエフェクトのことをいいます。 より一般的にはポストエフェクトと呼ばれます("post-" = 「~後の」、この場合は「レンダリング後の」)。

標準のイメージエフェクトは"Assets"->"Import Package"->"Effect"をインポートすると使用できます。
標準のものをみるとぼかしや発光など特殊な演出に使いそうなものが多いですが、 もっと単純にシーンチェンジのエフェクトなんかもつくれます。 というわけで今回はその辺をつくってみようと思います。

カスタムイメージエフェクトの基本

Cameraと同じGameObjectに

イメージエフェクトは一枚の画像を入力として、一枚の画像を出力します (もちろん入力を増やすことはできますが)。 そこで入力となる画像が必要となるのですが、 UnityではこれをCameraコンポーネントのレンダリング結果から取得します。 そのため作成したイメージエフェクトはCameraコンポーネントと同じGameObjectにアタッチする必要があります。

またCameraのレンダリング結果をもとにエフェクトをかけるため、 そのCameraがレンダリングの対象としていないものにはエフェクトはかかりません。 最初のサンプルをみてもらうと、 画面上部の文字にはエフェクトがかかっていないのが確認できると思います。

OnRenderImageで描画

カスタムイメージエフェクトは通常のスクリプトと同様にMonoBehaviourを継承したクラスを利用します。
その際利用するメッセージが OnRenderImage です。

void OnRenderImage(RenderTexture source, RenderTexture destination)

引数のsourceに入力となる画像、destinationに出力先の画像が渡されてきます。

イメージエフェクトをカスタマイズするためにはこの内部で source画像をもとにdestinationへと描画すればよいのですが、 わざわざそのためにMeshなんかを作成するのは面倒なので 通常は Graphics.Blit という静的メソッドを使います。

public static void Blit(Texture source, RenderTexture dest, Material mat)

いくつかのオーバーロードがありますが、 基本的には上記のものを使うことになります。
このメソッドはsourceに渡した画像をメインテクスチャにセットしたうえで、 画面全体を描画するようなUVつきのMeshを、指定したマテリアルで、destに描画してくれます。

カスタムイメージエフェクトのつくりかた

基礎を抑えたところでソースコードをみていきます。

まずはカスタムイメージエフェクトのベースクラスから。

using UnityEngine;

[RequireComponent(typeof(Camera))]
public abstract class CustomImageEffect : MonoBehaviour
{
   #region Fields

    private Material m_Material;

   #endregion

   #region Properties

    public abstract string ShaderName { get; }

    protected Material Material { get { return m_Material; } }

   #endregion

   #region Messages

    protected virtual void Awake()
    {
        Shader shader = Shader.Find(ShaderName);
        m_Material = new Material(shader);
    }

    protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        UpdateMaterial();

        Graphics.Blit(source, destination, m_Material);
    }

   #endregion

   #region Methods

    protected abstract void UpdateMaterial();

   #endregion

}

なにやらごちゃごちゃと書いてありますが、 ほとんどはシェーダ名からマテリアルを作成して子クラスで使えるようにしているだけです。
重要なのはOnRenderImage内のみで、 マテリアルの更新後、Graphics.Blitをそのマテリアルで呼び出しています。

続いて、エフェクトの要となるシェーダプログラム。

Shader "シェーダの名前" {
    // シェーダのプロパティ。必要なら追加する。
    Properties {
        // 入力画像がここにはいる
        _MainTex ("Source", 2D) = "white" {}
    }
    SubShader {
        // 不必要な機能はすべて切る
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            // シェーダプログラムの本体
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f i) : SV_TARGET {
                // ここに表示したい色の計算処理を書く
                ...
            }
            ENDCG
        }
    }
    FallBack Off
}

Unityのシェーダプログラムは若干特殊ですが、 カスタムイメージエフェクトに関してはそれほど難しいことはありません。
だいたいの部分はコピペでどうにかなるので、 ここではCGPROGRAM~ENDCGまでを解説します。

最初の#pragmaは頂点シェーダとフラグメントシェーダを表す関数を指定しています。

次にincludeしている"UnityCG.cginc"はUnityが提供してくれる情報にアクセスするための様々な定義が書いてあります。 公式のUnityダウンロードページに地味に標準シェーダコードをダウンロードする場所があり、 そのなかをみると詳しい内容が読めます。 appdata_img構造体やUNITY_MATRIX_MVP行列、MultiplyUV関数、_ScreenParamsベクトル等すべてここに書いてあります。

v2f構造体は頂点シェーダからフラグメントシェーダへと渡す構造体です。 単純に頂点位置とUV座標を保持します。

頂点シェーダvertはappdata_img構造体から頂点位置とUV座標を変換して、 フラグメントシェーダに渡しています。 特別な処理はしていません。

struct appdata_img
{
    float4 vertex : POSITION;
    half2 texcoord : TEXCOORD0;
};

最後にフラグメントシェーダにてUV座標とメインテクスチャを(必要ならば他のパラメータも)用いて、 表示したい色を計算します。

カスタムイメージエフェクトいろいろ

最後に作成してみたイメージエフェクトのソースコードを置いておきます。

正直、手抜き満載でパフォーマンス的にどうなの?というところはありますが、 まあ必要ならば適当に直して使ってください。
ちなみに何か起こっても一切責任は負いませんので自己責任でお願いします。

簡易モザイクエフェクト

ボックスの中心の色とっているだけという簡単なものですが、 なんとなくそれっぽいですよね・・・?

f:id:fspace:20150811163101g:plain

using UnityEngine;

public class Mosaic : CustomImageEffect
{
   #region Fields

    [SerializeField]
    [Range(1, 100)]
    private float m_Size;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Mosaic"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetFloat("_Size", m_Size);
    }

   #endregion

}
Shader "Custom/Mosaic" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Size ("Size", Float) = 1
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float _Size;

            fixed4 frag(v2f i) : SV_TARGET {
                float2 delta = _Size / _ScreenParams.xy;
                float2 uv = (floor(i.uv / delta) + 0.5 ) * delta;

                return tex2D(_MainTex, uv);
            }
            ENDCG
        }
    }
    FallBack Off
}

切れ込みエフェクト

ノベルゲームのシーン変更とかで使われそうなやつ。

f:id:fspace:20150811163105g:plain

using UnityEngine;

public class Slit : CustomImageEffect
{
   #region Fields

    [SerializeField]
    [Range(1, 100)]
    private float m_Size;

    [SerializeField]
    [Range(0, 1)]
    private float m_Time;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Slit"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetFloat("_Size", m_Size);
        Material.SetFloat("_AnimTime", m_Time);
    }

   #endregion

}
Shader "Custom/Slit" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Size ("Size", Float) = 1
        _AnimTime ("Animation Time", Range(0,1)) = 0
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float _Size;
            float _AnimTime;

            fixed4 frag(v2f i) : SV_TARGET {
                float delta = _Size / _ScreenParams.x;
                float visible = 1.0 - floor(frac(i.uv.x / delta) + _AnimTime);

                return fixed4(tex2D(_MainTex, i.uv).rgb * visible, 1.0);
            }
            ENDCG
        }
    }
    FallBack Off
}

のぞきあなエフェクト

昔のゲームでよく使われていたような気がする。

f:id:fspace:20150811163110g:plain

using UnityEngine;

public class Hole : CustomImageEffect
{
   #region Fields

    [SerializeField]
    private Vector2 m_Center;

    [SerializeField]
    [Range(100, 2000)]
    private float m_Size;

    [SerializeField]
    [Range(0, 1)]
    private float m_Time;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Hole"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetVector("_Center", m_Center);
        Material.SetFloat("_Size", m_Size);
        Material.SetFloat("_AnimTime", m_Time);
    }

   #endregion

}
Shader "Custom/Hole" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Center ("Center", Vector) = (0,0,0,0)
        _Size ("Size", Float) = 1
        _AnimTime ("Animation Time", Range(0,1)) = 0
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float2 _Center;
            float _Size;
            float _AnimTime;

            fixed4 frag(v2f i) : SV_TARGET {
                float d = distance(_Center, i.uv * _ScreenParams.xy);
                float visible = 1.0 - floor(d / _Size + _AnimTime);

                return fixed4(tex2D(_MainTex, i.uv).rgb * visible, 1.0);
            }
            ENDCG
        }
    }
    FallBack Off
}

ユニティちゃんライセンス

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています

uGUIによる謎のdiff

前回の記事で書いた「しろたま」のアップデート中に謎のdiffがあったのでメモ。

www.f-sp.com

謎のdiff

アップデートのためにシーンファイル(.unity)を弄っていたところおかしなdiffが出てきました。

f:id:fspace:20150808200255p:plain
diff その1

f:id:fspace:20150808200302p:plain
diff その2

変更したうえで元に戻してる・・・!?
もちろん自分で変更したわけではなく、uGUIによる自動レイアウトの結果です。
さらにレイアウトを変えたわけでもありません。

無意味なdiffを大量につくられるのは厄介なので少し調べてみました。

発生条件

まずはどういう場所で発生しているのかについて。
同じようなdiffが出ている場所を調べてみると、 どうやら以下の条件で発生している模様。

  • Prefabのインスタンスである
  • 親のLayoutGroup(HorizontalLayout等)によってレイアウトされている

実験

というわけでHorizontalLayoutの中にPrefabインスタンスを突っ込んで シーンファイルの状態を確認してみます。

Prefab:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 1915973137}
    m_Modifications:
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchorMin.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchorMin.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchorMax.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchorMax.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_SizeDelta.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_SizeDelta.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchoredPosition.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 22459688, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
      propertyPath: m_AnchoredPosition.y
      value: 0
      objectReference: {fileID: 0}
    m_RemovedComponents: []
  m_ParentPrefab: {fileID: 100100000, guid: 5bcaac5fee929154e9d487cbefe75a9d, type: 2}
  m_IsPrefabParent: 0

どうやらこれが正常状態みたいです。

LayoutGroupによって制御されるプロパティは、 内部にてDrivenRectTransformTrackerというものに登録され、 シーンファイルには記録されないようになっています(エディタ上で編集不可になるのもこれのおかげ)。 よってそれらの値が0として記録されるのは正しい動作と考えられます (本当ならm_Modificationsにも記録されないで欲しいのですが、 一度変更されたプロパティは記録され続けるようです・・・)。

値記録の謎

じゃあ値が記録されてしまうのは何故なのかという話。

結果からいうとよくわかりませんでした・・・。
ただ一点、LayoutGroup下のPrefabインスタンスを複製したときに値の記録が行われるという現象だけは確認できました。 もちろんそんな限定的な操作に対してのみ起こる現象ではないとは思いますが、 特定の操作後にDrivenRectTransformTrackerによる保護が一時的に効かなくなる状態があるようです。

結論

結論というかなんというか、まあバグっぽいのでそのうち修正されるでしょう。
再レイアウトすると0に戻るようなのでこまめに再レイアウトするとよさそうです。



使用したUnityのバージョン: 5.1.2f1