diff --git a/README.md b/README.md index 20868e5..4f7ac3f 100644 --- a/README.md +++ b/README.md @@ -3,34 +3,29 @@ Node graph creation tool based on unity experimental graphview and if possible l the main goal of the repo is to make graph creation easier and more intuitive. -Note **it's not usable and productive on current stage** and need a better -development . - -The tool separate its graph editor implementation and the graph creation logic. - -# Install - -currently under development - # Some Features -* Create graph script a the creator tool -* Node creation based on specified type of graph +* creator tool create graph easily * Easy port creation via attribute * Runtime graph * Blackboard for runtime graph as exposed parameters * Runtime graph execution * An easy test mode (Runtime graph only) -* Scene object nodes hold scene objects +* Scene object nodes hold scene objects like blackboard + +# Some to-dos +* Port connectivity of two types have implicit conversion +* Node placement +* Vertical node +* A universal merger handle multiple input +* Support static graph data traversal + +# Install & Usage +Right now this lib is still under development. + + -# Some To-dos -* Function as port -* Circular dependency support for some situations such as FSM -* Edge colors customization -# Usage -Not yet documented -### Convention diff --git a/TNode/Samples.meta b/TNode/Samples.meta new file mode 100644 index 0000000..102f2aa --- /dev/null +++ b/TNode/Samples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dc7f08d6196fcc24c97cf3b03543b697 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/Samples/Editor.meta b/TNode/Samples/Editor.meta new file mode 100644 index 0000000..5e4d0b6 --- /dev/null +++ b/TNode/Samples/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15edfa2efedd3854c95e45fefd5caf48 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/Samples/Editor/HelloEditor.asset b/TNode/Samples/Editor/HelloEditor.asset new file mode 100644 index 0000000..d60be1a --- /dev/null +++ b/TNode/Samples/Editor/HelloEditor.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 99ad0af56b40495cb6bd6165e652266c, type: 3} + m_Name: HelloEditor + m_EditorClassIdentifier: + graphElementsData: [] + graphImplType: 0 + autoUpdate: 0 diff --git a/TNode/Samples/Editor/HelloEditor.asset.meta b/TNode/Samples/Editor/HelloEditor.asset.meta new file mode 100644 index 0000000..0fd1ea2 --- /dev/null +++ b/TNode/Samples/Editor/HelloEditor.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 060ae44fcac9b534f9bdca3c3e5f1484 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/Samples/Editor/HelloEditor.cs b/TNode/Samples/Editor/HelloEditor.cs new file mode 100644 index 0000000..f7e4219 --- /dev/null +++ b/TNode/Samples/Editor/HelloEditor.cs @@ -0,0 +1,25 @@ +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; +using TNodeCore.Editor; +public class HelloEditor : GraphEditor{ + [OnOpenAsset] + public static bool OnOpenAsset(int instanceID, int line){ + var graph = EditorUtility.InstanceIDToObject(instanceID) as HelloGraph; + if (graph != null) { + var wnd = GetWindow(); + wnd.titleContent = new GUIContent("EasyGraph Editor"); + wnd.Show(); + wnd.SetupNonRuntime(graph); + return true; + } + return false; + } + [MenuItem("Window/HelloEditor")] + public static void ShowWindow(){ + var res = GetWindow(); + res.titleContent = new GUIContent("EasyGraph Editor"); + res.Show(); + } + +} \ No newline at end of file diff --git a/TNode/Samples/Editor/HelloEditor.cs.meta b/TNode/Samples/Editor/HelloEditor.cs.meta new file mode 100644 index 0000000..ec0600b --- /dev/null +++ b/TNode/Samples/Editor/HelloEditor.cs.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 7bffb928791aff340912bba71fc356e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - m_ViewDataDictionary: {instanceID: 0} + - mVisualTreeAsset: {fileID: 9197481963319205126, guid: b67f6dcbe2361b649ad2b7845207321b, type: 3} + - graphEditorData: {fileID: 11400000, guid: 060ae44fcac9b534f9bdca3c3e5f1484, type: 2} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/Samples/HelloGraph.cs b/TNode/Samples/HelloGraph.cs new file mode 100644 index 0000000..9c4dd64 --- /dev/null +++ b/TNode/Samples/HelloGraph.cs @@ -0,0 +1,8 @@ +using UnityEngine; +using System; +using TNodeCore.Runtime.Models; +[CreateAssetMenu(fileName = "New HelloGraph", menuName = "TNode/HelloGraph")] +[Serializable] +public class HelloGraph : GraphData{ + +} \ No newline at end of file diff --git a/TNode/Samples/HelloGraph.cs.meta b/TNode/Samples/HelloGraph.cs.meta new file mode 100644 index 0000000..3baf052 --- /dev/null +++ b/TNode/Samples/HelloGraph.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2051a0adbd1ba974084a535dd06ab7d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/Samples/New HelloGraph.asset b/TNode/Samples/New HelloGraph.asset new file mode 100644 index 0000000..58a1457 --- /dev/null +++ b/TNode/Samples/New HelloGraph.asset @@ -0,0 +1,24 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2051a0adbd1ba974084a535dd06ab7d7, type: 3} + m_Name: New HelloGraph + m_EditorClassIdentifier: + nodeList: [] + nodeLinks: [] + blackboardData: + rid: -2 + sceneReference: + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } diff --git a/TNode/Samples/New HelloGraph.asset.meta b/TNode/Samples/New HelloGraph.asset.meta new file mode 100644 index 0000000..e631b0a --- /dev/null +++ b/TNode/Samples/New HelloGraph.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 490933fc590be444780d73cd9f777ed4 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/TNode/TNodeCore/Runtime/Interfaces/IPortTypeConversion.cs b/TNode/TNodeCore/Runtime/Interfaces/IPortTypeConversion.cs index 4633e26..092dc61 100644 --- a/TNode/TNodeCore/Runtime/Interfaces/IPortTypeConversion.cs +++ b/TNode/TNodeCore/Runtime/Interfaces/IPortTypeConversion.cs @@ -3,5 +3,10 @@ public abstract class PortTypeConversion{ public abstract TTo Convert(TFrom tFrom); } + public abstract class TwoWayPortTypeConversion : PortTypeConversion{ + + public abstract TFrom ConvertBack(TTo tTo); + + } } \ No newline at end of file diff --git a/TNode/TNodeCore/Runtime/RuntimeCache/RuntimeCache.cs b/TNode/TNodeCore/Runtime/RuntimeCache/RuntimeCache.cs index c2db650..bb5046e 100644 --- a/TNode/TNodeCore/Runtime/RuntimeCache/RuntimeCache.cs +++ b/TNode/TNodeCore/Runtime/RuntimeCache/RuntimeCache.cs @@ -45,11 +45,23 @@ namespace TNodeCore.Runtime.RuntimeCache{ public object Convert(object value){ return _converter.Convert((T1)value); } - + + } + //Store a t1 to t2 conversion but use two way converter's convert back method + internal class PortConverterHelperReverse : IPortConverterHelper{ + private readonly TwoWayPortTypeConversion _converter; + public object Convert(object value){ + return _converter.ConvertBack((T1)value); + } + public PortConverterHelperReverse(Type type){ + _converter = Activator.CreateInstance(type) as TwoWayPortTypeConversion; + } + } internal interface IPortConverterHelper{ public object Convert(object value); + } public class PropertyNotFoundException : Exception{ @@ -126,18 +138,17 @@ namespace TNodeCore.Runtime.RuntimeCache{ else{ } } - + private void CacheRuntimePortTypeConversion(Type type){ + if (type.BaseType == null) return; if (type.BaseType != null){ var genericType = type.BaseType.GetGenericTypeDefinition(); - if (genericType != typeof(PortTypeConversion<,>)){ + if (genericType != typeof(PortTypeConversion<,>)|| genericType != typeof(TwoWayPortTypeConversion<,>)){ return; } } - else{ - return; - } - + + //Forward direction var type1 = type.BaseType.GetGenericArguments()[0]; var type2 = type.BaseType.GetGenericArguments()[1]; var specificType = typeof(PortConverterHelper<,>).MakeGenericType(type1, type2); @@ -149,17 +160,70 @@ namespace TNodeCore.Runtime.RuntimeCache{ CachedPortConverters.Add(type1,new Dictionary()); } CachedPortConverters[type1].Add(type2,instance); + + //Reverse direction + if(type.BaseType.GetGenericTypeDefinition()==typeof(TwoWayPortTypeConversion<,>)){ + var specificTypeReverse = typeof(PortConverterHelperReverse<,>).MakeGenericType(type2, type1); + var instanceReverse = Activator.CreateInstance(specificTypeReverse, type) as IPortConverterHelper; + if (instanceReverse == null){ + return; + } + if (!CachedPortConverters.ContainsKey(type2)){ + CachedPortConverters.Add(type2,new Dictionary()); + } + CachedPortConverters[type2].Add(type1,instanceReverse); + } + } + private readonly Dictionary,bool> _possibleImplicitConversions = new (); + private bool HasImplicitConversion(Type baseType, Type targetType){ + var tuple = new Tuple(baseType, targetType); + if (_possibleImplicitConversions.ContainsKey(tuple)){ + return _possibleImplicitConversions[tuple]; + } + var res =baseType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == "op_Implicit" && mi.ReturnType == targetType) + .Any(mi => { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == baseType; + }); + return _possibleImplicitConversions[tuple] = res; + } + + private void CachingImplicitConversion(Type baseType, Type targetType){ + if (HasImplicitConversion(baseType, targetType)) return; + + //Create Implicit Conversion Helper that caches the implicit cast function + var typeConverter = Activator.CreateInstance(typeof(ImplicitConversionHelper<,>).MakeGenericType(baseType, targetType)) as IPortConverterHelper; + + if (!CachedPortConverters.ContainsKey(baseType)){ + CachedPortConverters.Add(baseType,new Dictionary()); + } + CachedPortConverters[baseType].Add(targetType,typeConverter); + } - public object GetConvertedValue(Type from,Type to,object value){ + if(!CachedPortConverters.ContainsKey(from)){ - throw new ConversionFailedException("No converter found for type "+from); + //Find the cached port failed ,check if there is an implicit conversion + //This inner cache method would only run once,so add a guard to prevent it run again,even though the function itself has a guard statement. + if(HasImplicitConversion(from,to)){ + CachingImplicitConversion(from,to); + } } if(!CachedPortConverters[from].ContainsKey(to)){ + //Just like above, this function should be checked in here too + if(HasImplicitConversion(from,to)){ + CachingImplicitConversion(from,to); + } return value; } return CachedPortConverters[from][to].Convert(value); } + + private bool GetImplcitConvertedValue(Type from, Type to){ + throw new NotImplementedException(); + } + public List GetSupportedTypes(Type type){ if(!CachedPortConverters.ContainsKey(type)){ return null; @@ -248,6 +312,25 @@ namespace TNodeCore.Runtime.RuntimeCache{ } + public class ImplicitConversionHelper : IPortConverterHelper{ + public Func ConvertFunc; + public ImplicitConversionHelper(){ + //Caching the implicit method that converts t1 to t2 + var method = typeof(T2).GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(T1) }, null); + if (method == null){ + //Search it in T1 + method = typeof(T1).GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(T2) }, null); + } + //Create the delegate + if (method != null) + ConvertFunc = (Func) Delegate.CreateDelegate(typeof(Func), method); + } + + public object Convert(object value){ + return ConvertFunc((T1) value); + } + } + public class ConversionFailedException : Exception{ public ConversionFailedException(string noConverterFoundForType):base(noConverterFoundForType){ diff --git a/TNode/TNodeCore/Runtime/RuntimeNode.cs b/TNode/TNodeCore/Runtime/RuntimeNode.cs index 172d0e8..2bf09f7 100644 --- a/TNode/TNodeCore/Runtime/RuntimeNode.cs +++ b/TNode/TNodeCore/Runtime/RuntimeNode.cs @@ -23,16 +23,15 @@ namespace TNodeCore.Runtime{ var portType = _portAccessors[portName].Type; if(portType!=valueType && !portType.IsAssignableFrom(valueType)){ var res =RuntimeCache.RuntimeCache.Instance.GetConvertedValue(valueType, portType, value); - _portAccessors[portName].SetValue(this.NodeData, res); + _portAccessors[portName].SetValue(NodeData, res); } else{ - - _portAccessors[portName].SetValue(this.NodeData,value); + _portAccessors[portName].SetValue(NodeData,value); } } public object GetOutput(string portName){ - return _portAccessors[portName].GetValue(this.NodeData); + return _portAccessors[portName].GetValue(NodeData); } diff --git a/TNode/TNodeGtfImpl.meta b/TNode/TNodeGtfImpl.meta new file mode 100644 index 0000000..19f4b0e --- /dev/null +++ b/TNode/TNodeGtfImpl.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fcfff7d02bd7fbb41a515c4c4359aab2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: