Jump to content

Attach Existing Component to Node


photo

Recommended Posts

 

I want to add existing components (e.g., generated by factories) to nodes.

`node.AddComponent<Type>(typeInstance)`

The methods to add components to nodes all seem to lead to ComponentSystem -

`public static T AddComponent<T>(Node node) where T : Component, new();`

It takes a type, creates a component (that must have a parameterless constructor (`new()` constraint)) , and returns an "empty" Component that we can configure.

This can be limiting.

I tried a (longshot) workaround using `Component { protected T AddComponent<T>(Node node).. `

public class Wedge : Component {
    public string Value;
    public void Add(Node node) {
        base.AddComponent<Wedge>(node);
    }
}

Wedge component = new Wedge();
component.Value = "Foo";
N node = new NodeDummy();
component.Add(node); // "Component add-to Node"

Since I was calling `Add` from an instance, I had hopes the instance would get added to the container.

No luck. Still a new instance.

But should that be the case? What is the point of calling from an instance if the instance itself isn't used?

Idea:

/* ********   ComponentSystem  ******** */
// Add Static Method
public static void AddComponent<T>(Node node, T instance) where T : Component;

/* ********   Node  ******** */
// Add Method (or extension) - chainable
public static Node AddComponent<T>(this Node n, T instance){
  ComponentSystem.AddComponent<T>(n, instance);
  return n;
}

// Use =>
class Foo : Component { public string Name; }
class Bar : Component { public string Title; }
Bar b = new("Barf");

NodeDummy n = new()
  .AddComponent(new Foo("BarNone")
  .AddComponent<Bar>(b)                

/* ********   Component  ******** */
// Add Instance Method
public void Attach(Node node);

// Use =>
Foo foo = new();
Bar bar = new();
NodeDummy node = new();

foo.Attach(node);
bar.Attach(node)

                

 

Link to comment

Hi Tessalator,

I want to add existing components (e.g., generated by factories) to nodes.
What do you want to achieve? Sharing a component between multiple nodes, or making a copy from that "template" component?

In the first case I see several problems:
1) What should we return when we use the Component.node property? Which node's instance should we return?
2) When to initialize the component? On the first AddComponent<>()?
3) How to call Update / PostUpdate methods? Once per frame or by the number of nodes that use this shared component?
etc.

The second case also has problems:
What we should use: deep or shallow copy of components? How to understand what exactly (which fields) we need to copy "deep" and what as "shallow"?

In my opinion, the best solution would be something like this:

public class Wedge : Component {
	public string Value;
	public string Name;
	public string Title;
	public SharedClass SharedClassInstance;
	public SecondClass ClassInstance;
	
	public void Add(Node node) {
		Wedge w = AddComponent<Wedge>(node);
		
		// shallow copy
		w.Value = this.Value;
		w.Name = this.Name;
		w.Title = this.Title;
		w.SharedClassInstance = this.SharedClassInstance;
		
		// deep copy
		w.ClassInstance = new ClassInstance();
		w.ClassInstance.Value = this.ClassInstance.Value;
	}
}

It is straightforward, simple, understandable.

Best regards,
Alexander

Link to comment

Hi Alexander

I'm not trying to achieve either of those.

I guess basically I want to be able to add Component Objects to Nodes and not just component data.
Currently you must create an "empty" node and add data to what you get back.
Here is a scenario:

I have a game world with things like rocks. Most start as id-less, typed meshes.
When a player clicks a rock, a Node is created. The node gets a "Rock" component.
The rock can also have things like ores and gems. These are also "Ore" and "Gem" components.

A component is generated by a backend system. The component includes event-hooks to the system.
Basically, a component has an event that sends its id and "world state" to the System.
The system may be maintaining "system-state" info about the component.
The system will generate a new component (or components) for the Node.

The components represent the state of the rock in two parts - the World & Backend System.
They talk to each other by exchanging Components.

