Node based editor with arrays

I wasn’t sure whether to but this in scripting or here, as it’s one tied to the other. What I’m trying to do is make a node-based editor to write my XML files for me. The actual writing of the XML file is easy. What I’m having trouble with is having an array of nodes based on a class, and being able to create nodes (and therefore add to the array), and have the properties for each assigned to each member in the node class array. What I’ve got gives me an index out of range error, because it’s trying to get an array that’s not there. How can I sort this, so that the array is actually based on what windows I have, and created by that, instead of how they are now?

Here’s the code I have.

 	import System;
    import System.IO;
     
class DialogueGraph extends EditorWindow {
	@MenuItem ("RPG/Write Dialogue XML")
	static function ShowWindow() {
		EditorWindow.GetWindow(DialogueGraph);
	}
	

    var  fileName = "MyFile.xml";
	var numberOfNodes : int;
	var nodes : node[];
	
	var windowRect = Rect(100, 100, 100, 150);
	var windowRect2 = Rect(250, 250, 100, 150);
	
class node {
	
	var text : String;
	
	var hasReplies : boolean;
	var replies : String[];
	var links : int[];
	
	var giveItem : boolean;
	var itemToGive : int;
	var numToGive : int;
	
	var startsBattle :boolean;
	var enemyIndex : int;
	var eLevel : int;
	var eName : String;
	
	var endpoint : boolean;
	var newStartingPoint : int;
	
}

 function Write() {
           
		   if (File.Exists(fileName))
            {
                Debug.Log(fileName+" already exists.");
                return;
            }
			
            var sr = File.CreateText(fileName);
            sr.WriteLine ('<?xml version="1.0" encoding="utf-8" ?>');
			sr.WriteLine ('<dialogue>');
			sr.WriteLine (" ");
				for (var i : int = 0; i < numberOfNodes; i++) {
					
					//start writing a cnode with the ID (as all cnodes a required to have an ID)
					sr.WriteLine ('<cnode id="'+i+'" ');
					
					//check if node is an endpoint, and if so, write to it.
					//if (nodes[i].endpoint) { 
					sr.Write('endpoint="'+nodes[i].endpoint+'" ');// }
					//check if node has replies, and write it.
					sr.Write('hasReplies="'+nodes[i].hasReplies+'" ');
					//check if nodes gives an Item and write.
					sr.Write('giveItem="'+nodes[i].giveItem+'" ');
					//check if node starts a battle and write.
					sr.Write('startsBattle="'+nodes[i].startsBattle+'" ');
					//check if gives item, then give the item index and amount to give
					if (nodes[i].giveItem) {
						sr.Write('itemToGive="'+nodes[i].itemToGive+'" ');
						sr.Write('numToGive="'+nodes[i].numToGive+'" ');
					}
					//check if starts battle, then give enemyindex, level, and name
					if (nodes[i].startsBattle) {
						sr.Write('enemyName="'+nodes[i].eName+'" ');
						sr.Write('enemyLevel="'+nodes[i].eLevel+'" ');
						sr.Write('enemyIndex="'+nodes[i].enemyIndex+'" ');
					}
					//if it's an endpoint, give it the new start point for the next convo. (can be 0 for repeating the conversation)
					if(nodes[i].endpoint){
						sr.Write('multiNode="'+nodes[i].newStartingPoint+'" ');
					}
					//close the cnode
					sr.Write('>');
					
					//write the text prompt tag
					sr.WriteLine('text prompt="'+nodes[i].text+'"/>');
					
					//if the node has replies then list both.
					if (nodes[i].hasReplies) {
						sr.WriteLine('reply link="'+nodes[i].links[0]+'" text="'+nodes[0].replies[0]+'" />"');
					}
					//close the node
					sr.WriteLine("</cnode>");
					sr.WriteLine(" ");
				}
				
			sr.WriteLine ('</dialogue>');
            sr.Close();
    }


function OnGUI() {
		
	//numberOfNodes = nodes.length;
	GUILayout.Label("Use this to create dialogue readable by the DialogueHandler script");
	
	//for (var n : int = 0; i < nodes.length
	//}
	Handles.BeginGUI();
	Handles.DrawBezier(windowRect.center, windowRect2.center, 
	Vector2(windowRect.xMax + 50f,windowRect.center.y), 
	Vector2(windowRect2.xMin - 50f,windowRect2.center.y), Color.red, null, 5);
	Handles.EndGUI();
	
	BeginWindows();
	windowRect = GUI.Window (0, windowRect, WindowFunction, "Node 1");
	windowRect2 = GUI.Window (1, windowRect2, WindowFunction, "Node 2");
	EndWindows();
	}

