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