hildsoftのコード置き場

プログラム関連で調べたことやコードの保管場所です

Unityで複数のシーンを重複しないようにロードする

f:id:hildsoft:20181117155719j:plain

検証環境

Unity 2018.2.13f1

やりたい事

f:id:hildsoft:20181117161720j:plain

  • エディタで既にロードしているシーンを追加でロードしないようにする。
  • エディタでロードしていないけど、実行に必要なシーンを自動でロードする。

これを同時に行うスクリプトです。

問題点

エディタで追加のシーンをロードするのは簡単ですが、開き忘れた場合はエラーになるなど、開発時に他のシーンを開くときなど、必要なシーンを追加するのに面倒な場合があります。

必ず開くとエディタで開いているものと重複して開くことになるため、既に開いているかどうかをチェックしてロードします。

使用ケース

f:id:hildsoft:20181117161758j:plain

今回はゲーム内のシーンと、共通のUIをシーンで分けて二つを分けるつもりで作りました。
別のシーンからロードされるときは、固定で必要なシーンをロードするのですが、開発中はUIは呼び忘れても自動でロードしたいのです。

ロードタイミングなどで問題が出る可能性もありますので、実際にリリースで使用する場合は、同期や待機処理が必要になるかもしれません。

サンプルコード

LoadUIScene.cs

using UnityEngine;
using UnityEngine.SceneManagement;

public class LoadUIScene : MonoBehaviour
{
    public string loadSceneName;

    void Awake ()
    {
        LoadScene(this.loadSceneName);
    }

    private void LoadScene(string loadSceneName)
    {
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            Scene scene = SceneManager.GetSceneAt(i);

            if (scene.name == loadSceneName)
            {
                return;
            }
        }

        SceneManager.LoadScene(loadSceneName, LoadSceneMode.Additive);
    }
}

GUI Error: You are pushing more GUIClips than you are popping. Make sure they are balanced)

f:id:hildsoft:20181022005420j:plain

検証環境

Unity 2018.1.6f1

現象

UnityのGUI.Window、GUILayout.WindowのパラメータGUI.WindowFunctionにdelegate登録したメソッド内で
GUI.BeginScrollView、GUILayout.BeginScrollViewを使っていると、
EditorGUILayout.ColorFieldだけ
クリックしたときにエラー(You are pushing more GUIClips than you are popping)が出る。
ObjectFieldやCurveFieldではエラー出ない。

原因

今回の場合については不明。

同様のエラー内容の場合、多くの原因がスコープの閉じ忘れ。

Begin~、End~
のような対になるべきメソッドが条件分岐やreturnなどで実行されないパターンで発生する。

実験

サンプルコードを実行してカラー選択をクリックする。

カラー選択ダイアログは表示され、処理も行われるようだが、

GUI Error: You are pushing more GUIClips than you are popping. Make sure they are balanced) UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

のエラーメッセージが表示される。

サンプルコード

ColorFieldTestWindow .cs

using UnityEngine;
using UnityEditor;

public class ColorFieldTestWindow : EditorWindow
{
    Color color;
    Texture2D tex2D;
    AnimationCurve ac = new AnimationCurve();
    Vector2 sPos;
    Rect windowRect = new Rect(10f, 10f, 280f, 150f);

    [MenuItem("Window/ColorFieldTestWindow")]
    private static void OpenWindow()
    {
        ColorFieldTestWindow window = EditorWindow.CreateInstance<ColorFieldTestWindow>();
        window.Show();
    }

    void OnEnable()
    {
        ac = new AnimationCurve();
    }

    private void OnGUI()
    {
        BeginWindows();
        windowRect = GUI.Window(0, windowRect, WindowCallBack, "Parameter");
        EndWindows();
    }

    private void WindowCallBack(int id)
    {
        sPos = GUILayout.BeginScrollView(sPos);

        color = EditorGUILayout.ColorField("Color Field2", color);
        GUILayout.Space(50);

        tex2D = EditorGUILayout.ObjectField(tex2D, typeof(Texture2D), false) as Texture2D;
        GUILayout.Space(50);

        ac = EditorGUILayout.CurveField(ac);

        GUILayout.EndScrollView();
    }
}

結論

