Jump to content

Extending Nodes in UnigineScript (code snippet)


photo

Recommended Posts

Posted

Just a dump of some of the stuff I am doing here - I find this pattern quite useful. You can store extra information in a Node by giving it a property - the following is a simple property file.

 

<?xml version="1.0" encoding="utf-8"?>
<properties version="1.00">
<property editable="0" name="ai3d">
	<state name="selectable">0</state>
	<parameter name="origin" type="string"/>
	<parameter name="index" flags="expand">0</parameter>
	<parameter name="info" type="string"/>
</property>
<property editable="0" name="Layer" parent="ai3d">
	<parameter name="opacity" max="100">100</parameter>
	<parameter name="layer id" flags="expand">0</parameter>
</property>
</properties>

 

This defines custom data that can be attached to a node. To access this data you would typically do something like the following

 

Property prop = node.getProperty();
if(prop != NULL && prop.getName() == "Layer"){
   int id = prop.getParameterInt(prop.findParameter("layer id"));
   prop.setParameterInt(prop.findParameter("opacity"),100);
   prop.setState(prop.findState("selectable"),true);
}

 

To make things easier I have taken to wrapping these things up inside a class.

 

First thing to note is that states and parameters are accessed by index in order of declaration so looking at our property file we could use a few enums like the following.

 

	enum {NODEBASE_SELECTABLE=0, };
enum {NODEBASE_ORIGIN=0,NODEBASE_INDEX,NODEBASE_INFO,};

 

We then want to define the number of number of states and parameters defined for derived properties

#define NODEBASE_STATES 1
#define NODEBASE_PARAMS 3

 

and so a derived enum will look like

enum { 	AUTO_ZNEAR = NODEBASE_STATES, AUTO_SHADOW_SIZE, USE_DOF, AUTO_DOF, AUTO_HDR_EXPOSURE, 
		CAMERA_FIXED, SET_TIME, SET_DATE, SHOW_TO_USER,
};
enum { 	CAMERA_GROUP = NODEBASE_PARAMS, 
		SHADOW_SIZE, OBJECT_DISTANCE, LOD_SCALE, SCENE_TIME, TIME_SCALE,
		DOF_BLUR_RADIUS, DOF_BLUR_RANGE, DOF_FOCAL_RANGE, DOF_FOCAL_DISTANCE,
		MAX_UP, MAX_DOWN, MAX_LEFT, MAX_RIGHT,
		HDR_EXPOSURE, COLOR_BRIGHTNESS, COLOR_CONTRAST, COLOR_SATURATION,
		TIME, DATE, YEAR,
		THUMBNAIL,
 };

 

ok now we could write the our previous code snippet something like the following.

Property prop = node.getProperty();
if(prop != NULL && prop.getName() == "Layer"){
   int id = prop.getParameterInt(LAYER_ID);
   prop.setParameterInt(LAYER_OPACITY,100);
   prop.setState(NODEBASE_SELECTABLE,true);
}

 

for convenience sake I would like to wrap everything into a class so I can have an object that I would deal with like any other object.

here is the code for our base object.

 

#ifndef __AI3D_NODE_BASE_H__
#define __AI3D_NODE_BASE_H__

#define NODEBASE_STATES 1
#define NODEBASE_PARAMS 3
enum {SELECTABLE=0, };
enum {ORIGIN=0,INDEX,NODEBASE_INFO,};
enum {GAME_OBJECT_TYPE=0,GAME_OBJECT_INSTANCE,};
class nodeBase {

 private:
// hnode and node are the same unless we are dealing with 
// reference node in which case hnode is the reference node and node 
// is the node that is referred to 
// hnode is needed when we need to get the nodes position in the level hierachy node in all 
// other circumstances
Node node, hnode;
Property params;
int index;

 public:
nodeBase(Node nd, int index=0) {
	if(nd.getType() == NODE_REFERENCE) {
		NodeReference ref = node_cast(nd);
		node = ref.getNode();
		hnode = nd;
	} else {
		node = nd;
		hnode = nd;
	}
	params = nd.getProperty();
	this.index = index;
}


string	getName () 				{
	if (node == NULL) log.message("node not found \n");
	return node.getName();
}
void	setName (string n) 	{node.setName(n);}
int	getSelectable ()	{return params.getParameterToggle(NODEBASE_SELECTABLE);}
void	setSelectable (int i)	{params.setParameterToggle(NODEBASE_SELECTABLE,i);}
string	getInfo	()		{return params.getParameterString(NODEBASE_INFO);}
void	setInfo (string s)	{params.setParameterString(NODEBASE_INFO,s);}
Node	getNode() 		{return node;}
void	setNode(Node nd)	{node = nd; params = nd.getProperty();}
int	isEnabled()		{return node.isEnabled();}
void	setEnabled(int en)	{node.setEnabled(en);}
int	getIndex()		{return params.getParameterInt(NODEBASE_INDEX);}
void	setIndex(int i)		{params.setParameterInt(NODEBASE_INDEX,i);}

// checks if a unigine node is the same node as the one contained by this class
// input can either be an int which can be gotten by node.getID() or it can be the node itself.
// sending the ID has the advantage that it can cross between the editor and world scripts.
int	 isNode(Node nd) {
	if(is_int(nd) && nd == node.getID()) return true;
	else if(nd.getID() == node.getID()) return true;
	return false;
}

Node findChild(string type) {
	forloop (int i=0; node.getNumChilds()) {
		Node ch = node.getChild(i);
		Property prop = ch.getProperty();
		if(prop == NULL) continue;
		if(prop.getName() == type || (prop.getName() == "game_object" && prop.getParameterString(GAME_OBJECT_TYPE) == type)) return ch;
	}
	return NULL;
}

Node addChild(Node ch, string name, string type) {
	engine.editor.addNode(ch);
	ch.setName(name);
	ch.setProperty(type);
	ch.setParent(node);
	return node_cast(class_remove(ch));
}

int  getNumChilds() {
	return node.getNumChilds();
}
Node getChild(int i) {
	if(i>=0 && i < node.getNumChilds()) return node.getChild(i);
	return NULL;
}
void setChild (Node nd) {
	nd.setParent(node);
}
Node getParent() {
	return hnode.getParent();
} 
};

