Entt: Call for comments: blueprint/template

Created on 12 Mar 2018  路  35Comments  路  Source: skypjack/entt

Another step towards the next version of EnTT.
The idea is to create a templating system (where template isn't intended as _C++ template_) with which to register predefined sets of components to assign to entities during creation.

Something along this line:

registry.blueprint<AComponent, AnotherComponent>("button");

// ...

auto entity = registry.create("button");

Doubts for which feedback would be appreciated:

  • Should it be part of the registry and thus extend its API and introduce another vector internally or could it be an external tool?

  • Runtime vs compile-time: HashedString can be used in constant expressions, otherwise they risk to introduce performance hits.
    In other terms, this has better performance at runtime:

    registry.blueprint<HashedString{"button"}, AComponent, AnotherComponent>();
    // ...
    auto entity = registry.create<HashedString {"button"}>();
    

    This one has a nicer API:

    registry.blueprint<AComponent, AnotherComponent>("button");
    // ...
    auto entity = registry.create("button");
    
  • Is there any interest in such a feature? (Actually this one should have been the first question, btw)

In any case, I can't work on it for at least a week probably. Because of this, there is enough time to discuss the _best way_ to do it.

discussion

Most helpful comment

@ArnCarveris

Good point. However, if it's a matter of names, then accommodate is what we are looking for.
The documentation of the registry says that _it assigns or replaces a component_. This is what the prototype would do indeed.

It doesn't save from typing, but it's consistent at least.

All 35 comments

@skypjack

Should it be part of the registry and thus extend its API and introduce another vector internally or could it be an external tool?

Prefer external tool.

Runtime vs compile-time: HashedString can be used in constant expressions, otherwise they risk to introduce performance hits.
In other terms, this has better performance at runtime:

Important to have runtime creation. make it configurable from data/file/resource.

registry.blueprint<HashedString{"button"}, AComponent, AnotherComponent>();
// ...
auto entity = registry.create(HashedString {"button"});

@ArnCarveris I see your point. It makes sense.
Full support at runtime (instead of a mixed compile-time/runtime model) would make them usable also from within a plug-in system. Mod developers (if any) could write their own templates this way and then use them.
I didn't think about it actually. Interesting indeed.

@skypjack

plug-in system.

How to implement dynamic/shared library support? I mean custom components defined inside plug-in,

@ArnCarveris Actually I said plug-in but I meant script/runtime components in this case. My fault.
Anyway, you can create a plug-in system in exactly the same way you can create a script system: use a single component defined at compile-time to use to map runtime components defined by a plug-in. In this case, type erasure techniques can help probably.
See the mod directory for an example of mod based on Duktape.

So far so good. I got good points out of this issue. Thank you. Time to close it.

Any news about the blueprint?

@iderik See branch experimental. I added dependency helpers and signals. If you combine them, you get what was called _blueprint_ here.

I had a look at the dependency listeners and I think it might be more useful to create prototype entities. These are collections of initialized components that can be copied into a new entity. Views should not be able to iterate prototype entities so perhaps they should live in a separate data structure. Copying the prototypes should not require knowledge of what components that prototype contains.

This is what the interface might look like:

// Since the registry is already dependent on the Prototype class
// in reg.create(proto) it might make more sense to do this
entt::DefaultRegistry::Prototype proto;
// instead of
entt::Prototype<entt::DefaultRegistry::component_family> proto;

// similar interface to the registry
// remove, replace, accommodate, etc
// this object is essentially a registry optimized for one entity
proto.assign<AComponent>(4, 58.2);
// These values are probably being loaded from a file. We don't want to
// open and parse a file everytime we create the same entity.
proto.assign<AnotherComponent>(5e12);

entt::DefaultRegistry reg;
// The components from the prototype are copied into each of these entities
uint32_t entity = reg.create(proto);
// This is equivalent to the following
uint32_t entity = reg.create();
reg.assign<AComponent>(entity, 4, 58.2);
reg.assign<AnotherComponent>(entity, 5e12f);

This system has a wide variety of uses. In a tower defence game, each of the enemy units could be a prototype because they are all identical. A prototype could even be used for each of the defence towers because we want to create each tower in the same way (with the same function call) even though they have different components that are initialized to different values.

Thoughts?

Good point, this feature is quite missing.

// Since the registry is already dependent on the Prototype class
// in reg.create(proto) it might make more sense to do this
entt::DefaultRegistry::Prototype proto;

This would be better.

namespace entt
{
  template<typename Entity> class Prototype<Entity>;

  using DefaultPrototype = Prototype<uint32_t>;
}

Example
entt::DefaultPrototype proto;

An entity creation with prototype.
entity_type create(const Prototype<entity_type>& proto);

In combination with snapshot should be easy to implement prefab.

The prototype class would need the component_family_type from the registry in order for reg.create(proto) to be implemented.

@Kerndog73

Well, first of all, if you are ok with using proto(reg.create()); instead of reg.create(proto);, it can be developed entirely as an external tool that doesn't affect the API of the registry.
It shouldn't be difficult to implement that way.

The prototype class would need the component_family_type from the registry in order for reg.create(proto) to be implemented.

Naa, we can get away without it. ;-)