I'd like to see Components become more first-class citizens.
It would be good to be able to manage them better, and in groups.
Part of that is node assignment.

 In the example, something like this would convert a mesh rock into a rock node with "factory values".
The node will contain the components generated and doesn't copy to an "internal new".  

ManagedRocks.Add(int key, new NodeDummy()
  .AddComponent<Rock>(someInstance) // Instance
  .AddComponent<Ore>(OreSystem.New(n)) // Factory Invoke
  .AddComponent<Gem>(n => GemSystem.New(n)) // Delegate
);

I attach the node to a world node and delete (absorb) the mesh.

If I can gather moss from a rock, I add Moss

ManagedRocks[key].AddComponent<Moss>(mossState);

Just a quick direct Add or AddSet.
None of the rules need change

@alexander, So I'm not looking to share anything. To the contrary, I'm establishing a 1-1 between a node component and backend component.

13 hours ago, alexander said:

1) What should we return when we use the Component.node property? Which node's instance should we return?
2) When to initialize the component? On the first AddComponent<>()?
3) How to call Update / PostUpdate methods? Once per frame or by the number of nodes that use this shared component?

1. Nothing changes here. Components are NOT shared across Nodes, just created elsewhere.

2. This is core. I have an initialized instance. I want to use that one and not have you initialize a new one

3. NA. Components aren't shared.

13 hours ago, alexander said:

What we should use: deep or shallow copy of components? How to understand what exactly (which fields) we need to copy "deep" and what as "shallow"?

Also core. You don't need to create copies if you have an instance and can place it directly in the node. 
 

I'm already doing something similar using ServiceContainer (from System.ComponentModel.Design).
It works well and easy if you restrict to one of any component type on a node. Collections are another component.
The biggest problem is there is a lot of boiler plate (like your code) to make it work.
- Mostly to load component data from an existing (calculated/generated) component.

Edited by Tessalator
Link to comment

"...and not just component data"
"They talk to each other by exchanging Components"
"It would be good to be able to manage them better, and in groups"

It looks like you want to use something like Entity-Component-System (ECS) pattern, but we are using Component-based Architecture (CBA / CBD). Our "Component" is a data and a system at the same time. And it must be attached to a node.

"To the contrary, I'm establishing a 1-1 between a node component and backend component"
Maybe it would be better not to use our component-based architecture? Or create a bridge between your backend component (not inherited from Unigine.Component) and our node component. Then using pure ECS over CBA.
For example, how to make a bridge:

public class RockCBA: Component {
    public RockECS link_to_backend;
}
public class RockECS
{
    public int value0;
    public float value1;
    public double value2;
}
// ...
AddComponent<RockCBA>(new NodeDummy()).link_to_backend = someInstance;

Or, even better (but harder to manage):

public struct RockECS
{
    public int value0;
    public float value1;
    public double value2;
}
List<RockECS> rocks = new List<RockECS>();

public class RockCBA: Component {
    public int rock_index;
}

// ...
int rock_index = 5;
rocks[rock_index].value0 = 10;
rocks[rock_index].value1 = 2.0f;
// ...
AddComponent<RockCBA>(new NodeDummy()).rock_index = rock_index;

 

Link to comment
9 hours ago, alexander said:

Maybe it would be better not to use our component-based architecture?

It looks like you want to use something like Entity-Component-System (ECS) pattern

I do bypass much of it, but I'm augmenting it too lol. I borrow from ECS in some areas, but with different goals.
More of the approach comes from SOA and a lot from Lean Manufacturing.

Code below shows my basic approach for bridging.
The sides are taking directly here, but there is often a message bus in the middle. 

A Bridged System has a Component/Record Pair.

Things in my Worlds are often autonomous. Their nervous system is the set of components they have.
Most interactions are handled directly between nodes in the world. Some events use the backend.
The simple reaction is to send a message to the bridge (OnClick here) with the Node ID and Component.
The Bridge/Bus calls the paired backend system (Start). It produces a new Component.
The Bridge/Bus replaces the old component with the new one.