解決していないので原因は不明。 WindowCallBackのようなコールバックメソッド内ではなく、EditorWindow.OnGUIで GUILayout.BeginScrollView~GUILayout.EndScrollViewの中に記述する分には問題なし。

参考

https://answers.unity.com/questions/774863/gui-error-you-are-pushing-more-guiclips-than-you-a-5.html

https://www.facebook.com/groups/unityuserj/permalink/821541751239100/

Unityでエディタ拡張を作るときのUndo、Redoの落とし穴

f:id:hildsoft:20180818151746j:plain

検証環境

Unity 2018.1.6f1

現象

ScriptabelObjectを継承しない自作クラスの参照を持っている状態でRedoを行うと、正しく参照が保持されない。

こちらの記事で指摘されている現象です。
http://sassembla.github.io/Public/2015:09:17%203-14-23/2015:09:17%203-14-23.html

原因

データの持ち方に問題がある。

実験

  • ScriptableObjectを継承したTestDocumentクラスを作成。
  • EditorWindow内にフィールド(doc)として保持。
  • TestDocument内に何種類かデータを保持。
  • Undo.RecordObject(this.doc, "Add Node")にてUndo、Redoを行う。
  • Undo、Redoでデータがどうなるか簡単に調査。

プリミティブ

public class TestDocument : ScriptableObject

    public int PublicCounter;

    private int privateCounter;
    public int PrivateCounter { get { return this.privateCounter; } set { this.privateCounter = value; } }

    [SerializeField]
    private int serializeCounter;
    public int SerializeCounter { get { return this.serializeCounter; } set { this.serializeCounter = value; } }

こちらはprivate以外はUndo、Redo共に問題ありませんでした。
privateについてはUndo、Redoで値が変更されません。

クラス

public class TestDocument : ScriptableObject

    // クラス
    [SerializeField]
    private List<Node> nodeList;
    public List<Node> NodeList { get { return this.nodeList; } }

    public List<Connection> ConnectionList;

クラスについてはprivateは実験しませんでした。
SerializeFieldもpublicもどちらもUndo、Redoで更新されます。

参照の問題点

Connectionクラスに

    [SerializeField]
    private Node inNode;
    public Node InNode { get { return this.inNode; } set { this.inNode = value; } }

    [SerializeField]
    private Node outNode;
    public Node OutNode { get { return this.outNode; } set { this.outNode = value; } }

と、Nodeクラスへの参照を持っているわけですが、

f:id:hildsoft:20180818154446j:plain

Nodeクラスが[System.Serializable]によってシリアライズ対象になっているときは、ConnectionをUndoで削除した後のRedo時にNodeがnewで生成されてしまいます。

NodeクラスがScriptableObjectを継承することによりシリアライズされている場合はコネクションの再生成時に、docで管理されているnodeListの中にあるNodeへの参照を設定してくれます。
その際、コネクションがScriptableObjectでなく[System.Serializable]でも問題はありませんが、コネクションが参照される可能性も含めてScriptableObjectにしておいた方が良いでしょう。

実験はしていないのですが、おそらくdoc単位でUndoしているので正しく参照関係を保持して戻してくれているのでしょう。

確かにぱっと見では正しく動いているように見えるので、浮いたノードを参照しているコネクション経由でノードへ変更をかけるアクセスをしない限り発覚しないので怖いですね。

結論

  • Undo、Redoを行う変数は、publicで宣言するか[SerializeField]を設定する
  • Undo、Redoの対象となる自作クラスはScriptableObjectを継承しておく
  • Undoで記録する際は参照範囲にあるオブジェクトを纏めて放り込む

を心がけておくと良いかと思います。

サンプルコード

WindowTest.cs

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

public class WindowTest : EditorWindow
{
    static WindowTest window;

    // データ管理インスタンス
    private TestDocument doc;

    private Node selectNode;

    [MenuItem("Window/UndoRedo Test")]
    private static void OpenWindow()
    {
        if (window == null)
        {
            window = EditorWindow.CreateInstance<WindowTest>();
            window.titleContent = new GUIContent("UndoRedo Test");
        }
        window.doc = CreateInstance<TestDocument>(); // ScriptableObjectはnewで作成しない
        window.doc.init();
        window.Show();

        // Undo、Redo実行時に再描画
        Undo.undoRedoPerformed += () =>
        {
            window.Repaint();
        };
    }

