Jump to content

Logical Nodes, Base Components, Sealed Ref Nodes, and Message Ports


Recommended Posts

Basically, these ideas reimagine the component system. This is what I'm working on in Unigine right now, so if you implement it, you will save me a lot of work... :).
 So... where to start...
Vision: Reusable, complex components that expose a simple interface, that is accessible at node level.

Most assets are not singular but are composed of a hierarchy of items. The items (nodes) at any level can have components. Conceptually (using coding terms), the components/parts can be (or have)
Private - Only interact with their host node
Protected - Can interact with components on the same host node (siblings)
Internal - Only interact with components* under the same root node
Public - Able to interact with components* in other root nodes

*Or with nodes, but IMHO nodes should only be manipulated by their own components. 

Encapsulate a hierarchy of nodes and components. Migrate and/or aggregate the data and information from children to or towards the root node. Basically, public go to the top, others at parent groups (as collections), etc.  Expose the public parts through a single message-based interface at the root (Logical) node. Seal the component from external change. The component is now an execution context, and you can tune it (e.g., replace "Find" with direct references). Use these components to compose scenes where you can address components and create hierarchies by logical names, without any need to know about nodes. 

Logical Node

Consider two nodes - a parent and child, with Parent and Child components. Nodes are physical. They always have a position in the world, and they always participate in the hierarchy inheritance. Components are (more) logical in many ways - A node is a child when it is "under" a parent in World Nodes.  Component Child can be a child of Component Parent regardless of the physical arraignment though.

When a part (node/component set) is a node child, it can navigate directly to components through things like `[theComponent].node.Parent.GetComponent`.  But this is the rare case. More often, there are multiple physical nodes (nodes without components) between logical node (nodes with components). Navigation via Parent gets weird and "Find" becomes the tool. Essentially making most everything "public". This may or may not be a concern to you. In either case though, a mapping between the logical and physical needs to be maintained (if just in your head). But importantly, you are drilling down to get "shared"/public features.

The overall concept is "Logical Nodes and Roots". Essentially, when a component is placed on a node, it looks up through the hierarchy for a Logical Root and registers itself. Functions of registration include adding shared data to a common root datastore and registering operations on the root interface. Alternately, a component can be added to a node lower in the hierarchy directly through the Logical Root. Of course, there are many variations on approaches. But in the end, a single component is aggregating data and exposing methods for a hierarchy of children nodes and components. With this level of encapsulation, we need to apply some protections. A goal of the logical nodes is to surface logic that must physically live at lower levels up to a shared workspace with public interfaces.

Sealed Ref Nodes

Reference nodes allow us to pack parts for reuse. They can be used as references to a "static" ref object, or they can be unpacked into PONS (Plain Old Nodes :)). Once unpacked the containment is lost, and the children are exposed. Seemingly missing (though I may be way off here) is a way to "tailor" ref node instances - (ref node x with y = 4) without unpacking. 

Logical Nodes surface internals into an interface, but (currently) still leave children exposed. A Sealed Ref Node hides the internals and exposes the interface. There is no runtime unpacking. This gives developers a "known structure" to work with internally that won't be changed at runtime - this should lead to less need to find and cast. Potentially, this could lead to some type of compiled/baked component with optimized complex behavior.


Logical nodes are assembled into systems using a HierarchyId (based in part on SQL Server HierarchyId type). The basic form is: /n/n/n/n/. with each division indicating depth and the (number) value node order in the level. Each Logical Root has a default hid of "/".  Strings can be used if clarity is more important than order. You (can) create assemblies virtually - If Elflord and Body are two roots with hid "/" each, a virtual relationship is forms by the string "/Eldlord/Body/. This is basically placing a layer of abstraction over linking objects in the editor. An advantage being the ability make changes without worrying about breaking object references. The string hid /Elflord/Body/RightArm/Hand/ would indicate a hierarchy of logical node interfaces (aka Node Controllers). There may be several (physical) nodes between or under these, but they have been encapsulated and protected. In fact, most likely is that there are no nodes between them. Unless truly compositional, a logical relationship is better. In the logical system, once set, until changed (something like), `[.../RightArm/Hand/].Grasp()` will invoke a method on the Controller for the right hand. And, Hand.Parent would be RightArm no matter how many physical nodes where between.

Base Components

I've found creating and deriving from base components really useful. Easy enough in code. Not so much (directly) in the editor. In itself not a big deal, but...  I'm developing low-level frameworks and tools. In a few cases these will require users to derive a component from a custom base class. To support work like mine, it would be nice if in the editor you could have, "New Component from BaseType....". And, though likely a rare edge case, have a config switch that sets a base component to be used by all components.

Message Ports
Using components is at the least a three-step process - find the node, find the component, invoke the action. Finding by name (Global or in parent/children) can be a little risky (CSS type problems). This is another case where detailed internal knowledge and access is required. With the example `[.../RightArm/Hand/].Grasp()`, there is still too much internal knowledge required - why do I need to know about a Grasp method and why do I have to invoke it? You don't. Just send `/RightArm/Hand/Grasp/`. Where? If we send it to an object (i.e., the Logical Root) we are no better off (node->(known)object). Unigine makes it simple - Node.Data.  The Logical Root monitors Node.Data and acts on messages. A dedicated port or port/mask situation would be better.

Putting It Together
Hope I haven't lost you :).
I view Unigine as two systems. There is the World System (Sim). It has a Node (Physical) system that handles the mechanics "underground". Items in the world are semi-automatous, and interact with each other according to rules that only exist in the sim. To a large extent the sim is purly reactive.

The other system is the Control Room. This system manages the world meta and the orchestration. It monitors the world, listens for messages and handles things like external IO and background services. The control room tells the world to change, and the world tries to make those changes using its internal rules/components. The control room runs on a separate thread, providing async support to these processes.

Two goals are to isolate and reduce the work of the engine. The control room only knows about nodes that expose a Logical Root. And it doesn't really care about the node - aside from its port (node.Data). Using a simple logical hierarchy syntax (/comp/comp/comp/) for addressing the room can send a text message to a node (.Data) and the correct component(s) will act on it. The interface between the systems is just a mapping between logical paths and nodeIds. Work that was in loops (especially slowly changing) can be offloaded to control via messaging. Complex components can have a "Sim Side" and a "Control Side". There is a standard approach to messaging and formats.  

 My goal in all of this is to develop a sub-framework that can help component and framework developers develop in an easy, uniform way, with few hard dependencies, and without imposing too much of my own "flavor" in the process. The whole thing is modeled on hardware, with a chassis/backplane, message and timing busses, and a "slot" system for plug and play mounting. 

I'm looking for thoughts on the approach and sharing ideas devs may find interesting

If there is one idea that I would really like to see though is a good, buffered and masked message port setup on the node. Even though integration is already pretty easy, it would really help with background tasks, etc. Using Data gets pretty risky once you start using components from different sources. 



Link to comment
  • Create New...