Communication between objects

@zicklag I just rediscovered your post and your according nodes in Armory. Thanks a lot , it’s very useful to improve the possibilities of a full object programming in Armory and as I am in the redesign of all ATRAP, I plan to use it a lot :wink:
(I have experienced before that the use of a large number of node trees and events can become a nightmare in the end when you have to debug with a large number of nodes and events to remember as we have not the tools to make search or to print node trees in a usefull way today )

1 Like

@zicklag … I would like to discuss in a few words with you how to consider the object-oriented programming** evolution that your call function allows now.

** If we start from the basic principle that we must be able to work with objects that are instances of a given class, with datas that correspond to the properties of the object and behaviors to the methods of the object

Within Armory, we can go through several paths to establish an object-oriented approach that will allow an object to invoke behaviors of another object by delegating the implementation of these behaviors to the called object (via the traits, node trees events, parenting, class extends and override function in nodes,…).

From your evolution (Call Function, Function, Function Output), in the sense of a full object-oriented programming, how do you see things?

My first thoughts are, if you were to ignore any form of static typing or type checking, you could achieve something along the lines of an object oriented approach with function nodes and object properties, things that we already have at this point. But that has absolutely no inheritance and little encapsulation, as attributes are on the Armory object, not on the trait ( aka. NodeTree ).

Another approach could be to create a more advanced, “Object” node that lets you click “+” buttons to create new attributes and methods on the object with the possibility to choose an inherited parent class, but then you have to make a decision between compiling to static typed Haxe and throwing compiler errors when you arrange your nodes incorrectly, or doing everything dynamic and throwing exceptions at runtime.

Another thought is maybe creating a special workflow for creating “Classes” separated from typical traits through Node Trees. Each class lets you add Functions, which are node trees by themselves, that have a “Class Inputs” node that gives them access to the class attributes. Inheritance could be handled by that somehow as well. It would be best to have some type checking done inside of blender so that you don’t have to compile the game to find out that there are issues.

That is just my very quick first thoughts brainstorming. I might think more about this later.

In the light of your initial thoughts, I think maybe that the best limitations/constraints that could help to focus this brainstorming, could be to favorize here an approach that favorize improvement of code safety and maintainability by using the compiler to find errors rather than with runtime errors, and using only what we have in hands today.

1 Like

Yes, I think that, similar to the current function nodes, an approach to try out initially should seek to make the nodes compile to Haxe that is as conceptually similar as possible to node structure. If we have some sort of node “Classes” those should compile rather straight forwardly into Haxe classes. Like the function nodes that actually compile into Haxe functions. That makes it as seamless as an integration between Haxe and the logic nodes as possible.

1 Like

This article http://old.haxe.org/ref/oop could be a reference/starting point to start with this logic node “Classes” you evok, as well as Class Instance - Haxe - The Cross-platform Toolkit when we speak about objects as instance, when using a logic node “Classes” …

