diff --git a/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp
new file mode 100644
index 000000000000..9487aa3a05f7
--- /dev/null
+++ b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp
@@ -0,0 +1,157 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+#include "EventsBasedObjectVariantHelper.h"
+
+#include "GDCore/Project/EventsBasedObject.h"
+#include "GDCore/Project/InitialInstancesContainer.h"
+#include "GDCore/Project/Object.h"
+#include "GDCore/Project/ObjectGroup.h"
+#include "GDCore/Project/ObjectsContainer.h"
+#include "GDCore/Project/ObjectsContainersList.h"
+#include "GDCore/Project/Project.h"
+#include "GDCore/Project/Variable.h"
+#include "GDCore/Project/VariablesContainer.h"
+#include "GDCore/String.h"
+
+namespace gd {
+
+void EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+    const gd::Project &project, gd::EventsBasedObject &eventsBasedObject) {
+  auto &defaultObjects = eventsBasedObject.GetDefaultVariant().GetObjects();
+
+  for (const auto &variant :
+       eventsBasedObject.GetVariants().GetInternalVector()) {
+    auto &objects = variant->GetObjects();
+
+    // Delete extra objects
+    for (auto it = objects.GetObjects().begin();
+         it != objects.GetObjects().end(); ++it) {
+      const auto &objectName = it->get()->GetName();
+      if (!defaultObjects.HasObjectNamed(objectName)) {
+        variant->GetInitialInstances().RemoveInitialInstancesOfObject(
+            objectName);
+        // Do it in last because it unalloc objectName.
+        objects.RemoveObject(objectName);
+        --it;
+      }
+    }
+    for (const auto &defaultObject : defaultObjects.GetObjects()) {
+      const auto &objectName = defaultObject->GetName();
+      const auto &defaultVariables = defaultObject->GetVariables();
+      const auto &defaultBehaviors = defaultObject->GetAllBehaviorContents();
+
+      // Copy missing objects
+      if (!objects.HasObjectNamed(objectName)) {
+        objects.InsertObject(*defaultObject,
+                             defaultObjects.GetObjectPosition(objectName));
+        objects.AddMissingObjectsInRootFolder();
+        continue;
+      }
+      // Change object types
+      auto &object = objects.GetObject(objectName);
+      if (object.GetType() != defaultObject->GetType()) {
+        // Keep a copy of the old object.
+        auto oldObject = objects.GetObject(objectName);
+        objects.RemoveObject(objectName);
+        objects.InsertObject(*defaultObject,
+                             defaultObjects.GetObjectPosition(objectName));
+        object.CopyWithoutConfiguration(oldObject);
+        objects.AddMissingObjectsInRootFolder();
+      }
+
+      // Copy missing behaviors
+      auto &behaviors = object.GetAllBehaviorContents();
+      for (const auto &pair : defaultBehaviors) {
+        const auto &behaviorName = pair.first;
+        const auto &defaultBehavior = pair.second;
+
+        if (object.HasBehaviorNamed(behaviorName) &&
+            object.GetBehavior(behaviorName).GetTypeName() !=
+                defaultBehavior->GetTypeName()) {
+          object.RemoveBehavior(behaviorName);
+        }
+        if (!object.HasBehaviorNamed(behaviorName)) {
+          auto *behavior = object.AddNewBehavior(
+              project, defaultBehavior->GetTypeName(), behaviorName);
+          gd::SerializerElement element;
+          defaultBehavior->SerializeTo(element);
+          behavior->UnserializeFrom(element);
+        }
+      }
+      // Delete extra behaviors
+      for (auto it = behaviors.begin(); it != behaviors.end(); ++it) {
+        const auto &behaviorName = it->first;
+        if (!defaultObject->HasBehaviorNamed(behaviorName)) {
+          object.RemoveBehavior(behaviorName);
+          --it;
+        }
+      }
+
+      // Sort and copy missing variables
+      auto &variables = object.GetVariables();
+      for (size_t defaultVariableIndex = 0;
+           defaultVariableIndex < defaultVariables.Count();
+           defaultVariableIndex++) {
+        const auto &variableName =
+            defaultVariables.GetNameAt(defaultVariableIndex);
+        const auto &defaultVariable =
+            defaultVariables.Get(defaultVariableIndex);
+
+        auto variableIndex = variables.GetPosition(variableName);
+        if (variableIndex == gd::String::npos) {
+          variables.Insert(variableName, defaultVariable, defaultVariableIndex);
+        } else {
+          variables.Move(variableIndex, defaultVariableIndex);
+        }
+        if (variables.Get(variableName).GetType() != defaultVariable.GetType()) {
+          variables.Remove(variableName);
+          variables.Insert(variableName, defaultVariable, defaultVariableIndex);
+        }
+      }
+      // Remove extra variables
+      auto variableToRemoveCount = variables.Count() - defaultVariables.Count();
+      for (size_t iteration = 0; iteration < variableToRemoveCount;
+           iteration++) {
+        variables.Remove(variables.GetNameAt(variables.Count() - 1));
+      }
+
+      // Remove extra instance variables
+      variant->GetInitialInstances().IterateOverInstances(
+          [&objectName,
+           &defaultVariables](gd::InitialInstance &initialInstance) {
+            if (initialInstance.GetObjectName() != objectName) {
+              return false;
+            }
+            auto &instanceVariables = initialInstance.GetVariables();
+            for (size_t instanceVariableIndex = 0;
+                 instanceVariableIndex < instanceVariables.Count();
+                 instanceVariableIndex++) {
+              const auto &variableName =
+                  defaultVariables.GetNameAt(instanceVariableIndex);
+
+              if (!defaultVariables.Has(variableName)) {
+                instanceVariables.Remove(variableName);
+              }
+            }
+            return false;
+          });
+    }
+    auto &defaultObjectGroups =
+        eventsBasedObject.GetDefaultVariant().GetObjects().GetObjectGroups();
+    auto &objectGroups = variant->GetObjects().GetObjectGroups();
+    auto objectGroupsCount = objectGroups.Count();
+    // Clear groups
+    for (size_t index = 0; index < objectGroupsCount; index++) {
+      objectGroups.Remove(objectGroups.Get(0).GetName());
+    }
+    // Copy groups
+    for (size_t index = 0; index < defaultObjectGroups.Count(); index++) {
+      objectGroups.Insert(defaultObjectGroups.Get(index), index);
+    }
+  }
+}
+
+} // namespace gd
diff --git a/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h
new file mode 100644
index 000000000000..bd78252325d2
--- /dev/null
+++ b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h
@@ -0,0 +1,26 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+#pragma once
+
+namespace gd {
+class EventsBasedObject;
+class Project;
+} // namespace gd
+
+namespace gd {
+
+class GD_CORE_API EventsBasedObjectVariantHelper {
+public:
+  /**
+   * @brief Apply the changes done on events-based object children to all its
+   * variants.
+   */
+  static void
+  ComplyVariantsToEventsBasedObject(const gd::Project &project,
+                                    gd::EventsBasedObject &eventsBasedObject);
+};
+
+} // namespace gd
\ No newline at end of file
diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.cpp b/Core/GDCore/IDE/ObjectAssetSerializer.cpp
index 959864f286c8..d40835528d6c 100644
--- a/Core/GDCore/IDE/ObjectAssetSerializer.cpp
+++ b/Core/GDCore/IDE/ObjectAssetSerializer.cpp
@@ -17,6 +17,7 @@
 #include "GDCore/IDE/Project/ResourcesRenamer.h"
 #include "GDCore/Project/Behavior.h"
 #include "GDCore/Project/CustomBehavior.h"
+#include "GDCore/Project/CustomObjectConfiguration.h"
 #include "GDCore/Project/EventsFunctionsExtension.h"
 #include "GDCore/Project/Layout.h"
 #include "GDCore/Project/Object.h"
@@ -75,6 +76,16 @@ void ObjectAssetSerializer::SerializeTo(
 
   cleanObject->SerializeTo(objectAssetElement.AddChild("object"));
 
+  if (project.HasEventsBasedObject(object.GetType())) {
+    SerializerElement &variantsElement =
+        objectAssetElement.AddChild("variants");
+    variantsElement.ConsiderAsArrayOf("variant");
+
+    std::unordered_set<gd::String> alreadyUsedVariantIdentifiers;
+    gd::ObjectAssetSerializer::SerializeUsedVariantsTo(
+        project, object, variantsElement, alreadyUsedVariantIdentifiers);
+  }
+
   SerializerElement &resourcesElement =
       objectAssetElement.AddChild("resources");
   resourcesElement.ConsiderAsArrayOf("resource");
@@ -108,4 +119,46 @@ void ObjectAssetSerializer::SerializeTo(
       objectAssetElement.AddChild("customization");
   customizationElement.ConsiderAsArrayOf("empty");
 }
+
+void ObjectAssetSerializer::SerializeUsedVariantsTo(
+    gd::Project &project, const gd::Object &object,
+    SerializerElement &variantsElement,
+    std::unordered_set<gd::String> &alreadyUsedVariantIdentifiers) {
+
+  if (!project.HasEventsBasedObject(object.GetType())) {
+    return;
+  }
+  const auto *customObjectConfiguration =
+      dynamic_cast<const gd::CustomObjectConfiguration *>(
+          &object.GetConfiguration());
+  if (customObjectConfiguration
+          ->IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ||
+      customObjectConfiguration
+          ->IsForcedToOverrideEventsBasedObjectChildrenConfiguration()) {
+    return;
+  }
+  const auto &variantName = customObjectConfiguration->GetVariantName();
+  const auto &variantIdentifier =
+      object.GetType() + gd::PlatformExtension::GetNamespaceSeparator() +
+      variantName;
+  auto insertResult = alreadyUsedVariantIdentifiers.insert(variantIdentifier);
+  if (insertResult.second) {
+    const auto &eventsBasedObject =
+        project.GetEventsBasedObject(object.GetType());
+    const auto &variants = eventsBasedObject.GetVariants();
+    const auto &variant = variants.HasVariantNamed(variantName)
+                              ? variants.GetVariant(variantName)
+                              : eventsBasedObject.GetDefaultVariant();
+
+    SerializerElement &pairElement = variantsElement.AddChild("variant");
+    pairElement.SetAttribute("objectType", object.GetType());
+    SerializerElement &variantElement = pairElement.AddChild("variant");
+    variant.SerializeTo(variantElement);
+    // TODO Recursivity
+    for (auto &object : variant.GetObjects().GetObjects()) {
+      gd::ObjectAssetSerializer::SerializeUsedVariantsTo(
+          project, *object, variantsElement, alreadyUsedVariantIdentifiers);
+    }
+  }
+}
 } // namespace gd
diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.h b/Core/GDCore/IDE/ObjectAssetSerializer.h
index bb678449213b..51b641a90b62 100644
--- a/Core/GDCore/IDE/ObjectAssetSerializer.h
+++ b/Core/GDCore/IDE/ObjectAssetSerializer.h
@@ -6,6 +6,7 @@
 #pragma once
 #include <map>
 #include <vector>
+#include <unordered_set>
 
 #include "GDCore/String.h"
 
@@ -52,6 +53,11 @@ class GD_CORE_API ObjectAssetSerializer {
   ObjectAssetSerializer(){};
 
   static gd::String GetObjectExtensionName(const gd::Object &object);
+
+  static void SerializeUsedVariantsTo(
+      gd::Project &project, const gd::Object &object,
+      SerializerElement &variantsElement,
+      std::unordered_set<gd::String> &alreadyUsedVariantIdentifiers);
 };
 
 } // namespace gd
diff --git a/Core/GDCore/IDE/ObjectVariableHelper.cpp b/Core/GDCore/IDE/ObjectVariableHelper.cpp
index d9fabc624365..c1b37e188908 100644
--- a/Core/GDCore/IDE/ObjectVariableHelper.cpp
+++ b/Core/GDCore/IDE/ObjectVariableHelper.cpp
@@ -6,6 +6,7 @@
 #include "ObjectVariableHelper.h"
 
 #include "GDCore/IDE/WholeProjectRefactorer.h"
+#include "GDCore/Project/EventsBasedObject.h"
 #include "GDCore/Project/InitialInstancesContainer.h"
 #include "GDCore/Project/Object.h"
 #include "GDCore/Project/ObjectGroup.h"
@@ -173,6 +174,7 @@ void ObjectVariableHelper::ApplyChangesToObjects(
                                 groupVariablesContainer.Get(variableName),
                                 variablesContainer.Count());
     }
+    // TODO Check what happens if 2 variables exchange their names.
     for (const auto &pair : changeset.oldToNewVariableNames) {
       const gd::String &oldVariableName = pair.first;
       const gd::String &newVariableName = pair.second;
@@ -215,6 +217,7 @@ void ObjectVariableHelper::ApplyChangesToObjectInstances(
               destinationVariablesContainer.Remove(variableName);
             }
           }
+          // TODO Check what happens if 2 variables exchange their names.
           for (const auto &pair : changeset.oldToNewVariableNames) {
             const gd::String &oldVariableName = pair.first;
             const gd::String &newVariableName = pair.second;
@@ -236,6 +239,66 @@ void ObjectVariableHelper::ApplyChangesToObjectInstances(
             }
           }
         }
+        return false;
       });
 }
+
+void ObjectVariableHelper::ApplyChangesToVariants(
+    gd::EventsBasedObject &eventsBasedObject, const gd::String &objectName,
+    const gd::VariablesChangeset &changeset) {
+  auto &defaultVariablesContainer = eventsBasedObject.GetDefaultVariant()
+                                        .GetObjects()
+                                        .GetObject(objectName)
+                                        .GetVariables();
+  for (auto &variant : eventsBasedObject.GetVariants().GetInternalVector()) {
+    if (!variant->GetObjects().HasObjectNamed(objectName)) {
+      continue;
+    }
+    auto &object = variant->GetObjects().GetObject(objectName);
+    auto &variablesContainer = object.GetVariables();
+
+    for (const gd::String &variableName : changeset.removedVariableNames) {
+      variablesContainer.Remove(variableName);
+    }
+    for (const gd::String &variableName : changeset.addedVariableNames) {
+      if (variablesContainer.Has(variableName)) {
+        // It can happens if a child-object already had the variable but it was
+        // missing in other variant child-object.
+        continue;
+      }
+      variablesContainer.Insert(variableName,
+                                defaultVariablesContainer.Get(variableName),
+                                variablesContainer.Count());
+    }
+    // TODO Check what happens if 2 variables exchange their names.
+    for (const auto &pair : changeset.oldToNewVariableNames) {
+      const gd::String &oldVariableName = pair.first;
+      const gd::String &newVariableName = pair.second;
+      if (variablesContainer.Has(newVariableName)) {
+        // It can happens if a child-object already had the variable but it was
+        // missing in other variant child-object.
+        variablesContainer.Remove(oldVariableName);
+      } else {
+        variablesContainer.Rename(oldVariableName, newVariableName);
+      }
+    }
+    // Apply type changes
+    for (const gd::String &variableName : changeset.valueChangedVariableNames) {
+      size_t index = variablesContainer.GetPosition(variableName);
+
+      if (variablesContainer.Has(variableName) &&
+          variablesContainer.Get(variableName).GetType() !=
+              defaultVariablesContainer.Get(variableName).GetType()) {
+        variablesContainer.Remove(variableName);
+        variablesContainer.Insert(
+            variableName, defaultVariablesContainer.Get(variableName), index);
+      }
+    }
+
+    gd::ObjectVariableHelper::ApplyChangesToObjectInstances(
+        variablesContainer, variant->GetInitialInstances(), objectName,
+        changeset);
+  }
+}
+
 } // namespace gd
diff --git a/Core/GDCore/IDE/ObjectVariableHelper.h b/Core/GDCore/IDE/ObjectVariableHelper.h
index bafc21429c27..c9950ab9e895 100644
--- a/Core/GDCore/IDE/ObjectVariableHelper.h
+++ b/Core/GDCore/IDE/ObjectVariableHelper.h
@@ -8,6 +8,7 @@
 #include "GDCore/Project/VariablesContainer.h"
 
 namespace gd {
+class EventsBasedObject;
 class InitialInstancesContainer;
 class ObjectsContainersList;
 class ObjectsContainer;
@@ -53,7 +54,7 @@ class GD_CORE_API ObjectVariableHelper {
    * Objects can be added during the group edition and may not necessarily have
    * all the variables initially shared by the group.
    *
-   * \see gd::GroupVariableHelper::MergeVariableContainers
+   * \see gd::ObjectVariableHelper::MergeVariableContainers
    */
   static void FillMissingGroupVariablesToObjects(
       gd::ObjectsContainer &globalObjectsContainer,
@@ -72,16 +73,21 @@ class GD_CORE_API ObjectVariableHelper {
                         const gd::ObjectGroup &objectGroup,
                         const gd::VariablesChangeset &changeset);
 
+  /**
+   * @brief Apply the changes done on an object to all its instances.
+   */
   static void ApplyChangesToObjectInstances(
       gd::VariablesContainer &objectVariablesContainer,
       gd::InitialInstancesContainer &initialInstancesContainer,
       const gd::String &objectName, const gd::VariablesChangeset &changeset);
 
-private:
-  static void ApplyChangesToVariableContainer(
-      const gd::VariablesContainer &originalVariablesContainer,
-      gd::VariablesContainer &destinationVariablesContainer,
-      const gd::VariablesChangeset &changeset, bool shouldApplyValueChanges);
+  /**
+   * @brief Apply the changes done on events-based object child to all its
+   * variants.
+   */
+  static void ApplyChangesToVariants(gd::EventsBasedObject &eventsBasedObject,
+                                     const gd::String &objectName,
+                                     const gd::VariablesChangeset &changeset);
 };
 
 } // namespace gd
\ No newline at end of file
diff --git a/Core/GDCore/IDE/ProjectBrowserHelper.cpp b/Core/GDCore/IDE/ProjectBrowserHelper.cpp
index 628289f05ddd..8a684686ea78 100644
--- a/Core/GDCore/IDE/ProjectBrowserHelper.cpp
+++ b/Core/GDCore/IDE/ProjectBrowserHelper.cpp
@@ -314,6 +314,12 @@ void ProjectBrowserHelper::ExposeProjectObjects(
          eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
       auto eventsBasedObject = eventsBasedObjectUniquePtr.get();
       worker.Launch(eventsBasedObject->GetObjects());
+
+      for (auto &&variantUniquePtr :
+           eventsBasedObject->GetVariants().GetInternalVector()) {
+        auto variant = variantUniquePtr.get();
+        worker.Launch(variant->GetObjects());
+      }
     }
   }
 };
diff --git a/Core/GDCore/IDE/WholeProjectRefactorer.cpp b/Core/GDCore/IDE/WholeProjectRefactorer.cpp
index a24d12629201..8ec2e67d931d 100644
--- a/Core/GDCore/IDE/WholeProjectRefactorer.cpp
+++ b/Core/GDCore/IDE/WholeProjectRefactorer.cpp
@@ -2129,6 +2129,26 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject(
       groups[g].RenameObject(oldName, newName);
     }
   }
+
+  for (auto &variant : eventsBasedObject.GetVariants().GetInternalVector()) {
+    auto &variantObjects = variant->GetObjects();
+    auto &variantObjectGroups = variantObjects.GetObjectGroups();
+    if (isObjectGroup) {
+      if (variantObjectGroups.Has(oldName)) {
+        variantObjectGroups.Get(oldName).SetName(newName);
+      }
+      // Object groups can't have instances or be in other groups
+    }
+    else {
+      if (variantObjects.HasObjectNamed(oldName)) {
+        variantObjects.GetObject(oldName).SetName(newName);
+      }
+      variant->GetInitialInstances().RenameInstancesOfObject(oldName, newName);
+      for (std::size_t g = 0; g < variantObjectGroups.size(); ++g) {
+        variantObjectGroups[g].RenameObject(oldName, newName);
+      }
+    }
+  }
 }
 
 void WholeProjectRefactorer::ObjectOrGroupRenamedInEventsFunction(
diff --git a/Core/GDCore/Project/CustomObjectConfiguration.cpp b/Core/GDCore/Project/CustomObjectConfiguration.cpp
index 5040f04672b1..ad29305938af 100644
--- a/Core/GDCore/Project/CustomObjectConfiguration.cpp
+++ b/Core/GDCore/Project/CustomObjectConfiguration.cpp
@@ -19,6 +19,7 @@ using namespace gd;
 
 void CustomObjectConfiguration::Init(const gd::CustomObjectConfiguration& objectConfiguration) {
   project = objectConfiguration.project;
+  variantName = objectConfiguration.variantName;
   objectContent = objectConfiguration.objectContent;
   animations = objectConfiguration.animations;
   isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
@@ -165,6 +166,7 @@ void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const
     animations.SerializeTo(animatableElement);
   }
 
+  element.SetAttribute("variant", variantName);
   if (IsOverridingEventsBasedObjectChildrenConfiguration()) {
     auto &childrenContentElement = element.AddChild("childrenContent");
     for (auto &pair : childObjectConfigurations) {
@@ -184,6 +186,7 @@ void CustomObjectConfiguration::DoUnserializeFrom(Project& project,
     animations.UnserializeFrom(animatableElement);
   }
 
+  variantName = element.GetStringAttribute("variant");
   isMarkedAsOverridingEventsBasedObjectChildrenConfiguration =
       element.HasChild("childrenContent");
   if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
@@ -247,9 +250,26 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
   }
   const auto &eventsBasedObject = project->GetEventsBasedObject(GetType());
 
-  for (auto& childObject : eventsBasedObject.GetObjects().GetObjects()) {
-    auto &configuration = GetChildObjectConfiguration(childObject->GetName());
-    configuration.ExposeResources(worker);
+  if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) {
+    for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) {
+      auto &configuration = GetChildObjectConfiguration(childObject->GetName());
+      configuration.ExposeResources(worker);
+    }
+  } else {
+    if (variantName.empty() ||
+        !eventsBasedObject.GetVariants().HasVariantNamed(variantName)) {
+      for (auto &childObject :
+           eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) {
+        childObject->GetConfiguration().ExposeResources(worker);
+      }
+    } else {
+      for (auto &childObject : eventsBasedObject.GetVariants()
+                                   .GetVariant(variantName)
+                                   .GetObjects()
+                                   .GetObjects()) {
+        childObject->GetConfiguration().ExposeResources(worker);
+      }
+    }
   }
 }
 
