Upbge: Replace logic bricks by logic nodes

Created on 4 Jun 2018  Â·  42Comments  Â·  Source: UPBGE/upbge

Replacement of logic bricks

Logic brick can be replaced by logic node which bring more flexibility and help the user to create complex logic by not being restricted to sensors / controllers / actuators pipeline.

Logic nodes look like material nodes but have two types of sockets, trigger sockets and value sockets.

  • Trigger sockets are used to activate logic nodes, a logic node always have on trigger input and could have multiple trigger outputs (e.g branch condition).
  • Value sockets contains different type of value (float, int, string, KX_GameObject (and derived), ...) at each update of a node the value inputs are read and the outputs are updated, these outputs are lazilly updated if no nodes are using them.

Logic nodes are defined and proceed in python inside a blender addon. Each node UI is defined inheriting bpy.types.Node and a corresponding class with the same node name is called during engine run. Only the conversion part is made in C++ for converting editor nodes to game engine nodes.

BGE node definition

The engine nodes inherit from bge.types.KX_LogicNode, this class expose common functions and attributes :

  • in : A dictionnary of the inputs. The type of the values could be int, float, string or bge.types classes.
  • out: A dictionnary of the outputs connected, unconnected outputs are absent. The type of the values could be int, float, string or bge.types classes.
  • triggers : Outputs trigger, list of nodes.
  • object : The game object owning the node tree.
  • start(self) : Initialize internal data of a node like cache and check inputs value, if the inputs are wrong return False.
  • run(self) : Execute the node and return the node to execute next.
class ParentNode(bge.types.KX_LogicNode):
    def start(self):
        return True

    def run(self):
        self.object.setParent(self.in["parent"], compound=self.in["compound"], ghost=self.in["ghost"])
        return triggers[0]
class DelayNode(bge.types.KX_LogicNode):
    def start(self):
        self.ticks = 0
        return True

    def run(self):
        if self.ticks >= self.in["ticks"]:
            self.ticks = 0
            return triggers[0]
        self.ticks += 1

The nodes values are not modifiable from outside, so the node tree isn't exposed into KX_GameObject.

Compatibility

The current logic bricks will be converted into multiple logic nodes with some restriction :

  • Shared logic bricks will be duplicated over all objects using them.
  • Bricks could be converted into a complex set of nodes.
  • The bricks API will be broken.

Some note on the compatibility :

SCA_Sensors

  • usePosPulseMode, useNegPulseMode : remove
  • skippedTicks : Use a logic node to skip logic update, NodeEvent
  • level, tap : Use a node to detect state edges, NodeEvent
  • invert : inversion after NodeEvent
  • triggered, positive : remove
  • pos_ticks, neg_tick : add tick count in NodeEvent
  • status : remove
  • reset() : remove

SCA_AlwaysSensor

Replace by the root logic node activated each frame, RootNode.
This node only have on trigger output.

SCA_ANDController

SCA_NANDController

SCA_ORController

SCA_NORController

SCA_XORController

SCA_XNORController

Replace by NodeBranch. This node take as many as possible trigger inputs (each time one is connected an new unconnected input is added) and apply a boolean condition on inputs.

KX_ArmatureSensor

Replace by a pretty similar API, NodeArmatureConstraint.

KX_CollisionSensor

  • propName : keep
  • useMaterial : add material name filter material
  • usePulseCollision : remove
  • hitObject : remove
  • hitObjectList : keep
  • hitMaterial : remove

** KX_MouseFocusSensor

Transform to two nodes, NodeMouseFocus and NodeRayCast. The first compute the ray source and direction :

  • raySource, rayTarget, rayDirection : keep

KX_NearSensor

Use a new API to create sphere collision sensor e.g :

sensor = bge.contraints.createSphereSensor()
sensor.transform = self.object.worldTransform
sensor.collisionCallbacks = [func]
...
bge.constraints.removeSensor(sensor)
  • distance, resetDistance : keep

KX_NetworkMessageSensor

  • subject, subjects, bodies : keep
  • frameMessageCount : remove

KX_RadarSensor

Keep all, look API changes for KX_NearSensor

