danni.coy Posted November 26, 2010 Posted November 26, 2010 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. 1
ulf.schroeter Posted November 26, 2010 Posted November 26, 2010 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 !
ulf.schroeter Posted November 26, 2010 Posted November 26, 2010 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>
danni.coy Posted November 28, 2010 Author Posted November 28, 2010 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.
danni.coy Posted November 28, 2010 Author Posted November 28, 2010 <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
Recommended Posts