SanderMertens / flecs

A fast entity component system (ECS) for C & C++
https://www.flecs.dev
Other
6.46k stars 454 forks source link

Event Based Systems Design Approach #164

Closed teksoc-1 closed 4 years ago

teksoc-1 commented 4 years ago

Describe the problem you are trying to solve. @SanderMertens The while ( ecs_progress() ) construct works well when you continuously are wanting to perform work (i.e. animation/simulation), however in the event that an iteration does not match any systems, subsequent calls to ecs_progress will not (ever) introduce any changes into the environment (unless you introduce a change outside of the ecs_progress() pipeline). Conceptually the goal is to leverage the ecs_progress() pipeline as long as work is being performed (systems matched), and throttle back when systems are no longer matched, yet provide a way for N Event Based Systems to asynchronously fire an event and trigger the ecs_progresss() pipeline again (which may introduce new Event Based Systems).

Describe the solution you'd like Looking for a solution that will spin ecs_progess() until work is no longer being performed (no systems matched), or ecs_quit() called ... and then perhaps an ecs_wait_for_event() like call that blocks until some Event Based System that is asynchronously running fires an event (new entity for example) and subsequently spins ecs_progress() as long as work is being performed. This of course requires some sort of registration of Event Based Systems that can be run asynchronously outside of the periodic ecs_progress() pipeline.

SanderMertens commented 4 years ago

Interesting idea- if I were to add a function to the framework that returns true / false depending on whether any systems are active, would that allow you to build the functionality you mentioned?

This way, your asynchronous thread could periodically check this function, and depending on whether the value is true or false, you could unblock the main thread by signaling a condition variable or equivalent.

teksoc-1 commented 4 years ago

@SanderMertens Conceptually this is the kind of runtime would like to achieve :

ecs_main_run_loop()
{
    should_exit = false

   do 
   {
         if ( ecs_progress() )
         {
             if ( !ecs_systems_active() )
             {
                  // Asynchronously start all event based systems
                  ecs_run_event_based_systems()

                  // Blocking wait
                  ecs_wait_for_event()

                  // Event fired ... continue to cycle through ecs_progress() as long as systems are active
             }
         }
         else
         {
              should_exit = true
         }
   } while ( !should_exit )
}

The goal is to use ECS design architecture, yet be able to throttle back when work is no longer being performed, until some external event is fired that introduces change into the environment. There's obviously some concurrency and synchronization issues (staging?) to handle when events are being fired during the ecs_progress() phased loop. Hopefully this provides some better clarity on the intent behind this feature request.

SanderMertens commented 4 years ago

@teksoc-1 makes sense, this is consistent with what I understood from your initial description. The pattern looks good, but it's also quite specific so I would rather build functions that enable this vs. hardcoding it.

If I can give you a ecs_systems_active function, I think that will let you do everything you need. Your event-based systems can use the regular ECS API (provided that you ensure thread-safe access to the world) and at some point in time, the ecs_systems_active() function will return true again, because you performed an action that activated one or more systems.

In the same thread(s) as where you run your event based systems you could run ecs_systems_active() and when it returns true, signal a condition variable that you're waiting for in the main loop. Something like:


struct ctx {
    ecs_world_t *world;
    cond_t cond_var;
}

void* run_event_systems(ctx* arg) {
    do {
        // run event based systems
    } while ( !ecs_systems_active(world) ) ;

    cond_signal(arg->cond_var);
}

void main_loop() {
   should_exit = false
   do 
   {
         if ( ecs_progress(world, 0) )
         {
             if ( !ecs_systems_active(world) )
             {
                  // Asynchronously start all event based systems
                  thread_start(run_event_systems, {world, cond_var});

                  // Blocking wait
                  cond_wait(cond_var);

                  // Event fired ... continue to cycle through ecs_progress() as long as systems are active
             }
         }
         else
         {
              should_exit = true
         }
   } while ( !should_exit )
}
teksoc-1 commented 4 years ago