KX_RaySensor

  • propName, range, useXRay, mask, hitPosition, hitNormal, hitObject, rayDirection : keep
  • useMaterial : filter by material material
  • hitMaterial : remove

SCA_ActuatorSensor

Remove

SCA_JoystickSensor

Potentially split in axis and button logic node.

  • axisValues : remove
  • axisSingle : axisValue
  • numAxis : remove
  • numButtons : remove
  • connected : keep
  • index : keep
  • threshold : keep
  • button : keep
  • axis : keep

SCA_KeyboardSensor

  • key, hold1, hold2 : keep
  • toogleProperty : keep
  • targetProperty : remove
  • useAllKeys : keep
  • inputs, events : remove

SCA_MouseSensor

Potentially split in position/wheel and button node.

  • position : keep
  • mode : keep
  • grad : (dX, dY)

The description of compatiblity isn't finished, @lordloki, @youle31, @DCubix, @martinsh, @vlad1777d @adriansnetlis, @BluePrintRandom : What do you think or want to improve in this proposal ?

Open design feature request for later waiting developers

Most helpful comment

@marechal-p : To replace SCA_PyhonController I will add a node just calling a python function from a module or running a script. But this node will have no special inputs/outputs.

All 42 comments

Firstly, I'm all about this change.

Now, regarding how the nodes are handled: would it be possible to have nodes be optionally compiled into a program instead of being interpreted as a Python code? This would open door for more performance-bound operations to be handled by them.

would it be possible to have nodes be optionally compiled into a program instead of being interpreted as a Python code

For this I think about cython.

Yes, Cython should do it well. This, however, would require nodes to figure out where to use static datatypes to efficiently utilize C's speed advantage over Python.

It is a very good proposal.
A question, as the node/nodes tree belongs to one object, what do you think about persistent dictionaries across all objects/nodes (load/save mechanism)?

For SCA_MouseSensor a dX/dY would be useful.

@lordloki : This dictionnary is intented to store properties ? A kind of run state saved in a file ?

@panzergame not necessarly saved in a file, just something similar to the current bge.logic.globalDict.
Although being able to store any dictionnary on disk would be nice, like a storage node (just dumping the thing as json).

The proposal looks very refined ! I would also have worried about Python performance though... Are you familiar with Cython and how would it play here ? (currently documenting myself on it so that I don't just expect you to enlighten me here)

Another question: in order to link the bpy.types.Node to the bge.logic.KX_LogicNode, will you simply base yourself on the class name for both of the child classes ? Like:

class TestNode(bpy.types.Node):
    pass
class TestNode(bge.types.KX_LogicNode):
    pass

Also, in order to simplify a bit too the interfacing with native the native Python API to be used with nodes, why not having some kind of node that could be configured using a path such as module.nodeDefinition (a bit similar to how the current bge.logic.KX_PythonComponent works) ?

This to save the need to craft your own bpy addons everytime you want extra capabilities, having a PythonNode like the current PythonController ?

I just wanted to know if that made any sense or not :)

@panzergame as @marechal-p said something similar to globalDict (or maybe a multi globalDict) that you can save in a file (but not necessary) and you can edit it

Another question: in order to link the bpy.types.Node to the bge.logic.KX_LogicNode, will you simply base yourself on the class name for both of the child classes ? Like:

Yes.

Also, in order to simplify a bit too the interfacing with native the native Python API to be used with nodes, why not having some kind of node that could be configured using a path such as module.nodeDefinition (a bit similar to how the current bge.logic.KX_PythonComponent works) ?

If there's a way to auto load a script at blender start that register new nodes we could avoid import path. Else we add a procedure for the user to automatically register some node using a include path and save these paths in the blender file.
But anyway I would prefer register a node then add it by name instead adding the node directly by a import path like component.

@panzergame not necessarly saved in a file, just something similar to the current bge.logic.globalDict.

A node could be added to load/store a global dict and read/write a property.