	function WindowFunction (windowID : int) {
	
		GUI.DragWindow();
		GUILayout.Button("Button");
		GUILayout.Label(windowID.ToString());
		nodes[windowID].hasReplies = EditorGUILayout.BeginToggleGroup("Has Replies", nodes[windowID].hasReplies);
		
	}
	

}

The Write() function can be ignored at the moment, because that’s what’s writing my files and works as is.
Any help would be appreciated.

Array index starts at zero, so what you want is

numberOfNodes = nodes.length-1;
for (var n : int = 0; i < nodes.length-1

I’m not sure how that helps. I know arrays start at 0.

Here’s a new update. I can dynamically create windows, using a list. So I’m trying to do the same with nodes. However, I get this error :

but that IS the argument list, as specified in the node Class. So why won’t it take it?

 	import System;
    import System.IO;
    import System.Collections.Generic;
	
class DialogueGraph extends EditorWindow {
	@MenuItem ("RPG/Write Dialogue XML")
	static function ShowWindow() {
		EditorWindow.GetWindow(DialogueGraph);
	}
	

    var  fileName = "MyFile.xml";
	var numberOfNodes : int;
	//var nodes : node[];
	
	var windowRect = Rect(100, 100, 100, 150);
	var windowRect2 = Rect(250, 250, 100, 150);
	var windows : List.<Rect> = new List.<Rect>();


	class node {
	
	var text : String;
	
	var hasReplies : boolean;
	var replies : String[];
	var links : int[];
	
	var giveItem : boolean;
	var itemToGive : int;
	var numToGive : int;
	
	var startsBattle :boolean;
	var enemyIndex : int;
	var eLevel : int;
	var eName : String;
	
	var endpoint : boolean;
	var newStartingPoint : int;
	
	function node(t : String, hR : boolean, r : String[], l : int[], gI : boolean, 
	iG : int, nG : int, sB : boolean, eI : int, eL : int, eN : String, end : boolean, nSP : int) {
	
	text = t;  //string
	
	hasReplies= hR;  //bool
	replies = r; //string[]
	links = l;  //int[]
	
	giveItem = gI; //bool 
	itemToGive = iG; //int
	numToGive = nG; //int
	
	startsBattle = sB; //bool
	enemyIndex = eI; //int
	enemyLevel = eL; //int
	enemyName = eN; //string
	
	endpoint = end; //bool
	newStartingPoint = nSP; //int
	
	
	}
}

	var nodes : List.<node> = new List.<node>();
	
 function Write() {
           
		   if (File.Exists(fileName))
            {
                Debug.Log(fileName+" already exists.");
                return;
            }
			
            var sr = File.CreateText(fileName);
            sr.WriteLine ('<?xml version="1.0" encoding="utf-8" ?>');
			sr.WriteLine ('<dialogue>');
			sr.WriteLine (" ");
				for (var i : int = 0; i < numberOfNodes; i++) {
					
					//start writing a cnode with the ID (as all cnodes a required to have an ID)
					sr.WriteLine ('<cnode id="'+i+'" ');
					
					//check if node is an endpoint, and if so, write to it.
					//if (nodes[i].endpoint) { 
					sr.Write('endpoint="'+nodes[i].endpoint+'" ');// }
					//check if node has replies, and write it.
					sr.Write('hasReplies="'+nodes[i].hasReplies+'" ');
					//check if nodes gives an Item and write.
					sr.Write('giveItem="'+nodes[i].giveItem+'" ');
					//check if node starts a battle and write.
					sr.Write('startsBattle="'+nodes[i].startsBattle+'" ');
					//check if gives item, then give the item index and amount to give
					if (nodes[i].giveItem) {
						sr.Write('itemToGive="'+nodes[i].itemToGive+'" ');
						sr.Write('numToGive="'+nodes[i].numToGive+'" ');
					}
					//check if starts battle, then give enemyindex, level, and name
					if (nodes[i].startsBattle) {
						sr.Write('enemyName="'+nodes[i].eName+'" ');
						sr.Write('enemyLevel="'+nodes[i].eLevel+'" ');
						sr.Write('enemyIndex="'+nodes[i].enemyIndex+'" ');
					}
					//if it's an endpoint, give it the new start point for the next convo. (can be 0 for repeating the conversation)
					if(nodes[i].endpoint){
						sr.Write('multiNode="'+nodes[i].newStartingPoint+'" ');
					}
					//close the cnode
					sr.Write('>');
					
					//write the text prompt tag
					sr.WriteLine('text prompt="'+nodes[i].text+'"/>');
					
					//if the node has replies then list both.
					if (nodes[i].hasReplies) {
						sr.WriteLine('reply link="'+nodes[i].links[0]+'" text="'+nodes[0].replies[0]+'" />"');
					}
					//close the node
					sr.WriteLine("</cnode>");
					sr.WriteLine(" ");
				}
				
			sr.WriteLine ('</dialogue>');
            sr.Close();
    }


function OnGUI() {
		

	GUILayout.Label("Use this to create dialogue readable by the DialogueHandler script");


/* 	Handles.BeginGUI();
	Handles.DrawBezier(windowRect.center, windowRect2.center, 
	Vector2(windowRect.xMax + 50f,windowRect.center.y), 
	Vector2(windowRect2.xMin - 50f,windowRect2.center.y), Color.red, null, 5);
	Handles.EndGUI(); */
	
	BeginWindows();
		if (GUILayout.Button("Add Node")) { 
		
		var a : int = 0;
		 
		windows.Add(Rect(100 * a,10,100,150)); 
		
		nodes.Add("Text", false, ["Reply 1","Reply 2"], [0,0], false, 0, 0, false, 0, 0, "enemy name", false, 0 );
		
		a++;
		}

				for (var n : int = 0; n < windows.Count; n++) {
				
					windows[n] = GUI.Window(n, windows[n], WindowFunction, "Node "+n);
				}
	EndWindows();
	}

	function WindowFunction (windowID : int) {
		

		GUI.DragWindow();
		GUILayout.Button("Button");
		GUILayout.Label(windowID.ToString());
		//nodes[windowID].hasReplies = EditorGUILayout.BeginToggleGroup("Has Replies", nodes[windowID].hasReplies);
		
	}
}

Edit, stupid mistake, forgot to put node(conditions) in.

Just noticed that you build your XML manually:

var sr = File.CreateText(fileName);
sr.WriteLine ('<?xml version="1.0" encoding="utf-8" ?>');
sr.WriteLine ('<dialogue>');
// etc..

I strongly suggest getting a more robust approach: using the XML serialization.

This topic is covered throughout the web and is not specific to Unity.

It basically means that you should create classes that correspond to your XML nodes, create a structure of instantiated objects and pass the structure to XMLSerializer which should in turn produce your XML string.

Ah, well it doesn’t need to be anything pretty, and building it as text worked fine for the purpose. I’ll look into it.

Here we are again, now the togglegroup will not tick on and off. How strange.

 import System;
    import System.IO;
    import System.Collections.Generic;
	
class DialogueGraph extends EditorWindow {
	@MenuItem ("RPG/Write Dialogue XML")
	static function ShowWindow() {
		EditorWindow.GetWindow(DialogueGraph);
	}
	

    var  fileName = "MyFile.xml";
	var numberOfNodes : int;
	//var nodes : node[];
	
	var windowRect = Rect(100, 100, 100, 150);
	var windowRect2 = Rect(250, 250, 100, 150);
	var windows : List.<Rect> = new List.<Rect>();


	class node {
	
	var text : String;
	
	var hasReplies : boolean;
	var replies : String[];
	var links : int[];
	
	var giveItem : boolean;
	var itemToGive : int;
	var numToGive : int;
	
	var startsBattle :boolean;
	var enemyIndex : int;
	var eLevel : int;
	var eName : String;
	
	var endpoint : boolean;
	var newStartingPoint : int;
	
	function node(t : String, hR : boolean, r : String[], l : int[], gI : boolean, 
	iG : int, nG : int, sB : boolean, eI : int, eL : int, eN : String, end : boolean, nSP : int) {
	
	text = t;  //string
	
	hasReplies= hR;  //bool
	replies = r; //string[]
	links = l;  //int[]
	
	giveItem = gI; //bool 
	itemToGive = iG; //int
	numToGive = nG; //int
	
	startsBattle = sB; //bool
	enemyIndex = eI; //int
	enemyLevel = eL; //int
	enemyName = eN; //string
	
	endpoint = end; //bool
	newStartingPoint = nSP; //int
	
	//String, boolean, String[], int[], boolean, int, int, boolean, int, int, String, boolean, int)'.
	}
}

	var nodes : List.<node> = new List.<node>();
	
 function Write() {
           
		   if (File.Exists(fileName))
            {
                Debug.Log(fileName+" already exists.");
                return;
            }
			
            var sr = File.CreateText(fileName);
            sr.WriteLine ('<?xml version="1.0" encoding="utf-8" ?>');
			sr.WriteLine ('<dialogue>');
			sr.WriteLine (" ");
				for (var i : int = 0; i < numberOfNodes; i++) {
					
					//start writing a cnode with the ID (as all cnodes a required to have an ID)
					sr.WriteLine ('<cnode id="'+i+'" ');
					
					//check if node is an endpoint, and if so, write to it.
					//if (nodes[i].endpoint) { 
					sr.Write('endpoint="'+nodes[i].endpoint+'" ');// }
					//check if node has replies, and write it.
					sr.Write('hasReplies="'+nodes[i].hasReplies+'" ');
					//check if nodes gives an Item and write.
					sr.Write('giveItem="'+nodes[i].giveItem+'" ');
					//check if node starts a battle and write.
					sr.Write('startsBattle="'+nodes[i].startsBattle+'" ');
					//check if gives item, then give the item index and amount to give
					if (nodes[i].giveItem) {
						sr.Write('itemToGive="'+nodes[i].itemToGive+'" ');
						sr.Write('numToGive="'+nodes[i].numToGive+'" ');
					}
					//check if starts battle, then give enemyindex, level, and name
					if (nodes[i].startsBattle) {
						sr.Write('enemyName="'+nodes[i].eName+'" ');
						sr.Write('enemyLevel="'+nodes[i].eLevel+'" ');
						sr.Write('enemyIndex="'+nodes[i].enemyIndex+'" ');
					}
					//if it's an endpoint, give it the new start point for the next convo. (can be 0 for repeating the conversation)
					if(nodes[i].endpoint){
						sr.Write('multiNode="'+nodes[i].newStartingPoint+'" ');
					}
					//close the cnode
					sr.Write('>');
					
					//write the text prompt tag
					sr.WriteLine('text prompt="'+nodes[i].text+'"/>');
					
					//if the node has replies then list both.
					if (nodes[i].hasReplies) {
						sr.WriteLine('reply link="'+nodes[i].links[0]+'" text="'+nodes[0].replies[0]+'" />"');
					}
					//close the node
					sr.WriteLine("</cnode>");
					sr.WriteLine(" ");
				}
				
			sr.WriteLine ('</dialogue>');
            sr.Close();
    }

	function addNode(cnode : node) {
	
	//	nodes += [cnode];
	
	}

function OnGUI() {
		
	//numberOfNodes = nodes.length;
	GUILayout.Label("Use this to create dialogue readable by the DialogueHandler script");


/* 	Handles.BeginGUI();
	Handles.DrawBezier(windowRect.center, windowRect2.center, 
	Vector2(windowRect.xMax + 50f,windowRect.center.y), 
	Vector2(windowRect2.xMin - 50f,windowRect2.center.y), Color.red, null, 5);
	Handles.EndGUI(); */
	
	BeginWindows();
		if (GUILayout.Button("Add Node")) { 
		
		var a : int = 0;
		 
		windows.Add(Rect(100 * a,10,100,150)); 
		
		nodes.Add(node("Text", true, ["Reply 1","Reply 2"], [0,0], false, 0, 0, false, 0, 0, "enemy name", false, 0) );
		
		a++;
		}

				for (var n : int = 0; n < windows.Count; n++) {
				
					windows[n] = GUI.Window(n, windows[n], WindowFunction, "Node "+n);
				}
	EndWindows();
	}

	function WindowFunction (windowID : int) {
		
		var hReplies : boolean;
		
		GUI.DragWindow();
		//EditorGUILayout.Button("Button");
		//EditorGUILayout.Label(windowID.ToString());
		hReplies = EditorGUILayout.BeginToggleGroup("Has Replies", hReplies);
			//	nodes[windowID].hasReplies = hasReplies;
				nodes[windowID].replies[0] = EditorGUILayout.TextField("Reply : ", nodes[windowID].replies[0]);
				nodes[windowID].links[0] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[0]);
				nodes[windowID].replies[1] = EditorGUILayout.TextField("Reply : ", nodes[windowID].replies[1]);
				nodes[windowID].links[1] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[1]);
			EditorGUILayout.EndToggleGroup();
	}
	 

}

