hildsoftのコード置き場

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

【DoozyUIメモ】UI VIEWのShow、Hideが効かない

検証環境

Unity 2018.4.2f1

DoozyUI 3.0.c1

問題点

f:id:hildsoft:20190722200048p:plain

ゲーム再生中に表示されるShowとHideのボタンを押しても表示/非表示が切り替わらない。

解決法

Viewの名前が未設定

f:id:hildsoft:20190722200639p:plain

Viewの名前がデフォルトでUnnamedになっていますが、これだと反応しません。

恐らく中で文字列を元に(DB化して?)呼び出しをしていると思われるので、Viewの名前を設定する必要があります。

アニメーションの設定不足

f:id:hildsoft:20190722201042p:plain

アニメーションが未設定の時は赤く表示され、コンソールにエラーログも出るので分かりやすいです。

プリセットから何か選択してLoad Presetか、手動で個別にアニメーションを設定してください。

初期化していない

GameObjectがActiveではない状態でHierarchyに置くと、初期化処理が走らないのでVIEWの名前が登録されません。

f:id:hildsoft:20190722214528p:plain

初期表示で隠した状態にしておきたい場合は、Behavior at Startで制御します。

開発時にじゃまであれば、邪魔にならない所に移動させておきます。

どこに移動しておいても、再生時にCustom Start positionの設定位置に戻してくれます。

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」が追加されていればビルドが通ると思います。