I would also have worried about Python performance though... Are you familiar with Cython and how would it play here ? (currently documenting myself on it so that I don't just expect you to enlighten me here)

I don't have a big experience with cython (I almost compiled a dummy module and succeeded importing it in game). Currently i'm looking for doing python binding with Cython which could help the user to just use the binding defined by cython from a native python, or compile cython modules which fall down to C++ calls through the cython binding.

Does that mean that any logic to be run in the game engine must be implementing through a node of some sort ?

With the current Logic Brick system, you could make a module for a PythonController and do lot of logic just in there, sometimes it was for scripts that handle general concepts (and we had to kinda break out of the logic-per-object-only by hacking a bit). But how would it work in this case ?

@marechal-p : To replace SCA_PyhonController I will add a node just calling a python function from a module or running a script. But this node will have no special inputs/outputs.

node is a very good ideas

for python node i think the best way is
select in the node how many inputs/outputs you have and detect how is log on it

and have a inputs/outputs node for material

I feel like their is one problem with this. the current system has a way of seperating for concerns. 1 kinds of brick for detecting things. another kinds of brick for acting on things. and the third kinds for acting as a middleman. would a node system still do that?

@Anarchokawaii : The node system will intent to define more nodes than current bricks and no node will be conceived to detect and acting in the same time.

@lordloki : I think we should introduce nodes without triggers in the design. These node are just for computing value without side effect, for example dot product, add…

rebuilding the logic in nodes will remove any new ui work to get compatible
w/2.8 eh?

On Mon, Jun 11, 2018, 9:13 AM panzergame notifications@github.com wrote:

@Anarchokawaii https://github.com/Anarchokawaii : The node system will
intent to define more nodes than current bricks and no node will be
conceived to detect and acting in the same time.

@lordloki https://github.com/lordloki : I think we should introduce
nodes without triggers in the design. These node are just for computing
value without side effect, for example dot product, add…

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/UPBGE/blender/issues/710#issuecomment-396299302, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AG25WeTyzCn8EmukQhXQFBUyyC5J0vXBks5t7pc-gaJpZM4UY9CB
.

@panzergame I wonder about nodes without triggers, how do you determine the order of execution of the modifiers ? The "triggers" represent the execution flow right ? It's a literal thread showing where the execution flows, kinda. You might be able to fetch the order of application from what output is connected to what input... It just sounds harder to implement, but possible for sure.

_edit: thinking about it, material nodes already work like this, without trigger right ?
sounds like a good idea to me too, so if you know what you are doing it's a great idea !_

@BluePrintRandom It's a step I guess.

I wonder about nodes without triggers, how do you determine the order of execution of the modifiers ?

These node will compute the output value at each access (with a cache) depending on theirs inputs.

rebuilding the logic in nodes will remove any new ui work to get compatible w/2.8 eh?

For the moment I didn't read any specification about nodes UI in 2.8, do you have one ?

I have two possible way of defining node UI in python, one based on class attribute defining a list of inputs/ouputs, an other defining a init function setting the inputs/outputs. What is the most preferred for python users in general ?

These node will compute the output value at each access (with a cache) depending on theirs inputs.

Oh right, thanks !

What is the most preferred for python users in general ?

I would say the less overhead for the user the better, so that goes in favor of class attributes.

On the other hand, why not both ? The initialize function would look up its class for said attributes. When users derive the Python class to define their node, they just add the properties, the default initialize function does its job. The user wants something dynamic ? Just override the initialize function, call super().initialize() or not, done.

Do you see a problem with this approach ?

Do you see a problem with this approach ?

No :)

I added some modification to the first proposal, i would like to know your opinion:

1) KX_LogicNode is renamed LOG_Node and put in Logic directory along LOG_Tree and LOG_NodeSocket.

2) Nodes will need a third list dedicated to properties. The property are mainly enums (or modes) that can't be changed in game. For example a boolean node will have a property for the operation applied on inputs (or and…). These property will be exposed in BGE python API through LOG_Node.properties.

Hi, BPR reminded me that you wanted to have my opinion about this topic.

This is a great challenge and seems a great proposal and it goes in the sense of what Ton wanted for logic system in 2.8 so BF could be interested (if you want to try to do this proposal to BF) to discuss about this topic.

But anyway, even if you do that for upbge only, this is very cool, and this would be interesting development anyway I guess.