Won't it be more efficient to use component IDs instead of type erasure?

The fact is that we can't use the pools of the components for this, mainly because otherwise a view would return also prototype entities. I would avoid adding branches or whatever in the critical paths only to support this feature.
Because of this it's already a separated data structure then, no matter if you put it into the registry or wrap it somehow with an external tool.
So...

Moreover, efficiency in this case isn't much critical from my point of view. I mean, unlikely you'll create 10k entities at once in a loop from a prototype, right?

I thought it might be implemented similarly to this except not as messy.

template <typename Registry>
class Prototype {
public:
  // we'll have to make Registry::component_family public
  using component_family = typename Registry::component_family;

  template <typename Component, typename ...Args>
  void assign(Args... args) {
    const size_t id = component_family::type<Component>();
    while (components.size() <= id) {
      components.push_back(nullptr);
    }
    components[id] = new Component(args...);
  }

  std::vector<void *> components;
};

template<typename Entity>
class Registry {
public:
  void create(const Prototype<Registry<Entity>> &proto) {
    for (size_t id = 0; id != proto.components.size(); ++id) {
      if (proto.components[id] == nullptr) {
        continue;
      }
      // id is the component id and
      // proto.components[id] is the component
      // we can efficiently put this in a pool right?
    }  
  }
};

How do you think it should be implemented?

I would avoid adding branches or whatever in the critical paths only to support this feature.

This implementation doesn't affect code that doesn't use it which is one of the design philosophies of this framework.

@Kerndog73

A few notes:

  • Good point about the design philosophy of the framework, really appreciated your efforts to respect it.
  • Component families for tags and components are already exposed actually. See component and tag member functions on branch master, type on branch experimental.
  • Unfortunately, we cannot efficiently put it in a pool_ as you mentioned in the comments. The Registry class makes heavy use of type erasure techniques internally and we cannot cast a basic pool to its (let me say) _component version_ only from the family identifier. I'm sorry.

Yeah, I was just looking at the registry class and you're right, it can't be implemented this way. How do you think this should be implemented? Perhaps a std::vector<> of functions that assign components to an entity? Like this?

template <typename Registry>
class Prototype {
public:
  using entity_type = typename Registry::entity_type;  

  template <typename Comp, typename ...Args>
  void assign(Args &&... args) {
    factories.emplace_back(
      [Comp comp {std::forward<Args>(args)...}]
      (Registry &reg, entity_type entity) {
        reg.template assign<Comp>(entity, comp);
      }
    );
  }

  void operator()(Registry &reg, entity_type entity) {
    for (auto &factory : factories) {
      factory(reg, entity);
    }
  }
  entity_type operator()(Registry &reg) {
    entity_type entity = reg.create();
    (*this)(reg, entity);
    return entity;
  }

private:
  std::vector<std::function<void (Registry &, entity_type)>> factories;
};

On an unrelated note, what aren't component() and tag() static functions?

I think the main difference comes from the type we want to give to the prototype.
I mean, sort Prototype<Entity> vs Prototype<Entity, Comp1, ..., CompN>. The former requires necessarily to rely on type erasure and dynamic storage or something along this line. The latter is much easier to implement and to deal with, but you cannot create a vector or a map of prototypes this way.
What's your experience? You made the request, you drive the discussion. :-)


Side note: you are right, component and tag can be static. My fault. Thank you.

I think that a single prototype class that can hold any components is much more useful (at least in my situation). Right now, I'm in the process of moving away from a struct-of-component-pointers to a vector-of-factories prototype. I understand that Prototype<Entity, Comp1, ..., CompN> will be faster in every way but type lists are annoying and we can't store a bunch of different prototypes in a vector.

Got it. The funny part is that we could embed everything in the registry and use a sort of reserved area of the pools for the prototypes, but I don't like much the idea. Moreover, it would affect somehow also the internals for those that aren't interested in the feature, so... you know, I'm not a fan of the _put everything inside and let's see_.

So, has this feature made it onto the TODO list?

Yep. ;-) I'll be working on it during the weekend probably.

