見出し画像

【Unity備忘録】UnityEditor.Experimental.GraphView

参考文献

メモ

GraphView.GetCompatiblePorts

参考文献ではPortを接続するためにGraphView.GetCompatiblePortsをオーバーライドしている。
確かに、デフォルトの状態ではPortを作成しても接続出来ないようだ。
Portを作成する時にわざわざDirection(Input/Output)とTypeを指定するのに、何故Portが接続出来ないのか不思議だったのでソースを確認した。

結論から言うと、以下のような引数を持つ拡張メソッドを作成するとPortが接続出来るようになっていた。
ただし、所感としてはこの方法に合わせるよりもGetCompatiblePortsをオーバーライドした方が無難である。

using UnityEditor.Experimental.GraphView;

public static class NodeAdapterExtension
{
    //第1引数はNodeAdapter
    //第2引数・第3引数はPortSource<T>の型引数Tに接続したい型を指定
    //メソッド名は任意
    //処理は空で構わない
    public static void Compatible(this NodeAdapter adapter, PortSource<string> from, PortSource<string> to)
    { }
}

以下、ソースを確認していく。

//Modules > GraphViewEditor > Views > GraphView.cs
        
public virtual List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
    return ports.ToList().Where(nap =>
        nap.direction != startPort.direction &&
        nap.node != startPort.node &&
        nodeAdapter.GetAdapter(nap.source, startPort.source) != null)
        .ToList();
}

「ports.ToList().Where」はGraphViewの子孫要素のPortから接続すべきPortをフィルターしている。
全ての条件式を満たしたPortがstartPortの接続可能なPortである。

「nap.direction != startPort.direction」はPortのI/Oのチェック、
「nap.node != startPort.node」はPortの親であるNodeが同一でないかのチェックをしている。
一目見て分からないのが「nodeAdapter.GetAdapter(nap.source, startPort.source) != null)」

//Modules > GraphViewEditor > NodeAdapter.cs

// TODO: This is a straight port from Canvas2D. I don't think that having to check for types in the assembly using reflection is the way we want to go.
public class NodeAdapter

TODO: これは Canvas2D からの直接の移植です。 リフレクションを使用してアセンブリ内の型をチェックする必要があるとは思いません。

Google翻訳

NodeAdapterのTODOに書かれているように、GetAdapterではリフレクションによって拡張メソッドの情報をキャッシュしていた。

//Modules > GraphViewEditor > NodeAdapter.cs

if (s_NodeAdapterDictionary == null)
{
    s_NodeAdapterDictionary = new Dictionary<int, MethodInfo>();

    // add extension methods
    AppDomain currentDomain = AppDomain.CurrentDomain;
    foreach (Assembly assembly in currentDomain.GetAssemblies())
    {
        IEnumerable<MethodInfo> methods;

        try
        {
            methods = GetExtensionMethods(assembly, typeof(NodeAdapter));
        }
        // Invalid DLLs might raise this exception, simply ignore it
        catch (ReflectionTypeLoadException)
        {
            continue;
        }

        foreach (MethodInfo method in methods)
        {
            ParameterInfo[] methodParams = method.GetParameters();
            if (methodParams.Length == 3)
            {
                string pa = methodParams[1].ParameterType + methodParams[2].ParameterType.ToString();
                int hash = pa.GetHashCode();
                if (s_NodeAdapterDictionary.ContainsKey(hash))
                {
                    Debug.Log("NodeAdapter: multiple extensions have the same signature:\n" +
                        "1: " + method + "\n" +
                        "2: " + s_NodeAdapterDictionary[hash]);
                }
                else
                {
                    s_NodeAdapterDictionary.Add(hash, method);
                }
            }
        }
    }
}

「methods = GetExtensionMethods(assembly, typeof(NodeAdapter));」(GetExtensionMethodsの詳細は省略)によってNodeAdapterを第1引数とする拡張メソッドの情報を取得している。
「if (methodParams.Length == 3)」以降で、取得した拡張メソッドの第2引数・第3引数の情報を取得してハッシュ値を作成、キャッシュしている。
これらキャッシュとGetAdapterの引数が合致する時に、MethodInfoが返される。

この時に注意したいのが、拡張メソッドの第2引数・第3引数の型はPortSource<T>であるということだ。
PortSource<T>自体は空のクラスで、あくまで拡張メソッドの定義のために使われるだけのようだ。
GetCompatiblePortsでPort.sourceがGetAdapterに割り当てられる。
Port.sourceに代入されているのは、Port.portTypeのセッターでインスタンス化されたPortSource<T>である。
(余談だが、Port.sourceはpublicかつobjectなので後から任意のオブジェクトに変更しようと思えば簡単に出来てしまう)

//Modules > GraphViewEditor > Elements > Port.cs

private Type m_PortType;
public Type portType
{
    get { return m_PortType; }
    set
    {
        if (m_PortType == value)
            return;

        ManageTypeClassList(m_PortType, RemoveFromClassList);

        m_PortType = value;
        Type genericClass = typeof(PortSource<>);
        Type constructedClass = genericClass.MakeGenericType(m_PortType);
        source = Activator.CreateInstance(constructedClass);

        if (string.IsNullOrEmpty(m_ConnectorText.text))
            m_ConnectorText.text = m_PortType.Name;

        ManageTypeClassList(m_PortType, AddToClassList);
    }
}

結果的に、GraphView.GetCompatiblePortsは拡張メソッドを定義しなければPortが接続出来ないようになっている。
拡張メソッドの例を再掲する。

using UnityEditor.Experimental.GraphView;

public static class NodeAdapterExtension
{
    //第1引数はNodeAdapter
    //第2引数・第3引数はPortSource<T>の型引数Tに接続したい型を指定
    //メソッド名は任意
    //処理は空で構わない
    public static void Compatible(this NodeAdapter adapter, PortSource<string> from, PortSource<string> to)
    { }
}

現状の動作は、デフォルトというよりもオプション的である。
Experimentalであることも鑑みると、NodeAdapterに頼らない形でオーバーライドした方が使い勝手がいいだろう。
その場合は、参考文献のようにPort.portTypeを単純に比較する程度で十分だ。

ただし、GraphViewで継承クラスを扱うなら共変性・反変性を意識する必要があるだろう。
その場合、以下のようにstartPort.directionによって処理を分ける必要がある。

public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
    return startPort.direction == Direction.Input
        ? ports.Where(x => x.direction == Direction.Output &&
            startPort.portType.IsAssignableFrom(x.portType) &&
            x.node != startPort.node).ToList()
        : ports.Where(x => x.direction == Direction.Input &&
            x.portType.IsAssignableFrom(startPort.portType) &&
            x.node != startPort.node).ToList();
}

この記事が気に入ったらサポートをしてみませんか?