Add Components/Classes to "Scheduler" Programmatically


photo

Recommended Posts

Posted (edited)

I've written some background services and moved them to a DLL. Some of them are gated on frames, and depend on the "Scheduler". (So they get called on Update). Right now I've found two ways to do this. I can manually invoke them (e.g., from AppWorld/AppSystem), or I can create a "Dummy Component" and add it to World Nodes (manually or from code). Both approaches have some limitations and extra complexity.  

Is there a way to add items to the  "Scheduler" programmatically?

Edit: To clarify, by "Scheduler" I mean the Attribute driven "Order" features - [MethodUpdate( Order = xxx]. I'm able to use the Engine callbacks for basic capabilities.

Edited by Tessalator
Link to post
Posted (edited)

Here is an example in simplified form. It's all experimental at this point.

The use case is an event system that runs as a background service.
To participate, components derive from a base class (which derives from Component).
Components raise events using the "EventSink" class. This is a Singleton "bridge" injected (I use DI) into both the Service and component base.
Game objects raise events during a frame ( e.g., `sink.RaiseEvent(e)`), that are written to a Channel on the service.
At the beginning of each frame, initial processing of events in the service is started with `Engine.AddCallback(END_CONTROLS_UPDATE,...)`.

All of this is pretty straightforward and works fine.
During the frame, the events get additional processing . The workflow varies, and has sequential and parallel stages.

In a normal component this is easy to do:

[MethodUpdate(Order = 000_001_001]
public void Process 1(){...}

[MethodUpdate(Order = 000_001_002]
public void Process 2(){...}

This is the part of components I call the "Scheduler". It will schedule the execution of these methods on "passes" during the frame.

To engage the scheduler, two conditions must be met.
1. The class must derive from Component.
2. The class must be present in `World Nodes`. (Attached to a node)

Attaching to nodes seems pretty flexible:
Attaching either the .prop or .cs seems to work.
The class can be in `data` or `source`.
The class only needs ` : Component`. The `[Component(PropertyGuid..]` attribute isn't required.
The class can be attached in the editor (drag & drop), or in code `node.AddComponent<>()`. 

This too works well. (Usually. Some strange behaviors pop up now and then, but reloading usually fixes things. )

Now, getting to why my question...

Here is some code. (Not real, for illustration only.)  
 

Spoiler


// Singleton in DI Container
public class EventSink {
    public ChannelWriter<Event> Writer {get;set;}
	public void RaiseEvent(Event e){
	    _ = Writer.WriteAsync(e);
	}
}

// Participating Components derive from this.
public class ComponentBase : Component {
    protected readonly EventSink events;
    public ComponentBase(){
        events = services.GetRequiredComponent<EventSink>();
    }
}

// Event System
public class EventService : BackgroundService {
    private readonly Channel<Event> events = Channel.CreateUnbounded<Event>();

    // Example
    private List<Event> processingBuffer = new();

    public EventService(EventSink sink){
        sink.Writer = events.Writer;
        Engine.AddCallback(Engine.CALLBACK_INDEX.END_CONTROLS_UPDATE, Update);
    }

    protected override Task ExecuteAsync() {
        _ = Task.Run(async () => await EventProcessor());
    }
	
    // "On" Each Frame
    private readonly AutoResetEvent frameGate = new (false);

    private void Update(){
        frameGate.Set();
    }
	
    // Main Loop
    private async Task EventProcessor(){
        while(true){ // Cancellation Token "not cancelled" in real code.
            frameGate.WaitOne();
            while(eventBuffer.Reader.Count > 0) {
                Event eventMessage = await eventBuffer.Reader.ReadAsync();
                processingBuffer.Add(eventMessage);
            }
        }
    }
	
    /* ***********   Scheduled Sub Processes   *********** */
    // These happen "In" each frame. They are sequenced/scheduled.
    // They use things like the processingBuffer and other itermediate datastores. Some have external dependencies.
 
    [MethodUpdate( Order = 000_001_000)]
    public void ProcessStep1(){ }

    [MethodUpdate( Order = 000_001_001)]
    public void ProcessStep2(){ }

    [MethodUpdate( Order = 000_002_000)]
    public void ProcessStep3(){ }	
}

 

There are two things to notice about this code.
1. The Scheduler cannot be invoked on `EventService` because it derives from the `BackgroundService`, not Component.
2. This class isn't directly associated with any node - the association is through the bridge `EventSink`.

NOTE: In this example I used a base class that contains `Event Sink`.  Event Sink could be made into a component and just added it nodes too.
You loose DI with this approach. (Until Unigine implements it... wish lol) 

The second case is easy to work around - I just add a "dummy node" (ServiceRoot") and attach all of my services there. The only issue I really have with this is I'm building libraries and feel like I'm "intruding on user space". I would rather attach directly through `ComponentSystem`. 

The first case isn't as simple.  My initial response was just don't put scheduled code in the service. But as I worked with this concept more, that often over complicated things. Again, attaching to the scheduler through `ComponentSystem` would simplify.

I hope that all made some sense (In explaining what I am trying to do). Why is another question... lol. I don't know that this is something that would have broad use. 

Finally, I'll give my idea on how I might see this working. (I'm not asking for the feature or this approach, just my thoughts).

Define `IComponent` interface and `ComponentSystem.AddComponent(IComponent scheduled)`. 
We could do: `class EventService : BackgroundService, IComponent{}`. After adding, the scheduler would pick up the `[MethodUpdate]]`s, etc. the same way it does for regular components.

 

Edited by Tessalator
Link to post

Hi Tessalator,

Oh, I see. You want to have "nodeless" components ("system components" or "components as assets"). It makes sense, but we need to think it over well.
But, at the moment I don't see huge problems with adding DummyNode("ServiceRoot"), because the engine itself adds system, hidden nodes into the world (for example: cache nodes, AsyncQueue nodes, special physical objects for PlayerActor etc.). Yeah, I think this is a bad pattern, but as a "temporary" solution this is okay.
At least, until we have developed these components that will work without a real "body" (node).

Best regards,
Alexander

Link to post

Hi Alexander,

Yeah, that's it. Glad it makes sense. I call them Systems too, and have a base `class System : Component{}`.

On a related note, something that would be helpful in the future is the ability to add things like this to the Editor "Create" menus (Create>[MyLibrary]>System). It looks like it can be done with a plugin, but I'm not familiar with the tooling (Qt). Any chance of getting a standard plugin in your new features queue that let's you add items/subitems to the Create menu and calls a C# "constructor action"? (Possibly using C# Script). I don't need it now, but I can see this (and adding to Tooling too) being useful when the asset store comes up. Maybe even some type of "installer" down the road to support more complex third party stuff.

Link to post