    private void OnGUI()
    {
        CheckEvent(Event.current);

        foreach (var item in this.doc.ConnectionList)
        {
            item.Draw();
        }
        foreach (var item in this.doc.NodeList)
        {
            item.Draw();
        }

        GUI.Label(new Rect(10, 10, 200, 30), new GUIContent("Public:" + this.doc.PublicCounter));
        GUI.Label(new Rect(10, 30, 200, 30), new GUIContent("Private:" + this.doc.PrivateCounter));
        GUI.Label(new Rect(10, 50, 200, 30), new GUIContent("Serialize:" + this.doc.SerializeCounter));

        if (GUI.changed) Repaint();
    }

    private void CheckEvent(Event e)
    {
        switch(e.type)
        {
            case EventType.MouseDown:
                Node clickNode = null;
                foreach (var item in this.doc.NodeList)
                {
                    if (item.Contains(e.mousePosition)) {
                        clickNode = item;
                        break;
                    }
                }

                if (clickNode == null)
                {
                    // 何もないところをクリックしたらノード作成
                    Node n = new Node();
                    //Node n = CreateInstance<Node>();
                    Undo.RecordObject(this.doc, "Add Node");
                    this.doc.NodeList.Add(n);
                    n.Pos = e.mousePosition;

                    e.Use();
                    GUI.changed = true;
                }
                else { 
                    if(this.selectNode == null)
                    {
                        this.selectNode = clickNode;
                    }
                    else
                    {
                        Connection c = new Connection();
                        //Connection c = CreateInstance<Connection>();
                        Undo.RecordObject(this.doc, "Add Connection");
                        this.doc.ConnectionList.Add(c);
                        c.InNode = clickNode;
                        c.OutNode = this.selectNode;

                        this.selectNode = null;

                        e.Use();
                        GUI.changed = true;
                    }
                }
                break;

            case EventType.KeyDown:
                if(e.keyCode==KeyCode.A)
                {
                    Undo.RecordObject(this.doc, "CountUp");

                    int addCount = Random.Range(1, 100);

                    this.doc.PublicCounter += addCount;
                    this.doc.PrivateCounter += addCount;
                    this.doc.SerializeCounter += addCount;

                    GUI.changed = true;
                }
                if (e.keyCode == KeyCode.S)
                {
                    // 参照が一致するか確認
                    this.doc.Log();
                }
                break;
        }
    }
}


TestDocument.cs

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

public class TestDocument : ScriptableObject
{
    // クラス
    [SerializeField]
    private List<Node> nodeList;
    public List<Node> NodeList { get { return this.nodeList; } }

    public List<Connection> ConnectionList;

    // プリミティブ

    public int PublicCounter;

    private int privateCounter;
    public int PrivateCounter { get { return this.privateCounter; } set { this.privateCounter = value; } }

    [SerializeField]
    private int serializeCounter;
    public int SerializeCounter { get { return this.serializeCounter; } set { this.serializeCounter = value; } }
    public void init()
    {
        this.nodeList = new List<Node>();
        this.ConnectionList = new List<Connection>();
        this.PublicCounter = 0;
        this.privateCounter = 0;
    }

    public void Log()
    {
        foreach (var conn in this.ConnectionList)
        {
            bool inNodeChek = false;
            bool outNodeChek = false;

            foreach (var node in this.nodeList)
            {
                if(ReferenceEquals(conn.InNode, node))
                {
                    inNodeChek = true;
                }
                if (ReferenceEquals(conn.OutNode, node))
                {
                    outNodeChek = true;
                }
            }
            if (inNodeChek && outNodeChek) { Debug.Log("OK"); }
            else { Debug.Log("NG"); }
        }
    }
}

[System.Serializable]
public class Node
//public class Node : ScriptableObject
{
    public Vector2 Pos { get { return this.rect.position; } set { this.rect.position = value; } }

    [SerializeField]
    private Rect rect = new Rect(0f, 0f, 150f, 40f);

    public bool Contains(Vector2 point)
    {
        return this.rect.Contains(point);
    }

    public void Draw()
    {
        GUI.Box(this.rect, new GUIContent("Rect:" + this.rect.position.ToString()));
    }
}

