I am making a node-based visual scripting editor. It is very simple at the moment, but I am trying to make it as expandable as possible with ease for adding new nodes.
I based it off of some code (the Finite State Machine code) on the Unify community wiki, and have expanded it greatly.
I posted this here because of this question:
What I need to know is how the heck to fix the DrawNodeWindow function in Scripty.cs so I can have EditorGUILayout.ObjectField for any field inside of a node. From there I can easily do the rest (hopefully XD).
To use the code, create a folder named “Scripty” putting scripts in there BUT put the 4
nodes in another folder inside of Scripty named “Nodes” also make a folder inside of Scripty
called “Saves” so the editor saves.
To see that the core works, create a sphere with a rigidbody, and name it “TestSphere” then add DoSystem.cs to it. Change the exposed variable “File Name” to “test.asset” through the inspector. Open the editor once, and errors will happen but a save is made nonetheless. If you run the game, the sphere should start to go upwards.
The code is all as follows:
Here is the core, in NodeSystem.cs:
using UnityEngine;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
[System.Serializable]
public abstract class Node : ScriptableObject {
public static int NullTransition = 0;
public static int NullNodeID = 0;
// works like map[trans] = id
public List<int> map = new List<int>();
public int nodeID;
// For input nodes
public List<int> inputs = new List<int>();
// used for editor
public virtual string nodeName { get { return "nameHere"; } }
public Rect window = new Rect (10, 10, 170, 100);
public void SetTransition(int trans, int id) {
if (trans == NullTransition) {
Debug.LogError ("Scripty: Error: NullTransition is not allowed as a transition");
return;
}
if (id == NullNodeID) {
Debug.LogError ("Scripty: Error: NullNodeID is not allowed as an ID");
return;
}
// make sure space exists
if (map.Count < trans) {
for (int i = map.Count; i <= trans; i++) {
map.Add (0);
}
}
map[trans] = id;
}
public void DeleteTransition(int trans) {
if (trans == NullTransition) {
Debug.LogError ("Scripty: Error: NullTransition is not allowed as a transition");
return;
}
map[trans] = 0;
}
public int GetOutputNodeID(int trans) {
return map[trans];
}
public virtual void OnEnter () { }
public virtual void OnLeave () { }
// Step (self) should return an int, commonly 1, which is the transition to call next
public abstract int Step ();
// For nodes that are for giving outputs (Vector3, float, GameObject, etc.)
public abstract object Output ();
}
[System.Serializable]
public class NodeSystem {
public List<Node> nodes;
public NodeSystem () {
nodes = new List<Node>();
}
public List<Node> getNodes() {
return nodes;
}
/**
* Sets node n's input of fieldName to node settingInput
*/
public void SetNodeInput(Node n, Node input, string fieldName) {
const BindingFlags flags = BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public;
FieldInfo[] fields = n.GetType ().GetFields (flags);
for (int i = 0; i < fields.Length; i++) {
if (fields[i].Name == fieldName) {
// this is the right spot to set.
// make sure space exists
if (n.inputs.Count < i) {
for (int j = n.inputs.Count; j <= i; j++) {
n.inputs.Add (0);
}
}
n.inputs[i] = input.nodeID;
break;
}
}
}
public void UpdateNodeInputs(Node n) {
const BindingFlags flags = BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public;
FieldInfo[] fields = n.GetType ().GetFields (flags);
for (int i = 0; i < fields.Length; i++) {
if (n.inputs[i] > 0) {
// there is a node assigned as an input
foreach (Node possible in nodes) {
if (possible.nodeID == n.inputs[i]) {
// node found to get output from
// update it too
UpdateNodeInputs (possible);
// use its output
fields[i].SetValue (n, possible.Output ());
break;
}
}
break;
}
}
}
public void AddNode(Node n) {
if (n == null) {
Debug.LogError ("Scripty: Error: Null reference is not allowed");
return;
}
foreach (Node node in nodes) {
if (node.nodeID == n.nodeID) {
Debug.LogError ("Scripty: Error: Impossible to add node " + n.nodeID.ToString() + " because is has already been added");
return;
}
}
nodes.Add (n);
}
public void DeleteNode(int id) {
if (id == Node.NullNodeID) {
Debug.LogError ("Scripty: Error: NullNodeID is not allowed as a transition");
return;
}
foreach (Node node in nodes) {
if (node.nodeID == id) {
nodes.Remove (node);
return;
}
}
Debug.LogError ("Scripty: Error: Impossible to delete node " + id.ToString() + " because it hasn't been added or has already been deleted");
}
public void CallFunction (string funcName) {
// Make sure the function is even acceptable to search for first
if (funcName == null) {
Debug.LogError ("Scripty: Error: CallFunction cannot call a function by a null name!");
return;
}
if (funcName == "") {
Debug.LogError ("Scripty: Error: CallFunction cannot call a function by a blank name!");
return;
}
// search through the nodes for the function node of this function
foreach (Node node in nodes) {
if (node.GetType () == typeof (FunctionNode)) {
// This node is a function node
if (((FunctionNode) node).function == funcName) {
// right node. start then from this node
StartFromNode (node);
}
}
}
}
public void StartFromNode (Node startingNode) {
Node curNode = startingNode;
UpdateNodeInputs (curNode);
curNode.OnEnter ();
bool functioning = true;
while (functioning) {
UpdateNodeInputs (curNode);
int next = curNode.Step ();
if (next < 0) {
// given a negative, this means that the node wants to stop this chain.
functioning = false;
} else if (next == 0) {
// the node doesn't quite want to leave yet.
} else {
// move on to the next node.
// prepare
UpdateNodeInputs (curNode);
curNode.OnLeave ();
int nextID = curNode.GetOutputNodeID (next);
if (nextID < 1) {
// shouldn't happen at all.
Debug.LogError ("Scripty: Error: Null ID given on transition!");
}
// transition
foreach (Node possible in nodes) {
if (possible.nodeID == nextID) {
curNode = possible;
UpdateNodeInputs (curNode);
curNode.OnEnter ();
break;
}
}
}
}
}
}
Here are the different nodes:
FunctionNode.cs:
using UnityEngine;
using System.Collections;
public class FunctionNode : Node {
public string function = "Update";
public override string nodeName { get { return "Function"; } }
public FunctionNode (string funcName) {
function = funcName;
}
public override int Step() {
return 1;
}
public override void OnEnter() {
}
public override void OnLeave() {
}
public override object Output() {
return null;
}
}
StopNode.cs:
using UnityEngine;
using System.Collections;
public class StopNode : Node {
public override string nodeName { get { return "Stop"; } }
public StopNode () {
}
public override int Step() {
return -1;
}
public override void OnEnter() {
}
public override void OnLeave() {
}
public override object Output() {
return null;
}
}
FindGameObjectNode.cs:
using UnityEngine;
using System.Collections;
public class FindGameObjectNode : Node {
public string gameObjectName;
public override string nodeName { get { return "Find GameObject"; } }
public FindGameObjectNode (string gameObjectName) {
this.gameObjectName = gameObjectName;
}
public override int Step() {
return 1;
}
public override void OnEnter() {
}
public override void OnLeave() {
}
public override object Output() {
return GameObject.Find (gameObjectName);
}
}
PushNode.cs:
using UnityEngine;
using System.Collections;
public class PushNode : Node {
public Vector3 pushForce = Vector3.zero;
// You can set this to 1 to just apply the same force every time instead of a delta affected force
public float delta = 1.0f;
public GameObject toPush;
public override string nodeName { get { return "Push"; } }
public PushNode (Vector3 pushForce) {
this.pushForce = pushForce;
}
public override int Step() {
if (toPush != null && toPush.GetComponent<Rigidbody>() != null && !toPush.GetComponent<Rigidbody>().isKinematic) {
toPush.GetComponent<Rigidbody>().AddForce (pushForce * delta);
}
// do the first listed transition
return 1;
}
public override void OnEnter() {
}
public override void OnLeave() {
}
public override object Output() {
return null;
}
}
Here is for saving, ScriptyFile.cs:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
public class ScriptyFile : ScriptableObject {
public NodeSystem system;
}
Here is the editor, which gives errors in DrawNodeWindow but still successfully creates the save file to load, Scripty.cs:
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
public class Scripty : EditorWindow {
private NodeSystem system;
public const string savePath = "Assets/Scripty/Saves/";
[MenuItem ("Window/Scripty/Scripty Editor")]
static void OpenWindowFunc () {
Scripty editor = EditorWindow.GetWindow<Scripty>();
editor.title = "Scripty Editor";
editor.Init ();
}
public void Init () {
system = new NodeSystem ();
FunctionNode updateNode = new FunctionNode ("FixedUpdate");
updateNode.nodeID = 3;
PushNode pushNode = new PushNode(new Vector3(0, 20, 0));
pushNode.nodeID = 1;
StopNode stopNode = new StopNode ();
stopNode.nodeID = 2;
updateNode.SetTransition (1, 1);
pushNode.SetTransition (1, 2);
FindGameObjectNode findNode = new FindGameObjectNode ("TestSphere");
findNode.nodeID = 4;
system.AddNode (pushNode);
system.AddNode (updateNode);
system.AddNode (stopNode);
system.AddNode (findNode);
system.SetNodeInput (pushNode, findNode, "toPush");
saveScripty(savePath + "test.asset");
}
void saveScripty(string path) {
ScriptyFile saveFile = (ScriptyFile) ScriptableObject.CreateInstance (typeof (ScriptyFile));
saveFile.system = system;
AssetDatabase.CreateAsset (saveFile, path);
for (int n = 0; n < saveFile.system.nodes.Count; n++) {
Node node = saveFile.system.nodes [n];
AssetDatabase.AddObjectToAsset (node, saveFile);
}
AssetDatabase.SaveAssets ();
AssetDatabase.Refresh ();
Repaint ();
}
void OnGUI () {
BeginWindows ();
foreach (Node n in system.getNodes ()) {
DrawNode (n);
}
EndWindows ();
}
void DrawNode (Node n) {
// draw window
n.window = GUI.Window (n.nodeID, n.window, DrawNodeWindow, n.nodeName);
// draw connections
int offs = 2;
foreach (int input in n.inputs) {
// draw each input connection
foreach (Node check in system.getNodes ()) {
if (check.nodeID == input) {
// we've found our node to connect to
DrawNodeCurve (new Vector3(check.window.x + check.window.width / 2, check.window.y, 0), new Vector3 (n.window.x, n.window.y + offs));
break;
}
}
offs += 16;
}
}
void DrawNodeWindow(int id) {
foreach (Node n in system.getNodes ()) {
if (n.nodeID == id) {
// we've found our node to draw
int offs = 10;
const BindingFlags flags = BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public;
FieldInfo[] fields = n.GetType ().GetFields (flags);
for (int i = 0; i < fields.Length; i++) {
offs += 16;
GUILayout.BeginHorizontal ();
GUILayout.Label (fields[i].Name);
// make sure space exists in the list, or else annoying out of range exceptions happen
if (n.inputs.Count < fields.Length) {
for (int add = 0; add <= fields.Length; add++) {
n.inputs.Add (0);
}
}
if (n.inputs[i] > 0) {
// has a connected node input
if (GUI.Button (new Rect (2, offs - 8, 64, 16), "Detach")) {
// detach this node from the right input
n.inputs[i] = 0;
}
} else {
// has no connected node input
fields[i].SetValue (n, (object) EditorGUILayout.ObjectField ((Object) fields[i].GetValue (n), fields[i].FieldType, true));
}
GUILayout.EndHorizontal ();
}
break;
}
}
GUI.DragWindow ();
}
void DrawNodeCurve (Vector3 startPos, Vector3 endPos) {
Vector3 startTan = startPos + Vector3.right * 50;
Vector3 endTan = endPos + Vector3.left * 50;
Color shadowCol = new Color(0, 0, 0, 0.06f);
for (int i = 0; i < 3; i++) {
Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
}
Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, null, 1);
}
}
And lastly, here is the code that runs the save file, DoSystem.cs:
using UnityEngine;
using UnityEditor;
using System.Collections;
public class DoSystem : MonoBehaviour {
private const string savePath = "Assets/Scripty/Saves/";
public string fileName;
public NodeSystem system;
void Start () {
LoadSystem ();
system.CallFunction ("Start");
}
void Update () {
system.CallFunction ("Update");
}
void FixedUpdate () {
system.CallFunction ("FixedUpdate");
}
private void LoadSystem () {
system = new NodeSystem();
Object[] objects = AssetDatabase.LoadAllAssetsAtPath (savePath + fileName);
if (objects.Length == 0) {
Debug.LogError ("Scripty: Error loading assets for " + fileName + " !!!");
return;
}
ScriptyFile theFile = null;
for (int i = 0; i < objects.Length; i++) {
object o = objects [i];
if (o.GetType () == typeof (ScriptyFile))
theFile = o as ScriptyFile;
}
if (theFile == null) {
return;
Debug.LogError ("Scripty: Error, loaded asset file loads but is null!");
}
system = theFile.system;
AssetDatabase.Refresh ();
}
}