#endif //__AI3D_NODE_BASE_H__

 

All of my specific classes are subclassed from this class.

Our sample code snippet now ends up looking like this.

 

Property prop = node.getProperty();
if(prop != NULL && prop.getName() == "Layer"){
   nodeLayer layer = new nodeLayer(node);
   int id = layer.getLayerID();
   layer.setOpacity(100);
   layer.setSelectable(true);
}

 

In the case where you are doing a lot of manipulation I have found that this pattern really helps keep the code maintainable.

  • Like 1
Posted

Just a dump of some of the stuff I am doing here - I find this pattern quite useful.

 

Very good idea to share these things !

Posted

Properties are also quite usefull on object surfaces, where we use them to assign different surface material types. Can be easily extended to store additional material dependent parameters like food sounds for character movement or dust particle types for vehicles

 

<?xml version="1.0" encoding="utf-8"?>
<properties version="1.00">

   // double precision node world location
   <property name="disi_world_position" parent="node_base">
	<parameter name="x" type="double">0</parameter>
	<parameter name="y" type="double">0</parameter>
	<parameter name="z" type="double">0</parameter>
   </property>

   // surface material codes
   <property name="disi_surface" parent="surface_base">
<parameter name="SMC" hidden="1">-1</parameter>
   </property>

<property name="disi_surface_soil" parent="disi_surface">
	<parameter name="SMC" hidden="1">0</parameter>
</property>
<property name="disi_surface_road" parent="disi_surface">
	<parameter name="SMC" hidden="1">1</parameter>
</property>
<property name="disi_surface_grass" parent="disi_surface">
	<parameter name="SMC" hidden="1">2</parameter>
</property>
<property name="disi_surface_metal" parent="disi_surface">
	<parameter name="SMC" hidden="1">3</parameter>
</property>
<property name="disi_surface_wood" parent="disi_surface">
	<parameter name="SMC" hidden="1">4</parameter>
</property>
<property name="disi_surface_forest" parent="disi_surface">
	<parameter name="SMC" hidden="1">5</parameter>
</property>
<property name="disi_surface_foilage" parent="disi_surface">
	<parameter name="SMC" hidden="1">6</parameter>
</property>
<property name="disi_surface_agriculture" parent="disi_surface">
	<parameter name="SMC" hidden="1">7</parameter>
</property>
<property name="disi_surface_water" parent="disi_surface">
	<parameter name="SMC" hidden="1">8</parameter>
</property>
</properties>

Posted

Another reason for doing things this way is you can replace this class with a C++ one with minimal changes to your code.

 

And now a bit more of the story...

 

Giving the Artists Control

So far we can attach extra data to a node in the Node editor and you can modify that data from the Unigine Editor UI by opening up the property dialog box. This works but could be better. The limitations are that - the layout of controls is automatic and may not be as easy to figure out as they could be and the fact that the properties dialog box locks the interface so it is not possible to get realtime feedback. We can do better than this but we are going to have to make a few little changes to the editor. Lets assume that we want to have a number of evenly spaced objects moving along a spline and we want our artists to be able to in be able to adjust the speed and the spacing of those objects interactively.

 

Hacking the editor

First of all its worth noting that if you do this you are going to need to be merging changes with future upgrades of the engine. Which takes a little time and care when updating and some sort of code merging tool (I use Kompare). Ok the file you want is data/core/editor/editor_nodes.h... You will need to locate the following 3 functions run_function(string name), run_function(string name,int arg) and run_old_function(string name) to each of these you need to add a line like the following

if(node != NULL)run_property_function(node,name);

This is a function we are going to write and it will look something like this.

 

void run_property_function(Node nd, string f) {
Property prop = nd.getProperty();
if(!isAi3dProperty(prop)) return;
string fn = "Nodes::" +  prop.getName() + "::" + f;
if(is_function(fn)) call(fn);
}

 