"For the moment I didn't read any specification about nodes UI in 2.8, do you have one ?" I heard that they want logic nodes for 2.8 but that they want to "combine" logic with the depsgraph and I have no idea about how to do that. I think BF coders are too busy with 2.8 viewport to talk about that for now but it could be interesting to contact Benoit Bolsee to know his mind about that (I add @ben2610 if he wants to read this thread and contact @panzergame or the inverse)

@panzergame

KX_LogicNode is renamed LOG_Node and put in Logic directory along LOG_Tree and LOG_NodeSocket.

LOG or LGC would be fine depending on your preference.

Nodes will need a third list dedicated to properties.

So this means one list for inputs, one for outputs, and one for properties (similar to Python Components?). It seems like a good idea :)

Just wondering if there was any progress on this? Is there a branch more recent than DCubix's ge_new_nodes I could check out?

@ethicalfive : Hello, the branch ge_new_nodes is not related to logic. Concerning the branch ge_logic_node the work is in suspend while finishing the fixes of the current release (2 weeks more).

The main point know for me is to define correctly values nodes, nodes giving value without modifying game data.

As debated on discord, node providing only value without side effect will be functions.

I propose to define them using a new class type: LOG_ValueNode written as follow:

class ValueNodeExample(bge.types.LOG_ValueNode):
    def start(self):
        # Get data that will be used in get()

    def get(self):
        # return value

The function get is called when a node using it is activated, this function can use it's inputs socket of its variable to provide a value of any python type compatible with other nodes.

So value nodes will have 0 to n inputs, and only one output: the value returned by get() ?

Yes, I forgot to precise it.

Quick documentation to add nodes:

To add a node you first have to define its UI in release/scripts/startup/logicnode_builtins.py
In this file you add a class named using the following sheme : LogicNodeXXX
Two categories of node are used, one functional providing a value depending on its inputs only, an other similar to a procedure which is allowed to have side effect and can modifiy the scene, object and multiple outputs value depending on the scene, object or it's inputs.
These two categories are generalised by the class LogicNode and LogicNodeFunction in the same file.

Some attributes are common of these two categories for a node definition, they are : name, label, input sockets, properties.
The name and label is defined by class attribute bl_idname and bl_label. The input sockets are defined using a list of each socket name, type name and value. This list is named bl_inputs and is actually a dictionnary with as key the socket name and as value a tuple of the socket type name and the default value (None for nothing).

Here is an example of procedure node definition showing only the common part:

class LogicNodeBasicMotion(LogicNode):
    bl_idname = "LogicNodeBasicMotion"
    bl_label = "Basic Motion"

    bl_inputs = {
        "translation" : ("NodeSocketVectorTranslation", None),
        "rotation" : ("NodeSocketVectorEuler", None),
        "scale" : ("NodeSocketVector", Vector((1, 1, 1)))
        }

