Custom Nodes – How to
The add-on associates the tree nodes seen in the blender UI into logic cells, needed by underlying system to carry on the logic.
For each “logic” node in the UI there is a corresponding “logic” cell.
For that reason, to add a node means to create a subclass of bpy.types.Node and a subclass of netlogic.LogicNetworkCell.
When the game is deployed, the nodes are not needed, so the add-on keeps the two things into separate files in separate directories.
The add-on searches custom cells in a predefined folder. If our blender file is in the directory:
…/some
the addon will look for cell definitions in:
…/some/bgelogic/cells
and for nodes definitions in:
…/some/bgelogic/nodes
Custom nodes and cells are distributed in a zip file. The zip file must have the following structure:
/bgelogic/cells/cells_module_name.py
/bgelogic/nodes/nodes_module_name.py
So you create a directory bgelogic, two subdirectories cells and nodes, write the two python files in there then zip the bgelogic folder and it's done. It is not necessary for the two files to be named cells_module_name.py or nodes_module_name.py, the name are free as long as they are valid python module names. Cells defined in the python file some_name.py will be members of a module named some_name. The same holds for nodes but the qualified name of a node is never needed – the one of the Cell is, as we will see.
I want to create a pack of cells and nodes named “boombastic”.
First I create the aforementioned directories branch:
/bgenetlogic
/cells
/nodes
The in nodes I create an empty python file named “boombastic_nodes.py”
First line is the import:
from bge_netlogic import basicnodes
The predefined cells fall into one of the Action, Condition or Parameter categories. They all work the same, the difference is the predefined color. Let's create an Action. Our action will be called RotateObject and it will… make an object rotate. I declare a class that extends both bpy.types.Node and basicnodes.NLActionNode
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
Then I add two blender fields needed to identify and label our node type:
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
After that, we add the blender initialization of the node:
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
In this init I call the initializer of NLActionNode that will set the default color for an action node (and nothing else).
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
basicnodes.NLActionNode.init(self, context)
Now I create the input and output sockets. The small linkable button-things. In the basicnodes module there are a number of predefined sockets. They can all link together (I haven't found out how to avoid that, yet) and have predefined colors but some of them also offer customized user input fields. My action takes as input a condition (to decide when to apply itself) and a game object, as its target. It has no output. This is what I write:
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
basicnodes.NLActionNode.init(self, context)
self.inputs.new(basicnodes.NLConditionSocket.bl_idname, "Condition")
self.inputs.new(basicnodes.NLGameObjectSocket.bl_idname, "GameObject")
This means that my node will have two input sockets, one for a parameter of a condition type, one for a parameter of a game object type. And that's it for the visual structure of our node.
Now I need the fill the gaps needed by the addon to associate a cell and its structure to this node. The first thing to do is to tell which cell class will correspond to this node. I haven't created the class yet but I know that I'll make one named boombastic_cells.ActionRotate. I tell this to the add-on writing:
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
basicnodes.NLActionNode.init(self, context)
self.inputs.new(basicnodes.NLConditionSocket.bl_idname, "Condition")
self.inputs.new(basicnodes.NLGameObjectSocket.bl_idname, "GameObject")
def get_netlogic_class_name(self):
return "boombastic_cells.ActionRotate"
Then I have to tell the system how to associate my sockets to properties of my cell. I know that the first input will be associated to a field – that I will create later on – named condition and the second one to a field named game_object. I write this:
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
basicnodes.NLActionNode.init(self, context)
self.inputs.new(basicnodes.NLConditionSocket.bl_idname, "Condition")
self.inputs.new(basicnodes.NLGameObjectSocket.bl_idname, "GameObject")
def get_netlogic_class_name(self):
return "boombastic_cells.ActionRotate"
def get_input_sockets_field_names(self):
return ["condition", "game_object"]
Lastly, I have to register this node with the add-on. To do that, I call a predefined function of bge_netlogic, register_nodes(string, *classes):
from bge_netlogic import basicnodes
class RotateObject(bpy.types.Node, basicnodes.NLActionNode):
bl_idname = "RotateObject"
bl_label = "Rotate a Game Object"
def init(self, context):
basicnodes.NLActionNode.init(self, context)
self.inputs.new(basicnodes.NLConditionSocket.bl_idname, "Condition")
self.inputs.new(basicnodes.NLGameObjectSocket.bl_idname, "GameObject")
def get_netlogic_class_name(self):
return "boombastic_cells.ActionRotate"
def get_input_sockets_field_names(self):
return ["condition", "game_object"]
bge_netlogic.register_nodes("Boombastic Actions", RotateObject)
The register_nodes function takes a name, that will be used as a menu label, and one or more classes, each one representing a tree node, each one that will be put as an item inside the same menu. Note that the label also uniquely identifies a group of nodes in the UI, so for all nodes belonging to the same “menu category”, only one call to register_nodes must be made, otherwise the last call will override all the others. So if I have two actions and I want to register both of them under the menu Boombastic Actions I can't do:
bge_netlogic_register_nodes(“Boombastic Actions”, OneClass)
bge_netlogic_register_nodes(“Boombastic Actions”, AnotherClass)
but I must do:
bge_netlogic_register_nodes(“Boombastic Actions”, OneClass, AnotherClass)
Otherwise I'll see just one item in the UI, the one for AnotherClass – the other will be overwritten.
This completes the node part. Now I have to write the corresponding cell.
In the cells folder I create a file named boombastic_cells.py. Then I write the import section:
import bgelogic
The module bgelogic does for cells the same thing that bge_netlogic.basicnodes does for nodes: it provides the basic functionalities and the predefined nodes. The difference is that bgelogic is available at runtime. My cell will be an action, its name will be ActionRotate. Unlike the node, a cell is a standard python class:
import bgelogic
class ActionRotate(bgelogic.ActionCell):
In the initializer, I call the super constructor:
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
The super initializer is needed, otherwise the cell will lack the basic structure needed by the logic network to evaluate it. After that, I define the two members condition and game_object. Both will be initially None and, because I said to the node that condition and game_object are associated to two input sockets, they will be initialized with the proper values by the add-on:
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
This is all I need for the initialization. Now I write the part that will be executed by the logic network. This has to defined in a member function named evaluate.
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
def evaluate(self):
One can write pretty much anything he wants in this evaluate but for the cell to work as a normal cell, a pattern has to be followed. The parameters bound to input sockets can be linked to the output of other logic cells. Other cells might not be ready to spit their values when our cell is evaluated, so I have to check, first, if both self.condition and self.game_object do have a value. This is done by checking the result of a predefined function applied to the parameter against a constant, like this:
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
def evaluate(self):
condition_value = self.get_parameter_value(self.condition)
if condition_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
In words, this means: give me the actual value of my parameter “condition”. If the cell that produces that value has not finished its job, then I cannot execute my task, so I'll give up. This has the side effect of keeping the current status of my cell also to STATUS_WAITING, in case other cells need the output of mine – but mine has no output.
I do the same for self.game_object.
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
def evaluate(self):
condition_value = self.get_parameter_value(self.condition)
if condition_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
game_object_value = self.get_parameter_value(self.game_object)
if game_object_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
If both checks pass, I know that I have all the values needed to complete my action. Very important: when my cell is ready, I have to change its status to STATUS_READY. I do this calling a predefined function:
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
def evaluate(self):
condition_value = self.get_parameter_value(self.condition)
if condition_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
game_object_value = self.get_parameter_value(self.game_object)
if game_object_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
self._set_ready()
It is crucial to set the status to ready when the time has come. The logic network runner is programmed to stop its execution if even one of its cells doesn't reach the STATUS_READ when it should have – because it means that at least one branch of the logic tree is not decidable, so the network just gives up. After this very important step, the cell can finally do its job: rotating the object.
import bgelogic
class ActionRotate(bgelogic.ActionCell):
def __init__(self):
bgelogic.ActionCell.__init__(self)
self.condition = None
self.game_object = None
def evaluate(self):
condition_value = self.get_parameter_value(self.condition)
if condition_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
game_object_value = self.get_parameter_value(self.game_object)
if game_object_value is bgelogic.LogicNetworkCell.STATUS_WAITING:
return
self._set_ready()
game_object_value.applyRotation((0.0, 0.0, 0.05), True)
And that's it for the cell. Now I can zip my bgelogic temporary folder, name it MyLogicPack.zip if I want and load the zip with the add-on button Import Logic Nodes. The add-on will read the two files, copy them into the appropriate folders (located relative to the path of the blender file currently opened) and make the node available to the user.
Among all that is declared in basicnodes, there are a few types intended to be used by custom cells. These types are either nodes or sockets. The node types are:
NLConditionNode: the super type of all condition nodes.
NLActionNode: the super type of all action nodes.
NLParameterNode: the super type of all parameter nodes.
Custom nodes must extend one of these classes to inherit the functions used by the tree parser to create the logic counterpart.
The socket types are the following.
for input and output:
NLConditionSocket: plain yellow socket
NLParameterSocket: plain green socket
NLActionSocket: plain orange socket
NLGameObjectSocket: plain socket greenish socket, denotes a parameter of type KX_GameObject
NLSceneSocket: denote a KX_Scene value
for input:
NLQuotedStringFieldSocket: parameter, one text field, when not connected returns the value in that fields in double quotes (like “something”), used for text values
NLIntegerFieldSocket: parameter, one integer field, when not connected returns the value in that field as an integer
NLPositiveIntegerFieldSocket: same as above, but for positive only integers
NLValueFieldSocket: a socket field of alternative type None, Expression, Vector, String, Float and Integer, with the appropriate parser and value getter
NLNumericFieldSocket: same as value field but without the String option
NLOptionalRadiansFieldSocket: provides a field that can be either None or a Float value denoting an angle in radians
NLAttributeNameSocket: a socket field with a predefined set of values (localPosition, localOrientation, localScale...) denoting attributes of a KX_GameObject and an empty string field to insert a custom name for the attribute, if needed.
NLKeyboardKeySocket: a socket field that lets the user choose a keyboard key by pressing it
NLMouseButtonSocket: a socket field that lets the user choose a mouse button from a drop list.
NLFloatFieldSocket: a socket field that allows the user to insert a float value