diff --git a/Core/GDCore/Project/CustomObjectConfiguration.h b/Core/GDCore/Project/CustomObjectConfiguration.h
index acc649557015..935e83e39ecd 100644
--- a/Core/GDCore/Project/CustomObjectConfiguration.h
+++ b/Core/GDCore/Project/CustomObjectConfiguration.h
@@ -29,9 +29,9 @@ namespace gd {
  * "resource".
  */
 class CustomObjectConfiguration : public gd::ObjectConfiguration {
- public:
-  CustomObjectConfiguration(const Project& project_, const String& type_)
-      : project(&project_), isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(false) {
+public:
+  CustomObjectConfiguration(const Project &project_, const String &type_)
+      : project(&project_) {
     SetType(type_);
   }
   std::unique_ptr<gd::ObjectConfiguration> Clone() const override;
@@ -66,6 +66,18 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
 
   void ExposeResources(gd::ArbitraryResourceWorker& worker) override;
 
+  /**
+   * \brief Get the name of the events-based object variant used by this custom object.
+   */
+  const gd::String &GetVariantName() const { return variantName; };
+
+  /**
+   * \brief Set the name of the events-based object variant used by this custom object.
+   */
+  void SetVariantName(const gd::String &variantName_) {
+    variantName = variantName_;
+  }
+
   bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const;
 
   bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const {
@@ -145,6 +157,7 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
   gd::SerializerElement objectContent;
   std::unordered_set<gd::String> unfoldedChildren;
 
+  gd::String variantName = "";
   bool isMarkedAsOverridingEventsBasedObjectChildrenConfiguration = false;
   mutable std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;
 
diff --git a/Core/GDCore/Project/EventsBasedObject.cpp b/Core/GDCore/Project/EventsBasedObject.cpp
index ff8f7bbfa80e..831e276926d9 100644
--- a/Core/GDCore/Project/EventsBasedObject.cpp
+++ b/Core/GDCore/Project/EventsBasedObject.cpp
@@ -17,19 +17,13 @@ EventsBasedObject::EventsBasedObject()
     isAnimatable(false),
     isTextContainer(false),
     isInnerAreaFollowingParentSize(false),
-    isUsingLegacyInstancesRenderer(false),
-    areaMinX(0),
-    areaMinY(0),
-    areaMinZ(0),
-    areaMaxX(64),
-    areaMaxY(64),
-    areaMaxZ(64),
-    objectsContainer(gd::ObjectsContainer::SourceType::Object) {
+    isUsingLegacyInstancesRenderer(false) {
 }
 
 EventsBasedObject::~EventsBasedObject() {}
 
-void EventsBasedObject::SerializeTo(SerializerElement& element) const {
+
+void EventsBasedObject::SerializeToExternal(SerializerElement& element) const {
   element.SetAttribute("defaultName", defaultName);
   if (isRenderedIn3D) {
     element.SetBoolAttribute("is3D", true);
@@ -44,20 +38,16 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const {
     element.SetBoolAttribute("isInnerAreaFollowingParentSize", true);
   }
   element.SetBoolAttribute("isUsingLegacyInstancesRenderer", isUsingLegacyInstancesRenderer);
-  element.SetIntAttribute("areaMinX", areaMinX);
-  element.SetIntAttribute("areaMinY", areaMinY);
-  element.SetIntAttribute("areaMinZ", areaMinZ);
-  element.SetIntAttribute("areaMaxX", areaMaxX);
-  element.SetIntAttribute("areaMaxY", areaMaxY);
-  element.SetIntAttribute("areaMaxZ", areaMaxZ);
 
+  // The EventsBasedObjectVariant SerializeTo method override the name.
+  // AbstractEventsBasedEntity::SerializeTo must be done after.
+  defaultVariant.SerializeTo(element);
   AbstractEventsBasedEntity::SerializeTo(element);
-  objectsContainer.SerializeObjectsTo(element.AddChild("objects"));
-  objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure"));
-  objectsContainer.GetObjectGroups().SerializeTo(element.AddChild("objectsGroups"));
+}
 
-  layers.SerializeLayersTo(element.AddChild("layers"));
-  initialInstances.SerializeTo(element.AddChild("instances"));
+void EventsBasedObject::SerializeTo(SerializerElement& element) const {
+  SerializeToExternal(element);
+  variants.SerializeVariantsTo(element.AddChild("variants"));
 }
 
 void EventsBasedObject::UnserializeFrom(gd::Project& project,
@@ -68,36 +58,22 @@ void EventsBasedObject::UnserializeFrom(gd::Project& project,
   isTextContainer = element.GetBoolAttribute("isTextContainer", false);
   isInnerAreaFollowingParentSize =
       element.GetBoolAttribute("isInnerAreaFollowingParentSize", false);
-  areaMinX = element.GetIntAttribute("areaMinX", 0);
-  areaMinY = element.GetIntAttribute("areaMinY", 0);
-  areaMinZ = element.GetIntAttribute("areaMinZ", 0);
-  areaMaxX = element.GetIntAttribute("areaMaxX", 64);
-  areaMaxY = element.GetIntAttribute("areaMaxY", 64);
-  areaMaxZ = element.GetIntAttribute("areaMaxZ", 64);
 
+  defaultVariant.UnserializeFrom(project, element);
+  defaultVariant.SetName("");
   AbstractEventsBasedEntity::UnserializeFrom(project, element);
-  objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects"));
-  if (element.HasChild("objectsFolderStructure")) {
-    objectsContainer.UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0));
-  }
-  objectsContainer.AddMissingObjectsInRootFolder();
-  objectsContainer.GetObjectGroups().UnserializeFrom(
-      element.GetChild("objectsGroups"));
 
-  if (element.HasChild("layers")) {
-    layers.UnserializeLayersFrom(element.GetChild("layers"));
-  } else {
-    layers.Reset();
+  if (element.HasChild("variants")) {
+    variants.UnserializeVariantsFrom(project, element.GetChild("variants"));
   }
 
-  initialInstances.UnserializeFrom(element.GetChild("instances"));
   if (element.HasChild("isUsingLegacyInstancesRenderer")) {
     isUsingLegacyInstancesRenderer =
         element.GetBoolAttribute("isUsingLegacyInstancesRenderer", false);
   }
   else {
     // Compatibility with GD <= 5.4.212
-    isUsingLegacyInstancesRenderer = initialInstances.GetInstancesCount() == 0;
+    isUsingLegacyInstancesRenderer = GetInitialInstances().GetInstancesCount() == 0;
     // end of compatibility code
   }
 }
diff --git a/Core/GDCore/Project/EventsBasedObject.h b/Core/GDCore/Project/EventsBasedObject.h
index b336889109e7..b8f214129990 100644
--- a/Core/GDCore/Project/EventsBasedObject.h
+++ b/Core/GDCore/Project/EventsBasedObject.h
@@ -7,6 +7,8 @@
 
 #include <vector>
 #include "GDCore/Project/AbstractEventsBasedEntity.h"
+#include "GDCore/Project/EventsBasedObjectVariant.h"
+#include "GDCore/Project/EventsBasedObjectVariantsContainer.h"
 #include "GDCore/Project/ObjectsContainer.h"
 #include "GDCore/Project/InitialInstancesContainer.h"
 #include "GDCore/Project/LayersContainer.h"
@@ -162,18 +164,38 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    */
   bool IsTextContainer() const { return isTextContainer; }
 
+  /**
+   * \brief Get the default variant of the custom object.
+   */
+  const gd::EventsBasedObjectVariant& GetDefaultVariant() const { return defaultVariant; }
+
+  /**
+   * \brief Get the default variant of the custom object.
+   */
+  gd::EventsBasedObjectVariant& GetDefaultVariant() { return defaultVariant; }
+
+  /**
+   * \brief Get the variants of the custom object.
+   */
+  const gd::EventsBasedObjectVariantsContainer& GetVariants() const { return variants; }
+
+  /**
+   * \brief Get the variants of the custom object.
+   */
+  gd::EventsBasedObjectVariantsContainer& GetVariants() { return variants; }
+
   /** \name Layers
    */
   ///@{
   /**
    * \brief Get the layers of the custom object.
    */
-  const gd::LayersContainer& GetLayers() const { return layers; }
+  const gd::LayersContainer& GetLayers() const { return defaultVariant.GetLayers(); }
 
   /**
    * \brief Get the layers of the custom object.
    */
-  gd::LayersContainer& GetLayers() { return layers; }
+  gd::LayersContainer& GetLayers() { return defaultVariant.GetLayers(); }
   ///@}
 
   /** \name Child objects
@@ -183,14 +205,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \brief Get the objects of the custom object.
    */
   gd::ObjectsContainer& GetObjects() {
-    return objectsContainer;
+    return defaultVariant.GetObjects();
   }
 
   /**
    * \brief Get the objects of the custom object.
    */
   const gd::ObjectsContainer& GetObjects() const {
-    return objectsContainer;
+    return defaultVariant.GetObjects();
   }
   ///@}
 
@@ -201,14 +223,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \brief Get the instances of the custom object.
    */
   gd::InitialInstancesContainer& GetInitialInstances() {
-    return initialInstances;
+    return defaultVariant.GetInitialInstances();
   }
 
   /**
    * \brief Get the instances of the custom object.
    */
   const gd::InitialInstancesContainer& GetInitialInstances() const {
-    return initialInstances;
+    return defaultVariant.GetInitialInstances();
   }
 
   /**
@@ -219,14 +241,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMinX() const {
-    return areaMinX;
+    return defaultVariant.GetAreaMinX();
   }
 
   /**
    * \brief Set the left bound of the custom object.
    */
-  void SetAreaMinX(int areaMinX_) {
-    areaMinX = areaMinX_;
+  void SetAreaMinX(int areaMinX) {
+    defaultVariant.SetAreaMinX(areaMinX);
   }
 
   /**
@@ -237,14 +259,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMinY() const {
-    return areaMinY;
+    return defaultVariant.GetAreaMinY();
   }
 
   /**
    * \brief Set the top bound of the custom object.
    */
-  void SetAreaMinY(int areaMinY_) {
-    areaMinY = areaMinY_;
+  void SetAreaMinY(int areaMinY) {
+    defaultVariant.SetAreaMinY(areaMinY);
   }
 
   /**
@@ -255,14 +277,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMinZ() const {
-    return areaMinZ;
+    return defaultVariant.GetAreaMinZ();
   }
 
   /**
    * \brief Set the min Z bound of the custom object.
    */
-  void SetAreaMinZ(int areaMinZ_) {
-    areaMinZ = areaMinZ_;
+  void SetAreaMinZ(int areaMinZ) {
+    defaultVariant.SetAreaMinZ(areaMinZ);
   }
 
   /**
@@ -273,14 +295,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMaxX() const {
-    return areaMaxX;
+    return defaultVariant.GetAreaMaxX();
   }
 
   /**
    * \brief Set the right bound of the custom object.
    */
-  void SetAreaMaxX(int areaMaxX_) {
-    areaMaxX = areaMaxX_;
+  void SetAreaMaxX(int areaMaxX) {
+    defaultVariant.SetAreaMaxX(areaMaxX);
   }
 
   /**
@@ -291,14 +313,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMaxY() const {
-    return areaMaxY;
+    return defaultVariant.GetAreaMaxY();
   }
 
   /**
    * \brief Set the bottom bound of the custom object.
    */
-  void SetAreaMaxY(int areaMaxY_) {
-    areaMaxY = areaMaxY_;
+  void SetAreaMaxY(int areaMaxY) {
+    defaultVariant.SetAreaMaxY(areaMaxY);
   }
 
   /**
@@ -309,16 +331,22 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
    * \see EventsBasedObject::GetInitialInstances
    */
   int GetAreaMaxZ() const {
-    return areaMaxZ;
+    return defaultVariant.GetAreaMaxZ();
   }
 
   /**
    * \brief Set the bottom bound of the custom object.
    */
-  void SetAreaMaxZ(int areaMaxZ_) {
-    areaMaxZ = areaMaxZ_;
+  void SetAreaMaxZ(int areaMaxZ) {
+    defaultVariant.SetAreaMaxZ(areaMaxZ);
   }
   ///@}
+  
+  /**
+   * @brief Serialize the events-based object for an extension in an external file.
+   * Variants are not serialized.
+   */
+  void SerializeToExternal(SerializerElement& element) const;
 
   void SerializeTo(SerializerElement& element) const override;
 
@@ -332,15 +360,8 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity {
   bool isTextContainer;
   bool isInnerAreaFollowingParentSize;
   bool isUsingLegacyInstancesRenderer;
-  gd::InitialInstancesContainer initialInstances;
-  gd::LayersContainer layers;
-  gd::ObjectsContainer objectsContainer;
-  double areaMinX;
-  double areaMinY;
-  double areaMinZ;
-  double areaMaxX;
-  double areaMaxY;
-  double areaMaxZ;
+  gd::EventsBasedObjectVariant defaultVariant;
+  gd::EventsBasedObjectVariantsContainer variants;
 };
 
 }  // namespace gd
diff --git a/Core/GDCore/Project/EventsBasedObjectVariant.cpp b/Core/GDCore/Project/EventsBasedObjectVariant.cpp
new file mode 100644
index 000000000000..3a735c7fd3d2
--- /dev/null
+++ b/Core/GDCore/Project/EventsBasedObjectVariant.cpp
@@ -0,0 +1,71 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+#include "EventsBasedObjectVariant.h"
+#include "GDCore/Project/Object.h"
+#include "GDCore/Serialization/SerializerElement.h"
+
+namespace gd {
+
+EventsBasedObjectVariant::EventsBasedObjectVariant()
+    : areaMinX(0), areaMinY(0), areaMinZ(0), areaMaxX(64), areaMaxY(64),
+      areaMaxZ(64), objectsContainer(gd::ObjectsContainer::SourceType::Object) {
+}
+
+EventsBasedObjectVariant::~EventsBasedObjectVariant() {}
+
+void EventsBasedObjectVariant::SerializeTo(SerializerElement &element) const {
+  element.SetAttribute("name", name);
+  if (!GetAssetStoreAssetId().empty() && !GetAssetStoreOriginalName().empty()) {
+    element.SetAttribute("assetStoreAssetId", GetAssetStoreAssetId());
+    element.SetAttribute("assetStoreOriginalName", GetAssetStoreOriginalName());
+  }
+  element.SetIntAttribute("areaMinX", areaMinX);
+  element.SetIntAttribute("areaMinY", areaMinY);
+  element.SetIntAttribute("areaMinZ", areaMinZ);
+  element.SetIntAttribute("areaMaxX", areaMaxX);
+  element.SetIntAttribute("areaMaxY", areaMaxY);
+  element.SetIntAttribute("areaMaxZ", areaMaxZ);
+
+  objectsContainer.SerializeObjectsTo(element.AddChild("objects"));
+  objectsContainer.SerializeFoldersTo(
+      element.AddChild("objectsFolderStructure"));
+  objectsContainer.GetObjectGroups().SerializeTo(
+      element.AddChild("objectsGroups"));
+
+  layers.SerializeLayersTo(element.AddChild("layers"));
+  initialInstances.SerializeTo(element.AddChild("instances"));
+}
+
+void EventsBasedObjectVariant::UnserializeFrom(
+    gd::Project &project, const SerializerElement &element) {
+  name = element.GetStringAttribute("name");
+  assetStoreAssetId = element.GetStringAttribute("assetStoreAssetId");
+  assetStoreOriginalName = element.GetStringAttribute("assetStoreOriginalName");
+  areaMinX = element.GetIntAttribute("areaMinX", 0);
+  areaMinY = element.GetIntAttribute("areaMinY", 0);
+  areaMinZ = element.GetIntAttribute("areaMinZ", 0);
+  areaMaxX = element.GetIntAttribute("areaMaxX", 64);
+  areaMaxY = element.GetIntAttribute("areaMaxY", 64);
+  areaMaxZ = element.GetIntAttribute("areaMaxZ", 64);
+
+  objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects"));
+  if (element.HasChild("objectsFolderStructure")) {
+    objectsContainer.UnserializeFoldersFrom(
+        project, element.GetChild("objectsFolderStructure", 0));
+  }
+  objectsContainer.AddMissingObjectsInRootFolder();
+  objectsContainer.GetObjectGroups().UnserializeFrom(
+      element.GetChild("objectsGroups"));
+
+  if (element.HasChild("layers")) {
+    layers.UnserializeLayersFrom(element.GetChild("layers"));
+  } else {
+    layers.Reset();
+  }
+  initialInstances.UnserializeFrom(element.GetChild("instances"));
+}
+
+} // namespace gd
diff --git a/Core/GDCore/Project/EventsBasedObjectVariant.h b/Core/GDCore/Project/EventsBasedObjectVariant.h
new file mode 100644
index 000000000000..5729bea1c2be
--- /dev/null
+++ b/Core/GDCore/Project/EventsBasedObjectVariant.h
@@ -0,0 +1,229 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2025 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+#pragma once
+
+#include "GDCore/Project/InitialInstancesContainer.h"
+#include "GDCore/Project/LayersContainer.h"
+#include "GDCore/Project/ObjectsContainer.h"
+#include "GDCore/String.h"
+#include <vector>
+
+namespace gd {
+class SerializerElement;
+class Project;
+} // namespace gd
+
+namespace gd {
+/**
+ * \brief Represents a variation of style of an events-based object.
+ *
+ * \ingroup PlatformDefinition
+ */
+class GD_CORE_API EventsBasedObjectVariant {
+public:
+  EventsBasedObjectVariant();
+  virtual ~EventsBasedObjectVariant();
+
+  /**
+   * \brief Return a pointer to a new EventsBasedObjectVariant constructed from
+   * this one.
+   */
+  EventsBasedObjectVariant *Clone() const {
+    return new EventsBasedObjectVariant(*this);
+  };
+
+  /**
+   * \brief Get the name of the variant.
+   */
+  const gd::String &GetName() const { return name; };
+
+  /**
+   * \brief Set the name of the variant.
+   */
+  EventsBasedObjectVariant &SetName(const gd::String &name_) {
+    name = name_;
+    return *this;
+  }
+
+  /** \name Layers
+   */
+  ///@{
+  /**
+   * \brief Get the layers of the variant.
+   */
+  const gd::LayersContainer &GetLayers() const { return layers; }
+
+  /**
+   * \brief Get the layers of the variant.
+   */
+  gd::LayersContainer &GetLayers() { return layers; }
+  ///@}
+
+  /** \name Child objects
+   */
+  ///@{
+  /**
+   * \brief Get the objects of the variant.
+   */
+  gd::ObjectsContainer &GetObjects() { return objectsContainer; }
+
+  /**
+   * \brief Get the objects of the variant.
+   */
+  const gd::ObjectsContainer &GetObjects() const { return objectsContainer; }
+  ///@}
+
+  /** \name Instances
+   */
+  ///@{
+  /**
+   * \brief Get the instances of the variant.
+   */
+  gd::InitialInstancesContainer &GetInitialInstances() {
+    return initialInstances;
+  }
+
+  /**
+   * \brief Get the instances of the variant.
+   */
+  const gd::InitialInstancesContainer &GetInitialInstances() const {
+    return initialInstances;
+  }
+
+  /**
+   * \brief Get the left bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMinX() const { return areaMinX; }
+
+  /**
+   * \brief Set the left bound of the variant.
+   */
+  void SetAreaMinX(int areaMinX_) { areaMinX = areaMinX_; }
+
+  /**
+   * \brief Get the top bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMinY() const { return areaMinY; }
+
+  /**
+   * \brief Set the top bound of the variant.
+   */
+  void SetAreaMinY(int areaMinY_) { areaMinY = areaMinY_; }
+
+  /**
+   * \brief Get the min Z bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMinZ() const { return areaMinZ; }
+
+  /**
+   * \brief Set the min Z bound of the variant.
+   */
+  void SetAreaMinZ(int areaMinZ_) { areaMinZ = areaMinZ_; }
+
+  /**
+   * \brief Get the right bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMaxX() const { return areaMaxX; }
+
+  /**
+   * \brief Set the right bound of the variant.
+   */
+  void SetAreaMaxX(int areaMaxX_) { areaMaxX = areaMaxX_; }
+
+  /**
+   * \brief Get the bottom bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMaxY() const { return areaMaxY; }
+
+  /**
+   * \brief Set the bottom bound of the variant.
+   */
+  void SetAreaMaxY(int areaMaxY_) { areaMaxY = areaMaxY_; }
+
+  /**
+   * \brief Get the max Z bound of the variant.
+   *
+   * This is used only if there is any initial instances.
+   *
+   * \see EventsBasedObjectVariant::GetInitialInstances
+   */
+  int GetAreaMaxZ() const { return areaMaxZ; }
+
+  /**
+   * \brief Set the bottom bound of the variant.
+   */
+  void SetAreaMaxZ(int areaMaxZ_) { areaMaxZ = areaMaxZ_; }
+  ///@}
+
+  /** \brief Change the object asset store id of this variant.
+   */
+  void SetAssetStoreAssetId(const gd::String &assetStoreId_) {
+    assetStoreAssetId = assetStoreId_;
+  };
+
+  /** \brief Return the object asset store id of this variant.
+   */
+  const gd::String &GetAssetStoreAssetId() const { return assetStoreAssetId; };
+
+  /** \brief Change the original name of the variant in the asset.
+   */
+  void SetAssetStoreOriginalName(const gd::String &assetStoreOriginalName_) {
+    assetStoreOriginalName = assetStoreOriginalName_;
+  };
+
+  /** \brief Return the original name of the variant in the asset.
+   */
+  const gd::String &GetAssetStoreOriginalName() const {
+    return assetStoreOriginalName;
+  };
+
+  void SerializeTo(SerializerElement &element) const;
+
+  void UnserializeFrom(gd::Project &project, const SerializerElement &element);
+
+private:
+  gd::String name;
+  gd::InitialInstancesContainer initialInstances;
+  gd::LayersContainer layers;
+  gd::ObjectsContainer objectsContainer;
+  double areaMinX;
+  double areaMinY;
+  double areaMinZ;
+  double areaMaxX;
+  double areaMaxY;
+  double areaMaxZ;
+  /**
+   * The ID of the asset if the object comes from the store.
+   */
+  gd::String assetStoreAssetId;
+  /**
+   * The original name of the variant in the asset if the object comes from the
+   * store.
+   */
+  gd::String assetStoreOriginalName;
+};
+
+} // namespace gd
diff --git a/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h b/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h
new file mode 100644
index 000000000000..44f35effd328
--- /dev/null
+++ b/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h
@@ -0,0 +1,160 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-present Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+#pragma once
+
+#include <vector>
+#include "GDCore/Project/EventsBasedObjectVariant.h"
+#include "GDCore/String.h"
+#include "GDCore/Tools/SerializableWithNameList.h"
+
+namespace gd {
+class SerializerElement;
+}
+
+namespace gd {
+
+/**
+ * \brief Used as a base class for classes that will own events-backed
+ * variants.
+ *
+ * \see gd::EventsBasedObjectVariantContainer
+ * \ingroup PlatformDefinition
+ */
+class GD_CORE_API EventsBasedObjectVariantsContainer
+    : private SerializableWithNameList<gd::EventsBasedObjectVariant> {
+public:
+  EventsBasedObjectVariantsContainer() {}
+
+  EventsBasedObjectVariantsContainer(const EventsBasedObjectVariantsContainer &other) {
+    Init(other);
+  }
+
+  EventsBasedObjectVariantsContainer &operator=(const EventsBasedObjectVariantsContainer &other) {
+    if (this != &other) {
+      Init(other);
+    }
+    return *this;
+  }
+
+  /** \name Events Functions management
+   */
+  ///@{
+  /**
+   * \brief Check if the variant with the specified name exists.
+   */
+  bool HasVariantNamed(const gd::String& name) const {
+    return Has(name);
+  }
+
+  /**
+   * \brief Get the variant with the specified name.
+   *
+   * \warning Trying to access to a not existing variant will result in
+   * undefined behavior.
+   */
+  gd::EventsBasedObjectVariant& GetVariant(const gd::String& name) {
+    return Get(name);
+  }
+
+  /**
+   * \brief Get the variant with the specified name.
+   *
+   * \warning Trying to access to a not existing variant will result in
+   * undefined behavior.
+   */
+  const gd::EventsBasedObjectVariant& GetVariant(const gd::String& name) const {
+    return Get(name);
+  }
+
+  /**
+   * \brief Get the variant at the specified index in the list.
+   *
+   * \warning Trying to access to a not existing variant will result in
+   * undefined behavior.
+   */
+  gd::EventsBasedObjectVariant& GetVariant(std::size_t index) {
+    return Get(index);
+  }
+
+  /**
+   * \brief Get the variant at the specified index in the list.
+   *
+   * \warning Trying to access to a not existing variant will result in
+   * undefined behavior.
+   */
+  const gd::EventsBasedObjectVariant& GetVariant(std::size_t index) const {
+    return Get(index);
+  }
+
+  /**
+   * \brief Return the number of variants.
+   */
+  std::size_t GetVariantsCount() const { return GetCount(); }
+
+  gd::EventsBasedObjectVariant& InsertNewVariant(const gd::String& name,
+                                              std::size_t position) {
+    return InsertNew(name, position);
+  }
+  gd::EventsBasedObjectVariant& InsertVariant(const gd::EventsBasedObjectVariant& object,
+                                           std::size_t position) {
+    return Insert(object, position);
+  }
+  void RemoveVariant(const gd::String& name) { return Remove(name); }
+  void ClearVariants() { return Clear(); }
+  void MoveVariant(std::size_t oldIndex, std::size_t newIndex) {
+    return Move(oldIndex, newIndex);
+  };
+  std::size_t GetVariantPosition(const gd::EventsBasedObjectVariant& eventsFunction) {
+    return GetPosition(eventsFunction);
+  };
+
+  /**
+   * \brief Provide a raw access to the vector containing the variants.
+   */
+  const std::vector<std::unique_ptr<gd::EventsBasedObjectVariant>>& GetInternalVector()
+      const {
+    return elements;
+  };
+
+  /**
+   * \brief Provide a raw access to the vector containing the variants.
+   */
+  std::vector<std::unique_ptr<gd::EventsBasedObjectVariant>>& GetInternalVector() {
+    return elements;
+  };
+  ///@}
+
+  /** \name Serialization
+   */
+  ///@{
+  /**
+   * \brief Serialize events variants.
+   */
+  void SerializeVariantsTo(SerializerElement& element) const {
+    return SerializeElementsTo("variant", element);
+  };
+
+  /**
+   * \brief Unserialize the events variants.
+   */
+  void UnserializeVariantsFrom(gd::Project& project,
+                                      const SerializerElement& element) {
+    return UnserializeElementsFrom("variant", project, element);
+  };
+  ///@}
+ protected:
+  /**
+   * Initialize object using another object. Used by copy-ctor and assign-op.
+   * Don't forget to update me if members were changed!
+   */
+  void Init(const gd::EventsBasedObjectVariantsContainer& other) {
+    return SerializableWithNameList<gd::EventsBasedObjectVariant>::Init(other);
+  };
+
+private:
+};
+
+}  // namespace gd
diff --git a/Core/GDCore/Project/EventsFunctionsExtension.cpp b/Core/GDCore/Project/EventsFunctionsExtension.cpp
index 445ada4c69f3..13c71ebf4c3b 100644
--- a/Core/GDCore/Project/EventsFunctionsExtension.cpp
+++ b/Core/GDCore/Project/EventsFunctionsExtension.cpp
@@ -55,7 +55,7 @@ void EventsFunctionsExtension::Init(const gd::EventsFunctionsExtension& other) {
   sceneVariables = other.GetSceneVariables();
 }
 
-void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const {
+void EventsFunctionsExtension::SerializeTo(SerializerElement& element, bool isExternal) const {
   element.SetAttribute("version", version);
   element.SetAttribute("extensionNamespace", extensionNamespace);
   element.SetAttribute("shortDescription", shortDescription);
@@ -102,8 +102,18 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const {
       element.AddChild("eventsFunctions"));
   eventsBasedBehaviors.SerializeElementsTo(
       "eventsBasedBehavior", element.AddChild("eventsBasedBehaviors"));
-  eventsBasedObjects.SerializeElementsTo(
-      "eventsBasedObject", element.AddChild("eventsBasedObjects"));
+  if (isExternal) {
+    auto &eventsBasedObjectElement = element.AddChild("eventsBasedObjects");
+    eventsBasedObjectElement.ConsiderAsArrayOf("eventsBasedObject");
+    for (const auto &eventsBasedObject :
+         eventsBasedObjects.GetInternalVector()) {
+      eventsBasedObject->SerializeToExternal(
+          eventsBasedObjectElement.AddChild("eventsBasedObject"));
+    }
+  } else {
+    eventsBasedObjects.SerializeElementsTo(
+        "eventsBasedObject", element.AddChild("eventsBasedObjects"));
+  }
 }
 
 void EventsFunctionsExtension::UnserializeFrom(
diff --git a/Core/GDCore/Project/EventsFunctionsExtension.h b/Core/GDCore/Project/EventsFunctionsExtension.h
index 90e5fdd4b690..bf714c3fb3ae 100644
--- a/Core/GDCore/Project/EventsFunctionsExtension.h
+++ b/Core/GDCore/Project/EventsFunctionsExtension.h
@@ -286,7 +286,14 @@ class GD_CORE_API EventsFunctionsExtension {
   /**
    * \brief Serialize the EventsFunctionsExtension to the specified element
    */
-  void SerializeTo(gd::SerializerElement& element) const;
+  void SerializeTo(gd::SerializerElement& element, bool isExternal = false) const;
+
+  /**
+   * \brief Serialize the EventsFunctionsExtension to the specified element
+   */
+  void SerializeToExternal(gd::SerializerElement& element) const {
+    SerializeTo(element, true);
+  }
 
   /**
    * \brief Load the EventsFunctionsExtension from the specified element.
diff --git a/Core/GDCore/Project/InitialInstancesContainer.cpp b/Core/GDCore/Project/InitialInstancesContainer.cpp
index a6455d0258c7..e7cc5b0edcce 100644
--- a/Core/GDCore/Project/InitialInstancesContainer.cpp
+++ b/Core/GDCore/Project/InitialInstancesContainer.cpp
@@ -42,8 +42,13 @@ void InitialInstancesContainer::IterateOverInstances(
 }
 
 void InitialInstancesContainer::IterateOverInstances(
-  const std::function< void(gd::InitialInstance &) >& func) {
-  for (auto& instance : initialInstances) func(instance);
+  const std::function< bool(gd::InitialInstance &) >& func) {
+  for (auto& instance : initialInstances) {
+    bool shouldStop = func(instance);
+    if (shouldStop) {
+      return;
+    }
+  }
 }
 
 void InitialInstancesContainer::IterateOverInstancesWithZOrdering(
diff --git a/Core/GDCore/Project/InitialInstancesContainer.h b/Core/GDCore/Project/InitialInstancesContainer.h
index 173db82af392..2bb3c3a4c693 100644
--- a/Core/GDCore/Project/InitialInstancesContainer.h
+++ b/Core/GDCore/Project/InitialInstancesContainer.h
@@ -92,7 +92,7 @@ class GD_CORE_API InitialInstancesContainer {
    * \see InitialInstanceFunctor
    */
   void IterateOverInstances(
-    const std::function< void(gd::InitialInstance &) >& func);
+    const std::function< bool(gd::InitialInstance &) >& func);
 
   /**
    * Get the instances on the specified layer,
diff --git a/Core/GDCore/Project/Object.cpp b/Core/GDCore/Project/Object.cpp
index abf226c987c8..04c678ad5e0a 100644
--- a/Core/GDCore/Project/Object.cpp
+++ b/Core/GDCore/Project/Object.cpp
@@ -41,6 +41,11 @@ Object::Object(const gd::String& name_,
 }
 
 void Object::Init(const gd::Object& object) {
+  CopyWithoutConfiguration(object);
+  configuration = object.configuration->Clone();
+}
+
+void Object::CopyWithoutConfiguration(const gd::Object& object) {
   persistentUuid = object.persistentUuid;
   name = object.name;
   assetStoreId = object.assetStoreId;
@@ -51,8 +56,6 @@ void Object::Init(const gd::Object& object) {
   for (auto& it : object.behaviors) {
     behaviors[it.first] = gd::make_unique<gd::Behavior>(*it.second);
   }
-
-  configuration = object.configuration->Clone();
 }
 
 gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; }
diff --git a/Core/GDCore/Project/Object.h b/Core/GDCore/Project/Object.h
index bfe4a5540a80..a9eaeb354205 100644
--- a/Core/GDCore/Project/Object.h
+++ b/Core/GDCore/Project/Object.h
@@ -82,6 +82,8 @@ class GD_CORE_API Object {
     return gd::make_unique<gd::Object>(*this);
   }
 
+  void CopyWithoutConfiguration(const gd::Object& object);
+
   /**
    * \brief Return the object configuration.
    */
diff --git a/Core/GDCore/Project/Project.cpp b/Core/GDCore/Project/Project.cpp
index 7db30a67b7a5..d9e98563d549 100644
--- a/Core/GDCore/Project/Project.cpp
+++ b/Core/GDCore/Project/Project.cpp
@@ -920,6 +920,7 @@ void Project::UnserializeAndInsertExtensionsFrom(
       "eventsFunctionsExtension");
 
   std::map<gd::String, size_t> extensionNameToElementIndex;
+  std::map<gd::String, gd::SerializerElement> objectTypeToVariantsElement;
 
   // First, only unserialize behaviors and objects names.
   // As event based objects can contains custom behaviors and custom objects,
@@ -938,6 +939,16 @@ void Project::UnserializeAndInsertExtensionsFrom(
             ? GetEventsFunctionsExtension(name)
             : InsertNewEventsFunctionsExtension(
                   name, GetEventsFunctionsExtensionsCount());
+
+    // Backup the events-based object variants
+    for (auto &eventsBasedObject :
+         eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) {
+      gd::SerializerElement variantsElement;
+      eventsBasedObject->GetVariants().SerializeVariantsTo(variantsElement);
+      objectTypeToVariantsElement[gd::PlatformExtension::GetObjectFullType(
+          name, eventsBasedObject->GetName())] = variantsElement;
+    }
+
     eventsFunctionsExtension.UnserializeExtensionDeclarationFrom(
         *this, eventsFunctionsExtensionElement);
   }
@@ -966,6 +977,15 @@ void Project::UnserializeAndInsertExtensionsFrom(
     partiallyLoadedExtension
         ->UnserializeExtensionImplementationFrom(
             *this, eventsFunctionsExtensionElement);
+
+    for (auto &pair : objectTypeToVariantsElement) {
+      auto &objectType = pair.first;
+      auto &variantsElement = pair.second;
+
+      auto &eventsBasedObject = GetEventsBasedObject(objectType);
+      eventsBasedObject.GetVariants().UnserializeVariantsFrom(*this,
+                                                              variantsElement);
+    }
   }
 }
 
diff --git a/Core/tests/EventsBasedObjectVariantHelper.cpp b/Core/tests/EventsBasedObjectVariantHelper.cpp
new file mode 100644
index 000000000000..163ba281e6a5
--- /dev/null
+++ b/Core/tests/EventsBasedObjectVariantHelper.cpp
@@ -0,0 +1,522 @@
+/*
+ * GDevelop Core
+ * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
+ * reserved. This project is released under the MIT License.
+ */
+/**
+ * @file Tests covering events of GDevelop Core.
+ */
+#include "catch.hpp"
+
+#include <algorithm>
+#include <initializer_list>
+#include <map>
+
+#include "GDCore/CommonTools.h"
+#include "GDCore/IDE/EventsBasedObjectVariantHelper.h"
+
+#include "DummyPlatform.h"
+#include "GDCore/Extensions/Platform.h"
+#include "GDCore/Extensions/PlatformExtension.h"
+#include "GDCore/Project/Behavior.h"
+#include "GDCore/Project/EventsFunctionsExtension.h"
+#include "GDCore/Project/Object.h"
+#include "GDCore/Project/Project.h"
+#include "GDCore/Project/ProjectScopedContainers.h"
+#include "GDCore/Project/Variable.h"
+#include "catch.hpp"
+
+gd::InitialInstance *
+GetFirstInstanceOf(const gd::String objectName,
+                   gd::InitialInstancesContainer &initialInstances) {
+  gd::InitialInstance *variantInstance = nullptr;
+  initialInstances.IterateOverInstances(
+      [&variantInstance, &objectName](gd::InitialInstance &instance) {
+        if (instance.GetObjectName() == objectName) {
+          variantInstance = &instance;
+          return true;
+        }
+        return false;
+      });
+  return variantInstance;
+}
+
+gd::EventsBasedObject &SetupEventsBasedObject(gd::Project &project) {
+  auto &eventsExtension =
+      project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
+  auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
+      "MyEventsBasedObject", 0);
+  auto &object = eventsBasedObject.GetObjects().InsertNewObject(
+      project, "MyExtension::Sprite", "MyChildObject", 0);
+  object.GetVariables().InsertNew("MyVariable").SetValue(123);
+  object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
+  auto &instance =
+      eventsBasedObject.GetInitialInstances().InsertNewInitialInstance();
+  instance.SetObjectName("MyChildObject");
+  instance.GetVariables().InsertNew("MyVariable").SetValue(111);
+  auto &objectGroup =
+      eventsBasedObject.GetObjects().GetObjectGroups().InsertNew(
+          "MyObjectGroup");
+  objectGroup.AddObject("MyChildObject");
+  return eventsBasedObject;
+}
+
+TEST_CASE("EventsBasedObjectVariantHelper", "[common]") {
+  SECTION("Can add missing objects") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject2", 0);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject3", 0);
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject2"));
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject3"));
+  }
+
+  SECTION("Can remove objects") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject2", 0);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject3", 0);
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+    variant.GetInitialInstances().InsertNewInitialInstance().SetObjectName(
+        "MyChildObject2");
+    REQUIRE(variant.GetInitialInstances().HasInstancesOfObject(
+                "MyChildObject2") == true);
+
+    // Do the changes and launch the refactoring.
+    eventsBasedObject.GetObjects().RemoveObject("MyChildObject2");
+    eventsBasedObject.GetObjects().RemoveObject("MyChildObject3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject2") == false);
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject3") == false);
+    REQUIRE(variant.GetInitialInstances().HasInstancesOfObject(
+                "MyChildObject2") == false);
+  }
+
+  SECTION("Can change object type") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    eventsBasedObject.GetObjects().RemoveObject("MyChildObject");
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::FakeObjectWithDefaultBehavior", "MyChildObject",
+        0);
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    REQUIRE(variant.GetObjects().GetObject("MyChildObject").GetType() ==
+            "MyExtension::FakeObjectWithDefaultBehavior");
+    REQUIRE(variant.GetInitialInstances().GetInstancesCount() == 1);
+  }
+
+  SECTION("Can add missing object groups") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup2",
+                                                               0);
+    eventsBasedObject.GetObjects()
+        .GetObjectGroups()
+        .InsertNew("MyObjectGroup3", 0)
+        .AddObject("MyChildObject");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    auto &variantObjectGroups = variant.GetObjects().GetObjectGroups();
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup"));
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup2"));
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup3"));
+    REQUIRE(
+        variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() ==
+        1);
+    REQUIRE(
+        variantObjectGroups.Get("MyObjectGroup2").GetAllObjectsNames().size() ==
+        0);
+    REQUIRE(
+        variantObjectGroups.Get("MyObjectGroup3").GetAllObjectsNames().size() ==
+        1);
+  }
+
+  SECTION("Can remove object groups") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+
+    // Do the changes and launch the refactoring.
+    eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup2",
+                                                               0);
+    eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup3",
+                                                               0);
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    eventsBasedObject.GetObjects().GetObjectGroups().Remove("MyObjectGroup2");
+    eventsBasedObject.GetObjects().GetObjectGroups().Remove("MyObjectGroup3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    auto &variantObjectGroups = variant.GetObjects().GetObjectGroups();
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup"));
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup2") == false);
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup3") == false);
+  }
+
+  SECTION("Can add objects to groups") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject2", 0);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject3", 0);
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    auto &objectGroup =
+        eventsBasedObject.GetObjects().GetObjectGroups().Get("MyObjectGroup");
+    objectGroup.AddObject("MyChildObject2");
+    objectGroup.AddObject("MyChildObject3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    auto &variantObjectGroups = variant.GetObjects().GetObjectGroups();
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup"));
+    REQUIRE(
+        variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() ==
+        3);
+  }
+
+  SECTION("Can remove objects from groups") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject2", 0);
+    eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject3", 0);
+    auto &objectGroup =
+        eventsBasedObject.GetObjects().GetObjectGroups().Get("MyObjectGroup");
+    objectGroup.AddObject("MyChildObject2");
+    objectGroup.AddObject("MyChildObject3");
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    objectGroup.RemoveObject("MyChildObject2");
+    objectGroup.RemoveObject("MyChildObject3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    auto &variantObjectGroups = variant.GetObjects().GetObjectGroups();
+    REQUIRE(variantObjectGroups.Has("MyObjectGroup"));
+    REQUIRE(
+        variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() ==
+        1);
+  }
+
+  SECTION("Can add missing behaviors") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior2");
+    object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior"));
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior2"));
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior3"));
+  }
+
+  SECTION("Can remove missing behaviors") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior2");
+    object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior3");
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    object.RemoveBehavior("MyBehavior2");
+    object.RemoveBehavior("MyBehavior3");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior"));
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior2") == false);
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior3") == false);
+  }
+
+  SECTION("Can change behavior type") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.RemoveBehavior("MyBehavior");
+    object.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
+                          "MyBehavior");
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.HasBehaviorNamed("MyBehavior"));
+    REQUIRE(variantObject.GetBehavior("MyBehavior").GetTypeName() ==
+            "MyExtension::MyOtherBehavior");
+  }
+
+  SECTION("Can add missing variables") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456);
+    object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789);
+
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123);
+    REQUIRE(variantObject.GetVariables().Get("MyVariable2").GetValue() == 456);
+    REQUIRE(variantObject.GetVariables().Get("MyVariable3").GetValue() == 789);
+    {
+      auto *objectInstance =
+          GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable"));
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable2") == false);
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable3") == false);
+    }
+  }
+
+  SECTION("Can keep variable value") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+
+    // Do the changes and launch the refactoring.
+    object.GetVariables().Get("MyVariable").SetValue(456);
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123);
+    {
+      auto *objectInstance =
+          GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      REQUIRE(objectInstance->GetVariables().Get("MyVariable").GetValue() ==
+              111);
+    }
+  }
+
+  SECTION("Must not propagate instance variable value changes") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    {
+      auto *objectInstance = GetFirstInstanceOf(
+          "MyChildObject", eventsBasedObject.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      objectInstance->GetVariables().Get("MyVariable").SetValue(222);
+    }
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    {
+      auto *objectInstance =
+          GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      REQUIRE(objectInstance->GetVariables().Get("MyVariable").GetValue() ==
+              111);
+    }
+  }
+
+  SECTION("Can move variables") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456);
+    object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789);
+    {
+      auto *objectInstance = GetFirstInstanceOf(
+          "MyChildObject", eventsBasedObject.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      objectInstance->GetVariables().Get("MyVariable2").SetValue(222);
+      objectInstance->GetVariables().Get("MyVariable3").SetValue(333);
+    }
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+
+    // Do the changes and launch the refactoring.
+    object.GetVariables().Move(2, 0);
+    object.GetVariables().Get("MyVariable").SetValue(111);
+    object.GetVariables().Get("MyVariable2").SetValue(222);
+    object.GetVariables().Get("MyVariable3").SetValue(333);
+    REQUIRE(object.GetVariables().GetNameAt(0) == "MyVariable3");
+    REQUIRE(object.GetVariables().GetNameAt(1) == "MyVariable");
+    REQUIRE(object.GetVariables().GetNameAt(2) == "MyVariable2");
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123);
+    REQUIRE(variantObject.GetVariables().Get("MyVariable2").GetValue() == 456);
+    REQUIRE(variantObject.GetVariables().Get("MyVariable3").GetValue() == 789);
+    REQUIRE(variantObject.GetVariables().GetNameAt(0) == "MyVariable3");
+    REQUIRE(variantObject.GetVariables().GetNameAt(1) == "MyVariable");
+    REQUIRE(variantObject.GetVariables().GetNameAt(2) == "MyVariable2");
+  }
+
+  SECTION("Can remove variables") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+    object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456);
+    object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789);
+    {
+      auto *objectInstance = GetFirstInstanceOf(
+          "MyChildObject", eventsBasedObject.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      objectInstance->GetVariables().Get("MyVariable2").SetValue(222);
+      objectInstance->GetVariables().Get("MyVariable3").SetValue(333);
+    }
+
+    // Do the changes and launch the refactoring.
+    object.GetVariables().Remove("MyVariable2");
+    object.GetVariables().Remove("MyVariable3");
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.GetVariables().Has("MyVariable"));
+    REQUIRE(variantObject.GetVariables().Has("MyVariable2") == false);
+    REQUIRE(variantObject.GetVariables().Has("MyVariable3") == false);
+    {
+      auto *objectInstance =
+          GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances());
+      REQUIRE(objectInstance != nullptr);
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable"));
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable2") == false);
+      REQUIRE(objectInstance->GetVariables().Has("MyVariable3") == false);
+    }
+  }
+
+  SECTION("Can change variable type") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+    auto &eventsBasedObject = SetupEventsBasedObject(project);
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+    auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject");
+
+    // Do the changes and launch the refactoring.
+    object.GetVariables().Get("MyVariable").SetString("abc");
+    gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject(
+        project, eventsBasedObject);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    auto &variantObject = variant.GetObjects().GetObject("MyChildObject");
+    REQUIRE(variantObject.GetVariables().Get("MyVariable").GetString() ==
+            "abc");
+    REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("MyVariable") ==
+            false);
+  }
+}
diff --git a/Core/tests/ObjectAssetSerializer.cpp b/Core/tests/ObjectAssetSerializer.cpp
index 15b51993f1e2..7f6e1932b2d1 100644
--- a/Core/tests/ObjectAssetSerializer.cpp
+++ b/Core/tests/ObjectAssetSerializer.cpp
@@ -59,6 +59,8 @@ TEST_CASE("ObjectAssetSerializer", "[common]") {
     auto &configuration = object.GetConfiguration();
     auto *customObjectConfiguration =
         dynamic_cast<gd::CustomObjectConfiguration *>(&configuration);
+    customObjectConfiguration
+        ->SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(true);
     auto *spriteConfiguration = dynamic_cast<gd::SpriteObject *>(
         &customObjectConfiguration->GetChildObjectConfiguration("MyChild"));
     REQUIRE(spriteConfiguration != nullptr);
diff --git a/Core/tests/ObjectsContainersList.cpp b/Core/tests/ObjectsContainersList.cpp
index f4692f82984a..4dab4c7bc98f 100644
--- a/Core/tests/ObjectsContainersList.cpp
+++ b/Core/tests/ObjectsContainersList.cpp
@@ -150,7 +150,7 @@ TEST_CASE("ObjectContainersList (GetTypeOfObject)", "[common]") {
     gd::Object &object1 = layout.GetObjects().InsertNewObject(
         project, "MyExtension::Sprite", "MyObject1", 0);
     gd::Object &object2 = layout.GetObjects().InsertNewObject(
-        project, "FakeObjectWithDefaultBehavior", "MyObject2", 0);
+        project, "MyExtension::FakeObjectWithDefaultBehavior", "MyObject2", 0);
 
     auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0);
     group.AddObject(object1.GetName());
diff --git a/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp b/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp
index 8acf965c2618..50aabdf0d556 100644
--- a/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp
+++ b/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp
@@ -1072,6 +1072,99 @@ TEST_CASE("WholeProjectRefactorer::ApplyRefactoringForVariablesContainer",
     REQUIRE(instance.GetVariables().Get("MyRenamedVariable").GetValue() == 456);
   }
 
+  SECTION("Can rename an object variable (in events-based object)") {
+    gd::Project project;
+    gd::Platform platform;
+    SetupProjectWithDummyPlatform(project, platform);
+
+    auto &eventsExtension =
+        project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0);
+    auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew(
+        "MyEventsBasedObject", 0);
+    auto &object = eventsBasedObject.GetObjects().InsertNewObject(
+        project, "MyExtension::Sprite", "MyChildObject", 0);
+    object.GetVariables().InsertNew("MyVariable").SetValue(123);
+    auto &instance =
+        eventsBasedObject.GetInitialInstances().InsertNewInitialInstance();
+    instance.SetObjectName("MyChildObject");
+    instance.GetVariables().InsertNew("MyVariable").SetValue(456);
+
+    auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+        eventsBasedObject.GetDefaultVariant(), 0);
+    gd::InitialInstance *variantInstance = nullptr;
+    variant.GetInitialInstances().IterateOverInstances(
+        [&variantInstance](gd::InitialInstance &instance) {
+          variantInstance = &instance;
+          return true;
+        });
+    REQUIRE(variantInstance != nullptr);
+    variant.GetObjects()
+        .GetObject("MyChildObject")
+        .GetVariables()
+        .Get("MyVariable")
+        .SetValue(111);
+    variantInstance->GetVariables().Get("MyVariable").SetValue(222);
+
+    auto &objectFunction =
+        eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction(
+            "MyObjectEventsFunction", 0);
+    gd::StandardEvent &event = dynamic_cast<gd::StandardEvent &>(
+        objectFunction.GetEvents().InsertNewEvent(
+            project, "BuiltinCommonInstructions::Standard"));
+
+    {
+      gd::Instruction action;
+      action.SetType("SetNumberObjectVariable");
+      action.SetParametersCount(4);
+      action.SetParameter(0, gd::Expression("MyChildObject"));
+      action.SetParameter(1, gd::Expression("MyVariable"));
+      action.SetParameter(2, gd::Expression("="));
+      action.SetParameter(3, gd::Expression("MyChildObject.MyVariable"));
+      event.GetActions().Insert(action);
+    }
+
+    // Do the changes and launch the refactoring.
+    object.GetVariables().ResetPersistentUuid();
+    gd::SerializerElement originalSerializedVariables;
+    object.GetVariables().SerializeTo(originalSerializedVariables);
+
+    object.GetVariables().Rename("MyVariable", "MyRenamedVariable");
+    auto changeset =
+        gd::WholeProjectRefactorer::ComputeChangesetForVariablesContainer(
+            originalSerializedVariables, object.GetVariables());
+
+    REQUIRE(changeset.oldToNewVariableNames.size() == 1);
+
+    gd::WholeProjectRefactorer::ApplyRefactoringForObjectVariablesContainer(
+        project, object.GetVariables(), eventsBasedObject.GetInitialInstances(),
+        object.GetName(), changeset, originalSerializedVariables);
+    gd::ObjectVariableHelper::ApplyChangesToVariants(
+        eventsBasedObject, "MyChildObject", changeset);
+
+    REQUIRE(event.GetActions()[0].GetParameter(1).GetPlainString() ==
+            "MyRenamedVariable");
+    REQUIRE(event.GetActions()[0].GetParameter(3).GetPlainString() ==
+            "MyChildObject.MyRenamedVariable");
+
+    REQUIRE(eventsBasedObject.GetObjects().HasObjectNamed("MyChildObject"));
+    REQUIRE(eventsBasedObject.GetObjects()
+                .GetObject("MyChildObject")
+                .GetVariables()
+                .Get("MyRenamedVariable")
+                .GetValue() == 123);
+    REQUIRE(instance.GetVariables().Get("MyRenamedVariable").GetValue() == 456);
+
+    REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject"));
+    REQUIRE(variant.GetObjects()
+                .GetObject("MyChildObject")
+                .GetVariables()
+                .Get("MyRenamedVariable")
+                .GetValue() == 111);
+    REQUIRE(
+        variantInstance->GetVariables().Get("MyRenamedVariable").GetValue() ==
+        222);
+  }
+
   SECTION("Can delete an object variable") {
     gd::Project project;
     gd::Platform platform;
diff --git a/Core/tests/WholeProjectRefactorer.cpp b/Core/tests/WholeProjectRefactorer.cpp
index 5b6018774724..d1dba17216e2 100644
--- a/Core/tests/WholeProjectRefactorer.cpp
+++ b/Core/tests/WholeProjectRefactorer.cpp
@@ -1754,6 +1754,9 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
       eventsBasedObject.GetObjects().InsertNewObject(
           project, "MyExtension::Sprite", "Object2", 0);
 
+      auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+          eventsBasedObject.GetDefaultVariant(), 0);
+
       // Create the objects container for the events function
       gd::ObjectsContainer parametersObjectsContainer(
           gd::ObjectsContainer::SourceType::Function);
@@ -1765,6 +1768,10 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
       gd::WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject(
           project, projectScopedContainers, eventsBasedObject, "Object1",
           "Object3", /* isObjectGroup =*/false);
+
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object1") == false);
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object2") == true);
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object3") == true);
       REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups().size() == 1);
       REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups()[0].Find(
                   "Object1") == false);
@@ -1772,6 +1779,17 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
                   "Object2") == true);
       REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups()[0].Find(
                   "Object3") == true);