[System.Serializable]
public class Connection
//public class Connection : ScriptableObject
{
    [SerializeField]
    private Node inNode;
    public Node InNode { get { return this.inNode; } set { this.inNode = value; } }

    [SerializeField]
    private Node outNode;
    public Node OutNode { get { return this.outNode; } set { this.outNode = value; } }

    public void Draw()
    {
        if (this.inNode == null || this.outNode == null) return;

        float lineWidth = 3f;

        Vector3[] points = new Vector3[4];

        Vector3 inNodePos = this.inNode.Pos;
        Vector3 outNodePos = this.outNode.Pos;

        points[0] = new Vector3(inNodePos.x, inNodePos.y + 20f, 0);
        points[1] = new Vector3(inNodePos.x - 10f, inNodePos.y + 20f, 0);
        points[2] = new Vector3(outNodePos.x + 160f, outNodePos.y + 20f, 0);
        points[3] = new Vector3(outNodePos.x + 150f, outNodePos.y + 20f, 0);

        Handles.DrawAAPolyLine(lineWidth, points);
    }
}

Kotlinメモ

Kotlin独自の文法やイディオムを忘れやすいので自分用のメモです

f:id:hildsoft:20180731034211j:plain


公式サイト Kotlin Programming Language

nullかどうか判断して実行

// Java: extrasがnullの場合、intValueに初期値0を設定する
int intValue = 0;
if(intent.extras != null) {
  intValue = intent.extras?.getInt("key_intAAA");
}
// Kotlin: extrasがnullの場合、intValueに初期値0を設定する
val intValue = intent.extras?.getInt("key_intAAA") ?: 0

extrasがnullの場合だとgetIntでNullPointerExceptionになるので、安全呼び出し演算子(?.)でnullかどうかチェックしつつ実行し、 nullの場合はエルビス演算子(?:)でチェックして初期値を使用します。

三項演算子

// Java: 小さい方の値を取得
int a = 5;
int b = 10;
int minValue = a < b ? a : b;
// Kotlin: 小さい方の値を取得
val a = 5
val b = 10
val minValue = if (a < b) a else b

Kotlinには三項演算子が無いので、素直にif文を書いた方が良さそうです。

if文で値を返すことができるので、代入程度なら1行に書いても大差ないですし、 代入以外で三項演算子を使っても可読性が落ちる場合が多いので、特に問題は無いでしょう。

Kotlinでリスナーを書く方法とSAM変換

Kotlinでのリスナーの書き方

Kotlinは少し触っている程度で未だにSAM変換の書き方を覚えられていないため、必要なところだけ自分なりにまとめてみます。

何が何でもSAM変換する必要は無いと思っています。あくまでソースの述量減少に伴う可読性を重視することが大事なので、 コーディング規約に従ってある程度の裁量を持たせても良いかと思います。


SAM変換とSAM変換が使える条件

SAM変換はSAM(Single Abstract Method)を定義したinterfaceを、ラムダ式に置き換えることです。

名前から分かる通り、メソッドを1個だけ定義したインターフェースをクラス化するときに記述量を減らせるだけなので、 複数のメソッドを定義しているインターフェースは従来通りの書き方になります。

あと注意点として、Javaで定義されているinterface限定になります。Kotlinでinterfaceを定義した場合は使えません。

何故そのような仕様になっているのかは分からないですが、今後Kotlinに開発の主軸が移ってきたら対応されるのでしょうか?


冗長な書き方

Kotlinを使っている人はJavaの方が慣れているという人も多いと思うので、Javaのコードから始めていきます。

AndroidでボタンをクリックするとLogを出力する例を書いてみます。

Java

AAAButton.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    Log.v("ログ出力");
  }
});

Kotlin

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

この書き方ではKotlinの省略化のメリットは無いですね。
しかしほぼ1:1で対応しているのでJavaとKotlinを対比する上では見やすいと思います。

SAM変換による省略化

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

このKotlinのコードをベースに進めていきます。

まずsetOnClickListenerに渡すパラメータは、View.OnClickListenerインターフェースを実装したクラスインスタンス ということは文法上自明ですね。

またKotlinでのラムダ式の記述は

{ パラメータ -> メソッドの処理 }

で関数名を省略して書けます。