@Kerndog73 Delayed a bit mainly because of private projects. Don't worry, I'll be working on it soon.

That鈥檚 OK. There鈥檚 no need to rush!

@Kerndog73

Why did you use accommodate in your proposal for prototypes?
I mean, accommodate drops already existing instances if any, I'm not sure this is the intended behavior.

I'm going to use prototypes on empty entities. In all the other cases, probably I don't want to loose my components because of a call to prototype(registry, entity). I can imagine a lot of cases where I could want to use a prototype to somehow _complete_ the set of components of an entity that already has part of them and in no case I'd like to overwrite already existing components.

Is there a specific reason for which you did it? I think something like this would be maybe more suited:

if(!registry.template has<Component>(entity)) {
    registry.template assign<Component>(entity, static_cast<const Component &>(component));
}

What about?

I was originally using accommodate so that I had defined behavior when the entity already had some components. I only need prototypes on empty entities so I see no problem with your suggestion.

@skypjack Well there is use case where accommodate could be useful:

struct Stats
{
  float max_speed;
};

entt::DefaultPrototype basis;
basis.assign<Stats>(1.0f);

entt::DefaultPrototype level_1;
level_1.assign<Stats>(3.0f);

entt::DefaultPrototype level_2;
level_2.assign<Stats>(5.0f);

entt::DefaultRegistry registry;

auto entity = basis(registry);

level_1(registry, entity);
level_2(registry, entity);

@ArnCarveris

Good point. However, think about this.
If you use accommodate internally, there is no way to tell to the prototype - _ehi, please, do not overwrite already existing components, please._ Right?
On the other hand, if we use if/assign, you can still do something like this if you want to overwrite already existing components:

registry.reset(entity);
prototype(registry, entity);

Even though this drops all the components, not only those of the prototype.

Probably, the best solution would be to get rid of the operator() and offer two member functions: one that overwrite and one that doesn't overwrite.
operator() can just fallback on one of them, the most common one.

@skypjack

Probably, the best solution would be to get rid of the operator() and offer two member functions: one that overwrite and one that doesn't overwrite.
operator() can just fallback on one of them, the most common one.

Totally agree with you!

I think it will be something along this line:

// These won't overwrite existing components
const auto entity = prototype (registry);
prototype (registry, entity);
// This will replace existing components
prototype.reset(registry, entity);

That is, the default behavior is the safest one and works fine also for empty entities. reset is a way to force a reset of all the components when required.

What about?

@Kerndog73 @ArnCarveris

@skypjack reset sounds odd in following case, suggest use apply

struct Stats
{
  float max_speed;
};

struct Gun
{
  float max_damage;
};

struct Rank
{
  const char* name;
};

entt::DefaultPrototype base_level_prototype;
base_level_prototype.assign<Stats>(1.0f);
base_level_prototype.assign<Rank>("Novice");

entt::DefaultPrototype level_1_prototype;;
level_1_prototype.assign<Stats>(3.0f);
level_1_prototype.assign<Gun>(1.0f);

entt::DefaultPrototype level_2_prototype;;
level_2_prototype.assign<Stats>(5.0f);
level_2_prototype.assign<Gun>(1.5f);
level_2_prototype.assign<Rank>("Adept");

entt::DefaultRegistry registry;


auto entity_0 = registry.create();

// not sure
// - base_level_prototype(registry, entity_0)
base_level_prototype.init(registry, entity_0);

level_1_prototype.apply(registry, entity_0);
level_2_prototype.apply(registry, entity_0);


// not sure
// - auto entity_1 = base_level_prototype(registry);
auto entity_1 = base_level_prototype.create(registry); 

level_1_prototype.apply(registry, entity_1);
level_2_prototype.apply(registry, entity_1);

@ArnCarveris Why does it sound odd? I didn't get it, sorry.

@skypjack well in registry reset does this but prototype reset does completely opposite, using same word for different behaviors is bad idea.

There is method for applying components to specified entity, and just it nothing more.
Applying prototypes to same entity in series, makes it an 'sandwich' entity and apply method perfectly describes that behavior.
```
level_1_prototype.apply(registry, entity_1);
level_2_prototype.apply(registry, entity_1);

By having `reset` method is not clear about final entity state, just confuse.

level_1_prototype.reset(registry, entity_1);
level_2_prototype.reset(registry, entity_1);
```

@ArnCarveris

Good point. However, if it's a matter of names, then accommodate is what we are looking for.
The documentation of the registry says that _it assigns or replaces a component_. This is what the prototype would do indeed.

It doesn't save from typing, but it's consistent at least.

Was this page helpful?
0 / 5 - 0 ratings