Entity Component System - C++ Hot Reload Integration

0

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 :) 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 :)

Thanks for your answer and support.

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

asked 4 years ago332 views
12 Answers
0

@REDACTEDUSER

answered 4 years ago
0

Hi @REDACTEDUSER

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:

answered 4 years ago
0

[quote="PyraRP, post:3, topic:7866"] SwapComponents [/quote]

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!"

https://github.com/aws/lumberyard/blob/07228c605ce16cbf5aaa209a94a3cb9d6c1a4115/dev/Code/Framework/AzCore/AzCore/Component/Entity.cpp#L546

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?

answered 4 years ago
0

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.
 * @REDACTEDUSER
 */
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.

answered 4 years ago
0

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? https://github.com/aws/lumberyard/blob/07228c605ce16cbf5aaa209a94a3cb9d6c1a4115/dev/Code/Framework/AzCore/AzCore/Component/Entity.h#L475

And: https://github.com/aws/lumberyard/blob/07228c605ce16cbf5aaa209a94a3cb9d6c1a4115/dev/Code/Framework/AzCore/AzCore/Component/Entity.h#L57

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 @REDACTEDUSER

answered 4 years ago
0

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.

answered 4 years ago
0

Hey @REDACTEDUSER

answered 4 years ago
0

Hey @REDACTEDUSER

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.

answered 4 years ago
0

Thanks a lot @REDACTEDUSER

I still have a doubt... I studied a bit the reflection system of Lumberyard and I think, I'm not 100% sure, that a variable called m_template holds a pointer to every possible component. I think that is escaping to my reload! Not sure if you have around you an expert on those matters.

I'm asking this because I need to reload not only the active components pointers, I need to reload all of the systems for 2 reasons:

  1. All symbols point to the new dll, so you can still assign breakpoints.
  2. Newly created objects are cloned with the new reloaded class.

Atm I caught: AzToolsFramework::Components::GenericComponentWrapper AzToolsFramework::Components::EditorComponentBase and of course: AZ::Component

I know those have AZ::Component or they inherit from it. But I'm not 100% sure if I'm missing something else. I believe those of m_template deep in the Reflection system.

answered 4 years ago
0

@REDACTEDUSER

answered 4 years ago
0

Hi @REDACTEDUSER

GenericConmponentWrapper is what is used when there is no “editor” version of a given game component.

E.g. normally what happens is you make 2 classes derived from AZ::Component

            MyComponent : public AZ::Component
                            Reflected normally for serialization
                            * runtime code goes here

            MyEditorComponent : public EditorComponentBase
                            Reflected for serialization and edit and tagged with the “CAN CREATE IN EDITOR” tag that makes it show up in the add component menu.
                            * editor specific code goes here
                            * actually activated during editor time (ie, Activate is called).  Can do things like draw gizmos.
                            * contains a function that happens during level export time that produces a MyComponent saved into the final exported level, for use at runtime.

But sometimes, your MyComponent class for the runtime, it doesn’t do anything in the editor, its just a “property bag” – no special rendering, nothing crazy. In which case, you have this kind of setup: MyComponent : public AZ::Component Reflected normally for serialization Also reflected for edit, and “CAN CREATE IN EDITOR” flag shows up * runtime code goes here

But we don’t want the MyComponent class, which is meant for runtime, to actually be active during editor time, we don’t want to call its Activate, that may do things like delete entities, etc. So we need an EditorComponent that holds a pointer to it.

In this case, what really happens is that a GenericComponentWrapper gets created as the surrogate for the non-existent editor version. m_template just points at the MyComponent* instance that it holds. Its “export to game” function just duplicates the MyComponent* instance that m_template points at into the output file being exported.

Its called a template because it’s a pointer to the class that it will add to the level when exported, by duplicating it into the data being exported. It exposes its templates properties to the visual property editor. Note that the m_template pointed to component is not activated ever. Even when you press CTRL+G whats going on is the game executes the “export the level to runtime” function, which essentially loops over every (“Editor”) component that is active, and calls its ExportToGame function which is an invitation for it to add runtime components for serialization to a serialize stream. Then, the editor components are deactivated, the stream they just made is deserialized into the runtime, and the runtime activates those copies to run the game

So basically, m_template is a pointer to an ‘inactive’ AZ::Component-derived component that’s being used as a property bag for holding properties and showing them in the UI. It doesn’t point at every possible component, it always points at a particular type of component that the wrapper was created around (when the user clicked ‘add component’ button and the component they chose was not an editor one and there was no available editor specialization).

So basically, when the user clicks the “Add component button” the system checks to see if that type of component is an EditorCompoent or not. If its an editor component, it adds that actual component to the world and activates it. (So you end up with EditorLightComponent). If that is not an editor component, it adds a GenericComponentWrapper to the world, creates an instance of the component and points m_template at that instance. Then it activates the GenericComponentWrapper (not the thing pointed at in m_template). The wrapper acts like a proxy for the properties that offers all the functionality an “editor” component is supposed to provide, and runtimes do not provide, but never actually activates the target template component since its meant for runtime only.

The ’world’ you see in the editor view is all just things derived from EditorComponentBase – they’re either going to be actual editor components, or they’re going to be GenericComponentWrapper (which itself derives from EditorComponentBase) and has a m_template pointer to a Runtime-Only component that has no editor code.

answered 4 years ago
0

Sorry for my late reply I was waiting to have time to fully read it :slight_smile:

I think the pointer remaining to update is m_template, I need to check that out, everything seems to work perfectly, although in some cases I cannot setup breakpoints. Meaning Visual Studio don't know which is the right symbol. This happens normally when I'm not in play mode hmm...

Thanks a lot for all of this nice information and your effort collecting it! best.

answered 4 years ago

This post is closed: Adding new answers, comments, and votes is disabled.