(just a remark : As already discussed here Armor3d capability questions - #10 by RobDangerous, we have to deal too to offer a garbage collector help, thus as a first step with a class destructor for the management of resources / automatic garbage collection of inacessible objects and thus avoid unpredictable stalls … in this article, we have the Constructor and Initializer, but no Destructor)

I think the target to have in mind too is that OOP in Armory will be designed for the development of very large software systems.

Same thing as how to make possible to detect problems in Armory early during the design phase, before implementation starts.

When ready, we could open a new post “OOP in Armory” :wink: or "How to design Very Large SW in Armory)

1 Like

A proposal for the node that could create objects, that is instance of a Class, which name is Atoto
image

When selecting a class Toto into the Instantiation Node, the node changes, with the names of Methods of the selected Class, plus the Args for each Method appearing.

In the node tree Editor :

  • the name of the node tree is the name of the Class
  • a prefix assign to the name of the node tree tells that it is a Class definition, like for example with a prefix Class_

The following node is used to define the Method of the Class into the node tree of the Class Class_Toto
image

After instantiation, we could for example manage objects (that is instances) through a node comparable to an Array of Objects, or with a direct call considering that we have set the name for the Instance of the Class …

1 Like

Then the trick would just be compiling the NodeTree that defines the class to a literal .hx file with the class in it. That dropdown that has “Toto” selected would then browse other node trees and would read the tree to determine the attributes and methods of the class.

That could be relatively easy to implement.

1 Like

Is the Get Trait logic node code able to facilitate that ?

I don’t think so. That just resolves the Trait by its name.

Compiling the node tree to a Haxe file would be done as a part of the build process or node trees. There is the make_logic.py file that takes NodeTrees and compiles them to Haxe traits. We would have to add the logic to read the special nodes that indicate that the node defines a class and write out the Haxe file to take those nodes into account. Similar to how the function node already work.

1 Like

@zicklag Our actual brainstorming consists in trying to copy a usual POO approach on nodes and node trees.

If we reasoned differently by considering that a class instance is always a 3D object, for example an empty.

Then the methods are the traits added to the 3D object .
Inheritance takes place via the parenting of 3D objects.

The node Spwan Object is associated to the notion of Class constructor.
The node Remove Object is associated to the notion of Class destructor.

The scene could be associated to the notion of Package or maybe super-class/ virtual Class with already existing logic nodes to manage it.
image . Thus we have a way to group Class into a coherent set, and to provide a namespace for these elements.

Also as the Objects in a scene can be organized into named and nested collections, thus we could find here the notion of sub-package.

Objects (that is here for us with the meaning of the instantiation of a Class) are typically a member of one collection in a scene, but they can be put too in multiple collections, and we could make the link here too with the notion virtual class.

Collection could become a kind of virtual class that has already le logic nodes to use it image

1 Like

Thtat’s intriguing. It could be worth testing that out to see how it feels when using it.

1 Like

We could test with this first proposal :

  1. A Collection is a Container of Objects
  2. A Scene is a Container of Collections
  3. An Object is an Instance of a Class
  4. A Class is an Empty 3d object
  5. The Methods of a Class are defined by it’s Traits
  6. The Heritage is established with the Parenting

Thus for Containers, we have already things available like create empty container, add, delete, access to

Then for heritage between Classes :
When you spawn, after you do a Set Parent on this new object with the object from which it inherits (thus the empty that represents the parent Class)
Then when calling a Trait , that is a method of the class of this object, if this Trait doesn’t exist on this object, you do a get parent , again a get trait and the Call Function … which becomes equivalent to call the method of a Class parent.

That’s done for a first step in experimenting if this OOP with Armory “tient la route”.

This first step to test proposal is done by creating several Factory Class able to create instanciation of different Classes, taking the exemple of the Neural Network.

Here an exemple of result when printing the Neurones of each Layer, that is here the Container Collections of the scene.

![image|423x441]

Thus this first step shows that in this first OOP rules proposal :
1 + 2 is OK we have logic nodes to add a new collection in the active scene. Then Objects are added inside a collection simply by using an add Array logic node, with the Collection as the input Array.
3 + 4 + 5 is OK we have a Class Factory able to create 3D objects, where each object represents an instanciation of a Class. Each Factory class Methods are realized thanks to the logic node Function
made by @zicklag
6 is TBC as it needs now to be test further on, in order to confirm/define some rules to reproduce OOP inheritance.

I modify with 4. A Class is an Empty or a standard mesh object.

And always what’s amazing about Armory is that it’s very fast, even with a lot of neurons. Indeed, we could fear an additional cost of CPU time with a switch to OOP.
It seems to me that on the contrary with this first test, if I compare it with other realizations, it would allow to reduce the number of Logic Nodes, to use more native Blender possibilities and thus to gain even more in performance.
See therafter picture for a load test

image

For the next step, I will test a behavior well known in OOP, that is Observateur/Observable, and thus go deeper with point 6 of the OOP proposal for Armory.

Next step:

6 is now tested OK
Done by adding several “Observateurs” to an “Observable”, using a Method belonging to a child Class of the parent Class.

The technic used is as follow :

  • image The generic Class Neurone_0 has 2 children, that is 2 “Empty” Observateur and Observable

  • when I want to call the Method M_AjouterObservateur on an instance of the Neurone_0 (that is the objects created by the Factory of Neurones), I have to call the Method of it’s children Class Observable, that is in fact a specialized Class that has this Method. Thus all needed Observateurs become registered for this Neurone.

  • for example on the following system console picture, the method M_AjouterObservateur is called here with the following resulting print that shows that effectively the tab of the Observable, that is in this case for the neurone N0_1 (= Neurone 1 of the Layer Input ) register observateurs of the following Layer.
    image

:slot_machine:
After a few days of tests with OOP method for Armory SW dev … some news:

  • The new Neural Network made using this OOP approach is coming to life. See picture below

  • I discover using it that this OOP approach proposal gives great advantage in the lisibility of the application.

For example, when you clic on an object (that is a Class) into a Scene, as you see directly what are the Methods of it into the Armory Traits window, thus you kickly see what is the Logic Node Tree to select in order to modify the behavior of a Class. (for example here it’s about +15 traits and +12 Class, and the quantity of logic Nodes is divided by an approximate factor of +20 compared to a functional approach thanks to inheritance capabilities )

Hopping that you will find it usefull for you too and that you will soon give us some info on your own tests of this OOP proposed method for SW Dev within Armory.

News: I focus the design approach to get a best how-to with it for some things like to keep performances too in this OOP approach (as it was the case when using array of array of array in preceeding tests) that is to keep good alignment of datas in memory, keep cache friendly things and maximize the use of SIMD for example.

1 Like

Wow, that’s cool. Good job @Didier. Neat that you go that working and that you actually implemented it for your project.

@zicklag something useful at this stage would be to have feedback from those who have thoroughly tested/benchmarked the Kha.simd package :wink: but I fear that perhaps only @lubos is currently the best placed for this feedback when working on the Graphics2.hx for example?

Unfortunately simd only works on the hxcpp target, which Armory doesn’t use anymore. Development builds will use Krom and the production build will use the HL/C target so we won’t be getting any use out of it for Armory. It migh be possible to make simd for HL/C, but I wouldn’t know.

@zicklag At first sight, I’m looking for mainly reusing what’s available today in Armory/haxe…, so it’s easy to define for example custom iterators and iterable data types, and I focus on how to complete/provide/reuse a set of language abstractions/logic nodes/3D objects/… and optimizations for an object oriented high-performance computing in Armory.

Thus if the simd.hx is not interesting for all kind of platforms, it’s however I think an interesting example of a good way to organize things to make some cache-friendly things.

Looking for which strategy is best for an efficient OOP aproach in Armory will depend I think too as well on the hardware architecture/platform planned to use, but also the data access patterns+ dataset characteristics in the SW design.

Maybe @RobDangerous could give us advice in this domain on how to best use things like for example the haxe Map<K, V>(IMap<K, V>) , generics (see https://haxe.org/manual/type-system-generic.html) and iterators ?
It would be interesting to know for example how to get the equivalent of a hand-written SOA layout with Haxe, like when a compilator is able to make for us things like smart function inlining, constant folding, … in order to get a more efficient application.

Sorry guys, I don’t understand what you’re talking about. I can just tell you that the simd API does not need to be profiled because every function literally just creates a single intrinsic.