View.OnClickListenerインターフェースにはonClickメソッドにしかないので、
View.OnClickListenerで実装されているメソッドは一つで、onClickのことだとメソッド名を書かなくても特定出来るわけです。
メソッドの特定ができれば、もちろんパラメータの型も戻り値の型も特定できます。

となると、

  • View.OnClickListener
  • onClick
  • onClickのパラメータの型(View)、戻り値の型(Unit)

は特になくても良い情報となります。削ってしまいましょう。

削る前

AAAButton.setOnClickListener( object: View.OnClickListener{
  override fun onClick(view: View?): Unit {
    Log.v("ログ出力")
  }
})

削った後

AAAButton.setOnClickListener( { view ->
  Log.v("ログ出力")
})

ここからはラムダ式の文法で、

  • 引数が一つであれば省略できて、処理部分で必要ならitとして使用できる
  • メソッド(ここではsetOnClickListener)の最後の引数がラムダ式なら、{}部分を後ろに書ける
  • その状態で()内に引数が無ければ、つまりメソッドの引数がラムダ式だけなら()を省略できる

を適用して省略していくと

AAAButton.setOnClickListener({ Log.v("ログ出力") })
AAAButton.setOnClickListener(){ Log.v("ログ出力") }
AAAButton.setOnClickListener{ Log.v("ログ出力") }

と書くことができます。


中身の少ない同じような記述を並列して定義する場合は、わざわざ指定のinterfaceを実装した内部クラスを作ったりする必要なくシンプルに書くことができます。

個人的には内部の処理が長かったり複雑だったりするとクラス分けたくなりますが、この辺はコーディング規約などでケースバイケースの対応になるかと思います。


サンプル

SAM変換を理解したところで本題。いくつかのパターンを書いておきます。

インターフェース内のメソッド一つ

リスナークラスだけをパラメータに取るメソッド

AAA.setOnXXXListener{ Log.v("ログ出力") }

リスナークラスだけをパラメータに取るメソッドでメソッドの引数を使用したい場合(itを使用)

AAA.setOnXXXListener{ it.BBB() }

リスナー側で実装を要求されるメソッドにパラメータが複数ある場合

AAAButton.setOnClickListener{ p1, p2 ->
  Log.v("ログ出力")
}

インターフェース内のメソッドが複数

  AAA.setOnXXXListener(object : BBBinterface {
    override fun onCCCMethod(p: DDDParameterClass?) {
      Log.v("ログ出力")
    }
    override fun onEEEMethod(p: FFFParameterClass?) {
    }
})

処理が長い場合

  num = 10
  AAA.setOnXXXListener(BBBClass(num))

・・・中略・・・

// 要求されるインターフェースを実装する内部クラス
inner class BBBClass(num: Int) : OnXXXListener {
    override fun onCCCMethod(p: DDDParameterClass?) {
      ・・・中略・・・
    }
}

You need to add a reference to Mono.Android.Export.dll when you use ExportAttribute or ExportFieldAttribute.の対処法

Xamarin.AndroidでMono.Android.Export.dll参照エラーが出た時の対処法

エラー表示

You need to add a reference to Mono.Android.Export.dll when you use ExportAttribute or ExportFieldAttribute.

(ExportAttributeかExportFieldAttributeの属性を使う場合はMono.Android.Export.dllの参照を追加する必要があります。)

対応方法

参照が足りていないので追加するだけです。


f:id:hildsoft:20180527212709j:plain

ソリューションエクスプローラーのAndroidプロジェクトの参照に「Mono.Android.Export」がありません。


f:id:hildsoft:20180527212718j:plain

参照を右クリックしてメニューを表示し、「参照の追加」を選択します。


f:id:hildsoft:20180527211952j:plain

左側から「アセンブリ」を選択し、「Mono.Android.Export」にチェックを入れてOKボタンでダイアログを閉じます。


f:id:hildsoft:20180527212725j:plain

参照に「Mono.Android.Export」が追加されていればビルドが通ると思います。

UnityでuGUIを使ったUIに3Dオブジェクトを表示させたい

UnityでuGUIを使ったUIに3Dオブジェクトを表示させたい

f:id:hildsoft:20180110045224p:plain

検証環境

Unity:2017.2.0f3

Canvasの設定