In fact, none of the properties are editable. Not even the reply fields or any other control I add. Almost as if the window is being drawn on top of them, because i can click and drag the controls and it moves the whole window.

So a tiny bit of progress. However, now I’ve realised I can’t specify variables for each window in the window function because of OnGUI- but I also can’t use nodes[windowID].hasReplies = EditorGUILayout.BeginToggle("Has replies", nodes[windowID].hasReplies); because it ‘doesn’t support slicing’. Also, moving the GUI.DragWindow() disables any controls after it at the top, yet doesn’t work if at the bottom of the function.

Here’s the code again.

 	import System;
    import System.IO;
    import System.Collections.Generic;
	
class DialogueGraph extends EditorWindow {
	@MenuItem ("RPG/Write Dialogue XML")
	static function ShowWindow() {
		EditorWindow.GetWindow(DialogueGraph);
	}
	

    var  fileName = "MyFile.xml";
	var numberOfNodes : int;
	//var nodes : node[];
	
	var windowRect = Rect(100, 100, 100, 150);
	var windowRect2 = Rect(250, 250, 100, 150);
	var windows : List.<Rect> = new List.<Rect>();


	class node {
	
	var text : String;
	
	var hasReplies : boolean;
	var replies : String[];
	var links : int[];
	
	var giveItem : boolean;
	var itemToGive : int;
	var numToGive : int;
	