+
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object1") == false);
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object2") == true);
+      REQUIRE(variant.GetObjects().HasObjectNamed("Object3") == true);
+      REQUIRE(variant.GetObjects().GetObjectGroups().size() == 1);
+      REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object1") ==
+              false);
+      REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object2") ==
+              true);
+      REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object3") ==
+              true);
     }
 
     SECTION("Initial instances") {
@@ -1796,6 +1814,9 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
       eventsBasedObject.GetInitialInstances().InsertInitialInstance(instance1);
       eventsBasedObject.GetInitialInstances().InsertInitialInstance(instance2);
 
+      auto &variant = eventsBasedObject.GetVariants().InsertVariant(
+          eventsBasedObject.GetDefaultVariant(), 0);
+
       // Create the objects container for the events function
       gd::ObjectsContainer parametersObjectsContainer(
           gd::ObjectsContainer::SourceType::Function);
@@ -1807,10 +1828,16 @@ TEST_CASE("WholeProjectRefactorer", "[common]") {
       gd::WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject(
           project, projectScopedContainers, eventsBasedObject, "Object1",
           "Object3", /* isObjectGroup =*/false);
+
       REQUIRE(eventsBasedObject.GetInitialInstances().HasInstancesOfObject(
                   "Object1") == false);
       REQUIRE(eventsBasedObject.GetInitialInstances().HasInstancesOfObject(
                   "Object3") == true);
+
+      REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("Object1") ==
+              false);
+      REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("Object3") ==
+              true);
     }
 
     SECTION("Events") {
diff --git a/Extensions/BBText/JsExtension.js b/Extensions/BBText/JsExtension.js
index edf65e9dfb33..cf7dbc9cba8b 100644
--- a/Extensions/BBText/JsExtension.js
+++ b/Extensions/BBText/JsExtension.js
@@ -485,14 +485,16 @@ module.exports = {
         instance,
         associatedObjectConfiguration,
         pixiContainer,
-        pixiResourcesLoader
+        pixiResourcesLoader,
+        propertyOverridings
       ) {
         super(
           project,
           instance,
           associatedObjectConfiguration,
           pixiContainer,
-          pixiResourcesLoader
+          pixiResourcesLoader,
+          propertyOverridings
         );
 
         const bbTextStyles = {
@@ -532,7 +534,9 @@ module.exports = {
           gd.ObjectJsImplementation
         );
 
-        const rawText = object.content.text;
+        const rawText = this._propertyOverridings.has('Text')
+          ? this._propertyOverridings.get('Text')
+          : object.content.text;
         if (rawText !== this._pixiObject.text) {
           this._pixiObject.text = rawText;
         }
diff --git a/Extensions/BitmapText/JsExtension.js b/Extensions/BitmapText/JsExtension.js
index 69b6a88c8ac0..c38cd256ec9b 100644
--- a/Extensions/BitmapText/JsExtension.js
+++ b/Extensions/BitmapText/JsExtension.js
@@ -624,14 +624,16 @@ module.exports = {
         instance,
         associatedObjectConfiguration,
         pixiContainer,
-        pixiResourcesLoader
+        pixiResourcesLoader,
+        propertyOverridings
       ) {
         super(
           project,
           instance,
           associatedObjectConfiguration,
           pixiContainer,
-          pixiResourcesLoader
+          pixiResourcesLoader,
+          propertyOverridings
         );
 
         // We'll track changes of the font to trigger the loading of the new font.
@@ -657,8 +659,9 @@ module.exports = {
 
         // Update the rendered text properties (note: Pixi is only
         // applying changes if there were changed).
-        const rawText = object.content.text;
-        this._pixiObject.text = rawText;
+        this._pixiObject.text = this._propertyOverridings.has('Text')
+          ? this._propertyOverridings.get('Text')
+          : object.content.text;
 
         const align = object.content.align;
         this._pixiObject.align = align;
diff --git a/Extensions/JsExtensionTypes.d.ts b/Extensions/JsExtensionTypes.d.ts
index fcfacbd9a5d7..8621802b7c41 100644
--- a/Extensions/JsExtensionTypes.d.ts
+++ b/Extensions/JsExtensionTypes.d.ts
@@ -15,6 +15,7 @@ class RenderedInstance {
   _pixiContainer: PIXI.Container;
   _pixiResourcesLoader: Class<PixiResourcesLoader>;
   _pixiObject: PIXI.DisplayObject | null;
+  _propertyOverridings: Map<string, string>;
   wasUsed: boolean;
 
   /** Set to true when onRemovedFromScene is called. Allows to cancel promises/asynchronous operations (notably: waiting for a resource load). */
@@ -25,7 +26,8 @@ class RenderedInstance {
     instance: gdInitialInstance,
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
-    pixiResourcesLoader: Class<PixiResourcesLoader>
+    pixiResourcesLoader: Class<PixiResourcesLoader>,
+    propertyOverridings: Map<string, string> = new Map<string, string>()
   );
 
   /**
diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts
index 3c597d8ce06c..09606cd6c23c 100644
--- a/GDJS/Runtime/CustomRuntimeObject.ts
+++ b/GDJS/Runtime/CustomRuntimeObject.ts
@@ -12,6 +12,7 @@ namespace gdjs {
 
   export type CustomObjectConfiguration = ObjectConfiguration & {
     animatable?: SpriteAnimationData[];
+    variant: string;
     childrenContent: { [objectName: string]: ObjectConfiguration & any };
   };
 
@@ -92,34 +93,50 @@ namespace gdjs {
     }
 
     private _initializeFromObjectData(
-      objectData: ObjectData & CustomObjectConfiguration
+      customObjectData: ObjectData & CustomObjectConfiguration
     ) {
       const eventsBasedObjectData = this._runtimeScene
         .getGame()
-        .getEventsBasedObjectData(objectData.type);
+        .getEventsBasedObjectData(customObjectData.type);
       if (!eventsBasedObjectData) {
         logger.error(
-          `A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${objectData.type}".`
+          `A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${customObjectData.type}".`
         );
         return;
       }
+
+      let usedVariantData: EventsBasedObjectVariantData = eventsBasedObjectData;
+      if (customObjectData.variant) {
+        for (
+          let variantIndex = 0;
+          variantIndex < eventsBasedObjectData.variants.length;
+          variantIndex++
+        ) {
+          const variantData = eventsBasedObjectData.variants[variantIndex];
+          if (variantData.name === customObjectData.variant) {
+            usedVariantData = variantData;
+            break;
+          }
+        }
+      }
+
       this._isInnerAreaFollowingParentSize =
         eventsBasedObjectData.isInnerAreaFollowingParentSize;
-      if (eventsBasedObjectData.instances.length > 0) {
+      if (usedVariantData.instances.length > 0) {
         if (!this._innerArea) {
           this._innerArea = {
             min: [0, 0, 0],
             max: [0, 0, 0],
           };
         }
-        this._innerArea.min[0] = eventsBasedObjectData.areaMinX;
-        this._innerArea.min[1] = eventsBasedObjectData.areaMinY;
-        this._innerArea.min[2] = eventsBasedObjectData.areaMinZ;
-        this._innerArea.max[0] = eventsBasedObjectData.areaMaxX;
-        this._innerArea.max[1] = eventsBasedObjectData.areaMaxY;
-        this._innerArea.max[2] = eventsBasedObjectData.areaMaxZ;
-      }
-      this._instanceContainer.loadFrom(objectData, eventsBasedObjectData);
+        this._innerArea.min[0] = usedVariantData.areaMinX;
+        this._innerArea.min[1] = usedVariantData.areaMinY;
+        this._innerArea.min[2] = usedVariantData.areaMinZ;
+        this._innerArea.max[0] = usedVariantData.areaMaxX;
+        this._innerArea.max[1] = usedVariantData.areaMaxY;
+        this._innerArea.max[2] = usedVariantData.areaMaxZ;
+      }
+      this._instanceContainer.loadFrom(customObjectData, usedVariantData);
     }
 
     protected abstract _createRender():
diff --git a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts
index 0268d3ca689f..ecd28bd89bc1 100644
--- a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts
+++ b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts
@@ -65,21 +65,21 @@ namespace gdjs {
      */
     loadFrom(
       customObjectData: ObjectData & CustomObjectConfiguration,
-      eventsBasedObjectData: EventsBasedObjectData
+      eventsBasedObjectVariantData: EventsBasedObjectVariantData
     ) {
       if (this._isLoaded) {
         this.onDestroyFromScene(this._parent);
       }
 
-      this._setOriginalInnerArea(eventsBasedObjectData);
+      this._setOriginalInnerArea(eventsBasedObjectVariantData);
 
       // Registering objects
       for (
-        let i = 0, len = eventsBasedObjectData.objects.length;
+        let i = 0, len = eventsBasedObjectVariantData.objects.length;
         i < len;
         ++i
       ) {
-        const childObjectData = eventsBasedObjectData.objects[i];
+        const childObjectData = eventsBasedObjectVariantData.objects[i];
         if (customObjectData.childrenContent) {
           this.registerObject({
             ...childObjectData,
@@ -92,14 +92,14 @@ namespace gdjs {
         }
       }
 
-      if (eventsBasedObjectData.layers.length > 0) {
+      if (eventsBasedObjectVariantData.layers.length > 0) {
         // Load layers
         for (
-          let i = 0, len = eventsBasedObjectData.layers.length;
+          let i = 0, len = eventsBasedObjectVariantData.layers.length;
           i < len;
           ++i
         ) {
-          this.addLayer(eventsBasedObjectData.layers[i]);
+          this.addLayer(eventsBasedObjectVariantData.layers[i]);
         }
       } else {
         // Add a default layer
@@ -128,7 +128,7 @@ namespace gdjs {
       }
 
       this.createObjectsFrom(
-        eventsBasedObjectData.instances,
+        eventsBasedObjectVariantData.instances,
         0,
         0,
         0,
@@ -147,7 +147,7 @@ namespace gdjs {
      * `_initialInnerArea` is shared by every instance to save memory.
      */
     private _setOriginalInnerArea(
-      eventsBasedObjectData: EventsBasedObjectData
+      eventsBasedObjectData: EventsBasedObjectVariantData
     ) {
       if (eventsBasedObjectData.instances.length > 0) {
         if (!eventsBasedObjectData._initialInnerArea) {
diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts
index 8f4947b17911..193931138231 100644
--- a/GDJS/Runtime/types/project-data.d.ts
+++ b/GDJS/Runtime/types/project-data.d.ts
@@ -206,9 +206,16 @@ declare interface SceneAndExtensionsData {
   usedExtensionsWithVariablesData: EventsFunctionsExtensionData[];
 }
 
-declare interface EventsBasedObjectData extends InstanceContainerData {
+declare interface EventsBasedObjectData
+  extends EventsBasedObjectVariantData,
+    InstanceContainerData {
   name: string;
   isInnerAreaFollowingParentSize: boolean;
+  variants: Array<EventsBasedObjectVariantData>;
+}
+
+declare interface EventsBasedObjectVariantData extends InstanceContainerData {
+  name: string;
   // The flat representation of defaultSize.
   areaMinX: float;
   areaMinY: float;
@@ -225,6 +232,9 @@ declare interface EventsBasedObjectData extends InstanceContainerData {
     min: [float, float, float];
     max: [float, float, float];
   } | null;
+  instances: InstanceData[];
+  objects: ObjectData[];
+  layers: LayerData[];
 }
 
 declare interface BehaviorSharedData {
diff --git a/GDJS/tests/tests/CustomRuntimeObject.js b/GDJS/tests/tests/CustomRuntimeObject.js
index fac2cd9df477..d2910a4b8546 100644
--- a/GDJS/tests/tests/CustomRuntimeObject.js
+++ b/GDJS/tests/tests/CustomRuntimeObject.js
@@ -15,6 +15,7 @@ describe('gdjs.CustomRuntimeObject', function () {
     const customObject = new gdjs.CustomRuntimeObject2D(instanceContainer, {
       name: 'MyCustomObject',
       type: 'MyExtension::MyEventsBasedObject',
+      variant: '',
       variables: [],
       behaviors: [],
       effects: [],
diff --git a/GDJS/tests/tests/hot-reloader.js b/GDJS/tests/tests/hot-reloader.js
index e890af565085..5da4b89c2ed7 100644
--- a/GDJS/tests/tests/hot-reloader.js
+++ b/GDJS/tests/tests/hot-reloader.js
@@ -93,6 +93,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
   /** @type {ObjectData & gdjs.CustomObjectConfiguration} */
   const defaultCustomObject = {
     type: 'MyExtension::MyCustomObject',
+    variant: '',
     name: 'MyCustomObject',
     behaviors: [],
     variables: [],
@@ -214,6 +215,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
       areaMaxZ: 0,
       _initialInnerArea: null,
       isInnerAreaFollowingParentSize: false,
+      variants: [],
     };
   };
 
diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl
index d823b9f28a86..9b9aee55292d 100644
--- a/GDevelop.js/Bindings/Bindings.idl
+++ b/GDevelop.js/Bindings/Bindings.idl
@@ -372,6 +372,16 @@ interface ObjectVariableHelper {
         [Ref] ObjectsContainer globalObjectsContainer,
         [Ref] ObjectsContainer objectsContainer,
         [Const, Ref] ObjectGroup objectGroup);
+    void STATIC_ApplyChangesToVariants(
+        [Ref] EventsBasedObject eventsBasedObject,
+        [Const] DOMString objectName,
+        [Const, Ref] VariablesChangeset changeset);
+};
+
+interface EventsBasedObjectVariantHelper {
+    void STATIC_ComplyVariantsToEventsBasedObject(
+        [Ref, Const] Project project,
+        [Ref] EventsBasedObject eventsBasedObject);
 };
 
 interface ObjectGroupsContainer {
@@ -923,6 +933,8 @@ enum CustomObjectConfiguration_EdgeAnchor {
 interface CustomObjectConfiguration {
     [Value] UniquePtrObjectConfiguration Clone();
 
+    [Const, Ref] DOMString GetVariantName();
+    void SetVariantName([Const] DOMString name);
     boolean IsForcedToOverrideEventsBasedObjectChildrenConfiguration();
     boolean IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration();
     void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
@@ -3145,6 +3157,9 @@ interface EventsBasedObject {
     [Ref] EventsBasedObject MakAsUsingLegacyInstancesRenderer(boolean value);
     boolean IsUsingLegacyInstancesRenderer();
 
+    [Ref] EventsBasedObjectVariant GetDefaultVariant();
+    [Ref] EventsBasedObjectVariantsContainer GetVariants();
+
     [Ref] InitialInstancesContainer GetInitialInstances();
     [Ref] LayersContainer GetLayers();
     [Ref] ObjectsContainer GetObjects();
@@ -3168,6 +3183,48 @@ interface EventsBasedObject {
 };
 EventsBasedObject implements AbstractEventsBasedEntity;
 
+interface EventsBasedObjectVariant {
+    void EventsBasedObjectVariant();
+
+    [Const, Ref] DOMString GetName();
+    [Ref] EventsBasedObjectVariant SetName([Const] DOMString name);
+
+    [Ref] InitialInstancesContainer GetInitialInstances();
+    [Ref] LayersContainer GetLayers();
+    [Ref] ObjectsContainer GetObjects();
+    double GetAreaMinX();
+    double GetAreaMinY();
+    double GetAreaMinZ();
+    double GetAreaMaxX();
+    double GetAreaMaxY();
+    double GetAreaMaxZ();
+    void SetAreaMinX(double value);
+    void SetAreaMinY(double value);
+    void SetAreaMinZ(double value);
+    void SetAreaMaxX(double value);
+    void SetAreaMaxY(double value);
+    void SetAreaMaxZ(double value);
+    void SetAssetStoreAssetId([Const] DOMString assetStoreAssetId);
+    [Const, Ref] DOMString GetAssetStoreAssetId();
+    void SetAssetStoreOriginalName([Const] DOMString assetStoreOriginalName);
+    [Const, Ref] DOMString GetAssetStoreOriginalName();
+
+    void SerializeTo([Ref] SerializerElement element);
+    void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element);
+};
+
+interface EventsBasedObjectVariantsContainer {
+    [Ref] EventsBasedObjectVariant InsertNewVariant([Const] DOMString name, unsigned long pos);
+    [Ref] EventsBasedObjectVariant InsertVariant([Const, Ref] EventsBasedObjectVariant variant, unsigned long pos);
+    boolean HasVariantNamed([Const] DOMString name);
+    [Ref] EventsBasedObjectVariant GetVariant([Const] DOMString name);
+    [Ref] EventsBasedObjectVariant GetVariantAt(unsigned long pos);
+    void RemoveVariant([Const] DOMString name);
+    void MoveVariant(unsigned long oldIndex, unsigned long newIndex);
+    unsigned long GetVariantsCount();
+    unsigned long GetVariantPosition([Const, Ref] EventsBasedObjectVariant variant);
+};
+
 interface EventsBasedObjectsList {
     [Ref] EventsBasedObject InsertNew([Const] DOMString name, unsigned long pos);
     [Ref] EventsBasedObject Insert([Const, Ref] EventsBasedObject item, unsigned long pos);
@@ -3248,6 +3305,7 @@ interface EventsFunctionsExtension {
     [Ref] EventsBasedObjectsList GetEventsBasedObjects();
 
     void SerializeTo([Ref] SerializerElement element);
+    void SerializeToExternal([Ref] SerializerElement element);
     void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element);
 
     boolean STATIC_IsExtensionLifecycleEventsFunction([Const] DOMString eventsFunctionName);
diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp
index 5b62af751c75..16f01e2ad88f 100644
--- a/GDevelop.js/Bindings/Wrapper.cpp
+++ b/GDevelop.js/Bindings/Wrapper.cpp
@@ -47,6 +47,7 @@
 #include <GDCore/IDE/Events/ExampleExtensionUsagesFinder.h>
 #include <GDCore/IDE/EventsFunctionTools.h>
 #include <GDCore/IDE/ObjectVariableHelper.h>
+#include <GDCore/IDE/EventsBasedObjectVariantHelper.h>
 #include <GDCore/IDE/Project/ArbitraryResourceWorker.h>
 #include <GDCore/IDE/Project/ArbitraryObjectsWorker.h>
 #include <GDCore/IDE/Project/ObjectsUsingResourceCollector.h>
@@ -725,6 +726,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
   ComputeChangesetForVariablesContainer
 #define STATIC_MergeVariableContainers MergeVariableContainers
 #define STATIC_FillAnyVariableBetweenObjects FillAnyVariableBetweenObjects
+#define STATIC_ApplyChangesToVariants ApplyChangesToVariants
+#define STATIC_ComplyVariantsToEventsBasedObject ComplyVariantsToEventsBasedObject
 #define STATIC_RenameEventsFunctionsExtension RenameEventsFunctionsExtension
 #define STATIC_UpdateExtensionNameInEventsBasedBehavior \
   UpdateExtensionNameInEventsBasedBehavior
@@ -844,6 +847,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
 #define RemoveEventAt RemoveEvent
 #define RemoveAt Remove
 #define GetEventsFunctionAt GetEventsFunction
+#define GetVariantAt GetVariant
 #define GetEffectAt GetEffect
 #define GetParameterAt GetParameter
 
diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts
index 470216816e96..439c6c4cd4d8 100644
--- a/GDevelop.js/types.d.ts
+++ b/GDevelop.js/types.d.ts
@@ -384,6 +384,11 @@ export class ObjectGroup extends EmscriptenObject {
 export class ObjectVariableHelper extends EmscriptenObject {
   static mergeVariableContainers(objectsContainersList: ObjectsContainersList, objectGroup: ObjectGroup): VariablesContainer;
   static fillAnyVariableBetweenObjects(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, objectGroup: ObjectGroup): void;
+  static applyChangesToVariants(eventsBasedObject: EventsBasedObject, objectName: string, changeset: VariablesChangeset): void;
+}
+
+export class EventsBasedObjectVariantHelper extends EmscriptenObject {
+  static complyVariantsToEventsBasedObject(project: Project, eventsBasedObject: EventsBasedObject): void;
 }
 
 export class ObjectGroupsContainer extends EmscriptenObject {
@@ -768,6 +773,8 @@ export class ObjectJsImplementation extends ObjectConfiguration {
 
 export class CustomObjectConfiguration extends ObjectConfiguration {
   clone(): UniquePtrObjectConfiguration;
+  getVariantName(): string;
+  setVariantName(name: string): void;
   isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
   isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
   setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
@@ -2270,6 +2277,8 @@ export class EventsBasedObject extends AbstractEventsBasedEntity {
   isInnerAreaFollowingParentSize(): boolean;
   makAsUsingLegacyInstancesRenderer(value: boolean): EventsBasedObject;
   isUsingLegacyInstancesRenderer(): boolean;
+  getDefaultVariant(): EventsBasedObjectVariant;
+  getVariants(): EventsBasedObjectVariantsContainer;
   getInitialInstances(): InitialInstancesContainer;
   getLayers(): LayersContainer;
   getObjects(): ObjectsContainer;
@@ -2291,6 +2300,45 @@ export class EventsBasedObject extends AbstractEventsBasedEntity {
   static getPropertyToggleActionName(propertyName: string): string;
 }
 
+export class EventsBasedObjectVariant extends EmscriptenObject {
+  constructor();
+  getName(): string;
+  setName(name: string): EventsBasedObjectVariant;
+  getInitialInstances(): InitialInstancesContainer;
+  getLayers(): LayersContainer;
+  getObjects(): ObjectsContainer;
+  getAreaMinX(): number;
+  getAreaMinY(): number;
+  getAreaMinZ(): number;
+  getAreaMaxX(): number;
+  getAreaMaxY(): number;
+  getAreaMaxZ(): number;
+  setAreaMinX(value: number): void;
+  setAreaMinY(value: number): void;
+  setAreaMinZ(value: number): void;
+  setAreaMaxX(value: number): void;
+  setAreaMaxY(value: number): void;
+  setAreaMaxZ(value: number): void;
+  setAssetStoreAssetId(assetStoreAssetId: string): void;
+  getAssetStoreAssetId(): string;
+  setAssetStoreOriginalName(assetStoreOriginalName: string): void;
+  getAssetStoreOriginalName(): string;
+  serializeTo(element: SerializerElement): void;
+  unserializeFrom(project: Project, element: SerializerElement): void;
+}
+
+export class EventsBasedObjectVariantsContainer extends EmscriptenObject {
+  insertNewVariant(name: string, pos: number): EventsBasedObjectVariant;
+  insertVariant(variant: EventsBasedObjectVariant, pos: number): EventsBasedObjectVariant;
+  hasVariantNamed(name: string): boolean;
+  getVariant(name: string): EventsBasedObjectVariant;
+  getVariantAt(pos: number): EventsBasedObjectVariant;
+  removeVariant(name: string): void;
+  moveVariant(oldIndex: number, newIndex: number): void;
+  getVariantsCount(): number;
+  getVariantPosition(variant: EventsBasedObjectVariant): number;
+}
+
 export class EventsBasedObjectsList extends EmscriptenObject {
   insertNew(name: string, pos: number): EventsBasedObject;
   insert(item: EventsBasedObject, pos: number): EventsBasedObject;
@@ -2361,6 +2409,7 @@ export class EventsFunctionsExtension extends EmscriptenObject {
   getEventsBasedBehaviors(): EventsBasedBehaviorsList;
   getEventsBasedObjects(): EventsBasedObjectsList;
   serializeTo(element: SerializerElement): void;
+  serializeToExternal(element: SerializerElement): void;
   unserializeFrom(project: Project, element: SerializerElement): void;
   static isExtensionLifecycleEventsFunction(eventsFunctionName: string): boolean;
 }
diff --git a/GDevelop.js/types/gdcustomobjectconfiguration.js b/GDevelop.js/types/gdcustomobjectconfiguration.js
index 2dcee3d41e6f..c8324f34a650 100644
--- a/GDevelop.js/types/gdcustomobjectconfiguration.js
+++ b/GDevelop.js/types/gdcustomobjectconfiguration.js
@@ -6,6 +6,8 @@ declare class gdCustomObjectConfiguration extends gdObjectConfiguration {
   static Proportional: 3;
   static Center: 4;
   clone(): gdUniquePtrObjectConfiguration;
+  getVariantName(): string;
+  setVariantName(name: string): void;
   isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean;
   isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean;
   setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void;
diff --git a/GDevelop.js/types/gdeventsbasedobject.js b/GDevelop.js/types/gdeventsbasedobject.js
index 264fe4c61d95..bd9ffde0ec34 100644
--- a/GDevelop.js/types/gdeventsbasedobject.js
+++ b/GDevelop.js/types/gdeventsbasedobject.js
@@ -17,6 +17,8 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity {
   isInnerAreaFollowingParentSize(): boolean;
   makAsUsingLegacyInstancesRenderer(value: boolean): gdEventsBasedObject;
   isUsingLegacyInstancesRenderer(): boolean;
+  getDefaultVariant(): gdEventsBasedObjectVariant;
+  getVariants(): gdEventsBasedObjectVariantsContainer;
   getInitialInstances(): gdInitialInstancesContainer;
   getLayers(): gdLayersContainer;
   getObjects(): gdObjectsContainer;
diff --git a/GDevelop.js/types/gdeventsbasedobjectvariant.js b/GDevelop.js/types/gdeventsbasedobjectvariant.js
new file mode 100644
index 000000000000..c47a8c0d9020
--- /dev/null
+++ b/GDevelop.js/types/gdeventsbasedobjectvariant.js
@@ -0,0 +1,29 @@
+// Automatically generated by GDevelop.js/scripts/generate-types.js
+declare class gdEventsBasedObjectVariant {
+  constructor(): void;
+  getName(): string;
+  setName(name: string): gdEventsBasedObjectVariant;
+  getInitialInstances(): gdInitialInstancesContainer;
+  getLayers(): gdLayersContainer;
+  getObjects(): gdObjectsContainer;
+  getAreaMinX(): number;
+  getAreaMinY(): number;
+  getAreaMinZ(): number;
+  getAreaMaxX(): number;
+  getAreaMaxY(): number;
+  getAreaMaxZ(): number;
+  setAreaMinX(value: number): void;
+  setAreaMinY(value: number): void;
+  setAreaMinZ(value: number): void;
+  setAreaMaxX(value: number): void;
+  setAreaMaxY(value: number): void;
+  setAreaMaxZ(value: number): void;
+  setAssetStoreAssetId(assetStoreAssetId: string): void;
+  getAssetStoreAssetId(): string;
+  setAssetStoreOriginalName(assetStoreOriginalName: string): void;
+  getAssetStoreOriginalName(): string;
+  serializeTo(element: gdSerializerElement): void;
+  unserializeFrom(project: gdProject, element: gdSerializerElement): void;
+  delete(): void;
+  ptr: number;
+};
\ No newline at end of file
diff --git a/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js b/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js
new file mode 100644
index 000000000000..81c04eb686cb
--- /dev/null
+++ b/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js
@@ -0,0 +1,6 @@
+// Automatically generated by GDevelop.js/scripts/generate-types.js
+declare class gdEventsBasedObjectVariantHelper {
+  static complyVariantsToEventsBasedObject(project: gdProject, eventsBasedObject: gdEventsBasedObject): void;
+  delete(): void;
+  ptr: number;
+};
\ No newline at end of file
diff --git a/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js b/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js
new file mode 100644
index 000000000000..60b58d259bbd
--- /dev/null
+++ b/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js
@@ -0,0 +1,14 @@
+// Automatically generated by GDevelop.js/scripts/generate-types.js
+declare class gdEventsBasedObjectVariantsContainer {
+  insertNewVariant(name: string, pos: number): gdEventsBasedObjectVariant;
+  insertVariant(variant: gdEventsBasedObjectVariant, pos: number): gdEventsBasedObjectVariant;
+  hasVariantNamed(name: string): boolean;
+  getVariant(name: string): gdEventsBasedObjectVariant;
+  getVariantAt(pos: number): gdEventsBasedObjectVariant;
+  removeVariant(name: string): void;
+  moveVariant(oldIndex: number, newIndex: number): void;
+  getVariantsCount(): number;
+  getVariantPosition(variant: gdEventsBasedObjectVariant): number;
+  delete(): void;
+  ptr: number;
+};
\ No newline at end of file
diff --git a/GDevelop.js/types/gdeventsfunctionsextension.js b/GDevelop.js/types/gdeventsfunctionsextension.js
index f9d3cfbbe319..aff9ae689e82 100644
--- a/GDevelop.js/types/gdeventsfunctionsextension.js
+++ b/GDevelop.js/types/gdeventsfunctionsextension.js
@@ -40,6 +40,7 @@ declare class gdEventsFunctionsExtension {
   getEventsBasedBehaviors(): gdEventsBasedBehaviorsList;
   getEventsBasedObjects(): gdEventsBasedObjectsList;
   serializeTo(element: gdSerializerElement): void;
+  serializeToExternal(element: gdSerializerElement): void;
   unserializeFrom(project: gdProject, element: gdSerializerElement): void;
   static isExtensionLifecycleEventsFunction(eventsFunctionName: string): boolean;
   delete(): void;
diff --git a/GDevelop.js/types/gdobjectvariablehelper.js b/GDevelop.js/types/gdobjectvariablehelper.js
index dcfba80e0305..d4cd846f2442 100644
--- a/GDevelop.js/types/gdobjectvariablehelper.js
+++ b/GDevelop.js/types/gdobjectvariablehelper.js
@@ -2,6 +2,7 @@
 declare class gdObjectVariableHelper {
   static mergeVariableContainers(objectsContainersList: gdObjectsContainersList, objectGroup: gdObjectGroup): gdVariablesContainer;
   static fillAnyVariableBetweenObjects(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectGroup: gdObjectGroup): void;
+  static applyChangesToVariants(eventsBasedObject: gdEventsBasedObject, objectName: string, changeset: gdVariablesChangeset): void;
   delete(): void;
   ptr: number;
 };
\ No newline at end of file
diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js
index 2a000a10ca9c..c8b394b52454 100644
--- a/GDevelop.js/types/libgdevelop.js
+++ b/GDevelop.js/types/libgdevelop.js
@@ -74,6 +74,7 @@ declare class libGDevelop {
   VariablesContainersList: Class<gdVariablesContainersList>;
   ObjectGroup: Class<gdObjectGroup>;
   ObjectVariableHelper: Class<gdObjectVariableHelper>;
+  EventsBasedObjectVariantHelper: Class<gdEventsBasedObjectVariantHelper>;
   ObjectGroupsContainer: Class<gdObjectGroupsContainer>;
   PlatformSpecificAssets: Class<gdPlatformSpecificAssets>;
   LoadingScreen: Class<gdLoadingScreen>;
@@ -222,6 +223,8 @@ declare class libGDevelop {
   EventsBasedBehavior: Class<gdEventsBasedBehavior>;
   EventsBasedBehaviorsList: Class<gdEventsBasedBehaviorsList>;
   EventsBasedObject: Class<gdEventsBasedObject>;
+  EventsBasedObjectVariant: Class<gdEventsBasedObjectVariant>;
+  EventsBasedObjectVariantsContainer: Class<gdEventsBasedObjectVariantsContainer>;
   EventsBasedObjectsList: Class<gdEventsBasedObjectsList>;
   PropertiesContainer: Class<gdPropertiesContainer>;
   EventsFunctionsExtension: Class<gdEventsFunctionsExtension>;
diff --git a/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js b/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js
index db0de291a4ec..dd6af042db26 100644
--- a/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js
+++ b/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js
@@ -24,7 +24,7 @@ import ErrorBoundary from '../../UI/ErrorBoundary';
 type Props = {|
   project: gdProject,
   onClose: () => void,
-  onInstallExtension: ExtensionShortHeader => void,
+  onInstallExtension: (extensionName: string) => void,
   onExtensionInstalled: (extensionName: string) => void,
   onCreateNew?: () => void,
 |};
@@ -63,7 +63,7 @@ const ExtensionsSearchDialog = ({
     try {
       let installedOrImportedExtensionName: string | null = null;
       if (!!extensionShortHeader) {
-        onInstallExtension(extensionShortHeader);
+        onInstallExtension(extensionShortHeader.name);
         const wasExtensionInstalledOrImported = await installDisplayedExtension(
           i18n,
           project,
@@ -77,7 +77,8 @@ const ExtensionsSearchDialog = ({
         installedOrImportedExtensionName = await importExtension(
           i18n,
           eventsFunctionsExtensionsState,
-          project
+          project,
+          onInstallExtension
         );
       }
 
diff --git a/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js b/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js
index f298e49c6257..af59f6691056 100644
--- a/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js
+++ b/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js
@@ -47,7 +47,8 @@ export const installExtension = async (
 export const importExtension = async (
   i18n: I18nType,
   eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
-  project: gdProject
+  project: gdProject,
+  onWillInstallExtension: (extensionName: string) => void
 ): Promise<string | null> => {
   const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener();
   if (!eventsFunctionsExtensionOpener) return null;
@@ -69,6 +70,8 @@ export const importExtension = async (
       if (!answer) return null;
     }
 
+    onWillInstallExtension(serializedExtension.name);
+
     await addSerializedExtensionsToProject(
       eventsFunctionsExtensionsState,
       project,
diff --git a/newIDE/app/src/AssetStore/InstallAsset.js b/newIDE/app/src/AssetStore/InstallAsset.js
index 86bfb6e90861..7fba144598a3 100644
--- a/newIDE/app/src/AssetStore/InstallAsset.js
+++ b/newIDE/app/src/AssetStore/InstallAsset.js
@@ -155,6 +155,23 @@ export type InstallAssetArgs = {|
   targetObjectFolderOrObject?: ?gdObjectFolderOrObject,
 |};
 
+const findVariant = (
+  container: gdEventsBasedObjectVariantsContainer,
+  assetStoreAssetId: string,
+  assetStoreOriginalName: string
+): gdEventsBasedObjectVariant | null => {
+  for (let index = 0; index < container.getVariantsCount(); index++) {
+    const variant = container.getVariantAt(index);
+    if (
+      variant.getAssetStoreAssetId() === assetStoreAssetId &&
+      variant.getAssetStoreOriginalName() === assetStoreOriginalName
+    ) {
+      return variant;
+    }
+  }
+  return null;
+};
+
 export const addAssetToProject = async ({
   asset,
   project,
@@ -170,6 +187,88 @@ export const addAssetToProject = async ({
     const type: ?string = objectAsset.object.type;
     if (!type) throw new Error('An object has no type specified');
 
+    const variantRenamings: Array<{
+      objectType: string,
+      oldVariantName: string,
+      newVariantName: string,
+    }> = [];
+    const serializedVariants = objectAsset.variants;
+    if (serializedVariants) {
+      // Install variants
+      for (const {
+        objectType,
+        variant: serializedVariant,
+      } of serializedVariants) {
+        if (project.hasEventsBasedObject(objectType)) {
+          const eventsBasedObject = project.getEventsBasedObject(objectType);
+          const variants = eventsBasedObject.getVariants();
+          let variant = findVariant(variants, asset.id, serializedVariant.name);
+          if (!variant) {
+            // TODO Forbid name with `::`
+            const uniqueNewName = newNameGenerator(
+              serializedVariant.name || asset.name,
+              tentativeNewName => variants.hasVariantNamed(tentativeNewName)
+            );
+            variant = variants.insertNewVariant(
+              uniqueNewName,
+              variants.getVariantsCount()
+            );
+            const variantName = variant.getName();
+            unserializeFromJSObject(
+              variant,
+              serializedVariant,
+              'unserializeFrom',
+              project
+            );
+            variant.setName(variantName);
+            variant.setAssetStoreAssetId(asset.id);
+            variant.setAssetStoreOriginalName(serializedVariant.name);
+          }
+          if (variant.getName() !== serializedVariant.name) {
+            variantRenamings.push({
+              objectType,
+              oldVariantName: serializedVariant.name,
+              newVariantName: variant.getName(),
+            });
+          }
+        }
+      }
+      // Update variant names into variants object configurations.
+      for (const {
+        objectType,
+        variant: serializedVariant,
+      } of serializedVariants) {
+        if (project.hasEventsBasedObject(objectType)) {
+          const eventsBasedObject = project.getEventsBasedObject(objectType);
+          const variants = eventsBasedObject.getVariants();
+          let variant = findVariant(variants, asset.id, serializedVariant.name);
+          if (variant) {
+            for (
+              let index = 0;
+              index < variant.getObjects().getObjectsCount();
+              index++
+            ) {
+              const object = variant.getObjects().getObjectAt(index);
+
+              if (project.hasEventsBasedObject(object.getType())) {
+                const customObjectConfiguration = gd.asCustomObjectConfiguration(
+                  object.getConfiguration()
+                );
+                const customObjectVariantRenaming = variantRenamings.find(
+                  renaming => renaming.objectType === object.getType()
+                );
+                if (customObjectVariantRenaming) {
+                  customObjectConfiguration.setVariantName(
+                    customObjectVariantRenaming.newVariantName
+                  );
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
     // Insert the object
     const originalName = sanitizeObjectName(objectAsset.object.name);
     const newName = newNameGenerator(originalName, name =>
@@ -204,10 +303,27 @@ export const addAssetToProject = async ({
       'unserializeFrom',
       project
     );
-
-    object.setAssetStoreId(asset.id);
     // The name was overwritten after unserialization.
     object.setName(newName);
+    object.setAssetStoreId(asset.id);
+    if (project.hasEventsBasedObject(object.getType())) {
+      const customObjectConfiguration = gd.asCustomObjectConfiguration(
+        object.getConfiguration()
+      );
+      if (customObjectConfiguration.getVariantName()) {
+        customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
+          false
+        );
+      }
+      const customObjectVariantRenaming = variantRenamings.find(
+        renaming => renaming.objectType === object.getType()
+      );
+      if (customObjectVariantRenaming) {
+        customObjectConfiguration.setVariantName(
+          customObjectVariantRenaming.newVariantName
+        );
+      }
+    }
 
     // Add resources used by the object
     objectAsset.resources.forEach(serializedResource => {
diff --git a/newIDE/app/src/BehaviorsEditor/index.js b/newIDE/app/src/BehaviorsEditor/index.js
index 401d1aba8bc9..af638417068c 100644
--- a/newIDE/app/src/BehaviorsEditor/index.js
+++ b/newIDE/app/src/BehaviorsEditor/index.js
@@ -45,6 +45,7 @@ import CopyIcon from '../UI/CustomSvgIcons/Copy';
 import ResponsiveFlatButton from '../UI/ResponsiveFlatButton';
 import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer';
 import QuickCustomizationPropertiesVisibilityDialog from '../QuickCustomization/QuickCustomizationPropertiesVisibilityDialog';
+import Text from '../UI/Text';
 
 const gd: libGDevelop = global.gd;
 
@@ -80,6 +81,7 @@ type BehaviorConfigurationEditorProps = {|
   openBehaviorPropertiesQuickCustomizationDialog: (
     behaviorName: string
   ) => void,
+  isListLocked: boolean,
 |};
 
 const BehaviorConfigurationEditor = React.forwardRef<
@@ -100,6 +102,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
       pasteBehaviors,
       openExtension,
       openBehaviorPropertiesQuickCustomizationDialog,
+      isListLocked,
     },
     ref
   ) => {
@@ -195,6 +198,7 @@ const BehaviorConfigurationEditor = React.forwardRef<
                 {
                   label: i18n._(t`Delete`),
                   click: () => onRemoveBehavior(behaviorName),
+                  enabled: !isListLocked,
                 },
                 {
                   label: i18n._(t`Copy`),
@@ -203,7 +207,8 @@ const BehaviorConfigurationEditor = React.forwardRef<
                 {
                   label: i18n._(t`Paste`),
                   click: pasteBehaviors,
-                  enabled: canPasteBehaviors,
+                  // TODO Allow to paste behaviors that are already in the list.
+                  enabled: canPasteBehaviors && !isListLocked,
                 },
                 ...(project.hasEventsBasedBehavior(behaviorTypeName)
                   ? [
@@ -618,6 +623,7 @@ type Props = {|
     behaviorName: string
   ) => Promise<void>,
   onExtensionInstalled: (extensionName: string) => void,
+  isListLocked: boolean,
 |};
 
 const BehaviorsEditor = (props: Props) => {
@@ -636,6 +642,7 @@ const BehaviorsEditor = (props: Props) => {
     onUpdateBehaviorsSharedData,
     openBehaviorEvents,
     onExtensionInstalled,
+    isListLocked,
   } = props;
   const forceUpdate = useForceUpdate();
 
@@ -725,30 +732,41 @@ const BehaviorsEditor = (props: Props) => {
   return (
     <Column noMargin expand useFullHeight noOverflowParent>
       {allVisibleBehaviors.length === 0 ? (
-        <Column noMargin expand justifyContent="center">
-          <EmptyPlaceholder
-            title={<Trans>Add your first behavior</Trans>}
-            description={
-              <Trans>
-                Behaviors add features to objects in a matter of clicks.
-              </Trans>
-            }
-            helpPagePath="/behaviors"
-            tutorialId="intro-behaviors-and-functions"
-            actionButtonId="add-behavior-button"
-            actionLabel={
-              isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans>
-            }
-            onAction={openNewBehaviorDialog}
-            secondaryActionIcon={<PasteIcon />}
-            secondaryActionLabel={
-              isClipboardContainingBehaviors ? <Trans>Paste</Trans> : null
-            }
-            onSecondaryAction={() => {
-              pasteBehaviors();
-            }}
-          />
-        </Column>
+        isListLocked ? (
+          <Column noMargin expand justifyContent="center">
+            <Text size="block-title" align="center">
+              <Trans>No behavior</Trans>
+            </Text>
+            <Text align="center" noMargin>
+              <Trans>There is no behavior to set up for this object.</Trans>
+            </Text>
+          </Column>
+        ) : (
+          <Column noMargin expand justifyContent="center">
+            <EmptyPlaceholder
+              title={<Trans>Add your first behavior</Trans>}
+              description={
+                <Trans>
+                  Behaviors add features to objects in a matter of clicks.
+                </Trans>
+              }
+              helpPagePath="/behaviors"
+              tutorialId="intro-behaviors-and-functions"
+              actionButtonId="add-behavior-button"
+              actionLabel={
+                isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans>
+              }
+              onAction={openNewBehaviorDialog}
+              secondaryActionIcon={<PasteIcon />}
+              secondaryActionLabel={
+                isClipboardContainingBehaviors ? <Trans>Paste</Trans> : null
+              }
+              onSecondaryAction={() => {
+                pasteBehaviors();
+              }}
+            />
+          </Column>
+        )
       ) : (
         <React.Fragment>
           <ScrollView ref={scrollView}>
@@ -778,6 +796,7 @@ const BehaviorsEditor = (props: Props) => {
                   canPasteBehaviors={isClipboardContainingBehaviors}
                   pasteBehaviors={pasteBehaviors}
                   resourceManagementProps={props.resourceManagementProps}
+                  isListLocked={isListLocked}
                 />
               );
             })}
@@ -806,7 +825,7 @@ const BehaviorsEditor = (props: Props) => {
                   onClick={() => {
                     pasteBehaviors();
                   }}
-                  disabled={!isClipboardContainingBehaviors}
+                  disabled={!isClipboardContainingBehaviors || isListLocked}
                 />
               </LineStackLayout>
               <LineStackLayout justifyContent="flex-end" expand>
@@ -823,6 +842,7 @@ const BehaviorsEditor = (props: Props) => {
                   onClick={openNewBehaviorDialog}
                   icon={<Add />}
                   id="add-behavior-button"
+                  disabled={isListLocked}
                 />
               </LineStackLayout>
             </LineStackLayout>
diff --git a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js
index f236671075eb..e6b16e220c89 100644
--- a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js
+++ b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js
@@ -21,7 +21,9 @@ type Props = {|
   onEventsFunctionsAdded: () => void,
   onOpenCustomObjectEditor: () => void,
   unsavedChanges?: ?UnsavedChanges,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
 |};
 
 export default function EventsBasedObjectEditorPanel({
diff --git a/newIDE/app/src/EventsBasedObjectEditor/index.js b/newIDE/app/src/EventsBasedObjectEditor/index.js
index 77548b1b81ed..559eb17e32d6 100644
--- a/newIDE/app/src/EventsBasedObjectEditor/index.js
+++ b/newIDE/app/src/EventsBasedObjectEditor/index.js
@@ -23,7 +23,9 @@ type Props = {|
   eventsBasedObject: gdEventsBasedObject,
   onOpenCustomObjectEditor: () => void,
   unsavedChanges?: ?UnsavedChanges,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
 |};
 
 export default function EventsBasedObjectEditor({
@@ -127,7 +129,7 @@ export default function EventsBasedObjectEditor({
         onCheck={(e, checked) => {
           eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
           onChange();
-          onEventsBasedObjectChildrenEdited();
+          onEventsBasedObjectChildrenEdited(eventsBasedObject);
         }}
       />
       {isDev && (
@@ -137,7 +139,7 @@ export default function EventsBasedObjectEditor({
           onCheck={(e, checked) => {
             eventsBasedObject.makAsUsingLegacyInstancesRenderer(checked);
             onChange();
-            onEventsBasedObjectChildrenEdited();
+            onEventsBasedObjectChildrenEdited(eventsBasedObject);
           }}
         />
       )}
@@ -147,7 +149,7 @@ export default function EventsBasedObjectEditor({
         onCheck={(e, checked) => {
           eventsBasedObject.setPrivate(checked);
           onChange();
-          onEventsBasedObjectChildrenEdited();
+          onEventsBasedObjectChildrenEdited(eventsBasedObject);
         }}
         tooltipOrHelperText={
           eventsBasedObject.isPrivate() ? (
diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js
index 78c695040f4a..4570a2048b71 100644
--- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js
+++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js
@@ -77,7 +77,9 @@ type Props = {|
   unsavedChanges?: ?UnsavedChanges,
   onOpenCustomObjectEditor: gdEventsBasedObject => void,
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
   onRenamedEventsBasedObject: (
     eventsFunctionsExtension: gdEventsFunctionsExtension,
     oldName: string,
@@ -778,7 +780,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
     // Some custom object instances may target the pasted event-based object name.
     // It can happen when an event-based object is deleted and another one is
     // pasted to replace it.
-    this.props.onEventsBasedObjectChildrenEdited();
+    this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject);
   };
 
   _onEventsBasedBehaviorRenamed = () => {
@@ -797,7 +799,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
     }
   };
 
-  _onEventsBasedObjectRenamed = () => {
+  _onEventsBasedObjectRenamed = (eventsBasedObject: gdEventsBasedObject) => {
     // Name of an object changed, so notify parent
     // that an object was edited (to trigger reload of extensions)
     if (this.props.onObjectEdited) {
@@ -814,7 +816,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
     // Some custom object instances may target the new event-based object name.
     // It can happen when an event-based object is deleted and another one is
     // renamed to replace it.
-    this.props.onEventsBasedObjectChildrenEdited();
+    this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject);
   };
 
   _onDeleteEventsBasedBehavior = (
@@ -853,7 +855,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
       eventsFunctionsExtension,
       eventsBasedObject.getName()
     );
-    onEventsBasedObjectChildrenEdited();
+    onEventsBasedObjectChildrenEdited(eventsBasedObject);
   };
 
   _onCloseExtensionFunctionSelectorDialog = (
@@ -1702,6 +1704,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
             onCancel={() => this._editVariables(null)}
             onApply={() => this._editVariables(null)}
             hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
+            isListLocked={false}
           />
         )}
         {objectMethodSelectorDialogOpen && selectedEventsBasedObject && (
diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js
index b483b074af8a..e0ae4e2a5589 100644
--- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js
+++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js
@@ -32,7 +32,10 @@ export default class BrowserEventsFunctionsExtensionWriter {
     extension: gdEventsFunctionsExtension,
     filename: string
   ): Promise<void> => {
-    const serializedObject = serializeToJSObject(extension);
+    const serializedObject = serializeToJSObject(
+      extension,
+      'serializeToExternal'
+    );
     try {
       await downloadStringContentAsFile(
         filename,
diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js
index c1742bd489b9..c55f5acde32e 100644
--- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js
+++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js
@@ -56,7 +56,10 @@ export default class LocalEventsFunctionsExtensionWriter {
     extension: gdEventsFunctionsExtension,
     filepath: string
   ): Promise<void> => {
-    const serializedObject = serializeToJSObject(extension);
+    const serializedObject = serializeToJSObject(
+      extension,
+      'serializeToExternal'
+    );
     return writeJSONFile(serializedObject, filepath).catch(err => {
       console.error('Unable to write the events function extension:', err);
       throw err;
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js
index 2c2147d19566..227a04199919 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js
@@ -133,6 +133,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
             initiallySelectedVariableName={editorOpen.variableName}
             shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js
index 3f3cce7dcf58..7c8621c27449 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js
@@ -131,6 +131,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
             initiallySelectedVariableName={editorOpen.variableName}
             shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js
index f38c6268723a..3da9da8b72f4 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js
@@ -127,6 +127,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
             initiallySelectedVariableName={editorOpen.variableName}
             shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js
index c0c70ffff18e..24fa3bfe697e 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js
@@ -84,6 +84,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
             initiallySelectedVariableName={editorOpen.variableName}
             shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js
index ed0b1ec079b0..43a28ac1c9b7 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js
@@ -227,6 +227,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
               shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
               onComputeAllVariableNames={onComputeAllVariableNames}
               hotReloadPreviewButtonProps={null}
+              isListLocked={false}
             />
           )}
         {editorOpen &&
@@ -246,6 +247,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
               initiallySelectedVariableName={editorOpen.variableName}
               shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
               onComputeAllVariableNames={onComputeAllVariableNames}
+              isListLocked={false}
             />
           )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js
index fff2370e2c4d..44ed5a825f3b 100644
--- a/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js
+++ b/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js
@@ -110,6 +110,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
               editorOpen.shouldCreate || false
             }
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
         {editorOpen && eventsFunctionsExtension && !layout && (
@@ -122,6 +123,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
             initiallySelectedVariableName={editorOpen.variableName}
             shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate}
             hotReloadPreviewButtonProps={null}
+            isListLocked={false}
           />
         )}
       </React.Fragment>
diff --git a/newIDE/app/src/EventsSheet/index.js b/newIDE/app/src/EventsSheet/index.js
index d2fb99241e27..2851f232e47a 100644
--- a/newIDE/app/src/EventsSheet/index.js
+++ b/newIDE/app/src/EventsSheet/index.js
@@ -2158,6 +2158,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
             shouldCreateInitiallySelectedVariable={
               this.state.editedVariable.shouldCreateVariable
             }
+            isListLocked={false}
           />
         )}
         {this.state.layoutVariablesDialogOpen && (
@@ -2167,6 +2168,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
             onCancel={() => this.editLayoutVariables(false)}
             onApply={() => this.editLayoutVariables(false)}
             hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
+            isListLocked={false}
           />
         )}
         {this.state.textEditedEvent && (
diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
index 39a698aff1ef..50ce02ae534e 100644
--- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
+++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js
@@ -64,6 +64,7 @@ type Props = {|
   historyHandler?: HistoryHandler,
   tileMapTileSelection: ?TileMapTileSelection,
   onSelectTileMapTile: (?TileMapTileSelection) => void,
+  isVariableListLocked: boolean,
 |};
 
 export const CompactInstancePropertiesEditor = ({
@@ -83,6 +84,7 @@ export const CompactInstancePropertiesEditor = ({
   projectScopedContainersAccessor,
   tileMapTileSelection,
   onSelectTileMapTile,
+  isVariableListLocked,
 }: Props) => {
   const forceUpdate = useForceUpdate();
   const variablesListRef = React.useRef<?VariablesListInterface>(null);
@@ -276,16 +278,18 @@ export const CompactInstancePropertiesEditor = ({
                     >
                       <ShareExternal style={styles.icon} />
                     </IconButton>
-                    <IconButton
-                      size="small"
-                      onClick={
-                        variablesListRef.current
-                          ? variablesListRef.current.addVariable
-                          : undefined
-                      }
-                    >
-                      <Add style={styles.icon} />
-                    </IconButton>
+                    {isVariableListLocked ? null : (
+                      <IconButton
+                        size="small"
+                        onClick={
+                          variablesListRef.current
+                            ? variablesListRef.current.addVariable
+                            : undefined
+                        }
+                      >
+                        <Add style={styles.icon} />
+                      </IconButton>
+                    )}
                   </Line>
                 </Line>
               </Column>
@@ -314,6 +318,7 @@ export const CompactInstancePropertiesEditor = ({
                 compactEmptyPlaceholderText={
                   <Trans>There are no variables on this instance.</Trans>
                 }
+                isListLocked={isVariableListLocked}
               />
             </>
           ) : null}
diff --git a/newIDE/app/src/InstancesEditor/WindowBorder.js b/newIDE/app/src/InstancesEditor/WindowBorder.js
index 471ebb22f20e..c15082658479 100644
--- a/newIDE/app/src/InstancesEditor/WindowBorder.js
+++ b/newIDE/app/src/InstancesEditor/WindowBorder.js
@@ -7,14 +7,14 @@ import Rectangle from '../Utils/Rectangle';
 type Props = {|
   project: gdProject,
   layout: gdLayout | null,
-  eventsBasedObject: gdEventsBasedObject | null,
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant | null,
   toCanvasCoordinates: (x: number, y: number) => [number, number],
 |};
 
 export default class WindowBorder {
   project: gdProject;
   layout: gdLayout | null;
-  eventsBasedObject: gdEventsBasedObject | null;
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant | null;
   toCanvasCoordinates: (x: number, y: number) => [number, number];
   pixiRectangle = new PIXI.Graphics();
   windowRectangle: Rectangle = new Rectangle();
@@ -22,12 +22,12 @@ export default class WindowBorder {
   constructor({
     project,
     layout,
-    eventsBasedObject,
+    eventsBasedObjectVariant,
     toCanvasCoordinates,
   }: Props) {
     this.project = project;
     this.layout = layout;
-    this.eventsBasedObject = eventsBasedObject;
+    this.eventsBasedObjectVariant = eventsBasedObjectVariant;
     this.toCanvasCoordinates = toCanvasCoordinates;
 
     this.pixiRectangle.hitArea = new PIXI.Rectangle(0, 0, 0, 0);
@@ -38,15 +38,15 @@ export default class WindowBorder {
   }
 
   render() {
-    const { layout, eventsBasedObject } = this;
+    const { layout, eventsBasedObjectVariant } = this;
 
     this.windowRectangle.set(
-      eventsBasedObject
+      eventsBasedObjectVariant
         ? {
-            left: eventsBasedObject.getAreaMinX(),
-            top: eventsBasedObject.getAreaMinY(),
-            right: eventsBasedObject.getAreaMaxX(),
-            bottom: eventsBasedObject.getAreaMaxY(),
+            left: eventsBasedObjectVariant.getAreaMinX(),
+            top: eventsBasedObjectVariant.getAreaMinY(),
+            right: eventsBasedObjectVariant.getAreaMaxX(),
+            bottom: eventsBasedObjectVariant.getAreaMaxY(),
           }
         : {
             left: 0,
@@ -88,7 +88,7 @@ export default class WindowBorder {
       displayedRectangle.width(),
       displayedRectangle.height()
     );
-    if (eventsBasedObject) {
+    if (eventsBasedObjectVariant) {
       const origin = this.toCanvasCoordinates(0, 0);
       this.pixiRectangle.drawRect(origin[0] - 8, origin[1] - 1, 16, 2);
       this.pixiRectangle.drawRect(origin[0] - 1, origin[1] - 8, 2, 16);
diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js
index 2a47c7bc6060..0b5110b804fd 100644
--- a/newIDE/app/src/InstancesEditor/index.js
+++ b/newIDE/app/src/InstancesEditor/index.js
@@ -91,6 +91,7 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {|
   project: gdProject,
   layout: gdLayout | null,
   eventsBasedObject: gdEventsBasedObject | null,
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant | null,
   layersContainer: gdLayersContainer,
   globalObjectsContainer: gdObjectsContainer | null,
   objectsContainer: gdObjectsContainer,
@@ -564,7 +565,7 @@ export default class InstancesEditor extends Component<Props, State> {
     this.windowBorder = new WindowBorder({
       project: props.project,
       layout: props.layout,
-      eventsBasedObject: props.eventsBasedObject,
+      eventsBasedObjectVariant: props.eventsBasedObjectVariant,
       toCanvasCoordinates: this.viewPosition.toCanvasCoordinates,
     });
     this.windowMask = new WindowMask({
@@ -1477,13 +1478,13 @@ export default class InstancesEditor extends Component<Props, State> {
   };
 
   _getAreaRectangle = (): Rectangle => {
-    const { eventsBasedObject, project } = this.props;
-    return eventsBasedObject
+    const { eventsBasedObjectVariant, project } = this.props;
+    return eventsBasedObjectVariant
       ? new Rectangle(
-          eventsBasedObject.getAreaMinX(),
-          eventsBasedObject.getAreaMinY(),
-          eventsBasedObject.getAreaMaxX(),
-          eventsBasedObject.getAreaMaxY()
+          eventsBasedObjectVariant.getAreaMinX(),
+          eventsBasedObjectVariant.getAreaMinY(),
+          eventsBasedObjectVariant.getAreaMaxX(),
+          eventsBasedObjectVariant.getAreaMaxY()
         )
       : new Rectangle(
           0,
diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js
index 368e2c5eae15..9049d4ab8e00 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js
@@ -62,7 +62,8 @@ export type RenderEditorContainerProps = {|
   ) => void,
   onOpenCustomObjectEditor: (
     gdEventsFunctionsExtension,
-    gdEventsBasedObject
+    gdEventsBasedObject,
+    variantName: string
   ) => void,
   openObjectEvents: (extensionName: string, objectName: string) => void,
 
@@ -136,7 +137,9 @@ export type RenderEditorContainerProps = {|
 
   // Object editing
   openBehaviorEvents: (extensionName: string, behaviorName: string) => void,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
   onSceneObjectEdited: (
     scene: gdLayout,
     objectWithContext: ObjectWithContext
@@ -151,7 +154,17 @@ export type RenderEditorContainerProps = {|
     extensionName: string,
     eventsBasedObjectName: string
   ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
   onExtensionInstalled: (extensionName: string) => void,
+  onDeleteEventsBasedObjectVariant: (
+    eventsFunctionsExtension: gdEventsFunctionsExtension,
+    eventBasedObject: gdEventsBasedObject,
+    variant: gdEventsBasedObjectVariant
+  ) => void,
 |};
 
 export type RenderEditorContainerPropsWithRef = {|
diff --git a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js
index 4f646b8da719..7d5af6a40f2e 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js
@@ -120,9 +120,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
   getEventsFunctionsExtension(): ?gdEventsFunctionsExtension {
     const { project, projectItemName } = this.props;
     if (!project || !projectItemName) return null;
-    const extensionName = gd.PlatformExtension.getExtensionFromFullObjectType(
-      projectItemName
-    );
+    const extensionName = projectItemName.split('::')[0] || '';
 
     if (!project.hasEventsFunctionsExtensionNamed(extensionName)) {
       return null;
@@ -144,9 +142,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
     const extension = this.getEventsFunctionsExtension();
     if (!extension) return null;
 
-    const eventsBasedObjectName = gd.PlatformExtension.getObjectNameFromFullObjectType(
-      projectItemName
-    );
+    const eventsBasedObjectName = projectItemName.split('::')[1] || '';
 
     if (!extension.getEventsBasedObjects().has(eventsBasedObjectName)) {
       return null;
@@ -154,6 +150,24 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
     return extension.getEventsBasedObjects().get(eventsBasedObjectName);
   }
 
+  getVariantName(): string {
+    const { projectItemName } = this.props;
+    return (projectItemName && projectItemName.split('::')[2]) || '';
+  }
+
+  getVariant(): ?gdEventsBasedObjectVariant {
+    const { project, projectItemName } = this.props;
+    if (!project || !projectItemName) return null;
+
+    const eventsBasedObject = this.getEventsBasedObject();
+    if (!eventsBasedObject) return null;
+
+    const variantName = projectItemName.split('::')[2] || '';
+    return eventsBasedObject.getVariants().hasVariantNamed(variantName)
+      ? eventsBasedObject.getVariants().getVariant(variantName)
+      : eventsBasedObject.getDefaultVariant();
+  }
+
   getEventsBasedObjectName(): ?string {
     const { project, projectItemName } = this.props;
     if (!project || !projectItemName) return null;
@@ -176,6 +190,9 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
     const eventsBasedObject = this.getEventsBasedObject();
     if (!eventsBasedObject) return null;
 
+    const variant = this.getVariant();
+    if (!variant) return null;
+
     const projectScopedContainersAccessor = new ProjectScopedContainersAccessor(
       {
         project,
@@ -197,10 +214,11 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
           layout={null}
           eventsFunctionsExtension={eventsFunctionsExtension}
           eventsBasedObject={eventsBasedObject}
+          eventsBasedObjectVariant={variant}
           globalObjectsContainer={null}
-          objectsContainer={eventsBasedObject.getObjects()}
-          layersContainer={eventsBasedObject.getLayers()}
-          initialInstances={eventsBasedObject.getInitialInstances()}
+          objectsContainer={variant.getObjects()}
+          layersContainer={variant.getLayers()}
+          initialInstances={variant.getInitialInstances()}
           getInitialInstancesEditorSettings={() =>
             prepareInstancesEditorSettings(
               {}, // TODO
@@ -216,13 +234,24 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
           isActive={isActive}
           hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
           openBehaviorEvents={this.props.openBehaviorEvents}
-          onObjectEdited={this.props.onEventsBasedObjectChildrenEdited}
+          onObjectEdited={() =>
+            this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject)
+          }
+          onObjectGroupEdited={() =>
+            this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject)
+          }
           onEventsBasedObjectChildrenEdited={
             this.props.onEventsBasedObjectChildrenEdited
           }
           onExtractAsEventBasedObject={this.props.onExtractAsEventBasedObject}
           onOpenEventBasedObjectEditor={this.props.onOpenEventBasedObjectEditor}
+          onOpenEventBasedObjectVariantEditor={
+            this.props.onOpenEventBasedObjectVariantEditor
+          }
           onExtensionInstalled={this.props.onExtensionInstalled}
+          onDeleteEventsBasedObjectVariant={
+            this.props.onDeleteEventsBasedObjectVariant
+          }
         />
       </div>
     );
diff --git a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js
index aef19e6b3f93..8287712fa7f7 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js
@@ -169,7 +169,8 @@ export class EventsFunctionsExtensionEditorContainer extends React.Component<Ren
           onOpenCustomObjectEditor={eventsBasedObject => {
             this.props.onOpenCustomObjectEditor(
               eventsFunctionsExtension,
-              eventsBasedObject
+              eventsBasedObject,
+              ''
             );
           }}
           hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
diff --git a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js
index 23c5a8bb30c0..8c1a26d4e03d 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js
@@ -235,6 +235,7 @@ export class ExternalLayoutEditorContainer extends React.Component<
             layout={layout}
             eventsFunctionsExtension={null}
             eventsBasedObject={null}
+            eventsBasedObjectVariant={null}
             globalObjectsContainer={project.getObjects()}
             objectsContainer={layout.getObjects()}
             layersContainer={layout.getLayers()}
@@ -259,12 +260,20 @@ export class ExternalLayoutEditorContainer extends React.Component<
             onOpenEventBasedObjectEditor={
               this.props.onOpenEventBasedObjectEditor
             }
+            onOpenEventBasedObjectVariantEditor={
+              this.props.onOpenEventBasedObjectVariantEditor
+            }
             onObjectEdited={objectWithContext =>
               this.props.onSceneObjectEdited(layout, objectWithContext)
             }
+            // It's only used to refresh events-based object variants.
+            onObjectGroupEdited={() => {}}
             // Nothing to do as events-based objects can't have external layout.
             onEventsBasedObjectChildrenEdited={() => {}}
             onExtensionInstalled={this.props.onExtensionInstalled}
+            onDeleteEventsBasedObjectVariant={
+              this.props.onDeleteEventsBasedObjectVariant
+            }
           />
         )}
         {!layout && (
diff --git a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js
index a067f66738c5..b25ba5f3b880 100644
--- a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js
+++ b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js
@@ -129,6 +129,7 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
         layout={layout}
         eventsFunctionsExtension={null}
         eventsBasedObject={null}
+        eventsBasedObjectVariant={null}
         globalObjectsContainer={project.getObjects()}
         objectsContainer={layout.getObjects()}
         layersContainer={layout.getLayers()}
@@ -149,10 +150,18 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
         onExtractAsExternalLayout={this.props.onExtractAsExternalLayout}
         onExtractAsEventBasedObject={this.props.onExtractAsEventBasedObject}
         onOpenEventBasedObjectEditor={this.props.onOpenEventBasedObjectEditor}
+        onOpenEventBasedObjectVariantEditor={
+          this.props.onOpenEventBasedObjectVariantEditor
+        }
         onExtensionInstalled={this.props.onExtensionInstalled}
+        onDeleteEventsBasedObjectVariant={
+          this.props.onDeleteEventsBasedObjectVariant
+        }
         onObjectEdited={objectWithContext =>
           this.props.onSceneObjectEdited(layout, objectWithContext)
         }
+        // It's only used to refresh events-based object variants.
+        onObjectGroupEdited={() => {}}
         // Nothing to do as scenes are not events-based objects.
         onEventsBasedObjectChildrenEdited={() => {}}
       />
diff --git a/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js b/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js
index d1e59ef3c32b..79fd2ee29f31 100644
--- a/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js
+++ b/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js
@@ -374,6 +374,29 @@ export const closeCustomObjectTab = (
   });
 };
 
+export const closeEventsBasedObjectVariantTab = (
+  state: EditorTabsState,
+  eventsFunctionsExtensionName: string,
+  eventsBasedObjectName: string,
+  eventsBasedObjectVariantName: string
+) => {
+  return closeTabsExceptIf(state, editorTab => {
+    const editor = editorTab.editorRef;
+    if (editor instanceof CustomObjectEditorContainer) {
+      return (
+        (!editor.getEventsFunctionsExtensionName() ||
+          editor.getEventsFunctionsExtensionName() !==
+            eventsFunctionsExtensionName) &&
+        (!editor.getEventsBasedObjectName() ||
+          editor.getEventsBasedObjectName() !== eventsBasedObjectName) &&
+        (!editor.getVariantName() ||
+          editor.getVariantName() !== eventsBasedObjectVariantName)
+      );
+    }
+    return true;
+  });
+};
+
 export const getEventsFunctionsExtensionEditor = (
   state: EditorTabsState,
   eventsFunctionsExtension: gdEventsFunctionsExtension
@@ -394,14 +417,16 @@ export const getEventsFunctionsExtensionEditor = (
 export const getCustomObjectEditor = (
   state: EditorTabsState,
   eventsFunctionsExtension: gdEventsFunctionsExtension,
-  eventsBasedObject: gdEventsBasedObject
+  eventsBasedObject: gdEventsBasedObject,
+  variantName: string
 ): ?{| editor: CustomObjectEditorContainer, tabIndex: number |} => {
   for (let tabIndex = 0; tabIndex < state.editors.length; ++tabIndex) {
     const editor = state.editors[tabIndex].editorRef;
     if (
       editor instanceof CustomObjectEditorContainer &&
       editor.getEventsFunctionsExtension() === eventsFunctionsExtension &&
-      editor.getEventsBasedObject() === eventsBasedObject
+      editor.getEventsBasedObject() === eventsBasedObject &&
+      editor.getVariantName() === variantName
     ) {
       return { editor, tabIndex };
     }
diff --git a/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js b/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js
index 632fe807ada7..0fe8a31c056a 100644
--- a/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js
+++ b/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js
@@ -48,7 +48,17 @@ const projectHasItem = ({
     case 'external events':
       return project.hasExternalEventsNamed(name);
     case 'custom object':
-      return project.hasEventsBasedObject(name);
+      const nameElements = name.split('::');
+      const objectType = nameElements[0] + '::' + nameElements[1];
+      const variantName = nameElements[2];
+      return (
+        project.hasEventsBasedObject(objectType) &&
+        (!variantName ||
+          project
+            .getEventsBasedObject(objectType)
+            .getVariants()
+            .getVariant(variantName))
+      );
     default:
       return false;
   }
diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js
index 568df82f1e51..542f76ea6692 100644
--- a/newIDE/app/src/MainFrame/index.js
+++ b/newIDE/app/src/MainFrame/index.js
@@ -38,6 +38,7 @@ import {
   closeExternalEventsTabs,
   closeEventsFunctionsExtensionTabs,
   closeCustomObjectTab,
+  closeEventsBasedObjectVariantTab,
   saveUiSettings,
   type EditorTabsState,
   type EditorTab,
@@ -585,7 +586,8 @@ const MainFrame = (props: Props) => {
           : kind === 'layout events'
           ? name + ` ${i18n._(t`(Events)`)}`
           : kind === 'custom object'
-          ? name.split('::')[1] + ` ${i18n._(t`(Object)`)}`
+          ? name.split('::')[2] ||
+            name.split('::')[1] + ` ${i18n._(t`(Object)`)}`
           : name;
       const tabOptions =
         kind === 'layout'
@@ -1249,12 +1251,12 @@ const MainFrame = (props: Props) => {
     toolbar.current.setEditorToolbar(editorToolbar);
   };
 
-  const onInstallExtension = (extensionShortHeader: ExtensionShortHeader) => {
+  const onInstallExtension = (extensionName: string) => {
     const { currentProject } = state;
     if (!currentProject) return;
 
     // Close the extension tab before updating/reinstalling the extension.
-    const eventsFunctionsExtensionName = extensionShortHeader.name;
+    const eventsFunctionsExtensionName = extensionName;
 
     if (
       currentProject.hasEventsFunctionsExtensionNamed(
@@ -1378,6 +1380,20 @@ const MainFrame = (props: Props) => {
   };
 
   const onExtensionInstalled = (extensionName: string) => {
+    const { currentProject } = state;
+    if (!currentProject) {
+      return;
+    }
+    const eventsBasedObjects = currentProject
+      .getEventsFunctionsExtension(extensionName)
+      .getEventsBasedObjects();
+    for (let index = 0; index < eventsBasedObjects.getCount(); index++) {
+      const eventsBasedObject = eventsBasedObjects.getAt(index);
+      gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject(
+        currentProject,
+        eventsBasedObject
+      );
+    }
     // TODO Open the closed tabs back
     // It would be safer to close the tabs before the extension is installed
     // but it would make opening them back more complicated.
@@ -1563,6 +1579,29 @@ const MainFrame = (props: Props) => {
     }));
   };
 
+  const deleteEventsBasedObjectVariant = (
+    eventsFunctionsExtension: gdEventsFunctionsExtension,
+    eventBasedObject: gdEventsBasedObject,
+    variant: gdEventsBasedObjectVariant
+  ): void => {
+    const variants = eventBasedObject.getVariants();
+    const variantName = variant.getName();
+    if (!variants.hasVariantNamed(variantName)) {
+      return;
+    }
+    variants.removeVariant(variantName);
+
+    setState(state => ({
+      ...state,
+      editorTabs: closeEventsBasedObjectVariantTab(
+        state.editorTabs,
+        eventsFunctionsExtension.getName(),
+        eventBasedObject.getName(),
+        variantName
+      ),
+    }));
+  };
+
   const setPreviewedLayout = (
     previewLayoutName: ?string,
     previewExternalLayoutName?: ?string
@@ -2073,7 +2112,8 @@ const MainFrame = (props: Props) => {
   const openCustomObjectEditor = React.useCallback(
     (
       eventsFunctionsExtension: gdEventsFunctionsExtension,
-      eventsBasedObject: gdEventsBasedObject
+      eventsBasedObject: gdEventsBasedObject,
+      variantName: string
     ) => {
       const { currentProject, editorTabs } = state;
       if (!currentProject) return;
@@ -2081,7 +2121,8 @@ const MainFrame = (props: Props) => {
       const foundTab = getCustomObjectEditor(
         editorTabs,
         eventsFunctionsExtension,
-        eventsBasedObject
+        eventsBasedObject,
+        variantName
       );
       if (foundTab) {
         setState(state => ({
@@ -2098,7 +2139,10 @@ const MainFrame = (props: Props) => {
               name:
                 eventsFunctionsExtension.getName() +
                 '::' +
-                eventsBasedObject.getName(),
+                eventsBasedObject.getName() +
+                (eventsBasedObject.getVariants().hasVariantNamed(variantName)
+                  ? '::' + variantName
+                  : ''),
               project: currentProject,
             }),
           }),
@@ -2182,13 +2226,41 @@ const MainFrame = (props: Props) => {
     extensionName: string,
     eventsBasedObjectName: string
   ) => {
-    if (!currentProject) return;
+    if (
+      !currentProject ||
+      !currentProject.hasEventsFunctionsExtensionNamed(extensionName)
+    ) {
+      return;
+    }
     openEventsFunctionsExtension(
       extensionName,
       null,
       null,
       eventsBasedObjectName
     );
+    const eventsFunctionsExtension = currentProject.getEventsFunctionsExtension(
+      extensionName
+    );
+    const eventsBasedObjects = eventsFunctionsExtension.getEventsBasedObjects();
+    if (!eventsBasedObjects.has(eventsBasedObjectName)) {
+      return;
+    }
+    const eventsBasedObject = eventsBasedObjects.get(eventsBasedObjectName);
+    openCustomObjectEditor(eventsFunctionsExtension, eventsBasedObject, '');
+
+    // Trigger reloading of extensions as an extension was modified (or even added)
+    // to create the custom object.
+    eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions(
+      currentProject
+    );
+  };
+
+  const onOpenEventBasedObjectVariantEditor = (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => {
+    if (!currentProject) return;
     if (!currentProject.hasEventsFunctionsExtensionNamed(extensionName)) {
       return;
     }
@@ -2200,7 +2272,11 @@ const MainFrame = (props: Props) => {
       return;
     }
     const eventsBasedObject = eventsBasedObjects.get(eventsBasedObjectName);
-    openCustomObjectEditor(eventsFunctionsExtension, eventsBasedObject);
+    openCustomObjectEditor(
+      eventsFunctionsExtension,
+      eventsBasedObject,
+      variantName
+    );
 
     // Trigger reloading of extensions as an extension was modified (or even added)
     // to create the custom object.
@@ -2210,7 +2286,16 @@ const MainFrame = (props: Props) => {
   };
 
   const onEventsBasedObjectChildrenEdited = React.useCallback(
-    () => {
+    (eventsBasedObject: gdEventsBasedObject) => {
+      const project = state.currentProject;
+      if (!project) {
+        return;
+      }
+      gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject(
+        project,
+        eventsBasedObject
+      );
+
       for (const editor of state.editorTabs.editors) {
         const { editorRef } = editor;
         if (editorRef) {
@@ -2218,7 +2303,7 @@ const MainFrame = (props: Props) => {
         }
       }
     },
-    [state.editorTabs]
+    [state.editorTabs, state.currentProject]
   );
 
   const onSceneObjectEdited = React.useCallback(
@@ -3886,6 +3971,8 @@ const MainFrame = (props: Props) => {
                     onExtractAsExternalLayout: onExtractAsExternalLayout,
                     onExtractAsEventBasedObject: onOpenEventBasedObjectEditor,
                     onOpenEventBasedObjectEditor: onOpenEventBasedObjectEditor,
+                    onOpenEventBasedObjectVariantEditor: onOpenEventBasedObjectVariantEditor,
+                    onDeleteEventsBasedObjectVariant: deleteEventsBasedObjectVariant,
                     onEventsBasedObjectChildrenEdited: onEventsBasedObjectChildrenEdited,
                     onSceneObjectEdited: onSceneObjectEdited,
                     onExtensionInstalled: onExtensionInstalled,
diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js
index 32db274f6065..e2ce7e299ef2 100644
--- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js
+++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js
@@ -157,7 +157,7 @@ const TopLevelCollapsibleSection = ({
   renderContentAsHiddenWhenFolded?: boolean,
   noContentMargin?: boolean,
   onOpenFullEditor: () => void,
-  onAdd?: () => void,
+  onAdd?: (() => void) | null,
 |}) => (
   <>
     <Separator />
@@ -216,6 +216,8 @@ type Props = {|
   objects: Array<gdObject>,
   onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
   onExtensionInstalled: (extensionName: string) => void,
+  isVariableListLocked: boolean,
+  isBehaviorListLocked: boolean,
 |};
 
 export const CompactObjectPropertiesEditor = ({
@@ -234,6 +236,8 @@ export const CompactObjectPropertiesEditor = ({
   objects,
   onEditObject,
   onExtensionInstalled,
+  isVariableListLocked,
+  isBehaviorListLocked,
 }: Props) => {
   const forceUpdate = useForceUpdate();
   const [
@@ -545,7 +549,7 @@ export const CompactObjectPropertiesEditor = ({
             isFolded={isBehaviorsFolded}
             toggleFolded={() => setIsBehaviorsFolded(!isBehaviorsFolded)}
             onOpenFullEditor={() => onEditObject(object, 'behaviors')}
-            onAdd={openNewBehaviorDialog}
+            onAdd={isBehaviorListLocked ? null : openNewBehaviorDialog}
             renderContent={() => (
               <ColumnStackLayout noMargin>
                 {!allVisibleBehaviors.length && (
@@ -618,12 +622,16 @@ export const CompactObjectPropertiesEditor = ({
             isFolded={isVariablesFolded}
             toggleFolded={() => setIsVariablesFolded(!isVariablesFolded)}
             onOpenFullEditor={() => onEditObject(object, 'variables')}
-            onAdd={() => {
-              if (variablesListRef.current) {
-                variablesListRef.current.addVariable();
-              }
-              setIsVariablesFolded(false);
-            }}
+            onAdd={
+              isVariableListLocked
+                ? null
+                : () => {
+                    if (variablesListRef.current) {
+                      variablesListRef.current.addVariable();
+                    }
+                    setIsVariablesFolded(false);
+                  }
+            }
             renderContentAsHiddenWhenFolded={
               true /* Allows to keep a ref to the variables list for add button to work. */
             }
@@ -664,6 +672,7 @@ export const CompactObjectPropertiesEditor = ({
                     on this object.
                   </Trans>
                 }
+                isListLocked={isVariableListLocked}
               />
             )}
           />
diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js
deleted file mode 100644
index 8e985bb206e6..000000000000
--- a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js
+++ /dev/null
@@ -1,534 +0,0 @@
-// @flow
-import { Trans } from '@lingui/macro';
-import { t } from '@lingui/macro';
-import { I18n } from '@lingui/react';
-
-import * as React from 'react';
-import PropertiesEditor from '../../PropertiesEditor';
-import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema';
-import EmptyMessage from '../../UI/EmptyMessage';
-import { type EditorProps } from './EditorProps.flow';
-import { Column, Line } from '../../UI/Grid';
-import { getExtraObjectsInformation } from '../../Hints';
-import { getObjectTutorialIds } from '../../Utils/GDevelopServices/Tutorial';
-import AlertMessage from '../../UI/AlertMessage';
-import { ColumnStackLayout } from '../../UI/Layout';
-import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
-import { mapFor } from '../../Utils/MapFor';
-import ObjectsEditorService from '../ObjectsEditorService';
-import Text from '../../UI/Text';
-import useForceUpdate from '../../Utils/UseForceUpdate';
-import { Accordion, AccordionHeader, AccordionBody } from '../../UI/Accordion';
-import { IconContainer } from '../../UI/IconContainer';
-import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext';
-import AnimationList, {
-  type AnimationListInterface,
-} from './SpriteEditor/AnimationList';
-import PointsEditor from './SpriteEditor/PointsEditor';
-import CollisionMasksEditor from './SpriteEditor/CollisionMasksEditor';
-import {
-  hasAnyFrame,
-  getFirstAnimationFrame,
-  setCollisionMaskOnAllFrames,
-} from './SpriteEditor/Utils/SpriteObjectHelper';
-import { getMatchingCollisionMask } from './SpriteEditor/CollisionMasksEditor/CollisionMaskHelper';
-import ResourcesLoader from '../../ResourcesLoader';
-import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
-import FlatButton from '../../UI/FlatButton';
-import RaisedButton from '../../UI/RaisedButton';
-import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu';
-import { ResponsiveLineStackLayout } from '../../UI/Layout';
-import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer';
-import Add from '../../UI/CustomSvgIcons/Add';
-import Dialog from '../../UI/Dialog';
-import HelpButton from '../../UI/HelpButton';
-import RestoreIcon from '../../UI/CustomSvgIcons/Restore';
-
-const gd: libGDevelop = global.gd;
-
-const styles = {
-  icon: { width: 16, height: 16 },
-};
-
-type Props = EditorProps;
-
-const CustomObjectPropertiesEditor = (props: Props) => {
-  const forceUpdate = useForceUpdate();
-
-  const {
-    objectConfiguration,
-    project,
-    layout,
-    eventsFunctionsExtension,
-    eventsBasedObject,
-    object,
-    objectName,
-    resourceManagementProps,
-    onSizeUpdated,
-    onObjectUpdated,
-    unsavedChanges,
-    renderObjectNameField,
-    isChildObject,
-  } = props;
-
-  const { isMobile } = useResponsiveWindowSize();
-
-  const customObjectConfiguration = gd.asCustomObjectConfiguration(
-    objectConfiguration
-  );
-  const properties = customObjectConfiguration.getProperties();
-
-  const propertiesSchema = propertiesMapToSchema(
-    properties,
-    object => object.getProperties(),
-    (object, name, value) => object.updateProperty(name, value)
-  );
-
-  const extraInformation = getExtraObjectsInformation()[
-    customObjectConfiguration.getType()
-  ];
-
-  const { values } = React.useContext(PreferencesContext);
-  const tutorialIds = getObjectTutorialIds(customObjectConfiguration.getType());
-
-  const eventBasedObject = project.hasEventsBasedObject(
-    customObjectConfiguration.getType()
-  )
-    ? project.getEventsBasedObject(customObjectConfiguration.getType())
-    : null;
-
-  const animations = customObjectConfiguration.getAnimations();
-
-  // The matching collision mask only takes the first sprite of the first
-  // animation of the object. We consider this is enough to start with, and
-  // the user can then edit the collision mask for further needs.
-  const onCreateMatchingSpriteCollisionMask = React.useCallback(
-    async () => {
-      const firstSprite = getFirstAnimationFrame(animations);
-      if (!firstSprite) {
-        return;
-      }
-      const firstSpriteResourceName = firstSprite.getImageName();
-      const firstAnimationResourceSource = ResourcesLoader.getResourceFullUrl(
-        project,
-        firstSpriteResourceName,
-        {}
-      );
-      let matchingCollisionMask = null;
-      try {
-        matchingCollisionMask = await getMatchingCollisionMask(
-          firstAnimationResourceSource
-        );
-      } catch (e) {
-        console.error(
-          'Unable to create a matching collision mask for the sprite, fallback to full image collision mask.',
-          e
-        );
-      }
-      setCollisionMaskOnAllFrames(animations, matchingCollisionMask);
-      forceUpdate();
-    },
-    [animations, project, forceUpdate]
-  );
-
-  const scrollView = React.useRef<?ScrollViewInterface>(null);
-  const animationList = React.useRef<?AnimationListInterface>(null);
-
-  const [
-    justAddedAnimationName,
-    setJustAddedAnimationName,
-  ] = React.useState<?string>(null);
-  const justAddedAnimationElement = React.useRef<?any>(null);
-
-  React.useEffect(
-    () => {
-      if (
-        scrollView.current &&
-        justAddedAnimationElement.current &&
-        justAddedAnimationName
-      ) {
-        scrollView.current.scrollTo(justAddedAnimationElement.current);
-        setJustAddedAnimationName(null);
-        justAddedAnimationElement.current = null;
-      }
-    },
-    [justAddedAnimationName]
-  );
-
-  const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false);
-  const [
-    collisionMasksEditorOpen,
-    setCollisionMasksEditorOpen,
-  ] = React.useState(false);
-
-  return (
-    <I18n>
-      {({ i18n }) => (
-        <>
-          <ScrollView ref={scrollView}>
-            <ColumnStackLayout noMargin>
-              {renderObjectNameField && renderObjectNameField()}
-              {tutorialIds.map(tutorialId => (
-                <DismissableTutorialMessage
-                  key={tutorialId}
-                  tutorialId={tutorialId}
-                />
-              ))}
-              {propertiesSchema.length ||
-              (eventBasedObject &&
-                (eventBasedObject.getObjects().getObjectsCount() ||
-                  eventBasedObject.isAnimatable())) ? (
-                <React.Fragment>
-                  {extraInformation ? (
-                    <Line>
-                      <ColumnStackLayout noMargin>
-                        {extraInformation.map(({ kind, message }, index) => (
-                          <AlertMessage kind={kind} key={index}>
-                            {i18n._(message)}
-                          </AlertMessage>
-                        ))}
-                      </ColumnStackLayout>
-                    </Line>
-                  ) : null}
-                  <PropertiesEditor
-                    unsavedChanges={unsavedChanges}
-                    schema={propertiesSchema}
-                    instances={[customObjectConfiguration]}
-                    project={project}
-                    resourceManagementProps={resourceManagementProps}
-                  />
-                  {eventBasedObject &&
-                    (!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() &&
-                    !customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? (
-                      <Line alignItems="center">
-                        <Column expand noMargin>
-                          <Text size="block-title">Children objects</Text>
-                        </Column>
-                        <Column alignItems="right">
-                          <FlatButton
-                            label={
-                              <Trans>Override children configuration</Trans>
-                            }
-                            onClick={() => {
-                              customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
-                                true
-                              );
-                              customObjectConfiguration.clearChildrenConfiguration();
-                              if (onObjectUpdated) {
-                                onObjectUpdated();
-                              }
-                              forceUpdate();
-                            }}
-                          />
-                        </Column>
-                      </Line>
-                    ) : (
-                      <>
-                        {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && (
-                          <Line alignItems="center">
-                            <Column expand noMargin>
-                              <Text size="block-title">Children objects</Text>
-                            </Column>
-                            <Column alignItems="right">
-                              <FlatButton
-                                leftIcon={<RestoreIcon style={styles.icon} />}
-                                label={
-                                  <Trans>
-                                    Reset and hide children configuration
-                                  </Trans>
-                                }
-                                onClick={() => {
-                                  customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
-                                    false
-                                  );
-                                  customObjectConfiguration.clearChildrenConfiguration();
-                                  if (onObjectUpdated) {
-                                    onObjectUpdated();
-                                  }
-                                  forceUpdate();
-                                }}
-                              />
-                            </Column>
-                          </Line>
-                        )}
-                        {mapFor(
-                          0,
-                          eventBasedObject.getObjects().getObjectsCount(),
-                          i => {
-                            const childObject = eventBasedObject
-                              .getObjects()
-                              .getObjectAt(i);
-                            const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
-                              childObject.getName()
-                            );
-                            const editorConfiguration = ObjectsEditorService.getEditorConfiguration(
-                              project,
-                              childObjectConfiguration.getType()
-                            );
-                            const EditorComponent =
-                              editorConfiguration.component;
-
-                            const objectMetadata = gd.MetadataProvider.getObjectMetadata(
-                              gd.JsPlatform.get(),
-                              childObjectConfiguration.getType()
-                            );
-                            const iconUrl = objectMetadata.getIconFilename();
-                            const tutorialIds = getObjectTutorialIds(
-                              childObjectConfiguration.getType()
-                            );
-                            const enabledTutorialIds = tutorialIds.filter(
-                              tutorialId =>
-                                !values.hiddenTutorialHints[tutorialId]
-                            );
-                            // TODO EBO: Add a protection against infinite loops in case
-                            // of object cycles (thought it should be forbidden).
-                            return (
-                              <Accordion
-                                key={childObject.getName()}
-                                defaultExpanded
-                              >
-                                <AccordionHeader>
-                                  {iconUrl ? (
-                                    <IconContainer
-                                      src={iconUrl}
-                                      alt={childObject.getName()}
-                                      size={20}
-                                    />
-                                  ) : null}
-                                  <Column expand>
-                                    <Text size="block-title">
-                                      {childObject.getName()}
-                                    </Text>
-                                  </Column>
-                                </AccordionHeader>
-                                <AccordionBody>
-                                  <Column expand noMargin noOverflowParent>
-                                    {enabledTutorialIds.length ? (
-                                      <Line>
-                                        <ColumnStackLayout expand>
-                                          {tutorialIds.map(tutorialId => (
-                                            <DismissableTutorialMessage
-                                              key={tutorialId}
-                                              tutorialId={tutorialId}
-                                            />
-                                          ))}
-                                        </ColumnStackLayout>
-                                      </Line>
-                                    ) : null}
-                                    <Line noMargin>
-                                      <Column expand>
-                                        <EditorComponent
-                                          isChildObject
-                                          objectConfiguration={
-                                            childObjectConfiguration
-                                          }
-                                          project={project}
-                                          layout={layout}
-                                          eventsFunctionsExtension={
-                                            eventsFunctionsExtension
-                                          }
-                                          eventsBasedObject={eventsBasedObject}
-                                          resourceManagementProps={
-                                            resourceManagementProps
-                                          }
-                                          onSizeUpdated={
-                                            forceUpdate /*Force update to ensure dialog is properly positioned*/
-                                          }
-                                          objectName={
-                                            objectName +
-                                            ' ' +
-                                            childObject.getName()
-                                          }
-                                          onObjectUpdated={onObjectUpdated}
-                                          unsavedChanges={unsavedChanges}
-                                        />
-                                      </Column>
-                                    </Line>
-                                  </Column>
-                                </AccordionBody>
-                              </Accordion>
-                            );
-                          }
-                        )}
-                      </>
-                    ))}
-                  {eventBasedObject && eventBasedObject.isAnimatable() && (
-                    <Column expand>
-                      <Text size="block-title">
-                        <Trans>Animations</Trans>
-                      </Text>
-                      <AnimationList
-                        ref={animationList}
-                        animations={animations}
-                        project={project}
-                        layout={layout}
-                        eventsFunctionsExtension={eventsFunctionsExtension}
-                        eventsBasedObject={eventsBasedObject}
-                        object={object}
-                        objectName={objectName}
-                        resourceManagementProps={resourceManagementProps}
-                        onSizeUpdated={onSizeUpdated}
-                        onObjectUpdated={onObjectUpdated}
-                        isAnimationListLocked={false}
-                        scrollView={scrollView}
-                        onCreateMatchingSpriteCollisionMask={
-                          onCreateMatchingSpriteCollisionMask
-                        }
-                      />
-                    </Column>
-                  )}
-                </React.Fragment>
-              ) : (
-                <EmptyMessage>
-                  <Trans>
-                    There is nothing to configure for this object. You can still
-                    use events to interact with the object.
-                  </Trans>
-                </EmptyMessage>
-              )}
-            </ColumnStackLayout>
-          </ScrollView>
-          {eventBasedObject &&
-            eventBasedObject.isAnimatable() &&
-            !isChildObject && (
-              <Column noMargin>
-                <ResponsiveLineStackLayout
-                  justifyContent="space-between"
-                  noColumnMargin
-                >
-                  {!isMobile ? ( // On mobile, use only 1 button to gain space.
-                    <ResponsiveLineStackLayout noMargin noColumnMargin>
-                      <FlatButton
-                        label={<Trans>Edit collision masks</Trans>}
-                        onClick={() => setCollisionMasksEditorOpen(true)}
-                        disabled={!hasAnyFrame(animations)}
-                      />
-                      <FlatButton
-                        label={<Trans>Edit points</Trans>}
-                        onClick={() => setPointsEditorOpen(true)}
-                        disabled={!hasAnyFrame(animations)}
-                      />
-                    </ResponsiveLineStackLayout>
-                  ) : (
-                    <FlatButtonWithSplitMenu
-                      label={<Trans>Edit collision masks</Trans>}
-                      onClick={() => setCollisionMasksEditorOpen(true)}
-                      disabled={!hasAnyFrame(animations)}
-                      buildMenuTemplate={i18n => [
-                        {
-                          label: i18n._(t`Edit points`),
-                          disabled: !hasAnyFrame(animations),
-                          click: () => setPointsEditorOpen(true),
-                        },
-                      ]}
-                    />
-                  )}
-                  <RaisedButton
-                    label={<Trans>Add an animation</Trans>}
-                    primary
-                    onClick={() => {
-                      if (!animationList.current) {
-                        return;
-                      }
-                      animationList.current.addAnimation();
-                    }}
-                    icon={<Add />}
-                  />
-                </ResponsiveLineStackLayout>
-              </Column>
-            )}
-          {pointsEditorOpen && (
-            <Dialog
-              title={<Trans>Edit points</Trans>}
-              actions={[
-                <RaisedButton
-                  key="apply"
-                  label={<Trans>Apply</Trans>}
-                  primary
-                  onClick={() => setPointsEditorOpen(false)}
-                />,
-              ]}
-              secondaryActions={[
-                <HelpButton
-                  helpPagePath="/objects/sprite/edit-points"
-                  key="help"
-                />,
-              ]}
-              onRequestClose={() => setPointsEditorOpen(false)}
-              maxWidth="lg"
-              flexBody
-              fullHeight
-              open={pointsEditorOpen}
-            >
-              <PointsEditor
-                animations={animations}
-                resourcesLoader={ResourcesLoader}
-                project={project}
-                onPointsUpdated={onObjectUpdated}
-                onRenamedPoint={(oldName, newName) => {
-                  if (!object) {
-                    return;
-                  }
-                  if (layout) {
-                    gd.WholeProjectRefactorer.renameObjectPointInScene(
-                      project,
-                      layout,
-                      object,
-                      oldName,
-                      newName
-                    );
-                  } else if (eventsFunctionsExtension && eventsBasedObject) {
-                    gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject(
-                      project,
-                      eventsFunctionsExtension,
-                      eventsBasedObject,
-                      object,
-                      oldName,
-                      newName
-                    );
-                  }
-                }}
-              />
-            </Dialog>
-          )}
-          {collisionMasksEditorOpen && (
-            <Dialog
-              title={<Trans>Edit collision masks</Trans>}
-              actions={[
-                <RaisedButton
-                  key="apply"
-                  label={<Trans>Apply</Trans>}
-                  primary
-                  onClick={() => setCollisionMasksEditorOpen(false)}
-                />,
-              ]}
-              secondaryActions={[
-                <HelpButton
-                  helpPagePath="/objects/sprite/collision-mask"
-                  key="help"
-                />,
-              ]}
-              maxWidth="lg"
-              flexBody
-              fullHeight
-              onRequestClose={() => setCollisionMasksEditorOpen(false)}
-              open={collisionMasksEditorOpen}
-            >
-              <CollisionMasksEditor
-                animations={animations}
-                resourcesLoader={ResourcesLoader}
-                project={project}
-                onMasksUpdated={onObjectUpdated}
-                onCreateMatchingSpriteCollisionMask={
-                  onCreateMatchingSpriteCollisionMask
-                }
-              />
-            </Dialog>
-          )}
-        </>
-      )}
-    </I18n>
-  );
-};
-
-export default CustomObjectPropertiesEditor;
diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js
new file mode 100644
index 000000000000..043f3726e1bc
--- /dev/null
+++ b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js
@@ -0,0 +1,68 @@
+// @flow
+import { t, Trans } from '@lingui/macro';
+import React from 'react';
+import FlatButton from '../../../UI/FlatButton';
+import Dialog, { DialogPrimaryButton } from '../../../UI/Dialog';
+import SemiControlledTextField from '../../../UI/SemiControlledTextField';
+import HelpButton from '../../../UI/HelpButton';
+
+type Props = {|
+  initialName: string,
+  onApply: (variantName: string) => void,
+  onCancel: () => void,
+|};
+
+const NewVariantDialog = ({ initialName, onApply, onCancel }: Props) => {
+  const [variantName, setVariantName] = React.useState<string>(initialName);
+
+  const apply = React.useCallback(
+    () => {
+      onApply(variantName);
+    },
+    [onApply, variantName]
+  );
+
+  return (
+    <Dialog
+      title={<Trans>Create a new variant</Trans>}
+      id="create-variant-dialog"
+      actions={[
+        <FlatButton
+          key="cancel"
+          label={<Trans>Cancel</Trans>}
+          onClick={onCancel}
+        />,
+        <DialogPrimaryButton
+          key="apply"
+          label={<Trans>Create</Trans>}
+          primary
+          onClick={apply}
+        />,
+      ]}
+      secondaryActions={[
+        <HelpButton
+          key="help-button"
+          helpPagePath="/objects/custom-objects-prefab-template"
+        />,
+      ]}
+      onRequestClose={onCancel}
+      onApply={apply}
+      open
+      maxWidth="sm"
+    >
+      <SemiControlledTextField
+        fullWidth
+        id="variant-name"
+        commitOnBlur
+        floatingLabelText={<Trans>Variant name</Trans>}
+        floatingLabelFixed
+        value={variantName}
+        translatableHintText={t`Variant name`}
+        onChange={setVariantName}
+        autoFocus="desktop"
+      />
+    </Dialog>
+  );
+};
+
+export default NewVariantDialog;
diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js
new file mode 100644
index 000000000000..7c0bc0c3b16e
--- /dev/null
+++ b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js
@@ -0,0 +1,756 @@
+// @flow
+import { Trans } from '@lingui/macro';
+import { t } from '@lingui/macro';
+import { I18n } from '@lingui/react';
+import { type I18n as I18nType } from '@lingui/core';
+
+import * as React from 'react';
+import PropertiesEditor from '../../../PropertiesEditor';
+import propertiesMapToSchema from '../../../PropertiesEditor/PropertiesMapToSchema';
+import EmptyMessage from '../../../UI/EmptyMessage';
+import { type EditorProps } from '../EditorProps.flow';
+import { Column, Line } from '../../../UI/Grid';
+import { getExtraObjectsInformation } from '../../../Hints';
+import { getObjectTutorialIds } from '../../../Utils/GDevelopServices/Tutorial';
+import AlertMessage from '../../../UI/AlertMessage';
+import DismissableTutorialMessage from '../../../Hints/DismissableTutorialMessage';
+import { mapFor } from '../../../Utils/MapFor';
+import ObjectsEditorService from '../../ObjectsEditorService';
+import Text from '../../../UI/Text';
+import useForceUpdate from '../../../Utils/UseForceUpdate';
+import {
+  Accordion,
+  AccordionHeader,
+  AccordionBody,
+} from '../../../UI/Accordion';
+import { IconContainer } from '../../../UI/IconContainer';
+import PreferencesContext from '../../../MainFrame/Preferences/PreferencesContext';
+import AnimationList, {
+  type AnimationListInterface,
+} from '../SpriteEditor/AnimationList';
+import PointsEditor from '../SpriteEditor/PointsEditor';
+import CollisionMasksEditor from '../SpriteEditor/CollisionMasksEditor';
+import {
+  hasAnyFrame,
+  getFirstAnimationFrame,
+  setCollisionMaskOnAllFrames,
+} from '../SpriteEditor/Utils/SpriteObjectHelper';
+import { getMatchingCollisionMask } from '../SpriteEditor/CollisionMasksEditor/CollisionMaskHelper';
+import ResourcesLoader from '../../../ResourcesLoader';
+import ScrollView, { type ScrollViewInterface } from '../../../UI/ScrollView';
+import FlatButton from '../../../UI/FlatButton';
+import RaisedButton from '../../../UI/RaisedButton';
+import FlatButtonWithSplitMenu from '../../../UI/FlatButtonWithSplitMenu';
+import {
+  ResponsiveLineStackLayout,
+  LineStackLayout,
+  ColumnStackLayout,
+} from '../../../UI/Layout';
+import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer';
+import Add from '../../../UI/CustomSvgIcons/Add';
+import Trash from '../../../UI/CustomSvgIcons/Trash';
+import Edit from '../../../UI/CustomSvgIcons/ShareExternal';
+import Dialog from '../../../UI/Dialog';
+import HelpButton from '../../../UI/HelpButton';
+import RestoreIcon from '../../../UI/CustomSvgIcons/Restore';
+import SelectField from '../../../UI/SelectField';
+import SelectOption from '../../../UI/SelectOption';
+import NewVariantDialog from './NewVariantDialog';
+import newNameGenerator from '../../../Utils/NewNameGenerator';
+import {
+  serializeToJSObject,
+  unserializeFromJSObject,
+} from '../../../Utils/Serializer';
+
+const gd: libGDevelop = global.gd;
+
+const styles = {
+  icon: { width: 16, height: 16 },
+};
+
+const getVariantName = (
+  eventBasedObject: gdEventsBasedObject | null,
+  customObjectConfiguration: gdCustomObjectConfiguration
+): string =>
+  eventBasedObject &&
+  eventBasedObject
+    .getVariants()
+    .hasVariantNamed(customObjectConfiguration.getVariantName())
+    ? customObjectConfiguration.getVariantName()
+    : '';
+
+const getVariant = (
+  eventBasedObject: gdEventsBasedObject,
+  customObjectConfiguration: gdCustomObjectConfiguration
+): gdEventsBasedObjectVariant => {
+  const variantName = getVariantName(
+    eventBasedObject,
+    customObjectConfiguration
+  );
+  const variants = eventBasedObject.getVariants();
+  return variantName
+    ? variants.getVariant(variantName)
+    : eventBasedObject.getDefaultVariant();
+};
+
+type Props = EditorProps;
+
+const CustomObjectPropertiesEditor = (props: Props) => {
+  const forceUpdate = useForceUpdate();
+
+  const {
+    objectConfiguration,
+    project,
+    layout,
+    eventsFunctionsExtension,
+    eventsBasedObject,
+    object,
+    objectName,
+    resourceManagementProps,
+    onSizeUpdated,
+    onObjectUpdated,
+    unsavedChanges,
+    renderObjectNameField,
+    isChildObject,
+    onOpenEventBasedObjectVariantEditor,
+    onDeleteEventsBasedObjectVariant,
+  } = props;
+
+  const { isMobile } = useResponsiveWindowSize();
+
+  const customObjectConfiguration = gd.asCustomObjectConfiguration(
+    objectConfiguration
+  );
+  const properties = customObjectConfiguration.getProperties();
+
+  const propertiesSchema = propertiesMapToSchema(
+    properties,
+    object => object.getProperties(),
+    (object, name, value) => object.updateProperty(name, value)
+  );
+
+  const extraInformation = getExtraObjectsInformation()[
+    customObjectConfiguration.getType()
+  ];
+
+  const { values } = React.useContext(PreferencesContext);
+  const tutorialIds = getObjectTutorialIds(customObjectConfiguration.getType());
+
+  const eventBasedObject = project.hasEventsBasedObject(
+    customObjectConfiguration.getType()
+  )
+    ? project.getEventsBasedObject(customObjectConfiguration.getType())
+    : null;
+
+  const animations = customObjectConfiguration.getAnimations();
+
+  // The matching collision mask only takes the first sprite of the first
+  // animation of the object. We consider this is enough to start with, and
+  // the user can then edit the collision mask for further needs.
+  const onCreateMatchingSpriteCollisionMask = React.useCallback(
+    async () => {
+      const firstSprite = getFirstAnimationFrame(animations);
+      if (!firstSprite) {
+        return;
+      }
+      const firstSpriteResourceName = firstSprite.getImageName();
+      const firstAnimationResourceSource = ResourcesLoader.getResourceFullUrl(
+        project,
+        firstSpriteResourceName,
+        {}
+      );
+      let matchingCollisionMask = null;
+      try {
+        matchingCollisionMask = await getMatchingCollisionMask(
+          firstAnimationResourceSource
+        );
+      } catch (e) {
+        console.error(
+          'Unable to create a matching collision mask for the sprite, fallback to full image collision mask.',
+          e
+        );
+      }
+      setCollisionMaskOnAllFrames(animations, matchingCollisionMask);
+      forceUpdate();
+    },
+    [animations, project, forceUpdate]
+  );
+
+  const scrollView = React.useRef<?ScrollViewInterface>(null);
+  const animationList = React.useRef<?AnimationListInterface>(null);
+
+  const [
+    justAddedAnimationName,
+    setJustAddedAnimationName,
+  ] = React.useState<?string>(null);
+  const justAddedAnimationElement = React.useRef<?any>(null);
+
+  React.useEffect(
+    () => {
+      if (
+        scrollView.current &&
+        justAddedAnimationElement.current &&
+        justAddedAnimationName
+      ) {
+        scrollView.current.scrollTo(justAddedAnimationElement.current);
+        setJustAddedAnimationName(null);
+        justAddedAnimationElement.current = null;
+      }
+    },
+    [justAddedAnimationName]
+  );
+
+  const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false);
+  const [
+    collisionMasksEditorOpen,
+    setCollisionMasksEditorOpen,
+  ] = React.useState(false);
+  const [newVariantDialogOpen, setNewVariantDialogOpen] = React.useState(false);
+
+  const editVariant = React.useCallback(
+    () => {
+      onOpenEventBasedObjectVariantEditor &&
+        onOpenEventBasedObjectVariantEditor(
+          gd.PlatformExtension.getExtensionFromFullObjectType(
+            customObjectConfiguration.getType()
+          ),
+          gd.PlatformExtension.getObjectNameFromFullObjectType(
+            customObjectConfiguration.getType()
+          ),
+          customObjectConfiguration.getVariantName()
+        );
+    },
+    [customObjectConfiguration, onOpenEventBasedObjectVariantEditor]
+  );
+
+  const duplicateVariant = React.useCallback(
+    (i18n: I18nType, newName: string) => {
+      if (!eventBasedObject) {
+        return;
+      }
+      const variants = eventBasedObject.getVariants();
+      // TODO Forbid name with `::`
+      const uniqueNewName = newNameGenerator(
+        newName || i18n._(t`New variant`),
+        tentativeNewName => variants.hasVariantNamed(tentativeNewName)
+      );
+      const oldVariantName = getVariantName(
+        eventBasedObject,
+        customObjectConfiguration
+      );
+      const oldVariant = oldVariantName
+        ? variants.getVariant(oldVariantName)
+        : eventBasedObject.getDefaultVariant();
+      const newVariant = variants.insertNewVariant(uniqueNewName, 0);
+      unserializeFromJSObject(
+        newVariant,
+        serializeToJSObject(oldVariant),
+        'unserializeFrom',
+        project
+      );
+      newVariant.setName(uniqueNewName);
+      newVariant.setAssetStoreAssetId('');
+      newVariant.setAssetStoreOriginalName('');
+      customObjectConfiguration.setVariantName(uniqueNewName);
+      setNewVariantDialogOpen(false);
+      forceUpdate();
+    },
+    [customObjectConfiguration, eventBasedObject, forceUpdate, project]
+  );
+
+  const deleteVariant = React.useCallback(
+    () => {
+      if (!eventBasedObject || !onDeleteEventsBasedObjectVariant) {
+        return;
+      }
+      const variants = eventBasedObject.getVariants();
+      const selectedVariantName = customObjectConfiguration.getVariantName();
+      if (variants.hasVariantNamed(selectedVariantName)) {
+        customObjectConfiguration.setVariantName('');
+        const extensionName = gd.PlatformExtension.getExtensionFromFullObjectType(
+          customObjectConfiguration.getType()
+        );
+        if (!project.hasEventsFunctionsExtensionNamed(extensionName)) {
+          return;
+        }
+        const eventBasedExtension = project.getEventsFunctionsExtension(
+          extensionName
+        );
+        onDeleteEventsBasedObjectVariant(
+          eventBasedExtension,
+          eventBasedObject,
+          variants.getVariant(selectedVariantName)
+        );
+        forceUpdate();
+      }
+    },
+    [
+      customObjectConfiguration,
+      eventBasedObject,
+      forceUpdate,
+      onDeleteEventsBasedObjectVariant,
+      project,
+    ]
+  );
+
+  return (
+    <I18n>
+      {({ i18n }) => (
+        <>
+          <ScrollView ref={scrollView}>
+            <ColumnStackLayout noMargin>
+              {renderObjectNameField && renderObjectNameField()}
+              {tutorialIds.map(tutorialId => (
+                <DismissableTutorialMessage
+                  key={tutorialId}
+                  tutorialId={tutorialId}
+                />
+              ))}
+              {propertiesSchema.length ||
+              (eventBasedObject &&
+                (eventBasedObject.getObjects().getObjectsCount() ||
+                  eventBasedObject.isAnimatable())) ? (
+                <React.Fragment>
+                  {extraInformation ? (
+                    <Line>
+                      <ColumnStackLayout noMargin>
+                        {extraInformation.map(({ kind, message }, index) => (
+                          <AlertMessage kind={kind} key={index}>
+                            {i18n._(message)}
+                          </AlertMessage>
+                        ))}
+                      </ColumnStackLayout>
+                    </Line>
+                  ) : null}
+                  <PropertiesEditor
+                    unsavedChanges={unsavedChanges}
+                    schema={propertiesSchema}
+                    instances={[customObjectConfiguration]}
+                    project={project}
+                    resourceManagementProps={resourceManagementProps}
+                  />
+                  {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && (
+                    <>
+                      <Line>
+                        <Column expand noMargin>
+                          <Text size="block-title">Variant</Text>
+                        </Column>
+                      </Line>
+                      <ColumnStackLayout expand noMargin>
+                        <LineStackLayout>
+                          <FlatButton
+                            label={<Trans>Edit</Trans>}
+                            leftIcon={<Edit />}
+                            onClick={editVariant}
+                            disabled={
+                              !eventBasedObject ||
+                              getVariant(
+                                eventBasedObject,
+                                customObjectConfiguration
+                              ).getAssetStoreAssetId() !== ''
+                            }
+                          />
+                          <FlatButton
+                            label={<Trans>Duplicate</Trans>}
+                            leftIcon={<Add />}
+                            onClick={() => setNewVariantDialogOpen(true)}
+                          />
+                          <FlatButton
+                            label={<Trans>Delete</Trans>}
+                            leftIcon={<Trash />}
+                            onClick={deleteVariant}
+                          />
+                        </LineStackLayout>
+                        <SelectField
+                          floatingLabelText={<Trans>Variant</Trans>}
+                          value={getVariantName(
+                            eventBasedObject,
+                            customObjectConfiguration
+                          )}
+                          onChange={(e, i, value: string) => {
+                            customObjectConfiguration.setVariantName(value);
+                            forceUpdate();
+                          }}
+                        >
+                          <SelectOption
+                            key="default-variant"
+                            value=""
+                            label={t`Default`}
+                          />
+                          {eventBasedObject &&
+                            mapFor(
+                              0,
+                              eventBasedObject.getVariants().getVariantsCount(),
+                              i => {
+                                if (!eventBasedObject) {
+                                  return null;
+                                }
+                                const variant = eventBasedObject
+                                  .getVariants()
+                                  .getVariantAt(i);
+                                return (
+                                  <SelectOption
+                                    key={'variant-' + variant.getName()}
+                                    value={variant.getName()}
+                                    label={variant.getName()}
+                                  />
+                                );
+                              }
+                            )}
+                        </SelectField>
+                      </ColumnStackLayout>
+                    </>
+                  )}
+                  {!getVariantName(
+                    eventBasedObject,
+                    customObjectConfiguration
+                  ) &&
+                    (eventBasedObject &&
+                      (!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() &&
+                      !customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? (
+                        <Line alignItems="center">
+                          <Column expand noMargin>
+                            <Text size="block-title">Children objects</Text>
+                          </Column>
+                          <Column alignItems="right">
+                            <FlatButton
+                              label={
+                                <Trans>Override children configuration</Trans>
+                              }
+                              onClick={() => {
+                                customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
+                                  true
+                                );
+                                customObjectConfiguration.clearChildrenConfiguration();
+                                if (onObjectUpdated) {
+                                  onObjectUpdated();
+                                }
+                                forceUpdate();
+                              }}
+                            />
+                          </Column>
+                        </Line>
+                      ) : (
+                        <>
+                          <Line alignItems="center">
+                            <Column expand noMargin>
+                              <Text size="block-title">Children objects</Text>
+                            </Column>
+                            {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && (
+                              <Column alignItems="right">
+                                <FlatButton
+                                  leftIcon={<RestoreIcon style={styles.icon} />}
+                                  label={
+                                    <Trans>
+                                      Reset and hide children configuration
+                                    </Trans>
+                                  }
+                                  onClick={() => {
+                                    customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(
+                                      false
+                                    );
+                                    customObjectConfiguration.clearChildrenConfiguration();
+                                    if (onObjectUpdated) {
+                                      onObjectUpdated();
+                                    }
+                                    forceUpdate();
+                                  }}
+                                />
+                              </Column>
+                            )}
+                          </Line>
+                          {mapFor(
+                            0,
+                            eventBasedObject.getObjects().getObjectsCount(),
+                            i => {
+                              const childObject = eventBasedObject
+                                .getObjects()
+                                .getObjectAt(i);
+                              const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
+                                childObject.getName()
+                              );
+                              const editorConfiguration = ObjectsEditorService.getEditorConfiguration(
+                                project,
+                                childObjectConfiguration.getType()
+                              );
+                              const EditorComponent =
+                                editorConfiguration.component;
+
+                              const objectMetadata = gd.MetadataProvider.getObjectMetadata(
+                                gd.JsPlatform.get(),
+                                childObjectConfiguration.getType()
+                              );
+                              const iconUrl = objectMetadata.getIconFilename();
+                              const tutorialIds = getObjectTutorialIds(
+                                childObjectConfiguration.getType()
+                              );
+                              const enabledTutorialIds = tutorialIds.filter(
+                                tutorialId =>
+                                  !values.hiddenTutorialHints[tutorialId]
+                              );
+                              // TODO EBO: Add a protection against infinite loops in case
+                              // of object cycles (thought it should be forbidden).
+                              return (
+                                <Accordion
+                                  key={childObject.getName()}
+                                  defaultExpanded
+                                >
+                                  <AccordionHeader>
+                                    {iconUrl ? (
+                                      <IconContainer
+                                        src={iconUrl}
+                                        alt={childObject.getName()}
+                                        size={20}
+                                      />
+                                    ) : null}
+                                    <Column expand>
+                                      <Text size="block-title">
+                                        {childObject.getName()}
+                                      </Text>
+                                    </Column>
+                                  </AccordionHeader>
+                                  <AccordionBody>
+                                    <Column expand noMargin noOverflowParent>
+                                      {enabledTutorialIds.length ? (
+                                        <Line>
+                                          <ColumnStackLayout expand>
+                                            {tutorialIds.map(tutorialId => (
+                                              <DismissableTutorialMessage
+                                                key={tutorialId}
+                                                tutorialId={tutorialId}
+                                              />
+                                            ))}
+                                          </ColumnStackLayout>
+                                        </Line>
+                                      ) : null}
+                                      <Line noMargin>
+                                        <Column expand>
+                                          <EditorComponent
+                                            isChildObject
+                                            objectConfiguration={
+                                              childObjectConfiguration
+                                            }
+                                            project={project}
+                                            layout={layout}
+                                            eventsFunctionsExtension={
+                                              eventsFunctionsExtension
+                                            }
+                                            eventsBasedObject={
+                                              eventsBasedObject
+                                            }
+                                            resourceManagementProps={
+                                              resourceManagementProps
+                                            }
+                                            onSizeUpdated={
+                                              forceUpdate /*Force update to ensure dialog is properly positioned*/
+                                            }
+                                            objectName={
+                                              objectName +
+                                              ' ' +
+                                              childObject.getName()
+                                            }
+                                            onObjectUpdated={onObjectUpdated}
+                                            unsavedChanges={unsavedChanges}
+                                          />
+                                        </Column>
+                                      </Line>
+                                    </Column>
+                                  </AccordionBody>
+                                </Accordion>
+                              );
+                            }
+                          )}
+                        </>
+                      )))}
+                  {eventBasedObject && eventBasedObject.isAnimatable() && (
+                    <Column expand>
+                      <Text size="block-title">
+                        <Trans>Animations</Trans>
+                      </Text>
+                      <AnimationList
+                        ref={animationList}
+                        animations={animations}
+                        project={project}
+                        layout={layout}
+                        eventsFunctionsExtension={eventsFunctionsExtension}
+                        eventsBasedObject={eventsBasedObject}
+                        object={object}
+                        objectName={objectName}
+                        resourceManagementProps={resourceManagementProps}
+                        onSizeUpdated={onSizeUpdated}
+                        onObjectUpdated={onObjectUpdated}
+                        isAnimationListLocked={false}
+                        scrollView={scrollView}
+                        onCreateMatchingSpriteCollisionMask={
+                          onCreateMatchingSpriteCollisionMask
+                        }
+                      />
+                    </Column>
+                  )}
+                </React.Fragment>
+              ) : (
+                <EmptyMessage>
+                  <Trans>
+                    There is nothing to configure for this object. You can still
+                    use events to interact with the object.
+                  </Trans>
+                </EmptyMessage>
+              )}
+            </ColumnStackLayout>
+          </ScrollView>
+          {eventBasedObject &&
+            eventBasedObject.isAnimatable() &&
+            !isChildObject && (
+              <Column noMargin>
+                <ResponsiveLineStackLayout
+                  justifyContent="space-between"
+                  noColumnMargin
+                >
+                  {!isMobile ? ( // On mobile, use only 1 button to gain space.
+                    <ResponsiveLineStackLayout noMargin noColumnMargin>
+                      <FlatButton
+                        label={<Trans>Edit collision masks</Trans>}
+                        onClick={() => setCollisionMasksEditorOpen(true)}
+                        disabled={!hasAnyFrame(animations)}
+                      />
+                      <FlatButton
+                        label={<Trans>Edit points</Trans>}
+                        onClick={() => setPointsEditorOpen(true)}
+                        disabled={!hasAnyFrame(animations)}
+                      />
+                    </ResponsiveLineStackLayout>
+                  ) : (
+                    <FlatButtonWithSplitMenu
+                      label={<Trans>Edit collision masks</Trans>}
+                      onClick={() => setCollisionMasksEditorOpen(true)}
+                      disabled={!hasAnyFrame(animations)}
+                      buildMenuTemplate={i18n => [
+                        {
+                          label: i18n._(t`Edit points`),
+                          disabled: !hasAnyFrame(animations),
+                          click: () => setPointsEditorOpen(true),
+                        },
+                      ]}
+                    />
+                  )}
+                  <RaisedButton
+                    label={<Trans>Add an animation</Trans>}
+                    primary
+                    onClick={() => {
+                      if (!animationList.current) {
+                        return;
+                      }
+                      animationList.current.addAnimation();
+                    }}
+                    icon={<Add />}
+                  />
+                </ResponsiveLineStackLayout>
+              </Column>
+            )}
+          {pointsEditorOpen && (
+            <Dialog
+              title={<Trans>Edit points</Trans>}
+              actions={[
+                <RaisedButton
+                  key="apply"
+                  label={<Trans>Apply</Trans>}
+                  primary
+                  onClick={() => setPointsEditorOpen(false)}
+                />,
+              ]}
+              secondaryActions={[
+                <HelpButton
+                  helpPagePath="/objects/sprite/edit-points"
+                  key="help"
+                />,
+              ]}
+              onRequestClose={() => setPointsEditorOpen(false)}
+              maxWidth="lg"
+              flexBody
+              fullHeight
+              open={pointsEditorOpen}
+            >
+              <PointsEditor
+                animations={animations}
+                resourcesLoader={ResourcesLoader}
+                project={project}
+                onPointsUpdated={onObjectUpdated}
+                onRenamedPoint={(oldName, newName) => {
+                  if (!object) {
+                    return;
+                  }
+                  if (layout) {
+                    gd.WholeProjectRefactorer.renameObjectPointInScene(
+                      project,
+                      layout,
+                      object,
+                      oldName,
+                      newName
+                    );
+                  } else if (eventsFunctionsExtension && eventsBasedObject) {
+                    gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject(
+                      project,
+                      eventsFunctionsExtension,
+                      eventsBasedObject,
+                      object,
+                      oldName,
+                      newName
+                    );
+                  }
+                }}
+              />
+            </Dialog>
+          )}
+          {collisionMasksEditorOpen && (
+            <Dialog
+              title={<Trans>Edit collision masks</Trans>}
+              actions={[
+                <RaisedButton
+                  key="apply"
+                  label={<Trans>Apply</Trans>}
+                  primary
+                  onClick={() => setCollisionMasksEditorOpen(false)}
+                />,
+              ]}
+              secondaryActions={[
+                <HelpButton
+                  helpPagePath="/objects/sprite/collision-mask"
+                  key="help"
+                />,
+              ]}
+              maxWidth="lg"
+              flexBody
+              fullHeight
+              onRequestClose={() => setCollisionMasksEditorOpen(false)}
+              open={collisionMasksEditorOpen}
+            >
+              <CollisionMasksEditor
+                animations={animations}
+                resourcesLoader={ResourcesLoader}
+                project={project}
+                onMasksUpdated={onObjectUpdated}
+                onCreateMatchingSpriteCollisionMask={
+                  onCreateMatchingSpriteCollisionMask
+                }
+              />
+            </Dialog>
+          )}
+          {newVariantDialogOpen && eventBasedObject && (
+            <NewVariantDialog
+              initialName={
+                getVariantName(eventBasedObject, customObjectConfiguration) ||
+                i18n._(t`New variant`)
+              }
+              onApply={name => duplicateVariant(i18n, name)}
+              onCancel={() => {
+                setNewVariantDialogOpen(false);
+              }}
+            />
+          )}
+        </>
+      )}
+    </I18n>
+  );
+};
+
+export default CustomObjectPropertiesEditor;
diff --git a/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js b/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js
index 78b966023770..0c270b69f6fb 100644
--- a/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js
+++ b/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js
@@ -37,4 +37,18 @@ export type EditorProps = {|
   scrollView?: ScrollViewInterface,
   renderObjectNameField?: () => React.Node,
   isChildObject?: boolean,
+  onOpenEventBasedObjectEditor?: (
+    extensionName: string,
+    eventsBasedObjectName: string
+  ) => void,
+  onOpenEventBasedObjectVariantEditor?: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
+  onDeleteEventsBasedObjectVariant?: (
+    eventsFunctionsExtension: gdEventsFunctionsExtension,
+    eventBasedObject: gdEventsBasedObject,
+    variant: gdEventsBasedObjectVariant
+  ) => void,
 |};
diff --git a/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js b/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js
index ede51af79a8d..3a94dea21c8e 100644
--- a/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js
+++ b/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js
@@ -61,6 +61,22 @@ type Props = {|
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
   openBehaviorEvents: (extensionName: string, behaviorName: string) => void,
   onExtensionInstalled: (extensionName: string) => void,
+  onOpenEventBasedObjectEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string
+  ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
+  onDeleteEventsBasedObjectVariant: (
+    eventsFunctionsExtension: gdEventsFunctionsExtension,
+    eventBasedObject: gdEventsBasedObject,
+    variant: gdEventsBasedObjectVariant
+  ) => void,
+  isBehaviorListLocked: boolean,
+  isVariableListLocked: boolean,
 |};
 
 type InnerDialogProps = {|
@@ -90,6 +106,11 @@ const InnerDialog = (props: InnerDialogProps) => {
     onUpdateBehaviorsSharedData,
     onComputeAllVariableNames,
     onExtensionInstalled,
+    onOpenEventBasedObjectEditor,
+    onOpenEventBasedObjectVariantEditor,
+    onDeleteEventsBasedObjectVariant,
+    isBehaviorListLocked,
+    isVariableListLocked,
   } = props;
   const [currentTab, setCurrentTab] = React.useState<ObjectEditorTab>(
     initialTab || 'properties'
@@ -146,6 +167,13 @@ const InnerDialog = (props: InnerDialogProps) => {
       changeset,
       originalSerializedVariables
     );
+    if (eventsBasedObject) {
+      gd.ObjectVariableHelper.applyChangesToVariants(
+        eventsBasedObject,
+        object.getName(),
+        changeset
+      );
+    }
     object.clearPersistentUuid();
 
     // Do the renaming *after* applying changes, as "withSerializableObject"
@@ -287,6 +315,11 @@ const InnerDialog = (props: InnerDialogProps) => {
                 autoFocus="desktop"
               />
             )}
+            onOpenEventBasedObjectEditor={onOpenEventBasedObjectEditor}
+            onOpenEventBasedObjectVariantEditor={
+              onOpenEventBasedObjectVariantEditor
+            }
+            onDeleteEventsBasedObjectVariant={onDeleteEventsBasedObjectVariant}
           />
         </Column>
       ) : null}
@@ -303,6 +336,7 @@ const InnerDialog = (props: InnerDialogProps) => {
           onBehaviorsUpdated={notifyOfChange}
           openBehaviorEvents={askConfirmationAndOpenBehaviorEvents}
           onExtensionInstalled={onExtensionInstalled}
+          isListLocked={isBehaviorListLocked}
         />
       )}
       {currentTab === 'variables' && (
@@ -329,6 +363,7 @@ const InnerDialog = (props: InnerDialogProps) => {
             helpPagePath={'/all-features/variables/object-variables'}
             onComputeAllVariableNames={onComputeAllVariableNames}
             onVariablesUpdated={notifyOfChange}
+            isListLocked={isVariableListLocked}
           />
         </Column>
       )}
diff --git a/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js
index a2f6bf5cedf2..f8f0026a9da8 100644
--- a/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js
+++ b/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js
@@ -13,6 +13,7 @@ import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessag
 import VariablesList from '../VariablesList/VariablesList';
 import HelpButton from '../UI/HelpButton';
 import useValueWithInit from '../Utils/UseRefInitHook';
+import Text from '../UI/Text';
 
 const gd: libGDevelop = global.gd;
 
@@ -29,6 +30,8 @@ type Props = {|
   initialInstances: gdInitialInstancesContainer | null,
   initialTab: ?ObjectGroupEditorTab,
   onComputeAllVariableNames?: () => Array<string>,
+  isVariableListLocked: boolean,
+  isObjectListLocked: boolean,
 |};
 
 const EditedObjectGroupEditorDialog = ({
@@ -42,6 +45,8 @@ const EditedObjectGroupEditorDialog = ({
   initialInstances,
   initialTab,
   onComputeAllVariableNames,
+  isVariableListLocked,
+  isObjectListLocked,
 }: Props) => {
   const forceUpdate = useForceUpdate();
   const {
@@ -99,6 +104,16 @@ const EditedObjectGroupEditorDialog = ({
       changeset,
       originalSerializedVariables
     );
+    const { eventsBasedObject } = projectScopedContainersAccessor._scope;
+    if (eventsBasedObject) {
+      for (const objectName of group.getAllObjectsNames().toJSArray()) {
+        gd.ObjectVariableHelper.applyChangesToVariants(
+          eventsBasedObject,
+          objectName,
+          changeset
+        );
+      }
+    }
     groupVariablesContainer.clearPersistentUuid();
   };
 
@@ -169,17 +184,28 @@ const EditedObjectGroupEditorDialog = ({
         />
       }
     >
-      {currentTab === 'objects' && (
-        <ObjectGroupEditor
-          project={project}
-          projectScopedContainersAccessor={projectScopedContainersAccessor}
-          globalObjectsContainer={globalObjectsContainer}
-          objectsContainer={objectsContainer}
-          groupObjectNames={group.getAllObjectsNames().toJSArray()}
-          onObjectAdded={addObject}
-          onObjectRemoved={removeObject}
-        />
-      )}
+      {currentTab === 'objects' &&
+        (isObjectListLocked && group.getAllObjectsNames().size() === 0 ? (
+          <Column noMargin expand justifyContent="center">
+            <Text size="block-title" align="center">
+              {<Trans>Empty group</Trans>}
+            </Text>
+            <Text align="center" noMargin>
+              {<Trans>This object group is empty and locked.</Trans>}
+            </Text>
+          </Column>
+        ) : (
+          <ObjectGroupEditor
+            project={project}
+            projectScopedContainersAccessor={projectScopedContainersAccessor}
+            globalObjectsContainer={globalObjectsContainer}
+            objectsContainer={objectsContainer}
+            groupObjectNames={group.getAllObjectsNames().toJSArray()}
+            onObjectAdded={addObject}
+            onObjectRemoved={removeObject}
+            isObjectListLocked={isObjectListLocked}
+          />
+        ))}
       {currentTab === 'variables' && (
         <Column expand noMargin>
           {groupVariablesContainer.count() > 0 && DismissableTutorialMessage && (
@@ -205,6 +231,7 @@ const EditedObjectGroupEditorDialog = ({
             helpPagePath={'/all-features/variables/object-variables'}
             onComputeAllVariableNames={onComputeAllVariableNames}
             onVariablesUpdated={notifyOfVariableChange}
+            isListLocked={isVariableListLocked}
           />
         </Column>
       )}
diff --git a/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js
index 9a818cce1998..e076d9136817 100644
--- a/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js
+++ b/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js
@@ -134,6 +134,7 @@ const NewObjectGroupEditorDialog = ({
         groupObjectNames={groupObjectNames}
         onObjectAdded={addObject}
         onObjectRemoved={removeObject}
+        isObjectListLocked={false}
       />
     </Dialog>
   );
diff --git a/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js
index c0954af0bd3e..ef43af2eacc2 100644
--- a/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js
+++ b/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js
@@ -30,6 +30,8 @@ type Props = {|
   bypassedObjectGroupsContainer?: ?gdObjectGroupsContainer,
   initialTab?: ?ObjectGroupEditorTab,
   onComputeAllVariableNames?: () => Array<string>,
+  isVariableListLocked: boolean,
+  isObjectListLocked: boolean,
 |};
 
 const ObjectGroupEditorDialog = ({
@@ -45,6 +47,8 @@ const ObjectGroupEditorDialog = ({
   bypassedObjectGroupsContainer,
   initialTab,
   onComputeAllVariableNames,
+  isVariableListLocked,
+  isObjectListLocked,
 }: Props) => {
   const [
     editedObjectGroup,
@@ -108,7 +112,8 @@ const ObjectGroupEditorDialog = ({
   );
 
   return !editedObjectGroup ||
-    editedObjectGroup.getAllObjectsNames().size() === 0 ? (
+    (editedObjectGroup.getAllObjectsNames().size() === 0 &&
+      !isObjectListLocked) ? (
     <NewObjectGroupEditorDialog
       project={project}
       projectScopedContainersAccessor={projectScopedContainersAccessor}
@@ -130,6 +135,8 @@ const ObjectGroupEditorDialog = ({
       initialInstances={initialInstances}
       initialTab={selectedTab}
       onComputeAllVariableNames={onComputeAllVariableNames}
+      isVariableListLocked={isVariableListLocked}
+      isObjectListLocked={isObjectListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/ObjectGroupEditor/index.js b/newIDE/app/src/ObjectGroupEditor/index.js
index ae4d22b3c584..0b3c68211c10 100644
--- a/newIDE/app/src/ObjectGroupEditor/index.js
+++ b/newIDE/app/src/ObjectGroupEditor/index.js
@@ -27,6 +27,7 @@ type Props = {|
   onObjectGroupUpdated?: () => void,
   onObjectAdded: (objectName: string) => void,
   onObjectRemoved: (objectName: string) => void,
+  isObjectListLocked: boolean,
 |};
 
 const ObjectGroupEditor = ({
@@ -37,6 +38,7 @@ const ObjectGroupEditor = ({
   groupObjectNames,
   onObjectAdded,
   onObjectRemoved,
+  isObjectListLocked,
 }: Props) => {
   const [objectName, setObjectName] = React.useState<string>('');
 
@@ -111,7 +113,13 @@ const ObjectGroupEditor = ({
                 )}
               />
             ) : null;
-          return (
+          return isObjectListLocked ? (
+            <ListItem
+              key={objectName}
+              primaryText={objectName}
+              leftIcon={icon}
+            />
+          ) : (
             <ListItem
               key={objectName}
               primaryText={objectName}
@@ -135,6 +143,7 @@ const ObjectGroupEditor = ({
             noGroups
             hintText={t`Choose an object to add to the group`}
             fullWidth
+            disabled={isObjectListLocked}
           />
         </Column>
       </Paper>
diff --git a/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js b/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js
index b4957aad6e72..2143c11b6fcb 100644
--- a/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js
+++ b/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js
@@ -71,6 +71,7 @@ const ObjectGroupsListWithObjectGroupEditor = ({
         onGroupRenamed={onGroupsUpdated}
         canSetAsGlobalGroup={canSetAsGlobalGroup}
         unsavedChanges={unsavedChanges}
+        isListLocked={false}
       />
       {(editedGroup || isCreatingNewGroup) && (
         <ObjectGroupEditorDialog
@@ -102,6 +103,8 @@ const ObjectGroupsListWithObjectGroupEditor = ({
             }
           }}
           initialTab={'objects'}
+          isVariableListLocked={false}
+          isObjectListLocked={false}
         />
       )}
     </React.Fragment>
diff --git a/newIDE/app/src/ObjectGroupsList/index.js b/newIDE/app/src/ObjectGroupsList/index.js
index 92361e68f7ea..45fd8842978e 100644
--- a/newIDE/app/src/ObjectGroupsList/index.js
+++ b/newIDE/app/src/ObjectGroupsList/index.js
@@ -109,6 +109,7 @@ type Props = {|
   onGroupRenamed?: () => void,
   canSetAsGlobalGroup?: boolean,
   unsavedChanges?: ?UnsavedChanges,
+  isListLocked: boolean,
 |};
 
 const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
@@ -127,6 +128,7 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
       unsavedChanges,
       onEditGroup,
       canSetAsGlobalGroup,
+      isListLocked,
     } = props;
     const [
       selectedGroupWithContext,
@@ -472,6 +474,8 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
               {
                 label: i18n._(t`Duplicate`),
                 click: () => onDuplicate(item),
+                accelerator: 'CmdOrCtrl+D',
+                enabled: !isListLocked,
               },
               { type: 'separator' },
               {
@@ -482,11 +486,13 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
               {
                 label: i18n._(t`Rename`),
                 click: () => onEditName(item),
+                accelerator: 'F2',
+                enabled: !isListLocked,
               },
               globalObjectGroups
                 ? {
                     label: i18n._(t`Set as global group`),
-                    enabled: !isGroupWithContextGlobal(item),
+                    enabled: !isGroupWithContextGlobal(item) && !isListLocked,
                     click: () => setAsGlobalGroup(item),
                     visible: canSetAsGlobalGroup !== false,
                   }
@@ -494,22 +500,26 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
               {
                 label: i18n._(t`Delete`),
                 click: () => onDelete(item),
+                accelerator: 'Backspace',
+                enabled: !isListLocked,
               },
               { type: 'separator' },
               {
                 label: i18n._(t`Add a new group...`),
                 click: onCreateGroup,
+                enabled: !isListLocked,
               },
             ].filter(Boolean),
       [
+        isListLocked,
+        globalObjectGroups,
+        canSetAsGlobalGroup,
         onCreateGroup,
-        onEditName,
-        editItem,
-        onDelete,
         onDuplicate,
-        canSetAsGlobalGroup,
+        editItem,
+        onEditName,
         setAsGlobalGroup,
-        globalObjectGroups,
+        onDelete,
       ]
     );
 
@@ -521,9 +531,10 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
               label: i18n._(t`Add a new group`),
               click: onCreateGroup,
               id: 'add-new-group-top-button',
+              enabled: !isListLocked,
             }
           : null,
-      [onCreateGroup]
+      [isListLocked, onCreateGroup]
     );
 
     const labels = React.useMemo(
@@ -578,23 +589,29 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
       () => {
         if (keyboardShortcutsRef.current) {
           keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => {
-            if (!selectedGroupWithContext) return;
+            if (!selectedGroupWithContext || isListLocked) return;
             onDelete(selectedGroupWithContext);
           });
           keyboardShortcutsRef.current.setShortcutCallback(
             'onDuplicate',
             () => {
-              if (!selectedGroupWithContext) return;
+              if (!selectedGroupWithContext || isListLocked) return;
               onDuplicate(selectedGroupWithContext);
             }
           );
           keyboardShortcutsRef.current.setShortcutCallback('onRename', () => {
-            if (!selectedGroupWithContext) return;
+            if (!selectedGroupWithContext || isListLocked) return;
             onEditName(selectedGroupWithContext);
           });
         }
       },
-      [selectedGroupWithContext, onDelete, onDuplicate, onEditName]
+      [
+        selectedGroupWithContext,
+        onDelete,
+        onDuplicate,
+        onEditName,
+        isListLocked,
+      ]
     );
 
     // Force List component to be mounted again if globalObjectGroups or objectGroups
@@ -687,6 +704,7 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>(
               onClick={onCreateGroup}
               id="add-new-group-button"
               icon={<Add />}
+              disabled={isListLocked}
             />
           </Column>
         </Line>
diff --git a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js
index 6ba54b867116..2354884ddee7 100644
--- a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js
+++ b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js
@@ -72,6 +72,7 @@ export type ObjectFolderTreeViewItemProps = {|
   ) => void,
   forceUpdateList: () => void,
   forceUpdate: () => void,
+  isListLocked: boolean,
 |};
 
 export const getObjectFolderTreeViewItemId = (
@@ -197,6 +198,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent {
       onAddNewObject,
       onMovedObjectFolderOrObjectToAnotherFolderInSameContainer,
       forceUpdate,
+      isListLocked,
     } = this.props;
 
     const container = this._isGlobal
@@ -222,54 +224,61 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent {
           isGlobalObject: this._isGlobal,
           isFolder: true,
         }),
-        enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND),
+        enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND) && !isListLocked,
         click: () => this.paste(),
       },
       {
         label: i18n._(t`Rename`),
         click: () => this.props.editName(this.getId()),
         accelerator: 'F2',
+        enabled: !isListLocked,
       },
       {
         label: i18n._(t`Delete`),
         click: () => this.delete(),
         accelerator: 'Backspace',
+        enabled: !isListLocked,
       },
-      {
-        label: i18n._('Move to folder'),
-        submenu: [
-          ...filteredFolderAndPathsInContainer.map(({ folder, path }) => ({
-            label: path,
-            enabled: folder !== this.objectFolder.getParent(),
-            click: () => {
-              if (folder === this.objectFolder.getParent()) return;
-              this.objectFolder
-                .getParent()
-                .moveObjectFolderOrObjectToAnotherFolder(
-                  this.objectFolder,
-                  folder,
-                  0
-                );
-              onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
-                objectFolderOrObject: folder,
-                global: this._isGlobal,
-              });
-            },
-          })),
-
-          { type: 'separator' },
-          {
-            label: i18n._(t`Create new folder...`),
-            click: () =>
-              addFolder([
-                {
-                  objectFolderOrObject: this.objectFolder.getParent(),
-                  global: this._isGlobal,
+      isListLocked
+        ? {
+            label: i18n._('Move to folder'),
+            enabled: false,
+          }
+        : {
+            label: i18n._('Move to folder'),
+            submenu: [
+              ...filteredFolderAndPathsInContainer.map(({ folder, path }) => ({
+                label: path,
+                enabled: folder !== this.objectFolder.getParent(),
+                click: () => {
+                  if (folder === this.objectFolder.getParent()) return;
+                  this.objectFolder
+                    .getParent()
+                    .moveObjectFolderOrObjectToAnotherFolder(
+                      this.objectFolder,
+                      folder,
+                      0
+                    );
+                  onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
+                    objectFolderOrObject: folder,
+                    global: this._isGlobal,
+                  });
                 },
-              ]),
+              })),
+
+              { type: 'separator' },
+              {
+                label: i18n._(t`Create new folder...`),
+                click: () =>
+                  addFolder([
+                    {
+                      objectFolderOrObject: this.objectFolder.getParent(),
+                      global: this._isGlobal,
+                    },
+                  ]),
+              },
+            ],
           },
-        ],
-      },
       ...renderQuickCustomizationMenuItems({
         i18n,
         visibility: this.objectFolder.getQuickCustomizationVisibility(),
@@ -286,6 +295,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent {
             objectFolderOrObject: this.objectFolder,
             global: this._isGlobal,
           }),
+        enabled: !isListLocked,
       },
       {
         label: i18n._(t`Add a new folder`),
@@ -293,6 +303,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent {
           addFolder([
             { objectFolderOrObject: this.objectFolder, global: this._isGlobal },
           ]),
+        enabled: !isListLocked,
       },
       { type: 'separator' },
       {
diff --git a/newIDE/app/src/ObjectsList/ObjectSelector.js b/newIDE/app/src/ObjectsList/ObjectSelector.js
index b0eb4ce4812b..5a16a28fea56 100644
--- a/newIDE/app/src/ObjectsList/ObjectSelector.js
+++ b/newIDE/app/src/ObjectsList/ObjectSelector.js
@@ -45,6 +45,7 @@ type Props = {|
   onApply?: () => void,
   value: string,
   errorTextIfInvalid?: React.Node,
+  disabled?: boolean,
 
   fullWidth?: boolean,
   floatingLabelText?: React.Node,
@@ -189,6 +190,7 @@ const ObjectSelector = React.forwardRef<Props, ObjectSelectorInterface>(
       hintText,
       requiredCapabilitiesBehaviorTypes,
       requiredVisibleBehaviorTypes,
+      disabled,
       ...otherProps
     } = props;
 
@@ -241,7 +243,7 @@ const ObjectSelector = React.forwardRef<Props, ObjectSelectorInterface>(
       undefined
     );
 
-    return shouldAutofocusInput ? (
+    return disabled ? null : shouldAutofocusInput ? (
       <SemiControlledAutoComplete
         margin={margin}
         hintText={hintText || t`Choose an object`}
diff --git a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js
index 2eeb1a7e94c8..f751ed2ec4e9 100644
--- a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js
+++ b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js
@@ -44,6 +44,11 @@ export type ObjectTreeViewItemCallbacks = {|
     extensionName: string,
     eventsBasedObjectName: string
   ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
   onRenameObjectFolderOrObjectWithContextFinish: (
     objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
     newName: string,
@@ -82,6 +87,7 @@ export type ObjectTreeViewItemProps = {|
   addFolder: (items: Array<ObjectFolderOrObjectWithContext>) => void,
   forceUpdateList: () => void,
   forceUpdate: () => void,
+  isListLocked: boolean,
 |};
 
 export const addSerializedObjectToObjectsContainer = ({
@@ -279,9 +285,10 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent {
       swapObjectAsset,
       canSetAsGlobalObject,
       setAsGlobalObject,
-      onOpenEventBasedObjectEditor,
+      onOpenEventBasedObjectVariantEditor,
       selectObjectFolderOrObjectWithContext,
       addFolder,
+      isListLocked,
     } = this.props;
 
     const container = this._isGlobal
@@ -312,29 +319,33 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent {
       {
         label: i18n._(t`Cut`),
         click: () => this.cut(),
+        enabled: !isListLocked,
       },
       {
         label: this._getPasteLabel(i18n, {
           isGlobalObject: this._isGlobal,
           isFolder: false,
         }),
-        enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND),
+        enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND) && !isListLocked,
         click: () => this.paste(),
       },
       {
         label: i18n._(t`Duplicate`),
         click: () => this.duplicate(),
         accelerator: 'CmdOrCtrl+D',
+        enabled: !isListLocked,
       },
       {
         label: i18n._(t`Rename`),
         click: () => this.props.editName(this.getId()),
         accelerator: 'F2',
+        enabled: !isListLocked,
       },
       {
         label: i18n._(t`Delete`),
         click: () => this.delete(),
         accelerator: 'Backspace',
+        enabled: !isListLocked,
       },
       { type: 'separator' },
       {
@@ -359,15 +370,20 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent {
       project.hasEventsBasedObject(object.getType())
         ? {
             label: i18n._(t`Edit children`),
-            click: () =>
-              onOpenEventBasedObjectEditor(
+            click: () => {
+              const customObjectConfiguration = gd.asCustomObjectConfiguration(
+                object.getConfiguration()
+              );
+              onOpenEventBasedObjectVariantEditor(
                 gd.PlatformExtension.getExtensionFromFullObjectType(
                   object.getType()
                 ),
                 gd.PlatformExtension.getObjectNameFromFullObjectType(
                   object.getType()
-                )
-              ),
+                ),
+                customObjectConfiguration.getVariantName()
+              );
+            },
           }
         : null,
       { type: 'separator' },
@@ -383,46 +399,51 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent {
       { type: 'separator' },
       globalObjectsContainer && {
         label: i18n._(t`Set as global object`),
-        enabled: !this._isGlobal,
+        enabled: !this._isGlobal && !isListLocked,
         click: () => {
           selectObjectFolderOrObjectWithContext(null);
           setAsGlobalObject({ i18n, objectFolderOrObject: this.object });
         },
         visible: canSetAsGlobalObject !== false,
       },
-      {
-        label: i18n._('Move to folder'),
-        submenu: [
-          ...folderAndPathsInContainer.map(({ folder, path }) => ({
-            label: path,
-            enabled: folder !== this.object.getParent(),
-            click: () => {
-              this.object
-                .getParent()
-                .moveObjectFolderOrObjectToAnotherFolder(
-                  this.object,
-                  folder,
-                  0
-                );
-              onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
-                objectFolderOrObject: folder,
-                global: this._isGlobal,
-              });
-            },
-          })),
-          { type: 'separator' },
-          {
-            label: i18n._(t`Create new folder...`),
-            click: () =>
-              addFolder([
-                {
-                  objectFolderOrObject: this.object.getParent(),
-                  global: this._isGlobal,
+      isListLocked
+        ? {
+            label: i18n._('Move to folder'),
+            enabled: false,
+          }
+        : {
+            label: i18n._('Move to folder'),
+            submenu: [
+              ...folderAndPathsInContainer.map(({ folder, path }) => ({
+                label: path,
+                enabled: folder !== this.object.getParent(),
+                click: () => {
+                  this.object
+                    .getParent()
+                    .moveObjectFolderOrObjectToAnotherFolder(
+                      this.object,
+                      folder,
+                      0
+                    );
+                  onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({
+                    objectFolderOrObject: folder,
+                    global: this._isGlobal,
+                  });
                 },
-              ]),
+              })),
+              { type: 'separator' },
+              {
+                label: i18n._(t`Create new folder...`),
+                click: () =>
+                  addFolder([
+                    {
+                      objectFolderOrObject: this.object.getParent(),
+                      global: this._isGlobal,
+                    },
+                  ]),
+              },
+            ],
           },
-        ],
-      },
       { type: 'separator' },
       {
         label: i18n._(t`Add instance to the scene`),
diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js
index f4b30631924b..3962ff0e07c9 100644
--- a/newIDE/app/src/ObjectsList/index.js
+++ b/newIDE/app/src/ObjectsList/index.js
@@ -280,6 +280,7 @@ class LabelTreeViewItemContent implements TreeViewItemContent {
               id: rightButton.id,
               label: i18n._(rightButton.label),
               click: rightButton.click,
+              enabled: rightButton.enabled,
             }
           : null,
         ...(buildMenuTemplateFunction ? buildMenuTemplateFunction() : []),
@@ -463,6 +464,11 @@ type Props = {|
     extensionName: string,
     eventsBasedObjectName: string
   ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
   onExportAssets: () => void,
   onObjectCreated: gdObject => void,
   onObjectEdited: ObjectWithContext => void,
@@ -479,6 +485,7 @@ type Props = {|
   ) => string,
   unsavedChanges?: ?UnsavedChanges,
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
+  isListLocked: boolean,
 |};
 
 const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
@@ -502,6 +509,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
 
       onEditObject,
       onOpenEventBasedObjectEditor,
+      onOpenEventBasedObjectVariantEditor,
       onExportAssets,
       onObjectCreated,
       onObjectEdited,
@@ -513,6 +521,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
       getThumbnail,
       unsavedChanges,
       hotReloadPreviewButtonProps,
+      isListLocked,
     }: Props,
     ref
   ) => {
@@ -981,6 +990,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         onAddObjectInstance,
         initialInstances,
         onOpenEventBasedObjectEditor,
+        onOpenEventBasedObjectVariantEditor,
         getValidatedObjectOrGroupName,
         onRenameObjectFolderOrObjectWithContextFinish,
         onObjectModified,
@@ -994,6 +1004,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         addFolder,
         forceUpdateList,
         forceUpdate,
+        isListLocked,
       }),
       [
         project,
@@ -1007,6 +1018,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         onAddObjectInstance,
         initialInstances,
         onOpenEventBasedObjectEditor,
+        onOpenEventBasedObjectVariantEditor,
         getValidatedObjectOrGroupName,
         onRenameObjectFolderOrObjectWithContextFinish,
         onObjectModified,
@@ -1020,6 +1032,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         addFolder,
         forceUpdateList,
         forceUpdate,
+        isListLocked,
       ]
     );
 
@@ -1041,6 +1054,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         showDeleteConfirmation,
         forceUpdateList,
         forceUpdate,
+        isListLocked,
       }),
       [
         project,
@@ -1059,6 +1073,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         showDeleteConfirmation,
         forceUpdateList,
         forceUpdate,
+        isListLocked,
       ]
     );
 
@@ -1143,6 +1158,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
                   onAddNewObject(selectedObjectFolderOrObjectsWithContext[0]);
                 },
                 id: 'add-new-object-top-button',
+                enabled: !isListLocked,
               },
               () => [
                 {
@@ -1154,6 +1170,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
                         global: false,
                       },
                     ]),
+                  enabled: !isListLocked,
                 },
                 { type: 'separator' },
                 {
@@ -1184,16 +1201,18 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
         return treeViewItems;
       },
       [
-        addFolder,
-        expandFolders,
         globalObjectsRootFolder,
-        objectFolderTreeViewItemProps,
+        labels.higherScopeObjectsTitle,
+        labels.localScopeObjectsTitle,
         objectTreeViewItemProps,
+        objectFolderTreeViewItemProps,
         objectsRootFolder,
+        isListLocked,
+        addFolder,
+        expandFolders,
         onAddNewObject,
-        onExportAssets,
         selectedObjectFolderOrObjectsWithContext,
-        labels,
+        onExportAssets,
       ]
     );
 
@@ -1221,20 +1240,31 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
       () => {
         if (keyboardShortcutsRef.current) {
           keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => {
-            deleteItem(selectedItems[0]);
+            if (!isListLocked) {
+              deleteItem(selectedItems[0]);
+            }
           });
           keyboardShortcutsRef.current.setShortcutCallback(
             'onDuplicate',
             () => {
-              duplicateItem(selectedItems[0]);
+              if (!isListLocked) {
+                duplicateItem(selectedItems[0]);
+              }
             }
           );
           keyboardShortcutsRef.current.setShortcutCallback('onRename', () => {
-            editName(selectedItems[0].content.getId());
+            if (!isListLocked) {
+              editName(selectedItems[0].content.getId());
+            }
           });
         }
       },
-      [selectedObjectFolderOrObjectsWithContext, editName, selectedItems]
+      [
+        selectedObjectFolderOrObjectsWithContext,
+        editName,
+        selectedItems,
+        isListLocked,
+      ]
     );
 
     const canMoveSelectionTo = React.useCallback(
@@ -1549,6 +1579,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
               }
               id="add-new-object-button"
               icon={<Add />}
+              disabled={isListLocked}
             />
           </Column>
         </Line>
diff --git a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js
index 6a2c74675fa2..cd3ce3913965 100644
--- a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js
+++ b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js
@@ -83,7 +83,8 @@ const ObjectsRenderingService = {
     instance: gdInitialInstance,
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
-    threeGroup: THREE.Group | null
+    threeGroup: THREE.Group | null,
+    propertyOverridings: Map<string, string> = new Map<string, string>()
   ): RenderedInstance | Rendered3DInstance {
     const objectType = associatedObjectConfiguration.getType();
     if (threeGroup && this.renderers3D.hasOwnProperty(objectType)) {
@@ -101,7 +102,8 @@ const ObjectsRenderingService = {
         instance,
         associatedObjectConfiguration,
         pixiContainer,
-        PixiResourcesLoader
+        PixiResourcesLoader,
+        propertyOverridings
       );
     else {
       if (project.hasEventsBasedObject(objectType)) {
@@ -135,7 +137,8 @@ const ObjectsRenderingService = {
             associatedObjectConfiguration,
             pixiContainer,
             threeGroup,
-            PixiResourcesLoader
+            PixiResourcesLoader,
+            propertyOverridings
           );
         }
       }
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js
index 1c65d088a113..1e0434117111 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js
@@ -63,10 +63,10 @@ const getDefaultAnchor = () => ({
  * Build the layouts description from the custom object properties.
  */
 export const getObjectAnchor = (
-  eventBasedObject: gdEventsBasedObject,
+  eventBasedObjectVariant: gdEventsBasedObjectVariant,
   objectName: string
 ): ObjectAnchor => {
-  const objects = eventBasedObject.getObjects();
+  const objects = eventBasedObjectVariant.getObjects();
   if (!objects.hasObjectNamed(objectName)) {
     return getDefaultAnchor();
   }
@@ -309,7 +309,7 @@ export interface ChildRenderedInstance {
 export interface LayoutedParent<
   CovariantChildRenderedInstance: ChildRenderedInstance
 > {
-  eventBasedObject: gdEventsBasedObject | null;
+  getVariant(): gdEventsBasedObjectVariant | null;
   getWidth(): number;
   getHeight(): number;
   getRendererOfInstance: (
@@ -322,8 +322,8 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>(
   parent: LayoutedParent<T>,
   initialInstance: gdInitialInstance
 ): T | null => {
-  const eventBasedObject = parent.eventBasedObject;
-  if (!eventBasedObject) {
+  const eventBasedObjectVariant = parent.getVariant();
+  if (!eventBasedObjectVariant) {
     return null;
   }
 
@@ -333,7 +333,7 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>(
   );
 
   const objectAnchor = getObjectAnchor(
-    eventBasedObject,
+    eventBasedObjectVariant,
     layoutedInstance.getObjectName()
   );
   const leftEdgeAnchor = objectAnchor
@@ -349,10 +349,10 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>(
     ? objectAnchor.bottomEdgeAnchor
     : gd.CustomObjectConfiguration.NoAnchor;
 
-  const parentInitialMinX = eventBasedObject.getAreaMinX();
-  const parentInitialMinY = eventBasedObject.getAreaMinY();
-  const parentInitialMaxX = eventBasedObject.getAreaMaxX();
-  const parentInitialMaxY = eventBasedObject.getAreaMaxY();
+  const parentInitialMinX = eventBasedObjectVariant.getAreaMinX();
+  const parentInitialMinY = eventBasedObjectVariant.getAreaMinY();
+  const parentInitialMaxX = eventBasedObjectVariant.getAreaMaxX();
+  const parentInitialMaxY = eventBasedObjectVariant.getAreaMaxY();
   const parentInitialWidth = parentInitialMaxX - parentInitialMinX;
   const parentInitialHeight = parentInitialMaxY - parentInitialMinY;
 
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js
index c6418604df01..6df7d031bbe3 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js
@@ -624,7 +624,7 @@ type MockedRenderedInstanceConfiguration = {|
 |};
 
 class MockedParent implements LayoutedParent<MockedChildRenderedInstance> {
-  eventBasedObject: gdEventsBasedObject | null;
+  getVariant: () => gdEventsBasedObjectVariant | null;
   width: number;
   height: number;
   renderedInstances = new Map<number, MockedChildRenderedInstance>();
@@ -639,7 +639,7 @@ class MockedParent implements LayoutedParent<MockedChildRenderedInstance> {
     width: number,
     height: number
   ) {
-    this.eventBasedObject = eventBasedObject;
+    this.getVariant = () => eventBasedObject.getDefaultVariant();
     this.width = width;
     this.height = height;
   }
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js
index 5dfe955ec317..0d456a5613e4 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js
@@ -19,6 +19,7 @@ export default class Rendered3DInstance {
   _threeObject: THREE.Object3D | null;
   wasUsed: boolean;
   _wasDestroyed: boolean;
+  _propertyOverridings: Map<string, string>;
 
   constructor(
     project: gdProject,
@@ -26,7 +27,8 @@ export default class Rendered3DInstance {
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
     threeGroup: THREE.Group,
-    pixiResourcesLoader: Class<PixiResourcesLoader>
+    pixiResourcesLoader: Class<PixiResourcesLoader>,
+    propertyOverridings: Map<string, string> = new Map<string, string>()
   ) {
     this._pixiObject = null;
     this._threeObject = null;
@@ -36,6 +38,7 @@ export default class Rendered3DInstance {
     this._threeGroup = threeGroup;
     this._project = project;
     this._pixiResourcesLoader = pixiResourcesLoader;
+    this._propertyOverridings = propertyOverridings;
     this.wasUsed = true; //Used by InstancesRenderer to track rendered instance that are not used anymore.
     this._wasDestroyed = false;
   }
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js
index 24ceca0a6981..0efab5dd0b00 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js
@@ -1,6 +1,7 @@
 // @flow
 import RenderedInstance from './RenderedInstance';
 import Rendered3DInstance from './Rendered3DInstance';
+import RenderedUnknownInstance from './RenderedUnknownInstance';
 import PixiResourcesLoader from '../PixiResourcesLoader';
 import ResourcesLoader from '../../ResourcesLoader';
 import ObjectsRenderingService from '../ObjectsRenderingService';
@@ -14,6 +15,53 @@ import * as THREE from 'three';
 
 const gd: libGDevelop = global.gd;
 
+const getVariant = (
+  eventBasedObject: gdEventsBasedObject,
+  customObjectConfiguration: gdCustomObjectConfiguration
+): gdEventsBasedObjectVariant => {
+  const variants = eventBasedObject.getVariants();
+  const variantName = customObjectConfiguration.getVariantName();
+  return variants.hasVariantNamed(variantName)
+    ? variants.getVariant(variantName)
+    : eventBasedObject.getDefaultVariant();
+};
+
+type PropertyMappingRule = {
+  targetChild: string,
+  targetProperty: string,
+  sourceProperty: string,
+};
+
+const getPropertyMappingRules = (
+  eventBasedObject: gdEventsBasedObject
+): Array<PropertyMappingRule> => {
+  const properties = eventBasedObject.getPropertyDescriptors();
+  if (!properties.has('_PropertyMapping')) {
+    return [];
+  }
+  const extraInfos = properties
+    .get('_PropertyMapping')
+    .getExtraInfo()
+    .toJSArray();
+  return extraInfos
+    .map(extraInfo => {
+      const mapping = extraInfo.split('=');
+      if (mapping.length < 2) {
+        return null;
+      }
+      const targetPath = mapping[0].split('.');
+      if (mapping.length < 2) {
+        return null;
+      }
+      return {
+        targetChild: targetPath[0],
+        targetProperty: targetPath[1],
+        sourceProperty: mapping[1],
+      };
+    })
+    .filter(Boolean);
+};
+
 /**
  * Renderer for gd.CustomObject (the class is not exposed to newIDE)
  */
@@ -27,6 +75,7 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
 
   layoutedInstances = new Map<number, LayoutedInstance>();
   renderedInstances = new Map<number, RenderedInstance | Rendered3DInstance>();
+  _propertyMappingRules: Array<PropertyMappingRule>;
 
   constructor(
     project: gdProject,
@@ -34,7 +83,8 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
     threeGroup: THREE.Group,
-    pixiResourcesLoader: Class<PixiResourcesLoader>
+    pixiResourcesLoader: Class<PixiResourcesLoader>,
+    propertyOverridings: Map<string, string>
   ) {
     super(
       project,
@@ -42,7 +92,8 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
       associatedObjectConfiguration,
       pixiContainer,
       threeGroup,
-      pixiResourcesLoader
+      pixiResourcesLoader,
+      propertyOverridings
     );
 
     // Setup the PIXI object:
@@ -71,6 +122,7 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
     if (!eventBasedObject) {
       return;
     }
+    this._propertyMappingRules = getPropertyMappingRules(eventBasedObject);
     this._isRenderedIn3D = eventBasedObject.isRenderedIn3D();
 
     // Functor used to render an instance
@@ -142,21 +194,58 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
   ): RenderedInstance | Rendered3DInstance => {
     let renderedInstance = this.renderedInstances.get(instance.ptr);
     if (!renderedInstance) {
+      // No renderer associated yet, the instance must have been just created!...
+      let childObjectConfiguration = null;
+      const variant = this.getVariant();
+      if (variant) {
+        const childObjects = variant.getObjects();
+        if (childObjects.hasObjectNamed(instance.getObjectName())) {
+          const childObject = childObjects.getObject(instance.getObjectName());
+          childObjectConfiguration = childObject.getConfiguration();
+        }
+      }
+      // Apply property mapping rules on the child instance.
+      const childPropertyOverridings = new Map<string, string>();
       const customObjectConfiguration = gd.asCustomObjectConfiguration(
         this._associatedObjectConfiguration
       );
-      //No renderer associated yet, the instance must have been just created!...
-      const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
-        instance.getObjectName()
-      );
+      const customObjectProperties = customObjectConfiguration.getProperties();
+      for (const propertyMappingRule of this._propertyMappingRules) {
+        if (propertyMappingRule.targetChild !== instance.getObjectName()) {
+          continue;
+        }
+        const sourceValue = this._propertyOverridings.has(
+          propertyMappingRule.sourceProperty
+        )
+          ? this._propertyOverridings.get(propertyMappingRule.sourceProperty)
+          : customObjectProperties
+              .get(propertyMappingRule.sourceProperty)
+              .getValue();
+        if (sourceValue !== undefined) {
+          childPropertyOverridings.set(
+            propertyMappingRule.targetProperty,
+            sourceValue
+          );
+        }
+      }
       //...so let's create a renderer.
-      renderedInstance = ObjectsRenderingService.createNewInstanceRenderer(
-        this._project,
-        instance,
-        childObjectConfiguration,
-        this._pixiObject,
-        this._threeObject
-      );
+      renderedInstance = childObjectConfiguration
+        ? ObjectsRenderingService.createNewInstanceRenderer(
+            this._project,
+            instance,
+            childObjectConfiguration,
+            this._pixiObject,
+            this._threeObject,
+            childPropertyOverridings
+          )
+        : new RenderedUnknownInstance(
+            this._project,
+            instance,
+            // $FlowFixMe It's not actually used.
+            null,
+            this._pixiObject,
+            PixiResourcesLoader
+          );
       this.renderedInstances.set(instance.ptr, renderedInstance);
     }
     return renderedInstance;
@@ -260,12 +349,20 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
       }
       return 'res/unknown32.png';
     }
-    const childObjects = eventBasedObject.getObjects();
+    const variant = getVariant(eventBasedObject, customObjectConfiguration);
+    const childObjects = variant.getObjects();
     for (let i = 0; i < childObjects.getObjectsCount(); i++) {
       const childObject = childObjects.getObjectAt(i);
-      const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
-        childObject.getName()
-      );
+      const childObjectConfiguration =
+        customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() ||
+        customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration()
+          ? customObjectConfiguration.getChildObjectConfiguration(
+              childObject.getName()
+            )
+          : variant
+              .getObjects()
+              .getObject(childObject.getName())
+              .getConfiguration();
       const childType = childObjectConfiguration.getType();
       if (
         childType === 'Sprite' ||
@@ -291,12 +388,25 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
     });
   }
 
-  update() {
+  getVariant(): gdEventsBasedObjectVariant | null {
     const { eventBasedObject } = this;
     if (!eventBasedObject) {
+      return null;
+    }
+    const customObjectConfiguration = gd.asCustomObjectConfiguration(
+      this._associatedObjectConfiguration
+    );
+    return getVariant(eventBasedObject, customObjectConfiguration);
+  }
+
+  update() {
+    const { eventBasedObject } = this;
+    const variant = this.getVariant();
+    if (!eventBasedObject || !variant) {
       return;
     }
-    const layers = eventBasedObject.getLayers();
+
+    const layers = variant.getLayers();
     for (
       let layerIndex = 0;
       layerIndex < layers.getLayersCount();
@@ -304,13 +414,11 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
     ) {
       const layer = layers.getLayerAt(layerIndex);
       if (layer.getVisibility()) {
-        eventBasedObject
-          .getInitialInstances()
-          .iterateOverInstancesWithZOrdering(
-            // $FlowFixMe - gd.castObject is not supporting typings.
-            this.instancesRenderer,
-            layer.getName()
-          );
+        variant.getInitialInstances().iterateOverInstancesWithZOrdering(
+          // $FlowFixMe - gd.castObject is not supporting typings.
+          this.instancesRenderer,
+          layer.getName()
+        );
       }
     }
     this._updatePixiObjectsZOrder();
@@ -346,13 +454,10 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
         threeObject.scale.set(scaleX, scaleY, scaleZ);
       }
 
-      const { eventBasedObject } = this;
       const unscaledCenterX =
-        this.getDefaultWidth() / 2 +
-        (eventBasedObject ? eventBasedObject.getAreaMinX() : 0);
+        this.getDefaultWidth() / 2 + variant.getAreaMinX();
       const unscaledCenterY =
-        this.getDefaultHeight() / 2 +
-        (eventBasedObject ? eventBasedObject.getAreaMinY() : 0);
+        this.getDefaultHeight() / 2 + variant.getAreaMinY();
 
       this._pixiObject.pivot.x = unscaledCenterX;
       this._pixiObject.pivot.y = unscaledCenterY;
@@ -419,57 +524,44 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance
   }
 
   getDefaultWidth() {
-    const { eventBasedObject } = this;
-    return eventBasedObject
-      ? eventBasedObject.getAreaMaxX() - eventBasedObject.getAreaMinX()
-      : 48;
+    const variant = this.getVariant();
+    return variant ? variant.getAreaMaxX() - variant.getAreaMinX() : 48;
   }
 
   getDefaultHeight() {
-    const { eventBasedObject } = this;
-    return eventBasedObject
-      ? eventBasedObject.getAreaMaxY() - eventBasedObject.getAreaMinY()
-      : 48;
+    const variant = this.getVariant();
+    return variant ? variant.getAreaMaxY() - variant.getAreaMinY() : 48;
   }
 
   getDefaultDepth() {
-    const { eventBasedObject } = this;
-    return eventBasedObject
-      ? eventBasedObject.getAreaMaxZ() - eventBasedObject.getAreaMinZ()
-      : 48;
+    const variant = this.getVariant();
+    return variant ? variant.getAreaMaxZ() - variant.getAreaMinZ() : 48;
   }
 
   getOriginX(): number {
-    const { eventBasedObject } = this;
-    if (!eventBasedObject) {
+    const variant = this.getVariant();
+    if (!variant) {
       return 0;
     }
-    return (
-      (-eventBasedObject.getAreaMinX() / this.getDefaultWidth()) *
-      this.getWidth()
-    );
+    return (-variant.getAreaMinX() / this.getDefaultWidth()) * this.getWidth();
   }
 
   getOriginY(): number {
-    const { eventBasedObject } = this;
-    if (!eventBasedObject) {
+    const variant = this.getVariant();
+    if (!variant) {
       return 0;
     }
     return (
-      (-eventBasedObject.getAreaMinY() / this.getDefaultHeight()) *
-      this.getHeight()
+      (-variant.getAreaMinY() / this.getDefaultHeight()) * this.getHeight()
     );
   }
 
   getOriginZ(): number {
-    const { eventBasedObject } = this;
-    if (!eventBasedObject) {
+    const variant = this.getVariant();
+    if (!variant) {
       return 0;
     }
-    return (
-      (-eventBasedObject.getAreaMinZ() / this.getDefaultDepth()) *
-      this.getDepth()
-    );
+    return (-variant.getAreaMinZ() / this.getDefaultDepth()) * this.getDepth();
   }
 
   getCenterX() {
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js
index 1605d5ca3633..8bcadecc8e15 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js
@@ -15,13 +15,15 @@ export default class RenderedInstance {
   _pixiObject: PIXI.DisplayObject;
   wasUsed: boolean;
   _wasDestroyed: boolean;
+  _propertyOverridings: Map<string, string>;
 
   constructor(
     project: gdProject,
     instance: gdInitialInstance,
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
-    pixiResourcesLoader: Class<PixiResourcesLoader>
+    pixiResourcesLoader: Class<PixiResourcesLoader>,
+    propertyOverridings: Map<string, string> = new Map<string, string>()
   ) {
     this._pixiObject = null;
     this._instance = instance;
@@ -29,6 +31,7 @@ export default class RenderedInstance {
     this._pixiContainer = pixiContainer;
     this._project = project;
     this._pixiResourcesLoader = pixiResourcesLoader;
+    this._propertyOverridings = propertyOverridings;
     this.wasUsed = true; //Used by InstancesRenderer to track rendered instance that are not used anymore.
     this._wasDestroyed = false;
   }
diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js
index 630b56c11502..8a0fe6602766 100644
--- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js
+++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js
@@ -38,14 +38,16 @@ export default class RenderedTextInstance extends RenderedInstance {
     instance: gdInitialInstance,
     associatedObjectConfiguration: gdObjectConfiguration,
     pixiContainer: PIXI.Container,
-    pixiResourcesLoader: Class<PixiResourcesLoader>
+    pixiResourcesLoader: Class<PixiResourcesLoader>,
+    propertyOverridings: Map<string, string>
   ) {
     super(
       project,
       instance,
       associatedObjectConfiguration,
       pixiContainer,
-      pixiResourcesLoader
+      pixiResourcesLoader,
+      propertyOverridings
     );
 
     const style = new PIXI.TextStyle({
@@ -84,7 +86,9 @@ export default class RenderedTextInstance extends RenderedInstance {
     const textObjectConfiguration = gd.asTextObjectConfiguration(
       this._associatedObjectConfiguration
     );
-    this._pixiObject.text = textObjectConfiguration.getText();
+    this._pixiObject.text = this._propertyOverridings.has('Text')
+      ? this._propertyOverridings.get('Text')
+      : textObjectConfiguration.getText();
 
     //Update style, only if needed to avoid destroying text rendering performances
     if (
diff --git a/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js b/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js
index 82100d231640..73c10d5d624d 100644
--- a/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js
+++ b/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js
@@ -14,7 +14,7 @@ type Props = {|
   onClose: () => void,
   extensionShortHeader: ExtensionShortHeader,
   extensionName: string,
-  onInstallExtension: ExtensionShortHeader => void,
+  onInstallExtension: (extensionName: string) => void,
   onOpenEventsFunctionsExtension: string => void,
 |};
 
@@ -34,7 +34,7 @@ function InstalledExtensionDetails({
   const installOrUpdateExtension = async (i18n: I18nType) => {
     setIsInstalling(true);
     try {
-      onInstallExtension(extensionShortHeader);
+      onInstallExtension(extensionShortHeader.name);
       await installExtension(
         i18n,
         project,
diff --git a/newIDE/app/src/ProjectManager/index.js b/newIDE/app/src/ProjectManager/index.js
index 3d56d4a72ba7..fe935a68280d 100644
--- a/newIDE/app/src/ProjectManager/index.js
+++ b/newIDE/app/src/ProjectManager/index.js
@@ -419,7 +419,7 @@ type Props = {|
   onReloadEventsFunctionsExtensions: () => void,
   isOpen: boolean,
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
-  onInstallExtension: ExtensionShortHeader => void,
+  onInstallExtension: (extensionName: string) => void,
   onShareProject: () => void,
   onOpenHomePage: () => void,
   toggleProjectManager: () => void,
@@ -1421,6 +1421,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
                         setProjectVariablesEditorOpen(false);
                       }}
                       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
+                      isListLocked={false}
                     />
                   )}
                   {project && !!editedPropertiesLayout && (
@@ -1451,6 +1452,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>(
                         onOpenLayoutVariables(null);
                       }}
                       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
+                      isListLocked={false}
                     />
                   )}
                   {project && extensionsSearchDialogOpen && (
diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js
index 4eddf1548f96..e70ba2b542d9 100644
--- a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js
+++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js
@@ -25,6 +25,7 @@ export type SceneEditorsDisplayProps = {|
   layout: gdLayout | null,
   eventsFunctionsExtension: gdEventsFunctionsExtension | null,
   eventsBasedObject: gdEventsBasedObject | null,
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant | null,
   layersContainer: gdLayersContainer,
   globalObjectsContainer: gdObjectsContainer | null,
   objectsContainer: gdObjectsContainer,
@@ -46,6 +47,11 @@ export type SceneEditorsDisplayProps = {|
     extensionName: string,
     eventsBasedObjectName: string
   ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
   selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
   onSelectLayer: (layerName: string) => void,
   editLayerEffects: (layer: ?gdLayer) => void,
diff --git a/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js b/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js
index 22ab49b8188f..5c2ff0cc89f4 100644
--- a/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js
+++ b/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js
@@ -11,15 +11,19 @@ import Checkbox from '../UI/Checkbox';
 
 type Props = {|
   eventsBasedObject: gdEventsBasedObject,
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant,
   project: gdProject,
   onApply: () => void,
   onClose: () => void,
   getContentAABB: () => Rectangle | null,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
 |};
 
 const EventsBasedObjectScenePropertiesDialog = ({
   eventsBasedObject,
+  eventsBasedObjectVariant,
   project,
   onApply,
   onClose,
@@ -27,22 +31,22 @@ const EventsBasedObjectScenePropertiesDialog = ({
   onEventsBasedObjectChildrenEdited,
 }: Props) => {
   const [areaMinX, setAreaMinX] = React.useState<number>(
-    eventsBasedObject.getAreaMinX()
+    eventsBasedObjectVariant.getAreaMinX()
   );
   const [areaMinY, setAreaMinY] = React.useState<number>(
-    eventsBasedObject.getAreaMinY()
+    eventsBasedObjectVariant.getAreaMinY()
   );
   const [areaMinZ, setAreaMinZ] = React.useState<number>(
-    eventsBasedObject.getAreaMinZ()
+    eventsBasedObjectVariant.getAreaMinZ()
   );
   const [areaMaxX, setAreaMaxX] = React.useState<number>(
-    eventsBasedObject.getAreaMaxX()
+    eventsBasedObjectVariant.getAreaMaxX()
   );
   const [areaMaxY, setAreaMaxY] = React.useState<number>(
-    eventsBasedObject.getAreaMaxY()
+    eventsBasedObjectVariant.getAreaMaxY()
   );
   const [areaMaxZ, setAreaMaxZ] = React.useState<number>(
-    eventsBasedObject.getAreaMaxZ()
+    eventsBasedObjectVariant.getAreaMaxZ()
   );
   const [isRenderedIn3D, setRenderedIn3D] = React.useState<boolean>(
     eventsBasedObject.isRenderedIn3D()
@@ -56,28 +60,28 @@ const EventsBasedObjectScenePropertiesDialog = ({
 
   const onSubmit = () => {
     if (areaMinX < areaMaxX) {
-      eventsBasedObject.setAreaMinX(areaMinX);
-      eventsBasedObject.setAreaMaxX(areaMaxX);
+      eventsBasedObjectVariant.setAreaMinX(areaMinX);
+      eventsBasedObjectVariant.setAreaMaxX(areaMaxX);
     }
     if (areaMinY < areaMaxY) {
-      eventsBasedObject.setAreaMinY(areaMinY);
-      eventsBasedObject.setAreaMaxY(areaMaxY);
+      eventsBasedObjectVariant.setAreaMinY(areaMinY);
+      eventsBasedObjectVariant.setAreaMaxY(areaMaxY);
     }
     if (areaMinZ < areaMaxZ) {
-      eventsBasedObject.setAreaMinZ(areaMinZ);
-      eventsBasedObject.setAreaMaxZ(areaMaxZ);
+      eventsBasedObjectVariant.setAreaMinZ(areaMinZ);
+      eventsBasedObjectVariant.setAreaMaxZ(areaMaxZ);
     }
     const wasRenderedIn3D = eventsBasedObject.isRenderedIn3D();
     if (wasRenderedIn3D !== isRenderedIn3D) {
       eventsBasedObject.markAsRenderedIn3D(isRenderedIn3D);
-      onEventsBasedObjectChildrenEdited();
+      onEventsBasedObjectChildrenEdited(eventsBasedObject);
     }
     const wasInnerAreaFollowingParentSize = eventsBasedObject.isInnerAreaFollowingParentSize();
     if (wasInnerAreaFollowingParentSize !== isInnerAreaFollowingParentSize) {
       eventsBasedObject.markAsInnerAreaFollowingParentSize(
         isInnerAreaFollowingParentSize
       );
-      onEventsBasedObjectChildrenEdited();
+      onEventsBasedObjectChildrenEdited(eventsBasedObject);
     }
     onApply();
   };
@@ -117,7 +121,10 @@ const EventsBasedObjectScenePropertiesDialog = ({
     <Dialog
       title={
         <Trans>
-          {eventsBasedObject.getFullName() || eventsBasedObject.getName()}{' '}
+          {(eventsBasedObject.getFullName() || eventsBasedObject.getName()) +
+            ' (' +
+            eventsBasedObjectVariant.getName() +
+            ')'}{' '}
           properties
         </Trans>
       }
diff --git a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js
index e3bf2187ccf4..13bb333dd268 100644
--- a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js
+++ b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js
@@ -36,12 +36,14 @@ type Props = {|
   i18n: I18nType,
   historyHandler?: HistoryHandler,
   lastSelectionType: 'instance' | 'object',
+  isVariableListLocked: boolean,
 
   // For objects:
   objects: Array<gdObject>,
   onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
   onUpdateBehaviorsSharedData: () => void,
   onExtensionInstalled: (extensionName: string) => void,
+  isBehaviorListLocked: boolean,
 
   // For instances:
   instances: Array<gdInitialInstance>,
@@ -83,6 +85,7 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef<
     eventsFunctionsExtension,
     onUpdateBehaviorsSharedData,
     onExtensionInstalled,
+    isBehaviorListLocked,
 
     // For instances:
     instances,
@@ -116,6 +119,7 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef<
           eventsFunctionsExtension={eventsFunctionsExtension}
           onUpdateBehaviorsSharedData={onUpdateBehaviorsSharedData}
           onExtensionInstalled={onExtensionInstalled}
+          isBehaviorListLocked={isBehaviorListLocked}
           {...commonProps}
         />
       ) : (
diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js
index 1a13ac1b39e1..40e1d93e2958 100644
--- a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js
+++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js
@@ -87,6 +87,7 @@ const MosaicEditorsDisplay = React.forwardRef<
     layout,
     eventsFunctionsExtension,
     eventsBasedObject,
+    eventsBasedObjectVariant,
     updateBehaviorsSharedData,
     layersContainer,
     globalObjectsContainer,
@@ -252,6 +253,10 @@ const MosaicEditorsDisplay = React.forwardRef<
 
   const selectedObjectNames = selectedObjects.map(object => object.getName());
 
+  const isCustomVariant = eventsBasedObject
+    ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant
+    : false;
+
   const editors = {
     properties: {
       type: 'secondary',
@@ -284,6 +289,8 @@ const MosaicEditorsDisplay = React.forwardRef<
               onSelectTileMapTile={props.onSelectTileMapTile}
               lastSelectionType={props.lastSelectionType}
               onExtensionInstalled={props.onExtensionInstalled}
+              isVariableListLocked={isCustomVariant}
+              isBehaviorListLocked={isCustomVariant}
             />
           )}
         </I18n>
@@ -333,6 +340,7 @@ const MosaicEditorsDisplay = React.forwardRef<
           project={project}
           layout={layout}
           eventsBasedObject={eventsBasedObject}
+          eventsBasedObjectVariant={eventsBasedObjectVariant}
           globalObjectsContainer={globalObjectsContainer}
           objectsContainer={objectsContainer}
           layersContainer={layersContainer}
@@ -391,6 +399,9 @@ const MosaicEditorsDisplay = React.forwardRef<
               }
               onEditObject={props.onEditObject}
               onOpenEventBasedObjectEditor={props.onOpenEventBasedObjectEditor}
+              onOpenEventBasedObjectVariantEditor={
+                props.onOpenEventBasedObjectVariantEditor
+              }
               onExportAssets={props.onExportAssets}
               onDeleteObjects={(objectWithContext, cb) =>
                 props.onDeleteObjects(i18n, objectWithContext, cb)
@@ -414,6 +425,7 @@ const MosaicEditorsDisplay = React.forwardRef<
               ref={objectsListRef}
               unsavedChanges={props.unsavedChanges}
               hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps}
+              isListLocked={isCustomVariant}
             />
           )}
         </I18n>
@@ -444,6 +456,7 @@ const MosaicEditorsDisplay = React.forwardRef<
                 props.canObjectOrGroupBeGlobal(i18n, groupName)
               }
               unsavedChanges={props.unsavedChanges}
+              isListLocked={isCustomVariant}
             />
           )}
         </I18n>
diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js
index b5da19831fcf..326f3113a45b 100644
--- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js
+++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js
@@ -63,6 +63,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
     layout,
     eventsFunctionsExtension,
     eventsBasedObject,
+    eventsBasedObjectVariant,
     updateBehaviorsSharedData,
     layersContainer,
     globalObjectsContainer,
@@ -252,6 +253,10 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
       ? editorTitleById[selectedEditorId]
       : null;
 
+  const isCustomVariant = eventsBasedObject
+    ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant
+    : false;
+
   return (
     <FullSizeMeasurer>
       {({ width, height }) => (
@@ -267,6 +272,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
               project={project}
               layout={layout}
               eventsBasedObject={eventsBasedObject}
+              eventsBasedObjectVariant={eventsBasedObjectVariant}
               globalObjectsContainer={globalObjectsContainer}
               objectsContainer={objectsContainer}
               layersContainer={layersContainer}
@@ -331,6 +337,9 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
                       onOpenEventBasedObjectEditor={
                         props.onOpenEventBasedObjectEditor
                       }
+                      onOpenEventBasedObjectVariantEditor={
+                        props.onOpenEventBasedObjectVariantEditor
+                      }
                       onExportAssets={props.onExportAssets}
                       onDeleteObjects={(objectWithContext, cb) =>
                         props.onDeleteObjects(i18n, objectWithContext, cb)
@@ -362,6 +371,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
                       hotReloadPreviewButtonProps={
                         props.hotReloadPreviewButtonProps
                       }
+                      isListLocked={isCustomVariant}
                     />
                   )}
                 </I18n>
@@ -398,6 +408,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
                       onSelectTileMapTile={props.onSelectTileMapTile}
                       lastSelectionType={props.lastSelectionType}
                       onExtensionInstalled={props.onExtensionInstalled}
+                      isVariableListLocked={isCustomVariant}
+                      isBehaviorListLocked={isCustomVariant}
                     />
                   )}
                 </I18n>
@@ -430,6 +442,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
                         props.canObjectOrGroupBeGlobal(i18n, groupName)
                       }
                       unsavedChanges={props.unsavedChanges}
+                      isListLocked={isCustomVariant}
                     />
                   )}
                 </I18n>
diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js
index 6331a0b2927e..6d11d95eb09a 100644
--- a/newIDE/app/src/SceneEditor/index.js
+++ b/newIDE/app/src/SceneEditor/index.js
@@ -101,6 +101,7 @@ type Props = {|
   layout: gdLayout | null,
   eventsFunctionsExtension: gdEventsFunctionsExtension | null,
   eventsBasedObject: gdEventsBasedObject | null,
+  eventsBasedObjectVariant: gdEventsBasedObjectVariant | null,
 
   globalObjectsContainer: gdObjectsContainer | null,
   objectsContainer: gdObjectsContainer,
@@ -112,7 +113,10 @@ type Props = {|
   onOpenMoreSettings?: ?() => void,
   onOpenEvents: (sceneName: string) => void,
   onObjectEdited: (objectWithContext: ObjectWithContext) => void,
-  onEventsBasedObjectChildrenEdited: () => void,
+  onObjectGroupEdited: (objectGroupWithContext: GroupWithContext) => void,
+  onEventsBasedObjectChildrenEdited: (
+    eventsBasedObject: gdEventsBasedObject
+  ) => void,
 
   setToolbar: (?React.Node) => void,
   resourceManagementProps: ResourceManagementProps,
@@ -128,7 +132,17 @@ type Props = {|
     extensionName: string,
     eventsBasedObjectName: string
   ) => void,
+  onOpenEventBasedObjectVariantEditor: (
+    extensionName: string,
+    eventsBasedObjectName: string,
+    variantName: string
+  ) => void,
   onExtensionInstalled: (extensionName: string) => void,
+  onDeleteEventsBasedObjectVariant: (
+    eventsFunctionsExtension: gdEventsFunctionsExtension,
+    eventBasedObject: gdEventsBasedObject,
+    variant: gdEventsBasedObjectVariant
+  ) => void,
 
   // Preview:
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
@@ -536,6 +550,13 @@ export default class SceneEditor extends React.Component<Props, State> {
   };
 
   _closeObjectGroupEditorDialog = () => {
+    if (this.state.editedGroup) {
+      // TODO Set the `global` attribute correctly.
+      this.props.onObjectGroupEdited({
+        group: this.state.editedGroup,
+        global: false,
+      });
+    }
     this.setState({ editedGroup: null, isCreatingNewGroup: false });
   };
 
@@ -951,7 +972,7 @@ export default class SceneEditor extends React.Component<Props, State> {
     objectsWithContext: ObjectWithContext[],
     done: boolean => void
   ) => {
-    const { project, layout, eventsBasedObject } = this.props;
+    const { project, layout, eventsBasedObject, onObjectEdited } = this.props;
 
     objectsWithContext.forEach(objectWithContext => {
       const { object, global } = objectWithContext;
@@ -985,6 +1006,10 @@ export default class SceneEditor extends React.Component<Props, State> {
 
     done(true);
 
+    objectsWithContext.forEach(objectWithContext => {
+      // TODO Avoid to do this N times.
+      onObjectEdited(objectWithContext);
+    });
     // We modified the selection, so force an update of editors dealing with it.
     this.forceUpdatePropertiesEditor();
     this.updateToolbar();
@@ -1182,6 +1207,7 @@ export default class SceneEditor extends React.Component<Props, State> {
     done: boolean => void
   ) => {
     done(true);
+    this.props.onObjectGroupEdited(groupWithContext);
   };
 
   _onRenameObjectGroup = (
@@ -1229,8 +1255,8 @@ export default class SceneEditor extends React.Component<Props, State> {
         /* isObjectGroup=*/ true
       );
     }
-
     done(true);
+    this.props.onObjectGroupEdited(groupWithContext);
   };
 
   canObjectOrGroupBeGlobal = (
@@ -1550,15 +1576,20 @@ export default class SceneEditor extends React.Component<Props, State> {
         object && project.hasEventsBasedObject(object.getType())
           ? {
               label: i18n._(t`Edit children`),
-              click: () =>
-                this.props.onOpenEventBasedObjectEditor(
+              click: () => {
+                const customObjectConfiguration = gd.asCustomObjectConfiguration(
+                  object.getConfiguration()
+                );
+                this.props.onOpenEventBasedObjectVariantEditor(
                   gd.PlatformExtension.getExtensionFromFullObjectType(
                     object.getType()
                   ),
                   gd.PlatformExtension.getObjectNameFromFullObjectType(
                     object.getType()
-                  )
-                ),
+                  ),
+                  customObjectConfiguration.getVariantName()
+                );
+              },
             }
           : null,
         { type: 'separator' },
@@ -1888,6 +1919,7 @@ export default class SceneEditor extends React.Component<Props, State> {
       layout,
       eventsFunctionsExtension,
       eventsBasedObject,
+      eventsBasedObjectVariant,
       initialInstances,
       resourceManagementProps,
       isActive,
@@ -1921,6 +1953,10 @@ export default class SceneEditor extends React.Component<Props, State> {
         </Trans>
       ) : null;
 
+    const isCustomVariant = eventsBasedObject
+      ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant
+      : false;
+
     return (
       <ResponsiveWindowMeasurer>
         {({ isMobile }) => {
@@ -1953,6 +1989,7 @@ export default class SceneEditor extends React.Component<Props, State> {
                 layout={layout}
                 eventsFunctionsExtension={eventsFunctionsExtension}
                 eventsBasedObject={eventsBasedObject}
+                eventsBasedObjectVariant={eventsBasedObjectVariant}
                 layersContainer={this.props.layersContainer}
                 globalObjectsContainer={this.props.globalObjectsContainer}
                 objectsContainer={this.props.objectsContainer}
@@ -1994,6 +2031,9 @@ export default class SceneEditor extends React.Component<Props, State> {
                 onOpenEventBasedObjectEditor={
                   this.props.onOpenEventBasedObjectEditor
                 }
+                onOpenEventBasedObjectVariantEditor={
+                  this.props.onOpenEventBasedObjectVariantEditor
+                }
                 onRenameObjectFolderOrObjectWithContextFinish={
                   this._onRenameObjectFolderOrObjectWithContextFinish
                 }
@@ -2116,6 +2156,17 @@ export default class SceneEditor extends React.Component<Props, State> {
                         }
                         openBehaviorEvents={this.props.openBehaviorEvents}
                         onExtensionInstalled={this.props.onExtensionInstalled}
+                        onOpenEventBasedObjectEditor={
+                          this.props.onOpenEventBasedObjectEditor
+                        }
+                        onOpenEventBasedObjectVariantEditor={
+                          this.props.onOpenEventBasedObjectVariantEditor
+                        }
+                        onDeleteEventsBasedObjectVariant={
+                          this.props.onDeleteEventsBasedObjectVariant
+                        }
+                        isBehaviorListLocked={isCustomVariant}
+                        isVariableListLocked={isCustomVariant}
                       />
                     )}
                   </React.Fragment>
@@ -2146,6 +2197,11 @@ export default class SceneEditor extends React.Component<Props, State> {
                         objectGroup
                       );
                     }
+                    // TODO Set the `global` attribute correctly.
+                    this.props.onObjectGroupEdited({
+                      group: objectGroup,
+                      global: false,
+                    });
                   }}
                   initialTab={'objects'}
                   onComputeAllVariableNames={() => {
@@ -2160,6 +2216,8 @@ export default class SceneEditor extends React.Component<Props, State> {
                       editedGroup.getName()
                     );
                   }}
+                  isVariableListLocked={isCustomVariant}
+                  isObjectListLocked={isCustomVariant}
                 />
               )}
               {this.state.setupGridOpen && (
@@ -2196,6 +2254,7 @@ export default class SceneEditor extends React.Component<Props, State> {
                     hotReloadPreviewButtonProps={
                       this.props.hotReloadPreviewButtonProps
                     }
+                    isListLocked={true}
                   />
                 )}
               {!!this.state.layerRemoved &&
@@ -2242,22 +2301,25 @@ export default class SceneEditor extends React.Component<Props, State> {
                   resourceManagementProps={this.props.resourceManagementProps}
                 />
               )}
-              {this.state.scenePropertiesDialogOpen && eventsBasedObject && (
-                <EventsBasedObjectScenePropertiesDialog
-                  project={project}
-                  eventsBasedObject={eventsBasedObject}
-                  onClose={() => this.openSceneProperties(false)}
-                  onApply={() => this.openSceneProperties(false)}
-                  getContentAABB={
-                    this.editorDisplay
-                      ? this.editorDisplay.instancesHandlers.getContentAABB
-                      : () => null
-                  }
-                  onEventsBasedObjectChildrenEdited={
-                    this.props.onEventsBasedObjectChildrenEdited
-                  }
-                />
-              )}
+              {this.state.scenePropertiesDialogOpen &&
+                eventsBasedObject &&
+                eventsBasedObjectVariant && (
+                  <EventsBasedObjectScenePropertiesDialog
+                    project={project}
+                    eventsBasedObject={eventsBasedObject}
+                    eventsBasedObjectVariant={eventsBasedObjectVariant}
+                    onClose={() => this.openSceneProperties(false)}
+                    onApply={() => this.openSceneProperties(false)}
+                    getContentAABB={
+                      this.editorDisplay
+                        ? this.editorDisplay.instancesHandlers.getContentAABB
+                        : () => null
+                    }
+                    onEventsBasedObjectChildrenEdited={
+                      this.props.onEventsBasedObjectChildrenEdited
+                    }
+                  />
+                )}
               {!!this.state.layoutVariablesDialogOpen && layout && (
                 <SceneVariablesDialog
                   open
@@ -2268,6 +2330,7 @@ export default class SceneEditor extends React.Component<Props, State> {
                   hotReloadPreviewButtonProps={
                     this.props.hotReloadPreviewButtonProps
                   }
+                  isListLocked={false}
                 />
               )}
               <I18n>
diff --git a/newIDE/app/src/UI/TreeView/TreeViewRow.js b/newIDE/app/src/UI/TreeView/TreeViewRow.js
index 55c12732c64c..bc34ea0b65a9 100644
--- a/newIDE/app/src/UI/TreeView/TreeViewRow.js
+++ b/newIDE/app/src/UI/TreeView/TreeViewRow.js
@@ -458,6 +458,11 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
                           }
                         }}
                         tooltip={rightButton.label}
+                        disabled={
+                          rightButton.enabled === undefined
+                            ? false
+                            : !rightButton.enabled
+                        }
                       >
                         {rightButton.icon}
                       </IconButton>
diff --git a/newIDE/app/src/UI/TreeView/index.js b/newIDE/app/src/UI/TreeView/index.js
index 03d42a69d1e0..2b41cc5c000c 100644
--- a/newIDE/app/src/UI/TreeView/index.js
+++ b/newIDE/app/src/UI/TreeView/index.js
@@ -30,6 +30,7 @@ export type MenuButton = {|
   icon: React.Node,
   label: MessageDescriptor,
   click: ?() => void | Promise<void>,
+  enabled?: boolean,
 |};
 
 type FlattenedNode<Item> = {|
diff --git a/newIDE/app/src/Utils/GDevelopServices/Asset.js b/newIDE/app/src/Utils/GDevelopServices/Asset.js
index 59153bbb2af4..e07a963bda3c 100644
--- a/newIDE/app/src/Utils/GDevelopServices/Asset.js
+++ b/newIDE/app/src/Utils/GDevelopServices/Asset.js
@@ -45,6 +45,10 @@ export type ExtensionDependency = {|
 export type ObjectAsset = {|
   object: any /*(serialized gdObjectConfiguration)*/,
   resources: Array<any /*(serialized gdResource)*/>,
+  variants?: Array<{
+    objectType: string,
+    variant: any /*(serialized gdEventsBasedObjectVariant)*/,
+  }>,
   // TODO This can become mandatory after the migration of the asset repository.
   requiredExtensions?: Array<ExtensionDependency>,
 |};
diff --git a/newIDE/app/src/Utils/Serializer.js b/newIDE/app/src/Utils/Serializer.js
index a08fec8c5917..922974f38cd5 100644
--- a/newIDE/app/src/Utils/Serializer.js
+++ b/newIDE/app/src/Utils/Serializer.js
@@ -8,7 +8,7 @@ const gd: libGDevelop = global.gd;
  * and unserializeFrom method.
  *
  * @param {*} serializable
- * @param {*} methodName The name of the serialization method. "unserializeFrom" by default
+ * @param {*} methodName The name of the serialization method. "serializeTo" by default
  */
 export function serializeToJSObject(
   serializable: gdSerializable,
diff --git a/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js b/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js
index 25b3333a7253..de7040061476 100644
--- a/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js
@@ -15,6 +15,7 @@ type Props = {|
   isGlobalTabInitiallyOpen?: boolean,
   initiallySelectedVariableName?: string,
   shouldCreateInitiallySelectedVariable?: boolean,
+  isListLocked: boolean,
 |};
 
 const GlobalAndSceneVariablesDialog = ({
@@ -26,6 +27,7 @@ const GlobalAndSceneVariablesDialog = ({
   isGlobalTabInitiallyOpen,
   initiallySelectedVariableName,
   shouldCreateInitiallySelectedVariable,
+  isListLocked,
 }: Props) => {
   const {
     project,
@@ -127,6 +129,7 @@ const GlobalAndSceneVariablesDialog = ({
       helpPagePath={'/all-features/variables/scene-variables'}
       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
       id="global-and-scene-variables-dialog"
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/GlobalVariablesDialog.js b/newIDE/app/src/VariablesList/GlobalVariablesDialog.js
index 40337716e0f4..44092e4d3036 100644
--- a/newIDE/app/src/VariablesList/GlobalVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/GlobalVariablesDialog.js
@@ -14,6 +14,7 @@ type Props = {|
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null,
   initiallySelectedVariableName?: string,
   shouldCreateInitiallySelectedVariable?: boolean,
+  isListLocked: boolean,
 |};
 
 const GlobalVariablesDialog = ({
@@ -24,6 +25,7 @@ const GlobalVariablesDialog = ({
   hotReloadPreviewButtonProps,
   initiallySelectedVariableName,
   shouldCreateInitiallySelectedVariable,
+  isListLocked,
 }: Props) => {
   const onComputeAllVariableNames = React.useCallback(
     () =>
@@ -76,6 +78,7 @@ const GlobalVariablesDialog = ({
       helpPagePath={'/all-features/variables/global-variables'}
       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
       id="global-variables-dialog"
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/LocalVariablesDialog.js b/newIDE/app/src/VariablesList/LocalVariablesDialog.js
index a8f07b0d2747..ed20417f9596 100644
--- a/newIDE/app/src/VariablesList/LocalVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/LocalVariablesDialog.js
@@ -13,6 +13,7 @@ type Props = {|
   onCancel: () => void,
   initiallySelectedVariableName: string,
   shouldCreateInitiallySelectedVariable?: boolean,
+  isListLocked: boolean,
 |};
 
 const LocalVariablesDialog = ({
@@ -24,6 +25,7 @@ const LocalVariablesDialog = ({
   onApply,
   initiallySelectedVariableName,
   shouldCreateInitiallySelectedVariable,
+  isListLocked,
 }: Props) => {
   const tabs = React.useMemo(
     () => [
@@ -53,6 +55,7 @@ const LocalVariablesDialog = ({
         shouldCreateInitiallySelectedVariable
       }
       hotReloadPreviewButtonProps={null}
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js
index f376c4586f60..5eebeed3611d 100644
--- a/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js
@@ -34,6 +34,7 @@ type Props = {|
   shouldCreateInitiallySelectedVariable?: boolean,
   hotReloadPreviewButtonProps?: ?HotReloadPreviewButtonProps,
   onComputeAllVariableNames: () => Array<string>,
+  isListLocked: boolean,
 |};
 
 const ObjectGroupVariablesDialog = ({
@@ -50,6 +51,7 @@ const ObjectGroupVariablesDialog = ({
   initiallySelectedVariableName,
   shouldCreateInitiallySelectedVariable,
   onComputeAllVariableNames,
+  isListLocked,
 }: Props) => {
   const groupVariablesContainer = useValueWithInit(
     // The VariablesContainer is returned by value.
@@ -100,6 +102,16 @@ const ObjectGroupVariablesDialog = ({
       changeset,
       originalSerializedVariables
     );
+    const { eventsBasedObject } = projectScopedContainersAccessor.getScope();
+    if (eventsBasedObject) {
+      for (const objectName of objectGroup.getAllObjectsNames().toJSArray()) {
+        gd.ObjectVariableHelper.applyChangesToVariants(
+          eventsBasedObject,
+          objectName,
+          changeset
+        );
+      }
+    }
     groupVariablesContainer.clearPersistentUuid();
   };
 
@@ -198,6 +210,7 @@ const ObjectGroupVariablesDialog = ({
           onComputeAllVariableNames={onComputeAllVariableNames}
           onVariablesUpdated={notifyOfVariableChange}
           onSelectedVariableChange={onSelectedVariableChange}
+          isListLocked={isListLocked}
         />
       </Column>
     </Dialog>
diff --git a/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js
index a8fc04e1549a..bfcb98ede7be 100644
--- a/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js
@@ -20,6 +20,7 @@ type Props = {|
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
   initiallySelectedVariableName?: string,
   onEditObjectVariables: () => void,
+  isListLocked: boolean,
 |};
 
 const ObjectInstanceVariablesDialog = ({
@@ -35,6 +36,7 @@ const ObjectInstanceVariablesDialog = ({
   initiallySelectedVariableName,
   projectScopedContainersAccessor,
   onEditObjectVariables,
+  isListLocked,
 }: Props) => {
   const tabs = React.useMemo(
     () => {
@@ -91,6 +93,7 @@ const ObjectInstanceVariablesDialog = ({
       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
       id="instance-variables-dialog"
       onEditObjectVariables={onEditObjectVariables}
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/ObjectVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectVariablesDialog.js
index fe65423b516b..4e2b08ddb8e8 100644
--- a/newIDE/app/src/VariablesList/ObjectVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/ObjectVariablesDialog.js
@@ -18,6 +18,7 @@ type Props = {|
   initiallySelectedVariableName?: string,
   shouldCreateInitiallySelectedVariable?: boolean,
   onComputeAllVariableNames: () => Array<string>,
+  isListLocked: boolean,
 |};
 
 const ObjectVariablesDialog = ({
@@ -33,6 +34,7 @@ const ObjectVariablesDialog = ({
   shouldCreateInitiallySelectedVariable,
   projectScopedContainersAccessor,
   onComputeAllVariableNames,
+  isListLocked,
 }: Props) => {
   const tabs = React.useMemo(
     () => [
@@ -70,6 +72,7 @@ const ObjectVariablesDialog = ({
       helpPagePath={'/all-features/variables/object-variables'}
       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
       id="object-variables-dialog"
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/SceneVariablesDialog.js b/newIDE/app/src/VariablesList/SceneVariablesDialog.js
index e67102e0ce06..b75457281cf2 100644
--- a/newIDE/app/src/VariablesList/SceneVariablesDialog.js
+++ b/newIDE/app/src/VariablesList/SceneVariablesDialog.js
@@ -15,6 +15,7 @@ type Props = {|
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null,
   initiallySelectedVariableName?: string,
   shouldCreateInitiallySelectedVariable?: boolean,
+  isListLocked: boolean,
 |};
 
 const SceneVariablesDialog = ({
@@ -26,6 +27,7 @@ const SceneVariablesDialog = ({
   hotReloadPreviewButtonProps,
   initiallySelectedVariableName,
   shouldCreateInitiallySelectedVariable,
+  isListLocked,
 }: Props) => {
   const onComputeAllVariableNames = React.useCallback(
     () =>
@@ -78,6 +80,7 @@ const SceneVariablesDialog = ({
       helpPagePath={'/all-features/variables/scene-variables'}
       hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
       id="scene-variables-dialog"
+      isListLocked={isListLocked}
     />
   );
 };
diff --git a/newIDE/app/src/VariablesList/VariableTypeSelector.js b/newIDE/app/src/VariablesList/VariableTypeSelector.js
index 1480e4d4d63d..dd9f3c2ab285 100644
--- a/newIDE/app/src/VariablesList/VariableTypeSelector.js
+++ b/newIDE/app/src/VariablesList/VariableTypeSelector.js
@@ -25,6 +25,7 @@ type Props = {|
   readOnlyWithIcon?: boolean,
   id?: string,
   errorMessage: MessageDescriptor | null,
+  disabled?: boolean,
 |};
 
 let options;
@@ -127,6 +128,7 @@ const VariableTypeSelector = React.memo<Props>((props: Props) => {
                 : undefined,
             }}
             id={props.id}
+            disabled={props.disabled}
           >
             {getOptions()}
           </SelectField>
diff --git a/newIDE/app/src/VariablesList/VariablesEditorDialog.js b/newIDE/app/src/VariablesList/VariablesEditorDialog.js
index 9ec09467b808..7f32a03f6d7b 100644
--- a/newIDE/app/src/VariablesList/VariablesEditorDialog.js
+++ b/newIDE/app/src/VariablesList/VariablesEditorDialog.js
@@ -48,6 +48,7 @@ type Props = {|
   initiallyOpenTabId?: string,
   initiallySelectedVariableName?: string,
   shouldCreateInitiallySelectedVariable?: boolean,
+  isListLocked: boolean,
 
   project: gdProject,
   hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null,
@@ -73,6 +74,7 @@ const VariablesEditorDialog = ({
   projectScopedContainersAccessor,
   objectName,
   initialInstances,
+  isListLocked,
 }: Props) => {
   const serializableObjects = React.useMemo(
     () =>
@@ -295,6 +297,7 @@ const VariablesEditorDialog = ({
                   helpPagePath={helpPagePath}
                   onVariablesUpdated={notifyOfChange}
                   onSelectedVariableChange={onSelectedVariableChange}
+                  isListLocked={isListLocked}
                 />
               </Column>
             )
diff --git a/newIDE/app/src/VariablesList/VariablesList.js b/newIDE/app/src/VariablesList/VariablesList.js
index b1d8b6b83c50..6cb71c4c50c8 100644
--- a/newIDE/app/src/VariablesList/VariablesList.js
+++ b/newIDE/app/src/VariablesList/VariablesList.js
@@ -132,6 +132,7 @@ type Props = {|
   onVariablesUpdated?: () => void,
   toolbarIconStyle?: any,
   onSelectedVariableChange?: (Array<string>) => void,
+  isListLocked: boolean,
 |};
 
 const variableRowStyles = {
@@ -149,6 +150,8 @@ type VariableRowProps = {|
   draggedNodeId: { current: ?string },
   nodeId: string,
   isInherited: boolean,
+  isNameLocked: boolean,
+  isTypeLocked: boolean,
   canDrop: string => boolean,
   dropNode: (string, where: 'after' | 'before') => void,
   isSelected: boolean,
@@ -198,6 +201,8 @@ const VariableRow = React.memo<VariableRowProps>(
     draggedNodeId,
     nodeId,
     isInherited,
+    isNameLocked,
+    isTypeLocked,
     canDrop,
     dropNode,
     isSelected,
@@ -364,7 +369,9 @@ const VariableRow = React.memo<VariableRowProps>(
                           directlyStoreValueChangesWhileEditing
                         }
                         disabled={
-                          isInherited || parentType === gd.Variable.Array
+                          isNameLocked ||
+                          isInherited ||
+                          parentType === gd.Variable.Array
                         }
                         onChange={onChangeName}
                         additionalContext={JSON.stringify({ nodeId, depth })}
@@ -387,6 +394,7 @@ const VariableRow = React.memo<VariableRowProps>(
                             }
                             id={`variable-${index}-type`}
                             errorMessage={typeErrorMessage}
+                            disabled={isTypeLocked}
                           />
                         </Column>
                         <Column expand>
@@ -1053,11 +1061,27 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
           current,
           props.variablesContainer
         );
-        const { variable: draggedVariable } = draggedVariableContext;
+        const {
+          variable: draggedVariable,
+          lineage: draggedLineage,
+        } = draggedVariableContext;
         if (!draggedVariable) return false;
 
         if (isAnAncestryOf(draggedVariable, targetLineage)) return false;
 
+        const targetVariableParentVariable = getDirectParentVariable(
+          targetLineage
+        );
+        const draggedVariableParentVariable = getDirectParentVariable(
+          draggedLineage
+        );
+        if (
+          props.isListLocked &&
+          (!targetVariableParentVariable || !draggedVariableParentVariable)
+        ) {
+          return false;
+        }
+
         const movementType = getMovementTypeWithinVariablesContainer(
           draggedVariableContext,
           targetVariableContext
@@ -1080,7 +1104,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
             return false;
         }
       },
-      [props.variablesContainer]
+      [props.isListLocked, props.variablesContainer]
     );
 
     const dropNode = React.useCallback(
@@ -1504,6 +1528,8 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
           draggedNodeId={draggedNodeId}
           nodeId={nodeId}
           isInherited={isInherited}
+          isNameLocked={props.isListLocked && isTopLevel}
+          isTypeLocked={props.isListLocked && isTopLevel}
           canDrop={canDrop}
           dropNode={dropNode}
           isSelected={isSelected}
@@ -1832,6 +1858,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
       addVariable,
     }));
 
+    // TODO Allow to past child-variables of existing object variables even when the variable list is locked.
     const toolbar = (
       <VariablesListToolbar
         isNarrow={isNarrow}
@@ -1840,11 +1867,13 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
         onPaste={pasteClipboardContent}
         onDelete={deleteSelection}
         canCopy={selectedNodes.length > 0}
-        canPaste={Clipboard.has(CLIPBOARD_KIND)}
+        canPaste={Clipboard.has(CLIPBOARD_KIND) && !props.isListLocked}
         canDelete={
+          !props.isListLocked &&
           selectedNodes.length > 0 &&
           selectedNodes.every(nodeId => !nodeId.startsWith(inheritedPrefix))
         }
+        canAdd={!props.isListLocked}
         onUndo={_undo}
         onRedo={_redo}
         canUndo={_canUndo()}
@@ -1886,15 +1915,30 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
                         <Column noMargin expand justifyContent="center">
                           {props.emptyPlaceholderTitle &&
                           props.emptyPlaceholderDescription ? (
-                            <EmptyPlaceholder
-                              title={props.emptyPlaceholderTitle}
-                              description={props.emptyPlaceholderDescription}
-                              actionLabel={<Trans>Add a variable</Trans>}
-                              helpPagePath={props.helpPagePath || undefined}
-                              tutorialId="intermediate-advanced-variables"
-                              onAction={addVariable}
-                              actionButtonId="add-variable"
-                            />
+                            props.isListLocked ? (
+                              <Column>
+                                <Text size="block-title" align="center">
+                                  {<Trans>No variable</Trans>}
+                                </Text>
+                                <Text align="center" noMargin>
+                                  {
+                                    <Trans>
+                                      There is no variable to set up.
+                                    </Trans>
+                                  }
+                                </Text>
+                              </Column>
+                            ) : (
+                              <EmptyPlaceholder
+                                title={props.emptyPlaceholderTitle}
+                                description={props.emptyPlaceholderDescription}
+                                actionLabel={<Trans>Add a variable</Trans>}
+                                helpPagePath={props.helpPagePath || undefined}
+                                tutorialId="intermediate-advanced-variables"
+                                onAction={addVariable}
+                                actionButtonId="add-variable"
+                              />
+                            )
                           ) : null}
                           {props.compactEmptyPlaceholderText && (
                             <Line justifyContent="center">
@@ -1904,7 +1948,11 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
                                 align="center"
                                 noMargin
                               >
-                                {props.compactEmptyPlaceholderText}
+                                {props.isListLocked ? (
+                                  <Trans>There is no variable to set up.</Trans>
+                                ) : (
+                                  props.compactEmptyPlaceholderText
+                                )}
                               </Text>
                             </Line>
                           )}
diff --git a/newIDE/app/src/VariablesList/VariablesListToolbar.js b/newIDE/app/src/VariablesList/VariablesListToolbar.js
index 38032c847493..2a5c4a0dd5d9 100644
--- a/newIDE/app/src/VariablesList/VariablesListToolbar.js
+++ b/newIDE/app/src/VariablesList/VariablesListToolbar.js
@@ -24,6 +24,7 @@ type Props = {|
   canCopy: boolean,
   canPaste: boolean,
   canDelete: boolean,
+  canAdd: boolean,
   hideHistoryChangeButtons: boolean,
   onUndo?: () => void,
   onRedo?: () => void,
@@ -140,6 +141,7 @@ const VariablesListToolbar = React.memo<Props>((props: Props) => {
             tooltip={t`Add variable`}
             onClick={props.onAdd}
             size="small"
+            disabled={!props.canAdd}
           >
             <Add style={props.iconStyle} />
           </IconButton>
@@ -150,6 +152,7 @@ const VariablesListToolbar = React.memo<Props>((props: Props) => {
             onClick={props.onAdd}
             label={<Trans>Add variable</Trans>}
             leftIcon={<Add />}
+            disabled={!props.canAdd}
           />
         )}
       </Column>
diff --git a/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js b/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js
index 85df29977d86..af73f15230c1 100644
--- a/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js
+++ b/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js
@@ -298,6 +298,9 @@ export const WithObjectsList = () => (
                   resourceManagementProps={fakeResourceManagementProps}
                   onEditObject={action('On edit object')}
                   onOpenEventBasedObjectEditor={action('On edit children')}
+                  onOpenEventBasedObjectVariantEditor={action(
+                    'On edit variant'
+                  )}
                   onExportAssets={action('On export assets')}
                   onAddObjectInstance={action('On add instance to the scene')}
                   selectedObjectFolderOrObjectsWithContext={[]}
@@ -312,6 +315,7 @@ export const WithObjectsList = () => (
                   onObjectEdited={() => {}}
                   onObjectFolderOrObjectWithContextSelected={() => {}}
                   hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
+                  isListLocked={false}
                 />
               </TabContentContainer>
             }
diff --git a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js
index 1e8fbdc5a812..114ef0e71d94 100644
--- a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js
+++ b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js
@@ -39,6 +39,7 @@ export const Default = () => (
         project={testProject.project}
         layout={testProject.testLayout}
         eventsBasedObject={null}
+        eventsBasedObjectVariant={null}
         layersContainer={testProject.testLayout.getLayers()}
         globalObjectsContainer={testProject.project.getObjects()}
         objectsContainer={testProject.testLayout.getObjects()}
diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js
index d9fcb890a371..46ca65af78e2 100644
--- a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js
+++ b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js
@@ -39,6 +39,7 @@ export const InstanceSprite2d = () => (
             editObjectInPropertiesPanel={action('edit object')}
             tileMapTileSelection={null}
             onSelectTileMapTile={() => {}}
+            isVariableListLocked={false}
           />
         </SerializedObjectDisplay>
       )}
@@ -67,6 +68,7 @@ export const InstanceCube3d = () => (
             editObjectInPropertiesPanel={action('edit object')}
             tileMapTileSelection={null}
             onSelectTileMapTile={() => {}}
+            isVariableListLocked={false}
           />
         </SerializedObjectDisplay>
       )}
@@ -95,6 +97,7 @@ export const InstanceTextInput = () => (
             editObjectInPropertiesPanel={action('edit object')}
             tileMapTileSelection={null}
             onSelectTileMapTile={() => {}}
+            isVariableListLocked={false}
           />
         </SerializedObjectDisplay>
       )}
diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js
index 7fe8550fca90..b639c4fbeeef 100644
--- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js
+++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js
@@ -26,6 +26,8 @@ export const Default = () => (
     onApply={action('onApply')}
     onCancel={action('onCancel')}
     onObjectGroupAdded={action('onObjectGroupAdded')}
+    isVariableListLocked={false}
+    isObjectListLocked={false}
   />
 );
 
@@ -42,6 +44,8 @@ export const WithLongObjectNames = () => (
     onApply={action('onApply')}
     onCancel={action('onCancel')}
     onObjectGroupAdded={action('onObjectGroupAdded')}
+    isVariableListLocked={false}
+    isObjectListLocked={false}
   />
 );
 
@@ -58,5 +62,7 @@ export const Empty = () => (
     onApply={action('onApply')}
     onCancel={action('onCancel')}
     onObjectGroupAdded={action('onObjectGroupAdded')}
+    isVariableListLocked={false}
+    isObjectListLocked={false}
   />
 );
diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js
index f7d667a77d52..cd03677d5915 100644
--- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js
+++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js
@@ -35,6 +35,31 @@ export const Default = () => (
           onRenameGroup={action('onRenameGroup')}
           onDeleteGroup={action('onDeleteGroup')}
           getValidatedObjectOrGroupName={newName => newName}
+          isListLocked={false}
+        />
+      </div>
+    </SerializedObjectDisplay>
+  </DragAndDropContextProvider>
+);
+
+export const Locked = () => (
+  <DragAndDropContextProvider>
+    <SerializedObjectDisplay object={testProject.testLayout}>
+      <div style={{ height: 250 }}>
+        <ObjectGroupsList
+          globalObjectGroups={testProject.project
+            .getObjects()
+            .getObjectGroups()}
+          projectScopedContainersAccessor={
+            testProject.testSceneProjectScopedContainersAccessor
+          }
+          objectGroups={testProject.testLayout.getObjects().getObjectGroups()}
+          onCreateGroup={action('onCreateGroup')}
+          onEditGroup={action('onEditGroup')}
+          onRenameGroup={action('onRenameGroup')}
+          onDeleteGroup={action('onDeleteGroup')}
+          getValidatedObjectOrGroupName={newName => newName}
+          isListLocked={true}
         />
       </div>
     </SerializedObjectDisplay>
diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js
index 50aa7065ee6a..2b66138e6ae1 100644
--- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js
+++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js
@@ -36,6 +36,7 @@ export const Default = () => (
         resourceManagementProps={fakeResourceManagementProps}
         onEditObject={action('On edit object')}
         onOpenEventBasedObjectEditor={action('On edit children')}
+        onOpenEventBasedObjectVariantEditor={action('On edit variant')}
         onExportAssets={action('On export assets')}
         onAddObjectInstance={action('On add instance to the scene')}
         onObjectCreated={action('On object created')}
@@ -50,6 +51,7 @@ export const Default = () => (
         ) => cb(true)}
         onObjectFolderOrObjectWithContextSelected={() => {}}
         hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
+        isListLocked={false}
       />
     </div>
   </DragAndDropContextProvider>
@@ -72,6 +74,7 @@ export const WithSerializedObjectView = () => (
           resourceManagementProps={fakeResourceManagementProps}
           onEditObject={action('On edit object')}
           onOpenEventBasedObjectEditor={action('On edit children')}
+          onOpenEventBasedObjectVariantEditor={action('On edit variant')}
           onExportAssets={action('On export assets')}
           onAddObjectInstance={action('On add instance to the scene')}
           onObjectCreated={action('On object created')}
@@ -86,8 +89,46 @@ export const WithSerializedObjectView = () => (
           ) => cb(true)}
           onObjectFolderOrObjectWithContextSelected={() => {}}
           hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
+          isListLocked={false}
         />
       </div>
     </SerializedObjectDisplay>
   </DragAndDropContextProvider>
 );
+
+export const Locked = () => (
+  <DragAndDropContextProvider>
+    <div style={{ height: 400 }}>
+      <ObjectsList
+        getThumbnail={() => 'res/unknown32.png'}
+        project={testProject.project}
+        layout={testProject.testLayout}
+        eventsBasedObject={null}
+        projectScopedContainersAccessor={
+          testProject.testSceneProjectScopedContainersAccessor
+        }
+        globalObjectsContainer={testProject.project.getObjects()}
+        objectsContainer={testProject.testLayout.getObjects()}
+        resourceManagementProps={fakeResourceManagementProps}
+        onEditObject={action('On edit object')}
+        onOpenEventBasedObjectEditor={action('On edit children')}
+        onOpenEventBasedObjectVariantEditor={action('On edit variant')}
+        onExportAssets={action('On export assets')}
+        onAddObjectInstance={action('On add instance to the scene')}
+        onObjectCreated={action('On object created')}
+        onObjectEdited={action('On object edited')}
+        selectedObjectFolderOrObjectsWithContext={[]}
+        getValidatedObjectOrGroupName={newName => newName}
+        onDeleteObjects={(objectsWithContext, cb) => cb(true)}
+        onRenameObjectFolderOrObjectWithContextFinish={(
+          objectWithContext,
+          newName,
+          cb
+        ) => cb(true)}
+        onObjectFolderOrObjectWithContextSelected={() => {}}
+        hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
+        isListLocked={true}
+      />
+    </div>
+  </DragAndDropContextProvider>
+);
diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js
index 238fda9e3b40..942b0dd1685e 100644
--- a/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js
+++ b/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js
@@ -28,6 +28,7 @@ export const Default = () => (
       openBehaviorEvents={() => action('Open behavior events')}
       onBehaviorsUpdated={() => {}}
       onExtensionInstalled={action('extension installed')}
+      isListLocked={false}
     />
   </SerializedObjectDisplay>
 );
@@ -43,6 +44,39 @@ export const WithoutAnyBehaviors = () => (
       openBehaviorEvents={() => action('Open behavior events')}
       onBehaviorsUpdated={() => {}}
       onExtensionInstalled={action('extension installed')}
+      isListLocked={false}
+    />
+  </SerializedObjectDisplay>
+);
+
+export const Locked = () => (
+  <SerializedObjectDisplay object={testProject.spriteObjectWithBehaviors}>
+    <BehaviorsEditor
+      project={testProject.project}
+      eventsFunctionsExtension={null}
+      object={testProject.spriteObjectWithBehaviors}
+      resourceManagementProps={fakeResourceManagementProps}
+      onUpdateBehaviorsSharedData={() => {}}
+      openBehaviorEvents={() => action('Open behavior events')}
+      onBehaviorsUpdated={() => {}}
+      onExtensionInstalled={action('extension installed')}
+      isListLocked={true}
+    />
+  </SerializedObjectDisplay>
+);
+
+export const LockedWithoutAnyBehaviors = () => (
+  <SerializedObjectDisplay object={testProject.spriteObjectWithoutBehaviors}>
+    <BehaviorsEditor
+      project={testProject.project}
+      eventsFunctionsExtension={null}
+      object={testProject.spriteObjectWithoutBehaviors}
+      resourceManagementProps={fakeResourceManagementProps}
+      onUpdateBehaviorsSharedData={() => {}}
+      openBehaviorEvents={() => action('Open behavior events')}
+      onBehaviorsUpdated={() => {}}
+      onExtensionInstalled={action('extension installed')}
+      isListLocked={true}
     />
   </SerializedObjectDisplay>
 );
diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js
index 6a3e4b2fdf96..7208ec78c442 100644
--- a/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js
+++ b/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js
@@ -43,6 +43,17 @@ export const CustomObject = () => (
       }}
       openBehaviorEvents={() => action('Open behavior events')}
       onExtensionInstalled={action('extension installed')}
+      onOpenEventBasedObjectEditor={() =>
+        action('Open event-based object editor')
+      }
+      onOpenEventBasedObjectVariantEditor={action(
+        'Open event-based object variant editor'
+      )}
+      onDeleteEventsBasedObjectVariant={() =>
+        action('Delete event-based object variant')
+      }
+      isBehaviorListLocked={false}
+      isVariableListLocked={false}
     />
   </DragAndDropContextProvider>
 );
@@ -75,6 +86,60 @@ export const StandardObject = () => (
       }}
       openBehaviorEvents={() => action('Open behavior events')}
       onExtensionInstalled={action('extension installed')}
+      onOpenEventBasedObjectEditor={() =>
+        action('Open event-based object editor')
+      }
+      onOpenEventBasedObjectVariantEditor={action(
+        'Open event-based object variant editor'
+      )}
+      onDeleteEventsBasedObjectVariant={() =>
+        action('Delete event-based object variant')
+      }
+      isBehaviorListLocked={false}
+      isVariableListLocked={false}
+    />
+  </DragAndDropContextProvider>
+);
+
+export const LockedStandardObject = () => (
+  <DragAndDropContextProvider>
+    <ObjectEditorDialog
+      open={true}
+      projectScopedContainersAccessor={
+        testProject.testSceneProjectScopedContainersAccessor
+      }
+      object={testProject.panelSpriteObject}
+      onApply={() => action('Apply changes')}
+      onCancel={() => action('Cancel changes')}
+      onRename={() => action('Rename object')}
+      getValidatedObjectOrGroupName={newName => newName}
+      project={testProject.project}
+      layout={testProject.testLayout}
+      eventsFunctionsExtension={null}
+      eventsBasedObject={null}
+      resourceManagementProps={fakeResourceManagementProps}
+      onComputeAllVariableNames={() => []}
+      onUpdateBehaviorsSharedData={() => {}}
+      initialTab={null}
+      hotReloadPreviewButtonProps={{
+        hasPreviewsRunning: false,
+        launchProjectDataOnlyPreview: () => action('Hot-reload'),
+        launchProjectCodeAndDataPreview: action('Hot-reload with code'),
+        launchProjectWithLoadingScreenPreview: () => action('Reload'),
+      }}
+      openBehaviorEvents={() => action('Open behavior events')}
+      onOpenEventBasedObjectEditor={() =>
+        action('Open event-based object editor')
+      }
+      onOpenEventBasedObjectVariantEditor={action(
+        'Open event-based object variant editor'
+      )}
+      onDeleteEventsBasedObjectVariant={() =>
+        action('Delete event-based object variant')
+      }
+      onExtensionInstalled={action('extension installed')}
+      isBehaviorListLocked={true}
+      isVariableListLocked={true}
     />
   </DragAndDropContextProvider>
 );
diff --git a/newIDE/app/src/stories/componentStories/VariablesList.stories.js b/newIDE/app/src/stories/componentStories/VariablesList.stories.js
index b6c98d8a3975..461c2e55188d 100644
--- a/newIDE/app/src/stories/componentStories/VariablesList.stories.js
+++ b/newIDE/app/src/stories/componentStories/VariablesList.stories.js
@@ -24,6 +24,7 @@ export const Default = () => (
           'VariableFromSomeWhere',
           'InstanceVariable', // already defined variable in testSpriteObjectInstance
         ]}
+        isListLocked={false}
       />
     </FixedHeightFlexContainer>
   </DragAndDropContextProvider>
@@ -46,6 +47,7 @@ export const Compact = () => (
           'VariableFromSomeWhere',
           'InstanceVariable', // already defined variable in testSpriteObjectInstance
         ]}
+        isListLocked={false}
       />
     </FixedHeightFlexContainer>
   </DragAndDropContextProvider>
@@ -69,6 +71,76 @@ export const InstanceWithObjectVariables = () => (
           'VariableFromSomeWhere',
           'InstanceVariable', // already defined variable in testSpriteObjectInstance
         ]}
+        isListLocked={false}
+      />
+    </FixedHeightFlexContainer>
+  </DragAndDropContextProvider>
+);
+
+export const Locked = () => (
+  <DragAndDropContextProvider>
+    <FixedHeightFlexContainer height={600}>
+      <VariablesList
+        projectScopedContainersAccessor={
+          testProject.testSceneProjectScopedContainersAccessor
+        }
+        variablesContainer={testProject.testLayout.getVariables()}
+        emptyPlaceholderDescription="Variables help you store data"
+        emptyPlaceholderTitle="Variables"
+        helpPagePath="/variables"
+        onComputeAllVariableNames={() => [
+          'VariableFromEventSheet',
+          'VariableFromSomeWhere',
+          'InstanceVariable', // already defined variable in testSpriteObjectInstance
+        ]}
+        isListLocked={true}
+      />
+    </FixedHeightFlexContainer>
+  </DragAndDropContextProvider>
+);
+
+export const LockedCompact = () => (
+  <DragAndDropContextProvider>
+    <FixedHeightFlexContainer height={600}>
+      <VariablesList
+        projectScopedContainersAccessor={
+          testProject.testSceneProjectScopedContainersAccessor
+        }
+        size="compact"
+        variablesContainer={testProject.testLayout.getVariables()}
+        emptyPlaceholderDescription="Variables help you store data"
+        emptyPlaceholderTitle="Variables"
+        helpPagePath="/variables"
+        onComputeAllVariableNames={() => [
+          'VariableFromEventSheet',
+          'VariableFromSomeWhere',
+          'InstanceVariable', // already defined variable in testSpriteObjectInstance
+        ]}
+        isListLocked={true}
+      />
+    </FixedHeightFlexContainer>
+  </DragAndDropContextProvider>
+);
+
+export const LockedInstanceWithObjectVariables = () => (
+  <DragAndDropContextProvider>
+    <FixedHeightFlexContainer height={600}>
+      <VariablesList
+        projectScopedContainersAccessor={
+          testProject.testSceneProjectScopedContainersAccessor
+        }
+        variablesContainer={testProject.testSpriteObjectInstance.getVariables()}
+        areObjectVariables
+        emptyPlaceholderDescription="Variables help you store data"
+        emptyPlaceholderTitle="Variables"
+        helpPagePath="/variables"
+        inheritedVariablesContainer={testProject.spriteObject.getVariables()}
+        onComputeAllVariableNames={() => [
+          'VariableFromEventSheet',
+          'VariableFromSomeWhere',
+          'InstanceVariable', // already defined variable in testSpriteObjectInstance
+        ]}
+        isListLocked={true}
       />
     </FixedHeightFlexContainer>
   </DragAndDropContextProvider>