-
Notifications
You must be signed in to change notification settings - Fork 316
logicnodes
This page shows how to create your own custom logic nodes from scratch in a node package. A similar approach is used to edit or add new nodes to Armory itself. In this case, don't create a library as described on this page and please use the following paths for node sources instead:
- Python definitions of Armory nodes: https://github.com/armory3d/armory/tree/master/blender/arm/logicnode
- Haxe implementation: https://github.com/armory3d/armory/tree/master/Sources/armory/logicnode
Browsing Armory's node sources is a good reference point for creating new logic nodes! The same applies for Armory's material nodes (Python/GLSL), the sources can be found here.
There also exists an example project for creating logic node libraries.
Each logic node consists of two parts:
-
A Python class that describes the node's UI and functionality in Blender itself. Here you define the header (title) of the node, it's category (where it is found in the Blender menu) and all of its attributes like input/output sockets and various properties the user can set in the node UI. If you add properties called
property0
-property9
to the node, those properties are accessible during the node's execution while running the game.For bigger libraries it is recommended to put each class in a different Python file and create Python packages if the library consists of multiple node categories. Each logic node Python file name should start with with the prefix
LN_
and eachbl_idname
attribute of a logic node must start withLN
(without an underscore). The rest of thebl_idname
attribute must be the same as the class name used in the Haxe part of the node.Helpful links:
-
A Haxe file that describes the node's functionality in the game. When exporting the game, all logic nodes in a node tree are parsed into a Haxe script that executes the individual nodes (source). The only code that is included in the game is the Haxe code, there is no Python code used during execution.
The Haxe file of a logic node consists of a class (with the same name as in
bl_idname
without theLN
prefix), so each logic node Haxe file name must be named the same as the class. If there are properties in the Python code namedproperty0
-property9
, you must addpublic
attributes in the class for them. You may add more attributes with other names as you want.Helpful links:
- Armory Haxe logic node files
- Armory API documentation (out-of-date)
We will make a new library to store the sources of custom logic nodes and keep them portable with no modifications to engine sources.
Locate your blend file and create a new Libraries
folder alongside it. Navigate to the Libraries
folder and create a new mynodes
folder in it to place your new node.
Next, we will create the logic node definition for Blender.
To do so, we have to create a file named blender.py
in Libraries/mynodes
folder. Armory automatically picks this file up once the library is loaded.
Define a simple node with single in/out socket like the one in the example below. This is the content of blender.py
:
from bpy.types import Node
from arm.logicnode.arm_nodes import *
import arm.nodes_logic
# Extend from ArmLogicTreeNode so that the node is recognized as a logic node
class TestNode(ArmLogicTreeNode):
"""Test node"""
bl_idname = 'LNTestNode'
bl_label = 'Test'
# Use this as a tooltip in the add node menu.
# If `bl_description` does not exist, the docstring of this node is used instead.
bl_description = 'This is a test node'
# The category in which this node is listed in the user interface
arm_category = 'Custom Nodes'
# Set the version of this node. If you update the node's Python
# code later, increment this version so that older projects get
# updated automatically.
# See https://github.com/armory3d/armory/wiki/logicnodes#node-versioning
arm_version = 1
def init(self, context):
self.add_input('ArmNodeSocketAction', 'In')
self.add_output('ArmNodeSocketAction', 'Out')
def register():
"""This function is called when Armory loads this library."""
# Add a new category of nodes in which we will put the TestNode.
# This step is optional, you can also add nodes to Armory's default
# categories.
add_category('Custom Nodes', icon='EVENT_C')
# Register the TestNode
TestNode.on_register()
Restarting Blender and loading the project again, the new logic node is available for placement.
Armory provides a small API defined in arm_nodes.py
to ease working with logic nodes.
-
Adding input/output sockets:
def add_input(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket: def add_output(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
Small wrapper methods around
self.inputs.new()
andself.outputs.new()
.If a
default_value
is given, the socket will use this value if it has no connection. Ifis_var
is set toTrue
, the socket will have a small dot in the middle to show that this socket can be used for accessing a variable.Additional available socket types can be found in arm_sockets.py. Libraries can also define their own socket types.
-
Node versioning
def get_replacement_node(self, node_tree: bpy.types.NodeTree) -> arm.logicnode.arm_nodes.NodeReplacement:
See Node versioning.
There are a bunch of static methods that allow you to register nodes and create node categories. For a in-depth overview, please look at arm_nodes.py
. On this page, only some often-used methods are documented.
-
def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'default', is_obsolete: bool = False) -> None:
Registers a logic node so that it is displayed in the add node menu.
-
node_type
: The class of the node (see example code in the Python section) -
category
: The category this node belongs in (see example code in the Python section). If the category does not exist yet, it is created. If you passPKG_AS_CATEGORY
(defined inarm_nodes.py
), the capitalized name of the Python package the node definition file is in is used as the category name. When you later rename a category, you don't have to change all calls toadd_node
when using this constant. -
section
(optional): Add this node into a sub-section of nodes in that given category. Node sections are visually grouped together in the menu. If the section does not exist yet, it is created. -
is_obsolete
(optional): Todo
-
-
def add_category(category: str, section: str = 'default', icon: str = 'BLANK1', description: str = '') -> Optional[ArmNodeCategory]:
Adds a category of nodes to the node menu and returns the
ArmNodeCategory
object if the category didn't exist yet.-
category
: The name of the category -
section
(optional): Just like node sections explained above, categories can also be grouped into visually separated sections. If the section does not exist yet, it is created. -
icon
(optional): Blender icon constant to give each node in this category a icon. The icon is also displayed in the node menu. -
description
(optional): Description of this category. This value is currently unused but might be used in the future to display tooltips.
-
-
Adds a section of nodes to the sub menu of the given category to group multiple nodes visually together. The given name only acts as an ID and is not displayed in the user inferface.
def add_node_section(name: str, category: str) -> None:
-
Adds a section of categories to the node menu to group multiple categories visually together. The given name only acts as an ID and is not displayed in the user inferface.
def add_category_section(name: str) -> None:
Armory provides you with a node replacement system that updates all nodes when opening old files in a newer SDK version. If you change the functionality of a node, you should implement an update procedure so that old nodes can be updated, even if the old node is compatible with the new node. Without an update routine, the UI of the node is not updated. You can trigger updates manually by typing Replace nodes
into Blender's node operator search menu (F3
).
To update a node, increment the arm_version
attribute of the node and override the following method:
def get_replacement_node(self, node_tree: bpy.types.NodeTree) -> Union[NodeReplacement, ArmLogicTreeNode, list]:
It defines the action to be taken with the old node (self
). There are three allowed return types:
-
If a
NodeReplacement
object is returned, the node is updated according to the information stored in theNodeReplacement
object. It describes which sockets and properties are replaced with other sockets/properties and what their new default values are. For a detailed explanation, please have a look at the source docstring. -
If a
ArmLogicTreeNode
object is returned, the current node is replaced with the returned node. However, you must do all the update handling yourself (e.g. setting all connections between the new node and other nodes). This can be useful when working with nodes which support varying numbers of inputs or outputs. -
If a
list
ofArmLogicTreeNode
objects is returned, the same as above applies but for multiple new nodes.
You might also raise exceptions if the update failed for whatever reasons.
A detailed, more technical explanation can be found in the replacement system implementation here.
Before the project can be run, we need to implement the actual node logic in Haxe.
Start by creating the folder structure Sources/armory/logicnode/
in the same folder of blender.py
.
Next, create a TestNode.hx
file inside the logicnode
folder just created, and place the code from below in the file.
When the node gets executed, we let it print a 'Hello, World!' string.
package armory.logicnode;
class TestNode extends LogicNode {
public function new(tree:LogicTree) {
super(tree);
}
override function run(from: Int) {
// Logic for this node
trace("Hello, World!");
// Execute next action linked to this node, this activates the output socket at position/index 0
runOutput(0);
}
}
A subclass of armory.logicnode.Logicnode
may override the following functions:
-
run(from: Int): Void
: Called when the logic node is activated by an impulse input socket.from
contains the index of the activated socket. To activate an impulse output socket, callrunOutput(i)
wherei
is the index of the output socket you want to activate. -
get(from: Int): Dynamic
: Called when another node requests the value of a non-impulse (data) output socket.from
contains the index of the requested socket. To retrieve the value of an input socket, callinputs[0].get(i)
wherei
is the index of the input socket.
Impulse type sockets (red in Blender's UI) and boolean type sockets (yellow) are not the same thing! Impulses manage the execution flow of the tree whereas boolean sockets hold boolean (true/false) data.
Logic trees (armory.logicnode.LogicTree
) are subclasses of iron.Trait
, so you are able to use all trait related methods on them as well. You can access a node's tree with this.tree
.