We can identify three input sockets for this node, the list of socket type is available at subclass of NodeSocketStandard (https://pythonapi.upbge.org/bpy.types.NodeSocketStandard.html?highlight=nodesocketvectortranslation)

All node can embed properties, they are defined using the classic way in blender of bpy.props classes in class attribute. For displaying the properties in UI, they must be put in a list of propertie names: bl_props.

Functional nodes have one specialized attribute, the type of the unique output socket, its type name is set in bl_output.

Here is an example of functional node definition without inputs but with one property:

class LogicNodeBooleanValue(LogicNodeFunction):
    bl_idname = "LogicNodeBooleanValue"
    bl_label = "Boolean Value"
    bl_output = "NodeSocketBool"
    value = bpy.props.BoolProperty(name="Value")
    bl_props = ["value"]

We can identify the property "value" of type boolean which will be displayed in the UI as a check box. Also the output socket type of this node is bool as defined by bl_output.

Procedure nodes have two default sockets, the trigger input and the trigger output. The trigger input is needed to activate the node and execute its body, the trigger out is used to activate next procedure nodes or not. The trigger input is unique, but the trigger output can be plural. Branch node is the basic case of multiple trigger output where one trigger output is used when the condition is true and an other when the condition is false.

The triggers outputs can optionally be defined by a list of socket name: bl_triggers_out

Here is an example of branch node:

class LogicNodeBranch(LogicNode):
    bl_idname = "LogicNodeBranch"
    bl_label = "Branch"

    bl_inputs = {
        "value" : ("NodeSocketBool", True)
        }

    bl_triggers_out = [
        "Trigger Positive",
        "Trigger Negative"
        ]

We can identify the input value for the condition and the definition of two trigger outputs, one for positive test and an other for negative test.

Once these node are defined properly with theirs sockets, they must be registered in the node editor. First they are added in one of the category of logic_node_categories in the same file. And secondly each class is registered against blender in function register() using bpy.utils.register_class(CLASSNAME)

After defining the UI of the node, the behavoiur of it must be defined in game. To achieve it a class of the same name is defined into release/scripts/bge/nodes into subfolder by purpose category. Each of these file contain only one node behaviour description. This begin with the creation of a class of the same name inheriting from bge.types.LOG_FunctionNode for functional nodes or bge.types.LOG_Node for procedure nodes.

As for UI the two node categoris have generalized and specialized methods. The only common method is start(self) this method aim the initialization of the node and the local storing of the properties and trigger output sockets in the goal to optimise the update later by storing value unchanged.
Two common attributes are present, the list of inputs and properties. The last one is simply a dictionary accesible from properties and attribute inputs is a dictionary of sockets. Each socket have the attribute value which is used to read the value of the socket or to write to it if allowed.

Functional nodes have the specialized method get(self) which returns the value corresponding of the output socket.

Here an example of functional node providing a constant boolean value depending on its property:

import bge

class LogicNodeBooleanValue(bge.types.LOG_FunctionNode):
        def start(self):
                self.value = bool(self.properties["value"])

        def get(self):
                return self.value

We can identify the storing of the property value in a attribute in the initialization and the return of it in method get.

Procedure nodes have the ability to maintain multiple outputs but also the obligation to tell the next node to activate. The function update(self) update the outputs and must return a next node to activate or Node for no next node.
The outputs are stored in dictionary outputs in similar way than inputs, note that trigger input and trigger outputs are respectively considerated as inputs and outputs.

Here an example of branch node returning the next node to activate depending on a condition:

import bge

class LogicNodeBranch(bge.types.LOG_Node):
        def start(self):
                self.cond = self.inputs["value"]
                self.positive = self.outputs["Trigger Positive"].value
                self.negative = self.outputs["Trigger Negative"].value

        def update(self):
                return self.positive if self.cond.value else self.negative

As for functional node we can note the local copy in initialization.

Nice documentation!

Just had a couple questions again:

  • Are we forced to modify this release/scripts/startup/logicnode_builtins.py script in order to add new nodes? Or can we add the nodes through standard addons, given we correctly register the nodes using bpy?
  • Same goes the gamelogic part? It feels weird for extenders to have to dump files in these specific folders. It seems a correct place for builtins, but not for extensions...
  • Is this system intended to be extended in the first place? You are talking about adding nodes, so I assumed it was the case, but maybe most contributions will come from a specific node that would import a function from a module in order to customize its behavior, instead of adding a whole new node?

Other than that, it looks like a really good design! Separating the UI script from the game logic script also counters the issue that happened with PythonComponent, were you have to be careful with your gamelogic imports because the thing would also be loaded in Blender UI :)

Also, if I understand correctly, LOG_FunctionNode.get() will be called only if necessary? LOG_Node.update() decides which node to trigger next. So if a node has inputs plugged to some function node output's, only then will it evaluate get() ? What I am trying to ask is if get() will trigger something or rather being triggered when needed. update() seems to be a driver.

Does that mean that if the function node also has pluggable inputs, it will need to propagate back to the first nodes that don't depend on any inputs in order to resolve all the values?

Are we forced to modify this release/scripts/startup/logicnode_builtins.py script in order to add new nodes? Or can we add the nodes through standard addons, given we correctly register the nodes using bpy?
Same goes the gamelogic part? It feels weird for extenders to have to dump files in these specific folders. It seems a correct place for builtins, but not for extensions...
Is this system intended to be extended in the first place? You are talking about adding nodes, so I assumed it was the case, but maybe most contributions will come from a specific node that would import a function from a module in order to customize its behavior, instead of adding a whole new node?