CanvasにはRender Modeというプロパティがあります。

f:id:hildsoft:20180110031805p:plain

  • Screen Space - Overlay
  • Screen Space - Camera
  • World Space

Screen Space - Overlay

f:id:hildsoft:20180110033127p:plain

Overlayは被せるとか覆うという意味があります。

文字通り、カメラで映し出した映像の上にCanvasの情報を上書きします。

UIは通常画面の一番手前に表示され、他のもので隠されることは無いためこの使い方が一般的です。


Screen Space - Camera

f:id:hildsoft:20180110033356p:plain

この設定は、カメラから一定の位置にCanvasを配置するものです。

OverlayではUIが最前面に表示されるので3Dオブジェクトを配置することができません。

そこで、UI上でも3Dオブジェクトを使う場合は、UIをScene内に入れてしまいます。

今回はこの設定でUI上に3Dオブジェクトを表示させます。


World Space

f:id:hildsoft:20180110034327p:plain

せっかくなので、ついでにもう一つの設定も軽く見ておきましょう。

この設定は文字通り、ワールドスペース内にCanvasをオブジェクトとして配置します。

実際に空間内に存在するので、カメラを移動してもCanvasは動かないため、空間に固定されたものになります。

使ったことは無いですがVRなどで使用されるのかな?

ゲーム内でキャラクターが操作するパネルとして使用することも可能だと思います。


UI用のカメラを作成する

f:id:hildsoft:20180110033356p:plain

この例にもあるように、同じカメラを使用してしまうと位置関係がややこしくなったり、ボタンが隠れてしまったりするのでUI専用のカメラを用意します。

f:id:hildsoft:20180110035341p:plain

Hierarchyで右クリックして、Cameraを追加します。

ここでは「UI Camera」としておきます。

f:id:hildsoft:20180110035637p:plain

Cameraを追加するとAudio Listenerコンポーネントも付いてきますが、Main Cameraにもあるため、2個になってしまいます。

UIでは必要がないのと警告が出るので、これは削除します。

Canvasにカメラを設定

f:id:hildsoft:20180110040012p:plain

これでUI Cameraを移動してもCanvasは自動でUI Cameraに付いてきて、UI Cameraの視界内に入る3Dオブジェクトを描画することができます。


映したいものと映したくないものを分ける

UI用のカメラが3Dオブジェクトを映せるようになると一つ問題が出てきます。

UIだけに映したくてもMainカメラの視界内にUIのオブジェクトが存在すると表示されてしまいます。逆もしかりです。

そこで使用するのがレイヤー機能です。

f:id:hildsoft:20180110041700p:plain

レイヤーはオブジェクトを選択して、Inspectprから変更可能です。

f:id:hildsoft:20180110041854p:plain

0~7までの8個はシステムに予約されていますが、8~31までの24個はユーザーが自由に使用できます。

UI用のカメラに表示したい物を扱うレイヤーを追加します。ここでは「UI 3D Object」とします。


レイヤーを追加しただけでは何も変化はありません。

カメラ側の設定で、何を映すかを指定する必要があります。

f:id:hildsoft:20180110042456p:plain

メインカメラの設定ではUIに関する物を映さない様にCulling Maskから「UI」と「UI 3D Object」を外します。

f:id:hildsoft:20180110042625p:plain

UIカメラの設定ではUIに関する物を映す様にCulling Maskに「UI」と「UI 3D Object」をチェックします。

必要なら、Lightの設定も同様に変更してください。


カメラの描画順

このままではMain Camera描画後に、UI Cameraの映したものを上書きしてしまいます。

なので、あとから描画するUI Cameraの方は必要なところだけを上書きするようにします。

f:id:hildsoft:20180110043215p:plain

Clear FlagsをDepth onlyにすると、必要な個所だけが上書きで更新されます。

また、Depthの値がMain Cameraより大きな値になっていることを確認してください。(初期状態では問題無いと思います)


完成するとこんな感じに

f:id:hildsoft:20180110045224p:plain

作成した3Dオブジェクトは、紐づくUIの子要素にしておくことでカメラの回転やUI要素の移動時にも対応可能になります。

ただし、移動の際はパースの問題が出るため、カメラのProjectionを「Orthographic」にしておいた方がいいかもしれません