SanderMertens / flecs

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

Ability to specify the relationship/component types of a pair #972

Closed RPGHacker closed 1 year ago

RPGHacker commented 1 year ago

Describe the problem you are trying to solve. I'm currently trying to build a player state machine directly in flecs. The samples show one or two ways to do this, but they are a bit too limited for my lking. I'm trying to take things a step further. Each of my state structs contains data needed by the state, as well as enter and exit functions, and simulatenously acts as the target of an exclusive relationship. Here's my current code:

// application.h

// Tag of state machine relationship
struct RelationshipId_State
{
};

// Function for registering a state
template <typename StateStruct >
inline void registerStateMachineStateToFlecs()
{
    using Relationship = flecs::pair< RelationshipId_State, StateStruct >;

    m_world.observer< Relationship >()
        .event(flecs::OnAdd)
        .iter([](flecs::iter it, StateStruct* pState)
    {
        for (auto entityNo : it)
        {
            StateStruct::enter(it.entity(entityNo), pState[entityNo]);
        }
    });

    m_world.observer< Relationship >()
        .event(flecs::OnRemove)
        .iter([](flecs::iter it, StateStruct* pState)
    {
        for (auto entityNo : it)
        {
            StateStruct::exit(it.entity(entityNo), pState[entityNo]);
        }
    });
}

// Player state machine
struct PlayerState_Idle
{
    float dummy;

    static void enter(flecs::entity e, PlayerState_Idle& state);
    static void exit(flecs::entity e, PlayerState_Idle& state);
};

struct PlayerState_Walking
{
    float dummy;

    static void enter(flecs::entity e, PlayerState_Walking& state);
    static void exit(flecs::entity e, PlayerState_Walking& state);
};

// application.cpp
void initializeStateMachine()
{
    m_world.component< RelationshipId_State >().add(flecs::Exclusive);

    registerStateMachineStateToFlecs<PlayerState_Idle>();
    registerStateMachineStateToFlecs<PlayerState_Walking>();

    m_playerEntity = m_world.entity()
        .add< RelationshipId_State, PlayerState_Idle >();

    m_playerEntity.add< RelationshipId_State, PlayerState_Walking >();

};

void PlayerState_Idle::enter(flecs::entity e, PlayerState_Idle& state)
{
}

void PlayerState_Idle::exit(flecs::entity e, PlayerState_Idle& state)
{
}

void PlayerState_Walking::enter(flecs::entity e, PlayerState_Walking& state)
{
}

void PlayerState_Walking::exit(flecs::entity e, PlayerState_Walking& state)
{
}

Now this example generally works the way I want it to: When I put breakpoints in each state function, the application calls PlayerState_Idle::enter(), PlayerState_Idle::exit() and PlayerState_Walking::enter()in that order, and then calls PlayerState_Walking::exit() upon closing the app.

However, there is one small problem with this solution: As you can see, both of the PlayerState structs contain a dummy data member. This member isn't there for no reason, it's actually required to make this solution work, which is a bit annoying, since you might want to create player states that don't need any data. The reason for this is the way flecs::pair detects the component type. When both structs are empty, it choose the first one as the component.

Now you might think this problem had an easy solution: Just swap the order of types in the pair. It's something I immediately tried. However, doing so quickly reveals another problem: The flecs::Exclusive above no longer does what I want it to do. It seems this tag assumes the first type of a pair to always be the relationship. This causes my player states to no longer be mutually exclusive, and PlayerState_Idle::exit() is no longer called upon adding the PlayerState_Walking component.

Describe the solution you'd like Some means of manually specifying the relationship/component types of a pair could solve this issue. Currently, the mechanism in place relies solely on std::is_empty, and when both types are empty, it always favors the first type as the component type, which actually seems rather impractical in regards to how flecs::Exclusive works.

SanderMertens commented 1 year ago

I unfortunately can't make the pair type fully customizable. It doesn't just depend on std::is_empty, the underlying storage also needs to be able to derive the type from the two component ids. The current approach only works because the function that derives the type from a pair can be implemented both in a type-erased storage as well as with C++ templates.

I agree that the dummy member is not super clean, but it's a simple solution that doesn't require significantly complicating the API. One thing you could consider is making your state types inherit from a State_base type, like:

struct State_base {
private:
  bool dummy;
};

struct PlayerState_Idle : State_base
{
    static void enter(flecs::entity e, PlayerState_Idle& state);
    static void exit(flecs::entity e, PlayerState_Idle& state);
};

which would ensure the state targets are never empty.