【Unity】GraphViewで遊んでみた。【第三回】
はじめに
この記事は第三回になります。
執筆日は20222/11/14になります。
Unityのバージョンは2021.3.1f1を使用しています。
第一回:Windowの作成方法
第二回:Nodeの作成方法
自己紹介
活動名はHiruko、もしくはHirukoTimeで活動してます。
個人でSteamでゲームを販売したり、定期的にゲームジャムなどに参加をしています。
今回のお題
グラフの保存機能を作る。
GraphViewに保存に関するものは無いので頑張って実装する。
まずは保存用のボタンを作成する。
ファイル構成
- Editor
- Node
- BaseNode.cs
- FloatNode
- TestWindow.cs
- TestGraphView.cs
- SearchMenu.cs
- Node
- Resources
- UXMLファイル
- USSファイル
- Runtime
- GraphData.cs
ブログ用でアクセサーは適当にpublic。
見た目の基本方針としては、ShaderGraphっぽくする。

アセットのダブルクリックでGraphが開く。
開いた時に自動ロード
保存と新規保存。
GraphData.cs
using System; using System.Collections.Generic; using UnityEngine; public class GraphData : ScriptableObject { public List<NodeData> Nodes = new List<NodeData>(); public List<EdgeData> Edges = new List<EdgeData>(); public void Clear() { nodeDates.Clear(); edgeDates.Clear(); } } [Serializable] public class NodeData { public string guid; public string typeName; public string parameterJson; public Rect position; public NodeData(string targetGuid,string typeFullName,string paramJson,Rect pos) { guid = targetGuid; typeName = typeFullName; parameterJson = paramJson; position = pos; } } [Serializable] public class EdgeData { public string from; public string to; public EdgeData(string fromGuid, string toGuid) { from = fromGuid; to = toGuid; } }
TestWindow.cs
public class TestWindow : GraphViewEditorWindow
{
[SerializeField] private GraphData data;
private TestGraphView _graphView;
private const string DefaultFileName = "NewGraphData";
private const string Extension = "asset";
//Attributeはなくして、アセットファイルから開くようにする。
private static void ShowWindow(GraphData data)
{
if (data == null) return;
//呼び出した段階でOnEnableが走る
var window = GetWindow<TestWindow>(data.name);
window.Initialize(data);
window.Show();
}
//アセットを開く(ダブルクリックする)処理。
[OnOpenAsset(0)]
public static bool OnOpenAsset(int instanceId, int line)
{
var asset = EditorUtility.InstanceIDToObject(instanceId) as GraphData;
if (asset == null) return false;
ShowWindow(asset);
return true;
}
//タイミング的にOnEnableの後になる。
private void Initialize(GraphData graphData)
{
data = graphData;
OnLoad();
}
private void OnEnable()
{
_graphView = new TestGraphView();
_graphView.InitializeMenuWindow(this);
rootVisualElement.Add(_graphView);
var toolbar = new Toolbar();
toolbar.styleSheets.Add(Resources.Load<StyleSheet>("TestToolBarStyle"));
var saveButton = new ToolbarButton(OnSave){text = "Save",name = "save-button"};
var saveAsButton = new ToolbarButton(CreateFile){text = "SaveAs",name = "save-as-button"};
toolbar.Add(saveButton);
toolbar.Add(saveAsButton);
rootVisualElement.Add(toolbar);
//ここのタイミングはPlayなどでリロードが入った場合に再読み込みする。
OnLoad();
}
//このタイミングでSaveしないと編集内容が破棄されて保存前のデータがリロードされる。
//今回は何もしてないが、ダイアログなどだして、注意メッセージ後に保存でもいいかもしれない
private void OnDisable()
{
OnSave();
}
private void CreateFile()
{
var path
= EditorUtility
.SaveFilePanelInProject("Save Graph Data", DefaultFileName , Extension,"");
//Panelをキャンセルした時は空文字列になる。
if (string.IsNullOrEmpty(path)) return;
var fileName = System.IO.Path.GetFileNameWithoutExtension(path);
var newAsset = CreateInstance<GraphData>();
newAsset.name = fileName;
data = newAsset;
OnSave();
var oldAsset = AssetDatabase.LoadAssetAtPath<GraphData>(path);
if (oldAsset == null) {
AssetDatabase.CreateAsset(newAsset, path);
}
else {
EditorUtility.CopySerializedIfDifferent(newAsset, oldAsset);
AssetDatabase.SaveAssets();
}
}
private void OnSave()
{
if (data.IsUnityNull()) return;
data.Clear();
foreach (var edge in _graphView.edges)
{
var to = edge.input.node as BaseNode;
var from = edge.output.node as BaseNode;
if (from == null || to == null) continue;
var edgeData = new EdgeData(from.Guid, to.Guid);
data.Edges.Add(edgeData);
}
foreach (var node in _graphView.nodes)
{
if (!(node is BaseNode n)) continue;
var nodeData =
new NodeData(
n.Guid,
n.GetType().FullName,
n.ToJson(),
n.GetPosition());
data.Nodes.Add(nodeData);
}
}
private void OnLoad()
{
if (data.IsUnityNull()) return;
_graphView.nodes.ForEach(x => x.RemoveFromHierarchy());
_graphView.edges.ForEach(x => x.RemoveFromHierarchy());
foreach (var n in data.Nodes)
{
var t = Type.GetType(n.typeName);
if (t == null) continue;
var instance = Activator.CreateInstance(t) as BaseNode;
if (instance == null) continue;
_graphView.AddElement(instance);
instance.Initialize(n.guid,n.parameterJson,n.position);
}
var nodes = _graphView.nodes;
foreach (var e in data.Edges)
{
var from = nodes.Select(x => x as BaseNode).FirstOrDefault(x => x?.Guid == e.from);
var to = nodes.Select(x => x as BaseNode).FirstOrDefault(x => x?.Guid == e.to);
if (from == null || to == null) continue;
var input = to.inputContainer.Children().FirstOrDefault(x => x is Port) as Port;
var output = from.outputContainer.Children().FirstOrDefault(x => x is Port) as Port;
if (input == null || output == null) continue;
var edge = new Edge() {input = input, output = output};
edge.input.Connect(edge);
edge.output.Connect(edge);
_graphView.Add(edge);
}
}
}
TestToolBarStyle.uss
.unity-toolbar-button{ -unity-text-align: middle-center; margin: 0px 5px; }
BaseNode.cs
public abstract class BaseNode : Node { public string Guid { get; protected set; } protected BaseNode() : base() {} protected BaseNode(string uxmlPath) : base(uxmlPath){} public virtual void Initialize(string guid,string json,Rect pos) { Guid = guid; //JsonUtility.FromJson(json); SetPosition(pos); } public abstract string ToJson(); }
FloatNode.cs
public class FloatNode : BaseNode { public FloatField Value { get; private set; } public FloatNode() : base() { Guid = System.Guid.NewGuid().ToString("N"); UseDefaultStyling(); base.title = "Float"; var input = base.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,typeof(float)); input.portName = "X(1)"; inputContainer.Add(input); var output = base.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi,typeof(float)); output.portName = "Out(1)"; outputContainer.Add(output); Value = new FloatField(){name = "Value",label = "Value"}; extensionContainer.Add(Value); base.ToggleCollapse(); base.ToggleCollapse(); } public override string ToJson() { return JsonUtility.ToJson(Value); } public override void Initialize(string guid, string json, Rect pos) { Guid = guid; var field = JsonUtility.FromJson<FloatField>(json); Value.value = field.value; SetPosition(pos); } }
テスト動画(GIF)

一応、保存はできているが、不安が残る実装。
他にいい方法がないか検討。
変更箇所が多いので記載漏れがないか不安。
今後の予定
- ブラックボード
- ミニマップ
- 他何か思いついたら