Normally if you copy the mechanism in release/scripts/startup/logicnode_builtins.py you can register new nodes from an other place, but only in a new category.

About extending behaviour scripts, I can add a other import path for addons, but i have to look deeper at this solution. I written this documentation to help contributor writing native nodes for the moment.

Also, if I understand correctly, LOG_FunctionNode.get() will be called only if necessary? LOG_Node.update() decides which node to trigger next. So if a node has inputs plugged to some function node output's, only then will it evaluate get() ? What I am trying to ask is if get() will trigger something or rather being triggered when needed. update() seems to be a driver.

Yes, it will be called when necessary. Only "procedure" nodes follow an execution chain.

Does that mean that if the function node also has pluggable inputs, it will need to propagate back to the first nodes that don't depend on any inputs in order to resolve all the values?

Yes.

I'm trying to build this branch, but I get this error message:

2>d:\documents\github\blenderdev\blender\source\blender\nodes\logic\node_logic_util.c(57): error C4133: '=': incompatible types - from 'int (__cdecl *)(bNodeType *,bNodeTree *)' to 'bool (__cdecl *)(bNodeType *,bNodeTree *)'

How do I fix that?

@joelgomes1994 : you can try to build ge_23a_node, this is a bit like last master + @panzergame work about logic nodes and I think this is compilable (don't fotget to update your libraries)

Thanks, @youle31 , I'l try this branch later. Me and @UnidayStudio were talking about contributing to the logic nodes with the actual Python scripts for the nodes, but only if the C backend is finished. As I don't know the current status of this feature, it would be good to know what's up.

The development of upbge is stopped. Panzergame stopped for now.

I don't really know if C part was finished, and I don't know if it is possible to convert logic bricks code into node, because rewritting all logic from scratch seems a very important work. Logic bricks had an Update() function. I don't know if it can be compatible with the Update() function of logic nodes...

I just tested it quickly but didn't looked much at the code

I managed to get the ge_23a_node branch, it's building now (at least when I revert the opencollada lib to the past one, as the latest throws errors).

I don't really know if C part was finished

I don't know if under the hood it needs anything else, but as far as I tested it seems to work pretty well. I managed to create two new nodes (Ray and Vector) and improve most of the existing ones. If the backend is stable, maybe me and @UnidayStudio can proceed to the node creation step, as it relies in Python.

I don't know if it is possible to convert logic bricks code into node

Probably it is, as panzergame has proposed it, but is it really worth the effort? Because the converted logic surely will not have the same quality as the original logic (as written by panzergame above), and the developer will have to build everything from the ground up again anyways. I think that the argument of logic system improvement wins against the backward compatibility one.

@marechal-p - have you built the new node branch / tried making new nodes?

ge_23a_logicNodes ?

@BluePrintRandom I didn't and I also don't plan to play with the BGE in the near future.

Probably it is, as panzergame has proposed it, but is it really worth the effort? Because the converted logic surely will not have the same quality as the original logic (as written by panzergame above), and the developer will have to build everything from the ground up again anyways.

I don't really understand what you are saying here?

I think that the argument of logic system improvement wins against the backward compatibility one.

Backward compatibility in regards to what? IIUC nodes are meant to replace the bricks, this means no backward compatibility from the start. But it is true that the goal is to have an improved logic system :)

I think logic nodes removes old dependency no?

it's not meant to be backward compatible because it's moving into the future.

What I mean with 'backward compatibility' is the conversion of logic bricks from older blend files to logic nodes, not the logic nodes working together with logic bricks, I know they will be broken. Sorry for the confusion.

As we are using BGE Logic Nodes, I close this but I put a tag open and feature request for later in case anyone wants to work on it later. Thanks for all hard work.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

IzaZed picture IzaZed  Â·  6Comments

vlad0337187 picture vlad0337187  Â·  7Comments

BlenderDoc picture BlenderDoc  Â·  4Comments

UnidayStudio picture UnidayStudio  Â·  4Comments

joelgomes1994 picture joelgomes1994  Â·  7Comments