@SanderMertens Concur on the enabling functions, vice hard coding, and I believe the ecs_systems_active - like api is the core primitive, building block.

teksoc-1 commented 4 years ago

@SanderMertens So one way I was achieving execution of event based systems was to add an EventBased component to manual systems (since systems are first class citizens in the sense they are entities and can have components added to them as well), this functionality seems to however have broken with the recent updates after pulling them in. Adding components to a system seems to longer have an effect. System entities containing components are no longer matched against. Is there a better way I should perhaps be doing this in the first place?

SanderMertens commented 4 years ago

@teksoc-1 It should still be possible to add components to systems, but something may have broken with the SYSTEM modifier (although my tests are not showing it).

Can you provide a bit more detail about the issue?

teksoc-1 commented 4 years ago

@SanderMertens I was not using the SYSTEM modifier, but rather explicitly adding/setting components on the systems just like any entity - it was working before. This was my way of registering Event Based Systems - seemed natural to me at least - a manual system that fired up the async Event Based Systems. Below is a sample test case.

#include "flecs.h"

#define TRIGGER_ERROR 1
#define VERIFY_FIX 0

typedef int MyComponent;

int MyManualSystem1Ran = 0;
int MyManualSystem2Ran = 0;

void
MyManualSystem1( ecs_rows_t * rows )
{
    MyManualSystem1Ran = 1;
}

void
MyManualSystem2( ecs_rows_t * rows )
{
    MyManualSystem2Ran = 1;

    for ( unsigned int i = 0; i < rows->count; i++ )
    {
#if VERIFY_FIX
         ecs_run( rows->world, rows->entities[ i ], 0, NULL );
#endif
    }
}

int main( int argc, char ** argv )
{
    ecs_world_t * world = ecs_init();
    ecs_entity_t testEntity = ECS_INVALID_ENTITY;

    // Register components
    ECS_COMPONENT( world, MyComponent );

    // Register systems
    ECS_SYSTEM( world, MyManualSystem1, EcsManual, 0 );
    // Note: MyManualSystem2 registers interest in entities containting the MyComponent component
    ECS_SYSTEM( world, MyManualSystem2, EcsManual, MyComponent );

#if TRIGGER_ERROR
    testEntity = MyManualSystem1;
#else
    testEntity = ecs_new( world, 0 );
#endif

    // Add MyComponent component to an entity
    ecs_add( world, testEntity, MyComponent );

    // Verify this entity has the MyComponent component
    ecs_assert( ecs_has( world, testEntity, MyComponent), ECS_INTERNAL_ERROR, NULL );

    // Run the manual system that matches against entities with a MyComponent
    ecs_run( world, MyManualSystem2, 0, NULL );

    // if TRIGGER_ERROR, fails here
    ecs_assert( 0 != MyManualSystem2Ran, ECS_INTERNAL_ERROR, NULL );

#if VERIFY_FIX
    // When fix applied, this should work
    ecs_assert( 0 != MyManualSystem1Ran, ECS_INTERNAL_ERROR, NULL );
#endif

    return ecs_fini( world) ;
}
SanderMertens commented 4 years ago

Found the issue, it was introduced by the fix for #160 which caused systems to always be evaluated with a filter, even if none is provided - it would use 0 for include type.

When a filter without a type is provided, builtin entities are automatically filtered out, so that an application can (for example) do a bulk delete of all entities without accidentally deleting the entities for components / systems. In this case, that caused the 0-filter to filter out the system, which is why the manual system wasn't being invoked.

Fixed the issue by making sure that when using ecs_run no filter is used. If you're still seeing issues, feel free to reopen the issue.

teksoc-1 commented 4 years ago

@SanderMertens Awesome, thanks - works here.

teksoc-1 commented 4 years ago

@SanderMertens Incorporated the ecs_active_system_count() - this was a nice addition, and is working well. I think the ecs_merge_world() from #122 will be a nice complementary api to this event based design approach as well.