Entt: Configuring the initialization of components with Prototype

Created on 16 Aug 2018  Â·  8Comments  Â·  Source: skypjack/entt

The current implementation of entt::Prototype::set constructs a component from the arguments given and stores this component in the registry. entt::Prototype::create then copies the stored component into a new entity. This implementation works well most of the time. There are some drawbacks to this.

Storing a physics object in a prototype and then copying it to new entities doesn't make a whole lot of sense. Physics engines usually store their physics bodies in a pool. In the case of Box2D, b2Bodys are stored in a linked-list. b2Body has m_next and m_prev pointers. Long story short, copying a physics body is not the way to go. Prototype should really store the data required to initialize a physics body, but not the body itself. In the case of Box2D, Prototype should store b2BodyDef and a few b2Fixtures and b2Shapes. Then Prototype should construct a b2Body from those each time create is called.

I have personally never used a graphics library more abstract than OpenGL but I'm sure that they do something similar to physics engines. Copying might not be possible because objects are stored in a pool and the pool must be updated properly.

I propose an alternative version of set that does something slightly different. Let's call it setArgs. This will put the given arguments into a std::tuple and store that instead of a component. create will then construct the component in-place inside of the new entity using the tuple of arguments.

The old behavior is useful most of the time so there should be a second version of set that constructs the component from the given arguments and stores it in a std::tuple<Component>. create will that construct the component from the tuple which will end up calling the copy constructor.

Implementing this would involve replacing Wrapper with std::tuple and creating a new version of set. The get function will have to modified to take the types of the tuple as template parameters and return a reference to the tuple. If only one template parameter is passed to get, the object stored in the tuple (rather than the tuple itself) should be returned.

Code that is already using entt::Prototype should not break after this change. Actually, I have just realised that code using the version of get that gets multiple components simultaneously might need to be adjusted.

invalid

All 8 comments

A few more things which this new implementation will give us.

RAII doesnt work well with current Prototypes, since a component is constructed when a Prototype is created, it should only be constructed when the actual entity is created.
Also, a physics body requires some data on creation (cpBodyNew()), for example "mass", which should not be stored directly in the component, but somewhere in the physics engine.
Same goes for most other properties a physics engine wants to store in its own way in memory, for example position, velocity and shape vertices.

Example of current situation:

struct Body
{
    cpBody* handle;
    float mass; // "mass" is stored here AND somewhere in the physics engine!

    Body(): handle(nullptr) {}

    void new() // Call this manually when Prototype is creating an entity. I think this "init"-function is against RAII principles.
    {
        handle = cpBodyNew(mass);
    }

    ~Body()
    {
        if(handle != nullptr)
        {
            cpBodyDel(handle);
        }
    }

    float getMass()
    {
        return(cpBodyGetMass(handle));
    }
};

Example of how it should look like:

struct Body
{
    cpBody* handle;

    Body(float mass) // "mass" is now passed as argument from Prototype::create
    {
        handle = cpBodyNew(mass); // "mass" is now stored somewhere in the physics engine.
    }

    ~Body()
    {
        cpBodyDel(handle);
    }

    float getMass()
    {
        return(cpBodyGetMass(handle));
    }
};

Also, hieratical entities will be possible with the new implementation, but I'm not sure if this example below is good or not, since I am still new to programming. With the current implementation, you cant store children prototypes in other prototypes, because you cant copy a prototype.

// Component for entities that has a parent.
struct Parent
{
    Entity entity;
};

// Component for entities that has children.
struct Children
{
    std::vector<Entity> entities;
}

// This is not really a component for entities, but for prototypes.
// It holds prototypes of entities that will soon become children (foetus). 
struct Foetus 
{
    std::vector<entt::Prototype<Entity>> prototypes;
};