	var startsBattle :boolean;
	var enemyIndex : int;
	var eLevel : int;
	var eName : String;
	
	var endpoint : boolean;
	var newStartingPoint : int;
	
	function node(t : String, hR : boolean, r : String[], l : int[], gI : boolean, 
	iG : int, nG : int, sB : boolean, eI : int, eL : int, eN : String, end : boolean, nSP : int) {
	
	text = t;  //string
	
	hasReplies= hR;  //bool
	replies = r; //string[]
	links = l;  //int[]
	
	giveItem = gI; //bool 
	itemToGive = iG; //int
	numToGive = nG; //int
	
	startsBattle = sB; //bool
	enemyIndex = eI; //int
	enemyLevel = eL; //int
	enemyName = eN; //string
	
	endpoint = end; //bool
	newStartingPoint = nSP; //int
	
	//String, boolean, String[], int[], boolean, int, int, boolean, int, int, String, boolean, int)'.
	}
}

	var nodes : List.<node> = new List.<node>();
	function OnGUI() {
		
	//numberOfNodes = nodes.length;
	GUILayout.Label("Use this to create dialogue readable by the DialogueHandler script");


/* 	Handles.BeginGUI();
	Handles.DrawBezier(windowRect.center, windowRect2.center, 
	Vector2(windowRect.xMax + 50f,windowRect.center.y), 
	Vector2(windowRect2.xMin - 50f,windowRect2.center.y), Color.red, null, 5);
	Handles.EndGUI(); */
	
	BeginWindows();
		if (GUILayout.Button("Add Node")) { 
		
		var a : int = 0;
		 
		windows.Add(Rect(100 * a,10,100,150)); 
		
		nodes.Add(node("Text", true, ["Reply 1","Reply 2"], [0,0], false, 0, 0, false, 0, 0, "enemy name", false, 0) );
		
		a++;
		print(a);
		}

				for (var n : int = 0; n < windows.Count; n++) {
				
					windows[n] = GUI.Window(n, windows[n], WindowFunction, "Node "+n);
				}
	EndWindows();
	}