What can I not just attach/replace the component instead of having to do field by field assignment to an already attached component? 

public class MineralSystem {
    public record struct MineralRecord(...);

    public class MineralComponent : Component {
        // External Input 
        public MineralComponent(...data) { }

        public void OnClick() => _ = Start(this, node.ID);
    }

    // Bridge
    // Normally uses ServiceBus to get to backend tasks. 
    
    public static async Task Start(MineralComponent component, int worldTarget) {
        MineralRecord mineralRecord = (MineralRecord)component; // Conversion Operators preferred
        
        // Service PRODUCES COMPONENT
        MineralComponent updateComponent = await Engines.BackgroundTask(mineralRecord);
      
        // I need this
        World.GetNodeById(worldTarget).AddOrReplaceComponent<MineralComponent>(updateComponent);
    }
}

// Backend
public class Engines {
    public static async Task<MineralComponent> BackgroundTask(MineralRecord record)
        => await Task...
}

 

Edited by Tessalator
Link to comment

What can I not just attach/replace the component instead of having to do field by field assignment to an already attached component?
Because components are not designed for that kind of maneuvers.
And we can not add this feature because it would be highly unclear for users.
For example, AddOrReplaceComponent:
1) What if a node has 2 or more MineralComponents? Replace only the first one?
2) If I have an updateComponent, what should the engine do if we call something like this:
worldTargetNode1.AddOrReplaceComponent<MineralComponent>(updateComponent);
worldTargetNode2.AddOrReplaceComponent<MineralComponent>(updateComponent);
Copy? Sharing? Exception?
3) ...and more, and more

P.S. The engine does not work well with multithreading. Async/await/Task may lead to bugs in the future because the Component System works correctly in the MainThread only. Just like, for example, World.GetNodeById().

Best regards,
Alexander

Link to comment

OK. Makes sense. I work a lot with Service Containers (only one instance of any type allowed) and didn't really think about multiple instance issues.

6 hours ago, alexander said:

P.S. The engine does not work well with multithreading. Async/await/Task may lead to bugs in the future because the Component System works correctly in the MainThread only. Just like, for example, World.GetNodeById().

This is actually a driver for what I'm doing. I split the app into an async/threaded "brain" on the backend and "body" in the World. I was hoping to move assigning values to components off the main thread and just assign the whole component.

As always, thanks for your help Alexander.

 

After thought: It occurred to me that the Unigine approach on multiples is "first", so (not restarting the idea), Add/Replace would act on the first one it found only.  

 

Edited by Tessalator
After thought
Link to comment

Follow up question on threading @alexander 

I do this a lot. I have a Sim World (Engine.Main) with a browser GUI, and backend services.
It seems to be working well.

Any concerns or thing to be aware of with this? 

    internal class UnigineApp {
        [STAThread]
        private static void Main(string[] args) {
            Engine.Init(args);

            // Backend Services
            _ = Task.Factory.StartNew(() => {
                _ = new ProcessService().Start();
            }, TaskCreationOptions.LongRunning).ConfigureAwait(false);

            // Web Server && Launch Browser
            _ = Task.Factory.StartNew(() => {
                _ = new BrowserDisplay().Start();
            }, TaskCreationOptions.LongRunning).ConfigureAwait(false);

            Engine.Main();

            Engine.Shutdown();
        }
    }

 

Edited by Tessalator
Link to comment

Hi Tessalator,

After thought: It occurred to me that the Unigine approach on multiples is "first", so (not restarting the idea), Add/Replace would act on the first one it found only.
Hm... We will think about such functionality. In any case, we will keep this use case in mind when improving the Component System in the future. But I promise nothing. :-)

Any concerns or thing to be aware of with this?
It seems to be ok. I don't see any problems.

You should update the doc's for component
Oh thanks! We will fix the documentation.

Best regards,
Alexander

Link to comment
×
×
  • Create New...