Entity Component System - C++ Hot Reload Integration

Long story short
Does exist a global event that I can trigger to replace the old component pointer in Ly with the new one?

More info
C++ Hot Reload extracts the class file to build a new version of the C++ types inside, dependent and dependencies are re-compiled if needed. In that process the most important event is “Reload_Instance” where you get a uuid that identifies that pointer (custom or auto-generated), the type name and a void* that is the newly created class using the default constructor (this can be also customized, doesn’t need to be the default constructor).

So, making an example to reduce the context, let’s imagine this scenario:
class EcsRegistry
{
std::vector<Component*> components

};
//
// C++ Hot Reload Events file (very simplified)
//
void DidReloadInstance(const char* const typeName, void*& data, const char* uuid)
{
for (Component* component : EcsRegistry::GetComponents())
{
if (component.guid == uuid)
{
component = static_cast<Component*>(data); // QUESTION1
break;
// Alternative1
newVersionOfComponent = static_cast<Component*>(data); // QUESTION1
EcsRegistry::ReplaceComponent(component, newVersionOfComponent);
}
}
}

QUESTION1: knowing will not be as simple as this :slight_smile: how to do the same and which events, messages in the system should I call in Ly to be able to propagate the new pointer?

This is the fastest version, but also it’s ok to copy the data from the old component, register for deletion the old Component, and add to all the entities that were using the old component the new one. For instance is the default action done by UE4. I don’t like it but I understand some system are complex and with legacy code and dependencies on them :slight_smile:

Thanks for your answer and support.

PS: I’m trying to achieve an official and free community integration for all the Ly fans

@Prompt The SerializeContext::CloneObjectInplace and SerializeContext::CloneObject functions seem to be close to what you need. As long as the types are reflected to the SerializationContext then it should work.

1 Like

Hi @Prompt,

You’re asking how you could replace a version of a components with a newer hot reloaded component correct?

If I’m understanding correctly, I believe you’re looking for something like Entity::SwapComponents. That’s a good example of how a component swap on an entity is handled.

The important things to note here are

  1. The state of the entity during the swap. An entity needs to be in the constructed/init state for this to work. Not all entity states are safe for this type of operation.
  2. The call to re-evaluate dependencies.
  3. The component ID swap.

Effectively Lumberyard tries to avoid referring to components by address. Lumberyard operates (as much as it can) using buses to communicate so we can avoid having hard references. SwapComponents is in line with what I think you’d want to do for a given component per Entity.

I’ll be consulting with a few other engineers to get their opinions as well as this is what I’d consider a high difficulty operation. :slight_smile:

1 Like

Thanks for the answer. I have a doubt though, I see in the code an assert, “Can’t remove component while the entity is active!”

At at first glance I’m not able to find any info that describes when an Entity is in activated state or its lifecycle.

Example:

  • Editor
  • Play scene
  • I’m moving a character and I want to change its behavior
  • I modify the code, c++ hot reload gives me the new component pointer
  • That assert will be triggered because the component is activated?

I’m not 100% sure, I’m guessing I cannot remove a component while I’m in Play mode, for instance. Is the normal behavior for these cases deactivate components that are not used anymore and add new ones?

Yes, that’s what I referring to in point 1, the state of the entity is critical here.

/**
 * The state of the entity and its components.
 * @note An entity is only initialized once. It can be activated and deactivated multiple times.
 */
enum State : u8
{
    ES_CONSTRUCTED,         ///< The entity was constructed but is not initialized or active. This is the default state after an entity is created.
    ES_INITIALIZING,        ///< The entity is initializing itself and its components. This state is the transition between ES_CONSTRUCTED and ES_INIT. 
    ES_INIT,                ///< The entity and its components are initialized. You can add and remove components from the entity when it is in this state.
    ES_ACTIVATING,          ///< The entity is activating itself and its components. This state is the transition between ES_INIT and ES_ACTIVE. 
    ES_ACTIVE,              ///< The entity and its components are active and fully operational. You cannot add or remove components from the entity unless you first deactivate the entity.
    ES_DEACTIVATING,        ///< The entity is deactivating itself and its components. This state is the transition between ES_ACTIVE and ES_INIT. 
};

When you attempt the swap you’ll need to Deactivate the target entity to move it to the ES_INIT state, perform the swap, and then restore it to its previous state. I’m honestly not certain if you can deactivate in Play mode as I don’t recall having attempted it myself but I imagine that’d work.

Could be safe enough to go to the state Pause?

If it’s not enough I’ll try first std::move as the pointer assignment it’s, let say…, like an atomic operation.
oldComponent = std::move(newComponent);

or …

oldComponent = newComponent;

In the demos I built, even in multi-thread, there is no problem or crashes if I assign the new pointer. And doesn’t matter if during a frame process the half it’s processed by the old component as the important are the upcoming frames.

The best is find out a safe moment to either swap (remove and add component) or assign the new pointer leaking little bytes even (unless there is a way to defer the deletion).

Anyways, I’ll try assigning the new pointer while you can give me more details about how to be in that safe state.

An additional question in the meantime, change the state of the component to ES_INIT it’s something safe?

Regarding the assignment of the new component pointer, a little question. Is the next piece of code the unique place where the pointer is save?

And:

Does exist a global Registry or the pointers are referenced somewhere else? I remember to see a dependency management code.

Thanks again for the answers @PyraRP

To answer your questions, I imagine the Pause state would deactivate entities and move them to ES_INIT so that could work. I’m not 100% sure on this but I can test and find out if you’d like. A simple test would be a single entity level with a breakpoint in Entity’s Deactivate function.

Moving to the ES_INIT state should be safe as that’s what we use for Entity Deactivation. :slight_smile:

To my knowledge, m_components is the place for a given entity. I’m not aware of a global registry. EBuses can be sort of a global registry but buses that components subscribe to should be subscribing against the component ID vs the pointer (ComponentBus.h is an example of this).

Dependency management is generally handled by evaluating functions like GetProvidedServices, GetIncompatibleServices, GetRequiredServices, and GetDependentServices which are generally defined per component.

Hey @PyraRP, I’m still trying to find out if there is an official way to subscribe when any component is created. Can you help me with that one please?

Hey @Prompt,

Sorry for the time delay here, I missed the notification on this. For component creation, you mean when a component is added to an entity? I don’t believe an official way to subscribe for this exists but you could setup an EBus that notifies to do so around where m_components is appended to.