Custom Nodes – How to

 

How it works.

 

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.

 

Custom cell and node example.

 

I want to create a pack of cells and nodes named “boombastic”.

First I create the aforementioned directories branch:

 

/bgenetlogic

                /cells

                /nodes

 

The Node.

 

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.

 

The 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.

 

Useful types in basicnodes.

 

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