This looks for a function using the following namespace Nodes::[PropertyName]::function_name and if it finds it will run that function. We need to check that the property is one of ours just in case in this case that function looks like

 

	int isAi3dProperty(Property p) {
	if(p == NULL) return false; 
	Property parent = p.getParent();
	if(parent == NULL) return false;
	if(parent.getName() == "ai3d") return true;
	return isAi3dProperty(parent);
}

Which traverses up the property tree until it finds the property "ai3d" which will of course be different for anybody not working in my office.

 

How the node editor works (i think)

Now we can execute code in the editor based on the property of the node. Now it is time to do something interesting with that code. The functions that are interesting to us are

init(), update(), shutdown() and update_dynamic() so assuming that our object is called SplineGroup, the functions will be

Nodes::SplineGroup::init(), Nodes::SplineGroup::update() and so on.

 

init() sets up the ui in my example it looks like this.

void init() {
	parameters_tb.addTab("Spline Group");
	parameters_tb.addChild(main_vb,GUI_ALIGN_EXPAND);
	enableCallbacks(main_vb);
}

It adds an additional tab to node properties window called Spline Group This is where we can draw our custom controls.

 

update() is called when the node is selected this does our setup - Create an object based on the node and fill in the initial values of the UI controls.

	void update() {
	spline_group = new nodeSplineGroup(node);
	splinename_el.setText(spline_group.getSplinePath());
	speed_el.setText(string(spline_group.getSpeed()));
	min_spacing_el.setText(string(spline_group.getMinSpacing()));
	max_spacing_el.setText(string(spline_group.getMaxSpacing()));
	int play = spline_group.getPlaybackState();
	if(play == PLAYBACK_PLAY)playback_btn.setToggled(true);
}

shutdown() is called when a node is deselected (I think) so this is used to do any cleaning up.

 

void shutdown () {
	delete spline_group;
}

 

update_dynamic() is run each frame and is used for drawing the visualizer. In this case we are going to want to draw the spline.

 

	void update_dynamic() {
	forloop(int i=1; spline_group.getNumFrames()) {
		engine.visualizer.renderLine3D(spline_group.getPosition(i-1),spline_group.getPosition(i),vec4(1.0,0.0,0.0,0.75));
	}
}

 

Building Better Controls

ok now we need to define a gui file. The editor loads these in a list in editor.cpp you should add your file there. My file looks something like this

	<vbox name="Nodes::SplineGroup::main_vb" export="1" align="expand">
	<vbox space="4">
		<gridbox columns="2" align="expand">

			<label><text>spline name</text></label>
			<hbox align="expand">
				<editline name="Nodes::SplineGroup::splinename_el" export="1" align="expand">
					<callback type="pressed,focus_out">Nodes::SplineGroup::splinename_el_pressed</callback>
				</editline>
				<icon texture="ai3d/gui/20x20/load.png" width="20" height="20">
					<callback type="clicked">Nodes::SplineGroup::splinename_button_clicked</callback>
				</icon>
			</hbox>

			<label><text>speed</text></label>
			<hbox align="expand">
				<editline name="Nodes::SplineGroup::speed_el" export="1">
					<callback type="pressed,focus_out">Nodes::SplineGroup::speed_el_pressed</callback>
				</editline>
				<hslider align="expand">
					<attach flags="expand">Nodes::SplineGroup::speed_el</attach>
				</hslider>
			</hbox>

			<label><text>min spacing</text></label>
			<hbox align="expand">
				<editline name="Nodes::SplineGroup::min_spacing_el" export="1" >
					<callback type="pressed,focus_out">Nodes::SplineGroup::min_spacing_el_pressed</callback>
				</editline>
				<hslider align="expand">
					<attach flags="expand">Nodes::SplineGroup::min_spacing_el</attach>
				</hslider>
			</hbox>

			<label><text>max spacing</text></label>
			<hbox align="expand">
				<editline name="Nodes::SplineGroup::max_spacing_el" export="1" >
					<callback type="pressed,focus_out">Nodes::SplineGroup::max_spacing_el_pressed</callback>
				</editline>
				<hslider align="expand">
					<attach flags="expand">Nodes::SplineGroup::max_spacing_el</attach>
				</hslider>
			</hbox>
		</gridbox>

		<hbox align="expand">
			<button name="Nodes::SplineGroup::playback_btn" export="1" align="expand" toggleable="1">
				<text>play/pause</text>
				<tooltip>toggle pause/play</tooltip>
				<callback type="clicked">Nodes::SplineGroup::playback_btn_toggled</callback>
			</button>
		</hbox>

	</vbox>
</vbox>

 

The last step is to add the objects and callbacks.... which will look something like this.

	void speed_el_pressed() {
	spline_group.setSpeed(float(speed_el.getText()));
}

The final result should look something like the attached image.

post-48-0-76932500-1290947742_thumb.jpg

Posted

<property name="disi_surface_water" parent="disi_surface">

<parameter name="SMC" hidden="1">8</parameter>

</property>

</properties>

[/code]

 

I didn't know you could have hidden parameters... Thankyou

×
×
  • Create New...