// This function accepts prototypes for both single entities and entities with children.
void createEntity(const entt::Prototype<Entity>& prototype)
{
    Entity entity = prototype(registry);

    if(prototype.has<Foetus>() == true) // Is prototype a soon to be parent?
    {
        Children& parentChildren = registry.accommodate<Children>(entity); // Prepare parent's list of children identifiers.

        for(const entt::Prototype<Entity>& childPrototype: prototype.get<Foetus>().prototypes) // Iterate over foetus prototypes and create the children entities.
        {
            Entity childEntity = childPrototype(registry);

            registry.accommodate<Parent>(childEntity).entity = entity(); // Add parent's identifier to child.
            parentChildren.entities.push_back(entity);                   // Add child's identifier to parent's list of children identifiers.
        }
    }

    return(entity);
}

// Usage
entt::Prototype childPrototype;
childPrototype.set<Shape>(anOffsetVectorThatIsUsedToGenerateVertices); // glm::vec2 offset is given as parameter but should NOT be stored in component. Only used to generated vertices (which should eb stored in component).
childPrototype.set<Texture>();

entt::Prototype parentPrototype;
parentPrototype.set<Body>(someDataThatIsUsedWhenInitializePhysicsBodyButShouldntBeStoredInComponent);
parentPrototype.set<Foetus>().push_back(childPrototype);

Entity parentEntity = createEntity(parentPrototype);

I remember a long time ago when I was reading about entity systems, people were saying that systems should store their data internally and that the components only control this internal data. That is, for example, the render system uses the transform and animated-image components, but theanimated-image component doesn't store the image itself, but simply an identifier so that the render system knows what to render. The render system stores the images internally. The animation system doesn't actually know about the images at all, but knows how to update the animated-image component so that when the render system is run, the image appears animated. Simple example, but the point is that the physics component wouldn't store the b2Body directly, but rather stores only the controlling data and the physics system stores the b2Body since its internal data to the physics system. (Off the top of my head, it seems there are some issues to overcome here, but anyway... for example, making sure, the internal data may not be in the same order as what the view returns, messing with the cache -- but a pointer to b2Body has no such guarantee either; also, how does the system manage the lifecycle of its data..)

If such an approach is taken, then prototype wouldn't need to change since the component data is used to control the physics data, not store it directly.

Having said that, I have not yet tried it in practice, so I don't know if it will work well or not. I do think that the solution proposed here is still useful, though, so I'm all for it being added either way. Just adding the above thoughts in case its useful.

@danielytics What you're saying makes a lot of sense and I agree with you. But...

Prototype is intended to be the result of reading a JSON or XML file. You take the JSON file, deserialize it and store it in a Prototype. Then you can use the prototype as a factory for a set of similar entities.

If we have a physics component like this:

struct PhysicsBody {
  size_t bodyID;
};

And some physics bodies:

std::vector<b2Body> bodies;

Creating a new entity would involve initializing a b2Body and pushing it onto the vector of bodies. Then we'd have to get the index of the body and store it in the PhysicsBody component. How would you do that cleanly using the existing Prototype?

Using the new prototype, I would probably do something similar to this:

struct PhysicsBody {
  size_t bodyID;

  PhysicsBody(b2World &world, std::vector<b2Body> &bodies, b2BodyDef def)
    : bodyID{bodies.size()} {
    bodies.push_back(world.createBody(&def));
  }
};

entt::Prototype loadProto(std::string_view file) {
  entt::Prototype proto; // maybe bundle the world and bodies into a PhysicsSystem class
  proto.setArgs<PhysicsBody>(world, bodies, def);
  return proto;
}

@Kerndog73 I see what you mean.

I suppose I had something like this in mind — but I didn’t think it through very much, so... i (yours is simpler too): the definition is passed to the system that needs it (physics in this case) and a reference (hash or integer or whatever) to this is given back. The prototype is then given this reference and the component requests the actual bodyID in its constructor, by passing the reference to the physics system which then uses the stored definition. It basically achieves the same thing you’re doing, but with the disadvantage that after creating an entity from the prototype, the component now stores an uneeded reference (use a union? ugly though).

I like your way better.

I’m beginning to think that maybe this feature just over complicates this simple little tool. The feature doesn’t really fit with the name either. A prototype is a preinitialized object that is copied to initialise new objects. Parametrising properties doesn’t really make a whole lot of sense.

You never just use prototype on its own. Why would you want a bunch of identical entities? You normally do something like this:

auto makeThing(const entt::Prototype &thing, const glm::vec2 pos) {
  auto e = thing();
  thing.reg().assign<Position>(e, pos);
  return e;
}

If you have something that isn’t copyable (like a b2Body), you would initialise it in the factory.

I think it would be handy to make the internal registry reference accessible. When you have a prototype, you usually need a registry as well.

What do you mean with _internal registry reference_? The one of the prototype?

Yes, I’m talking about the registry reference in the prototype

__TL;DR__ I've had a good long think about this. If the current prototype isn't suitable for a particular problem, don't use it. The current implementation is suitable most of the time because components are copiable most of the time.


entt::Prototype stores a collection of components. These stored components are copied to initialize an entity. This is analogous with the prototype design pattern. A prototype object is initialized, then new objects are created by copying the members of the prototype. If components are parameterized, it's not really a "prototype" anymore. The name no longer fits the functionality.

entt::Prototype is the generalized equivalent of the following snippet and nothing more:

struct LaserDetectorProto {
  Activation act;
  Animation anim;
  AnimSpriteRendering ren;
  PowerOutput powOut;
  LaserDetector laser;
};

auto makeLaserDetector(const LaserDetectorProto &proto, entt::DefaultRegistry &reg) {
  auto e = reg.create();
  reg.assign<Activation>(e, proto.act);
  reg.assign<Animation>(e, proto.anim);
  reg.assign<AnimSpriteRendering>(e, proto.ren);
  reg.assign<PowerOutput>(e, proto.powOut);
  reg.assign<LaserDetector>(e, proto.laser);
  return e;
}

An instance of entt::Prototype can store any components, not a predefined set. This issue talks about solutions to the problem of dealing with components that aren't copyable.

__E__ ntities are intended to be integers
__C__ omponents are intended to be POD structs
__S__ ystems are intended to be functions

Are POD structs copyable? 😆

entt::Prototype doesn't need to support components that aren't POD structs. If you want to use prototype but your components don't "fit" then go back to the struct-with-factory that entt::Prototype replaces.

This is an example of struct-with-factory using Box2D:

// This is our component. It cannot be copied
struct PhysicsBody {
  b2Body *body;

  PhysicsBody(PhysicsBody &&other)
    : body{std::exchange(other.body, nullptr)} {}
  PhysicsBody &operator=(PhysicsBody &&other) {
    body->GetWorld()->DestroyBody(body);
    body = std::exchange(other.body, nullptr);
  }
  ~PhysicsBody() {
    body->GetWorld()->DestroyBody(body);
  }
};

// Another component. This one can be copied just fine
struct Health {
  int value;
};

// The prototype struct contains all of the data required to initialize 
// an entity with a PhysicsBody component and a Health component
struct ThingProto {
  // PhysicsBody data
  std::vector<b2Shape> shapes;
  std::vector<b2FixtureDef> fixtures;
  b2BodyDef def;
  // Health data
  Health health;
};

// A factory for making "Things"
auto makeThing(const ThingProto &proto, entt::DefaultRegistry &reg, b2World &world) {
  b2Body *body = world.createBody(&proto.def);
  for (const b2FixtureDef &fix : proto.fixtures) {
    body->createFixture(&fix);
  }
  auto e = reg.create();
  reg.assign<PhysicsBody>(e, body);
  reg.assign<Health>(e, proto.health);
  return e;
}

Neither a entt::Prototype nor a struct-with-factory completely initialize an entity. They initialize the common parts of the entity. The uncommon parts (like the position for example) are initialized separately. You would do this in a factory that takes the instance-specific data and completely initializes the entity.

auto makeThing(const entt::DefaultPrototype &proto, glm::vec2 pos) {
  auto e = proto();
  proto.reg().assign<Position>(pos);
  return e;
}

// or...

auto makeThing(const ThingProto &proto, entt::DefaultRegistry &reg, b2World &world, glm::vec2 pos) {
  auto e = makeThing(proto, reg, world);
  reg.assign<Position>(pos);
  return e;
}

This change also messes with generalized prototype serialization but this comment is long enough!

Was this page helpful?
0 / 5 - 0 ratings