	function WindowFunction (windowID : int) {
		
		var hReplies : boolean;
		
		
		//EditorGUILayout.Button("Button");
		//EditorGUILayout.Label(windowID.ToString());
	hReplies = EditorGUILayout.BeginToggleGroup("Has Replies", hReplies);
		
				//node[windowID].hasReplies = EditorGUILayout.ToggleGroup(, "toggle");
			nodes[windowID].hasReplies = hReplies;
				nodes[windowID].replies[0] = GUILayout.TextField(nodes[windowID].replies[0], 128);
				nodes[windowID].links[0] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[0]);
				nodes[windowID].replies[1] = EditorGUILayout.TextField("Reply : ", nodes[windowID].replies[1]);
				nodes[windowID].links[1] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[1]);
			
			EditorGUILayout.EndToggleGroup();
GUI.DragWindow();
	}
	 

}

Any help on this would be great.
Edit : drag problem fixed, slicing error still there for the node class boolean.

Edit2 : so the slicing was a typo (nodes being node), but the togglegroup does not work. You click the toggle, it presses, but doesn’t change.

the onGUI and window function :

function OnGUI() {
		
	GUILayout.Label("Use this to create dialogue readable by the DialogueHandler script");
	
	BeginWindows();
		if (GUILayout.Button("Add Node")) { 
		
		
		 
		windows.Add(Rect(100 * numberOfNodes, 10,100,150)); 
		
		nodes.Add(node("Text", true, ["Reply 1","Reply 2"], [0,0], false, 0, 0, false, 0, 0, "enemy name", false, 0));
		
		numberOfNodes++;
		print(numberOfNodes);
		}

				for (var n : int = 0; n < windows.Count; n++) {
				
					windows[n] = GUI.Window(n, windows[n], WindowFunction, "Node "+n);
				}
	EndWindows();
	}

	function WindowFunction (windowID : int) {
		
		var hReplies : boolean;
		
		
		//EditorGUILayout.Button("Button");
		//EditorGUILayout.Label(windowID.ToString());
	//hReplies = EditorGUILayout.BeginToggleGroup("Has Replies", hReplies);
		
				nodes[windowID].hasReplies = EditorGUILayout.BeginToggleGroup("toggle", nodes[windowID].hasReplies);
				nodes[windowID].hasReplies = hReplies;
				nodes[windowID].replies[0] = GUILayout.TextField(nodes[windowID].replies[0], 128);
				
				nodes[windowID].links[0] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[0]);
				nodes[windowID].replies[1] = EditorGUILayout.TextField("Reply : ", nodes[windowID].replies[1]);
				nodes[windowID].links[1] = EditorGUILayout.IntField("Links to : ", nodes[windowID].links[1]);
			
			EditorGUILayout.EndToggleGroup();
			GUI.DragWindow();
	}
	 

}

Edit 3 : forgot to remove hReplies. Shit. Fixed.