Allow to configure object resources to be loaded independently from the scene (#8514)

- This is an advanced, experimental feature which is subject to changes. It's in most cases not useful, unless you have a large game or low end devices for which controlling memory usage is important. It's mostly useful to delay loading of large custom objects.
- Objects can be configured so that its resources are not loaded when the scene is loaded. Instead, you can use an action to manually load the resources used by it. When the loading is done (this can be checked using a condition), the object can then be created.
  Be careful: if an object assets are not loaded, it will be broken when displayed on screen.
This commit is contained in:
D8H
2026-05-04 10:45:51 +02:00
committed by GitHub
parent c5c718354a
commit 8e50ddefe4
41 changed files with 1396 additions and 403 deletions
@@ -216,6 +216,48 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("sceneName", _("Scene name"))
.MarkAsAdvanced();
extension
.AddAction(
"LoadObjectAssets",
_("Preload object"),
_("Preload an object resources in background."),
_("Preload object _PARAM1_ in background"),
"",
"res/actions/hourglass_black.svg",
"res/actions/hourglass_black.svg")
.SetHelpPath("/all-features/resources-loading")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Object name"))
.MarkAsAdvanced();
extension
.AddAction(
"UnloadObjectAssets",
_("Unload object"),
_("Unload an object resources. The \"resource preloading\" property must be set to \"preload with an action\" for this action to actually unload resources."),
_("Unload object _PARAM1_"),
"",
"res/actions/hourglass_black.svg",
"res/actions/hourglass_black.svg")
.SetHelpPath("/all-features/resources-loading")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Object name"))
.MarkAsAdvanced();
extension
.AddCondition(
"AreObjectAssetsLoaded",
_("Object preloaded"),
_("Check if object resources have finished to load in background."),
_("Object _PARAM1_ was preloaded in background"),
"",
"res/actions/hourglass_black.svg",
"res/actions/hourglass_black.svg")
.SetHelpPath("/all-features/resources-loading")
.AddCodeOnlyParameter("currentScene", "")
.AddParameter("string", _("Object name"))
.MarkAsAdvanced();
}
} // namespace gd
@@ -4,8 +4,7 @@
* reserved. This project is released under the MIT License.
*/
#ifndef ARBITRARYOBJECTSWORKER_H
#define ARBITRARYOBJECTSWORKER_H
#pragma once
#include <map>
#include <memory>
@@ -37,6 +36,11 @@ class GD_CORE_API ArbitraryObjectsWorker {
*/
void Launch(gd::ObjectsContainer& objects) { VisitObjectContainer(objects); };
/**
* \brief Launch the worker on the specified object.
*/
void Launch(gd::Object& object) { VisitObject(object); };
private:
void VisitObjectContainer(gd::ObjectsContainer& objects);
void VisitObject(gd::Object& object);
@@ -60,4 +64,3 @@ class GD_CORE_API ArbitraryObjectsWorker {
} // namespace gd
#endif // ARBITRARYOBJECTSWORKER_H
@@ -291,6 +291,9 @@ GetResourceWorkerOnEvents(const gd::Project &project,
}
void ResourceWorkerInObjectsWorker::DoVisitObject(gd::Object &object) {
if (!shouldCheckObject(object)) {
return;
}
object.GetConfiguration().ExposeResources(worker);
auto& effects = object.GetEffects();
for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount(); effectIndex++)
@@ -307,7 +310,16 @@ void ResourceWorkerInObjectsWorker::DoVisitBehavior(gd::Behavior &behavior){
gd::ResourceWorkerInObjectsWorker
GetResourceWorkerOnObjects(const gd::Project &project,
gd::ArbitraryResourceWorker &worker) {
gd::ResourceWorkerInObjectsWorker resourcesWorker(project, worker);
std::function<bool(const gd::Object &)> shouldCheckObject =
[](const gd::Object &object) { return true; };
return GetResourceWorkerOnObjects(project, worker, shouldCheckObject);
}
gd::ResourceWorkerInObjectsWorker GetResourceWorkerOnObjects(
const gd::Project &project, gd::ArbitraryResourceWorker &worker,
std::function<bool(const gd::Object &)> &shouldCheckObject) {
gd::ResourceWorkerInObjectsWorker resourcesWorker(project, worker,
shouldCheckObject);
return resourcesWorker;
}
@@ -178,8 +178,11 @@ ResourceWorkerInEventsWorker GD_CORE_API GetResourceWorkerOnEvents(
class GD_CORE_API ResourceWorkerInObjectsWorker
: public gd::ArbitraryObjectsWorker {
public:
ResourceWorkerInObjectsWorker(const gd::Project &project_, gd::ArbitraryResourceWorker &worker_)
: project(project_), worker(worker_){};
ResourceWorkerInObjectsWorker(
const gd::Project &project_, gd::ArbitraryResourceWorker &worker_,
std::function<bool(const gd::Object &)> &shouldCheckObject_)
: project(project_), worker(worker_),
shouldCheckObject(shouldCheckObject_){};
~ResourceWorkerInObjectsWorker() {}
private:
@@ -188,9 +191,14 @@ private:
const gd::Project &project;
gd::ArbitraryResourceWorker &worker;
std::function<bool(const gd::Object &)> shouldCheckObject;
};
gd::ResourceWorkerInObjectsWorker GD_CORE_API
GetResourceWorkerOnObjects(const gd::Project &project, gd::ArbitraryResourceWorker &worker);
gd::ResourceWorkerInObjectsWorker GD_CORE_API GetResourceWorkerOnObjects(
const gd::Project &project, gd::ArbitraryResourceWorker &worker);
gd::ResourceWorkerInObjectsWorker GD_CORE_API GetResourceWorkerOnObjects(
const gd::Project &project, gd::ArbitraryResourceWorker &worker,
std::function<bool(const gd::Object &)> &shouldCheckObject);
} // namespace gd
@@ -9,6 +9,7 @@
#include "GDCore/Project/EventsBasedObjectVariant.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Serialization/SerializerElement.h"
namespace gd {
@@ -20,11 +21,29 @@ std::set<gd::String> SceneResourcesFinder::FindProjectResources(gd::Project &pro
return resourceWorker.resourceNames;
}
std::set<gd::String> SceneResourcesFinder::FindSceneResources(gd::Project &project,
gd::Layout &layout) {
std::set<gd::String>
SceneResourcesFinder::FindSceneResources(gd::Project &project,
gd::Layout &layout,
bool ignoreObjectResourcePreloading) {
gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager());
gd::ResourceExposer::ExposeLayoutResources(project, layout, resourceWorker);
std::function<bool(const gd::Object &)> shouldCheckObject =
[ignoreObjectResourcePreloading](const gd::Object &object) {
return ignoreObjectResourcePreloading ||
object.GetResourcesPreloading() != "manually";
};
gd::ResourceExposer::ExposeLayoutResources(project, layout, resourceWorker,
shouldCheckObject);
return resourceWorker.resourceNames;
}
std::set<gd::String>
SceneResourcesFinder::FindObjectResources(gd::Project &project,
gd::Object &object) {
gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager());
auto objectWorker = gd::GetResourceWorkerOnObjects(project, resourceWorker);
objectWorker.Launch(object);
return resourceWorker.resourceNames;
}
@@ -32,8 +32,9 @@ public:
*
* It doesn't include resources used globally.
*/
static std::set<gd::String> FindSceneResources(gd::Project &project,
gd::Layout &layout);
static std::set<gd::String>
FindSceneResources(gd::Project &project, gd::Layout &layout,
bool ignoreObjectResourcePreloading);
/**
* @brief Find resource that are used globally in the project.
@@ -42,6 +43,12 @@ public:
*/
static std::set<gd::String> FindProjectResources(gd::Project &project);
/**
* @brief Find resource usages in a given object.
*/
static std::set<gd::String> FindObjectResources(gd::Project &project,
gd::Object &object);
/**
* @brief Find resource usages in a given events-based object variant.
*/
-1
View File
@@ -26,7 +26,6 @@ void GD_CORE_API ProjectStripper::StripProjectForExport(gd::Project &project) {
wholeProjectBrowser.ExposeObjects(project, behaviorDefaultFlagClearer);
for (unsigned int i = 0; i < project.GetLayoutsCount(); ++i) {
project.GetLayout(i).GetObjects().GetObjectGroups().Clear();
project.GetLayout(i).GetEvents().Clear();
}
+13 -3
View File
@@ -15,6 +15,7 @@
#include "GDCore/Project/EventsFunctionsExtension.h"
#include "GDCore/Project/Layout.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/Object.h"
#include "GDCore/String.h"
namespace gd {
@@ -89,11 +90,20 @@ void ResourceExposer::ExposeProjectResources(
}
void ResourceExposer::ExposeLayoutResources(
gd::Project &project,
gd::Layout &layout,
gd::Project &project, gd::Layout &layout,
gd::ArbitraryResourceWorker &worker) {
std::function<bool(const gd::Object &)> shouldCheckObject =
[](const gd::Object &object) { return true; };
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker,
shouldCheckObject);
}
void ResourceExposer::ExposeLayoutResources(
gd::Project &project, gd::Layout &layout,
gd::ArbitraryResourceWorker &worker,
std::function<bool(const gd::Object &)> &shouldCheckObject) {
// Expose object configuration resources
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker, shouldCheckObject);
gd::ProjectBrowserHelper::ExposeLayoutObjects(layout, objectWorker);
// Expose layer effect resources
+14 -2
View File
@@ -5,6 +5,8 @@
*/
#pragma once
#include <functional>
namespace gd {
class Platform;
class Project;
@@ -13,6 +15,7 @@ class EventsBasedObjectVariant;
class EventsFunctionsExtension;
class Effect;
class Layout;
class Object;
} // namespace gd
namespace gd {
@@ -47,10 +50,19 @@ public:
*
* It doesn't include resources used globally.
*/
static void ExposeLayoutResources(gd::Project &project,
gd::Layout &layout,
static void ExposeLayoutResources(gd::Project &project, gd::Layout &layout,
gd::ArbitraryResourceWorker &worker);
/**
* @brief Expose the resources used in a given layout.
*
* It doesn't include resources used globally.
*/
static void ExposeLayoutResources(
gd::Project &project, gd::Layout &layout,
gd::ArbitraryResourceWorker &worker,
std::function<bool(const gd::Object &)> &shouldCheckObject);
/**
* @brief Expose the resources used in a given events-based object variant.
*/
+5
View File
@@ -52,6 +52,7 @@ void Object::CopyWithoutConfiguration(const gd::Object& object) {
objectVariables = object.objectVariables;
effectsContainer = object.effectsContainer;
behaviors = object.behaviors;
resourcesPreloading = object.resourcesPreloading;
}
gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; }
@@ -95,6 +96,7 @@ void Object::UnserializeFrom(gd::Project& project,
SetType(element.GetStringAttribute("type"));
assetStoreId = element.GetStringAttribute("assetStoreId");
name = element.GetStringAttribute("name", name, "nom");
resourcesPreloading = element.GetStringAttribute("resourcesPreloading", "with-scene");
objectVariables.UnserializeFrom(
element.GetChild("variables", 0, "Variables"));
@@ -134,6 +136,9 @@ void Object::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", GetName());
element.SetAttribute("assetStoreId", GetAssetStoreId());
element.SetAttribute("type", GetType());
if (GetResourcesPreloading() != "with-scene") {
element.SetAttribute("resourcesPreloading", GetResourcesPreloading());
}
objectVariables.SerializeTo(element.AddChild("variables"));
effectsContainer.SerializeTo(element.AddChild("effects"));
behaviors.SerializeTo(element.AddChild("behaviors"));
+18
View File
@@ -124,6 +124,22 @@ class GD_CORE_API Object {
*/
const gd::String& GetType() const { return configuration->GetType(); }
/**
* Set when the object resources must be preloaded: `with-scene`(default),
* `manually`.
*/
void SetResourcesPreloading(gd::String resourcesPreloading_) {
resourcesPreloading = resourcesPreloading_;
}
/**
* Get when the object resources must be preloaded: `with-scene`(default),
* `manually`.
*/
const gd::String& GetResourcesPreloading() const {
return resourcesPreloading;
}
///@}
/** \name Behaviors management
@@ -267,6 +283,8 @@ class GD_CORE_API Object {
effectsContainer; ///< The effects container for the object.
mutable gd::String persistentUuid; ///< A persistent random version 4 UUID,
///< useful for computing changesets.
/** When set to `"manually"`, its resources are not preloaded with the scene. */
gd::String resourcesPreloading = "with-scene";
/**
* Initialize object using another object. Used by copy-ctor and assign-op.
+4 -4
View File
@@ -996,16 +996,16 @@ class GD_CORE_API Project {
ResourcesContainer& GetResourcesManager() { return resourcesContainer; }
/**
* Set when the scenes must preload their resources: `at-startup`, `never`
* (default).
* Set when the scenes must preload their resources: `at-startup` (default),
* `never`.
*/
void SetSceneResourcesPreloading(gd::String sceneResourcesPreloading_) {
sceneResourcesPreloading = sceneResourcesPreloading_;
}
/**
* Get when the scenes must preload their resources: `at-startup`, `never`
* (default).
* Get when the scenes must preload their resources: `at-startup` (default),
* `never`.
*/
const gd::String& GetSceneResourcesPreloading() const {
return sceneResourcesPreloading;
@@ -38,6 +38,7 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -27,6 +27,7 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -26,6 +26,7 @@ describe('gdjs.LinksManager', function () {
variables: [],
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
r: 0,
v: 0,
@@ -34,6 +34,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -37,6 +37,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -39,6 +39,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -47,6 +47,7 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
variables: [],
usedResources: [],
@@ -68,6 +68,7 @@ describe('SaveState', () => {
updateIfNotVisible: false,
},
],
objectsGroups: [],
instances: instances || [],
variables: [],
usedResources: [],
@@ -58,6 +58,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
variables: [],
usedResources: [],
@@ -55,6 +55,7 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -32,6 +32,7 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
@@ -51,6 +51,12 @@ SceneExtension::SceneExtension() {
"gdjs.evtTools.runtimeScene.getSceneLoadingProgress");
GetAllExpressions()["SceneLoadingProgress"].SetFunctionName(
"gdjs.evtTools.runtimeScene.getSceneLoadingProgress");
GetAllActions()["LoadObjectAssets"].SetFunctionName(
"gdjs.evtTools.runtimeScene.loadObjectOrGroupAssets");
GetAllActions()["UnloadObjectAssets"].SetFunctionName(
"gdjs.evtTools.runtimeScene.unloadObjectOrGroupAssets");
GetAllConditions()["AreObjectAssetsLoaded"].SetFunctionName(
"gdjs.evtTools.runtimeScene.areObjectOrGroupAssetsLoaded");
StripUnimplementedInstructionsAndExpressions();
}
+50 -22
View File
@@ -578,7 +578,8 @@ void ExporterHelper::StripAndSerializeProjectData(
layoutIndex++) {
auto &layout = project.GetLayout(layoutIndex);
auto sceneUsedResources = gd::SceneResourcesFinder::FindSceneResources(
project, layout);
project, layout,
/* ignoreObjectResourcePreloading= */ isInGameEdition);
for (auto &&resourceName : projectUsedResources) {
sceneUsedResources.erase(resourceName);
}
@@ -619,9 +620,11 @@ void ExporterHelper::StripAndSerializeProjectData(
gd::ProjectStripper::StripProjectForExport(project);
project.SerializeTo(rootElement);
SerializeUsedResources(rootElement, projectUsedResources, scenesUsedResources,
eventsBasedObjectVariantsUsedResources);
SerializeUsedResourcesForRuntime(project, rootElement, projectUsedResources,
scenesUsedResources);
if (isInGameEdition) {
SerializeUsedResourcesForInGameEditor(
project, rootElement, eventsBasedObjectVariantsUsedResources);
auto &behaviorsElement = rootElement.AddChild("activatedByDefaultInEditorBehaviors");
behaviorsElement.ConsiderAsArrayOf("resourceReference");
auto &platform = project.GetCurrentPlatform();
@@ -638,23 +641,22 @@ void ExporterHelper::StripAndSerializeProjectData(
}
void ExporterHelper::SerializeUsedResources(
gd::SerializerElement &element, std::set<gd::String> &usedResources) {
auto &resourcesElement = element.AddChild("usedResources");
resourcesElement.ConsiderAsArrayOf("resourceReference");
for (auto &resourceName : usedResources) {
auto &resourceElement = resourcesElement.AddChild("resourceReference");
resourceElement.SetAttribute("name", resourceName);
}
}
void ExporterHelper::SerializeUsedResourcesForRuntime(
gd::Project &project,
gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources) {
auto serializeUsedResources =
[](gd::SerializerElement &element,
std::set<gd::String> &usedResources) -> void {
auto &resourcesElement = element.AddChild("usedResources");
resourcesElement.ConsiderAsArrayOf("resourceReference");
for (auto &resourceName : usedResources) {
auto &resourceElement = resourcesElement.AddChild("resourceReference");
resourceElement.SetAttribute("name", resourceName);
}
};
std::unordered_map<gd::String, std::set<gd::String>> &scenesUsedResources) {
serializeUsedResources(rootElement, projectUsedResources);
SerializeUsedResources(rootElement, projectUsedResources);
auto &layoutsElement = rootElement.GetChild("layouts");
for (std::size_t layoutIndex = 0;
@@ -663,10 +665,36 @@ void ExporterHelper::SerializeUsedResources(
auto &layoutElement = layoutsElement.GetChild(layoutIndex);
const auto layoutName = layoutElement.GetStringAttribute("name");
auto &layoutUsedResources = scenesUsedResources[layoutName];
serializeUsedResources(layoutElement, layoutUsedResources);
}
auto &sceneUsedResources = scenesUsedResources[layoutName];
SerializeUsedResources(layoutElement, sceneUsedResources);
auto &scene = project.GetLayout(layoutName);
auto &objectsElement = layoutElement.GetChild("objects");
for (std::size_t objectIndex = 0;
objectIndex < objectsElement.GetChildrenCount(); objectIndex++) {
auto &objectElement = objectsElement.GetChild(objectIndex);
const auto objectName = objectElement.GetStringAttribute("name");
auto &object = scene.GetObjects().GetObject(objectName);
if (object.GetResourcesPreloading() == "manually") {
auto objectUsedResources =
gd::SceneResourcesFinder::FindObjectResources(project, object);
for (auto &&resourceName : projectUsedResources) {
objectUsedResources.erase(resourceName);
}
for (auto &&resourceName : sceneUsedResources) {
objectUsedResources.erase(resourceName);
}
SerializeUsedResources(objectElement, objectUsedResources);
}
}
}
}
void ExporterHelper::SerializeUsedResourcesForInGameEditor(
gd::Project &project, gd::SerializerElement &rootElement,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources) {
auto &extensionsElement = rootElement.GetChild("eventsFunctionsExtensions");
for (std::size_t extensionIndex = 0;
extensionIndex < extensionsElement.GetChildrenCount();
@@ -685,7 +713,7 @@ void ExporterHelper::SerializeUsedResources(
gd::PlatformExtension::GetObjectFullType(extensionName, objectName);
auto &objectUsedResources =
eventsBasedObjectVariantsUsedResources[eventsBasedObjectType];
serializeUsedResources(objectElement, objectUsedResources);
SerializeUsedResources(objectElement, objectUsedResources);
auto &variantsElement = objectElement.GetChild("variants");
for (std::size_t variantIndex = 0;
@@ -697,7 +725,7 @@ void ExporterHelper::SerializeUsedResources(
extensionName, objectName, variantName);
auto &variantUsedResources =
eventsBasedObjectVariantsUsedResources[variantType];
serializeUsedResources(variantElement, variantUsedResources);
SerializeUsedResources(variantElement, variantUsedResources);
}
}
}
+12 -7
View File
@@ -751,14 +751,19 @@ class ExporterHelper {
///< be then copied to the final output directory.
private:
static void
SerializeUsedResources(gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources);
static void SerializeUsedResourcesForRuntime(
gd::Project &project, gd::SerializerElement &rootElement,
std::set<gd::String> &projectUsedResources,
std::unordered_map<gd::String, std::set<gd::String>>
&layersUsedResources);
static void SerializeUsedResourcesForInGameEditor(
gd::Project &project, gd::SerializerElement &rootElement,
std::unordered_map<gd::String, std::set<gd::String>>
&eventsBasedObjectVariantsUsedResources);
static void SerializeUsedResources(gd::SerializerElement &element,
std::set<gd::String> &usedResources);
/**
* \brief Strip a project and serialize it to JSON.
*/
File diff suppressed because it is too large Load Diff
@@ -363,6 +363,38 @@ namespace gdjs {
): boolean => {
return runtimeScene.getGame().areSceneAssetsLoaded(sceneName);
};
/**
* Preload an object assets in background.
*/
export const loadObjectOrGroupAssets = (
runtimeScene: gdjs.RuntimeScene,
objectOrGroupName: string
): void => {
runtimeScene.getGame().loadObjectOrGroupAssets(objectOrGroupName);
};
/**
* Unload an object assets.
*/
export const unloadObjectOrGroupAssets = (
runtimeScene: gdjs.RuntimeScene,
objectOrGroupName: string
): void => {
runtimeScene.getGame().unloadObjectOrGroupAssets(objectOrGroupName);
};
/**
* Check if object assets have finished to load in background.
*/
export const areObjectOrGroupAssetsLoaded = (
runtimeScene: gdjs.RuntimeScene,
objectOrGroupName: string
): boolean => {
return runtimeScene
.getGame()
.areObjectOrGroupAssetsLoaded(objectOrGroupName);
};
}
}
}
+110
View File
@@ -914,6 +914,116 @@ namespace gdjs {
return this._hasJustResumed;
}
/**
* Preload an object assets in background.
*/
loadObjectOrGroupAssets(objectOrGroupName: string): void {
const currentScene = this._sceneStack.getCurrentScene();
if (!currentScene) {
return;
}
const objectGroupData = this.getObjectGroupData(
currentScene.getName(),
objectOrGroupName
);
if (objectGroupData) {
for (const object of objectGroupData.objects) {
this._loadObjectAssets(currentScene, object.name);
}
} else {
this._loadObjectAssets(currentScene, objectOrGroupName);
}
}
private _loadObjectAssets(currentScene: RuntimeScene, objectName: string) {
const objectData = currentScene._objects.get(objectName);
if (!objectData) {
return;
}
const usedResources = objectData.usedResources;
if (!usedResources) {
return;
}
this._resourcesLoader.loadObjectResources(
currentScene.getName(),
objectName,
usedResources
);
}
/**
* @returns true when all the resources of the given object are loaded.
*/
areObjectOrGroupAssetsLoaded(objectOrGroupName: string): boolean {
const currentScene = this._sceneStack.getCurrentScene();
if (!currentScene) {
return false;
}
const objectGroupData = this.getObjectGroupData(
currentScene.getName(),
objectOrGroupName
);
if (objectGroupData) {
for (const object of objectGroupData.objects) {
if (
!this._resourcesLoader.areObjectAssetsReady(
currentScene.getName(),
object.name
)
) {
return false;
}
}
return true;
}
return this._resourcesLoader.areObjectAssetsReady(
currentScene.getName(),
objectOrGroupName
);
}
/**
* Unload an object assets.
*/
unloadObjectOrGroupAssets(objectOrGroupName: string): void {
const currentScene = this._sceneStack.getCurrentScene();
if (!currentScene) {
return;
}
const objectGroupData = this.getObjectGroupData(
currentScene.getName(),
objectOrGroupName
);
if (objectGroupData) {
for (const object of objectGroupData.objects) {
this._resourcesLoader.unloadObjectResources(
currentScene.getName(),
object.name
);
}
} else {
this._resourcesLoader.unloadObjectResources(
currentScene.getName(),
objectOrGroupName
);
}
}
private getObjectGroupData(
sceneName: string,
objectGroupName: string
): ObjectGroupData | null {
const sceneData = this.getSceneData(sceneName);
if (sceneData) {
for (const objectGroup of sceneData.objectsGroups) {
if (objectGroup.name === objectGroupName) {
return objectGroup;
}
}
}
return null;
}
/**
* Preload a scene assets as soon as possible in background.
*/
+12
View File
@@ -38,6 +38,17 @@ declare type ObjectData = {
behaviors: Array<BehaviorData & any>;
/** The list of effects. */
effects: Array<EffectData>;
/**
* The resources used by this object excluding the one already used by the
* scene and/or the project.
* This attribute is only set for objects that are preloaded manually.
*/
usedResources?: ResourceReference[];
};
declare type ObjectGroupData = {
name: string;
objects: Array<{ name: string }>;
};
declare type GetNetworkSyncDataOptions = {
@@ -277,6 +288,7 @@ declare interface InstanceContainerData {
variables: RootVariableData[];
instances: InstanceData[];
objects: ObjectData[];
objectsGroups: ObjectGroupData[];
layers: LayerData[];
}
@@ -91,6 +91,7 @@ gdjs.getPixiRuntimeGameWithAssets = (props = null) => {
mangledName: '',
name: '',
objects: [],
objectsGroups: [],
layers: [],
instances: [],
behaviorsSharedData: [],
+227 -16
View File
@@ -6,15 +6,33 @@
describe('gdjs.ResourceLoader', () => {
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
/** @returns {LayoutData} */
const createSceneData = (name, usedResources) => {
/** @typedef {{name: string, usedResources: Array<ResourceReference>}} ResourcesContainer */
/**
* @param {ResourcesContainer & {objects?: Array<ResourcesContainer>}} props
* @returns {LayoutData}
*/
const createSceneData = ({ name, usedResources, objects }) => {
return {
r: 0,
v: 0,
b: 0,
mangledName: name,
name,
objects: [],
objects: objects
? objects.map((object) => ({
...object,
type: '',
variables: [],
behaviors: [],
effects: [],
}))
: [],
objectsGroups: [
{
name: 'MyGroup',
objects: [{ name: 'Object1' }, { name: 'Object2' }],
},
],
layers: [],
instances: [],
behaviorsSharedData: [],
@@ -34,25 +52,50 @@ describe('gdjs.ResourceLoader', () => {
gridColor: 0,
gridAlpha: 1,
snap: false,
}
},
};
};
/** @type {{layouts?: LayoutData[], resources?: ResourcesData}} */
const gameSettingsWithThreeScenes = {
layouts: [
createSceneData('Scene1', [
{ name: 'scene1-resource1.png' },
{ name: 'scene1-resource2.png' },
]),
createSceneData('Scene2', [
{ name: 'scene2-resource1.png' },
{ name: 'shared-resource.png' },
]),
createSceneData('Scene3', [
{ name: 'scene3-resource1.png' },
{ name: 'shared-resource.png' },
]),
createSceneData({
name: 'Scene1',
usedResources: [
{ name: 'scene1-resource1.png' },
{ name: 'scene1-resource2.png' },
],
objects: [
{
name: 'Object1',
usedResources: [
{ name: 'scene1-object1-resource1.png' },
{ name: 'scene1-object1-resource2.png' },
],
},
{
name: 'Object2',
usedResources: [
{ name: 'scene1-object2-resource1.png' },
{ name: 'scene1-object2-resource2.png' },
],
},
],
}),
createSceneData({
name: 'Scene2',
usedResources: [
{ name: 'scene2-resource1.png' },
{ name: 'shared-resource.png' },
],
}),
createSceneData({
name: 'Scene3',
usedResources: [
{ name: 'scene3-resource1.png' },
{ name: 'shared-resource.png' },
],
}),
],
resources: {
resources: [
@@ -70,6 +113,34 @@ describe('gdjs.ResourceLoader', () => {
file: 'scene1-resource2.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-object1-resource1.png',
metadata: '',
file: 'scene1-object1-resource1.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-object1-resource2.png',
metadata: '',
file: 'scene1-object1-resource2.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-object2-resource1.png',
metadata: '',
file: 'scene1-object2-resource1.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene1-object2-resource2.png',
metadata: '',
file: 'scene1-object2-resource2.png',
userAdded: true,
},
{
kind: 'fake-resource-kind-for-testing-only',
name: 'scene2-resource1.png',
@@ -357,4 +428,144 @@ describe('gdjs.ResourceLoader', () => {
// Progress should be complete (1.0)
expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(1);
});
/**
* @param {gdjs.RuntimeGame} runtimeGame
* @param {gdjs.MockedResourceManager} mockedResourceManager
*/
const loadObject1AndCheck = async (runtimeGame, mockedResourceManager) => {
runtimeGame.loadObjectOrGroupAssets('Object1');
// Object1 resources should be pending download
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object1-resource1.png'
)
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object1-resource2.png'
)
).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false);
// Mark Object1 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object1-resource1.png'
);
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object1-resource2.png'
);
await delay(10);
// Object1 should now be ready
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true);
};
it('can load object resources with an action', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Start loading first scene and background loading
runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1');
// Mark Scene1 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png');
await delay(10);
// Scene1 should now be ready
expect(runtimeGame.areSceneAssetsLoaded('Scene1')).to.be(true);
expect(runtimeGame.areSceneAssetsReady('Scene1')).to.be(true);
runtimeGame._sceneStack.push('Scene1');
await loadObject1AndCheck(runtimeGame, mockedResourceManager);
runtimeGame.unloadObjectOrGroupAssets('Object1');
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false);
await loadObject1AndCheck(runtimeGame, mockedResourceManager);
});
it('can load object group resources with an action', async () => {
const mockedResourceManager = new gdjs.MockedResourceManager();
const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes);
const resourceLoader = runtimeGame.getResourceLoader();
resourceLoader.injectMockResourceManagerForTesting(
'fake-resource-kind-for-testing-only',
mockedResourceManager
);
// Start loading first scene and background loading
runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1');
// Mark Scene1 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png');
mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png');
await delay(10);
// Scene1 should now be ready
expect(runtimeGame.areSceneAssetsLoaded('Scene1')).to.be(true);
expect(runtimeGame.areSceneAssetsReady('Scene1')).to.be(true);
runtimeGame._sceneStack.push('Scene1');
runtimeGame.loadObjectOrGroupAssets('MyGroup');
// Object1 resources should be pending download
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object1-resource1.png'
)
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object1-resource2.png'
)
).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false);
// Mark Object1 resources as loaded
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object1-resource1.png'
);
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object1-resource2.png'
);
await delay(10);
// Object1 should now be ready
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false);
// Object2 resources should be pending download
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object2-resource1.png'
)
).to.be(true);
expect(
mockedResourceManager.isResourceDownloadPending(
'scene1-object2-resource2.png'
)
).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object2')).to.be(false);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false);
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object2-resource1.png'
);
mockedResourceManager.markPendingResourcesAsLoaded(
'scene1-object2-resource2.png'
);
await delay(10);
// Object1 and Object2 should now be ready
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object2')).to.be(true);
expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(true);
});
});
+1
View File
@@ -105,6 +105,7 @@ describe('gdjs.EffectsManager', () => {
title: '',
behaviorsSharedData: [],
objects: [],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
+2
View File
@@ -182,6 +182,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
objects: objects || [
createSpriteData({ name: 'MyObject', image: 'ResourceA' }),
],
objectsGroups: [],
instances: instances
? instances.map((instance) => ({ ...defaultInstance, ...instance }))
: [],
@@ -221,6 +222,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => {
objects: objects || [
createSpriteData({ name: 'MyChildObject', image: 'ResourceA' }),
],
objectsGroups: [],
layers: [baseLayer],
areaMinX: 0,
areaMinY: 0,
+1
View File
@@ -45,6 +45,7 @@ describe('gdjs.RuntimeScene integration tests', function () {
effects: [],
},
],
objectsGroups: [],
instances: [],
usedResources: [],
uiSettings: {
+1
View File
@@ -14,6 +14,7 @@ describe('gdjs.SceneStack', () => {
mangledName: name,
name,
objects: [],
objectsGroups: [],
layers: [],
instances: [],
behaviorsSharedData: [],
+2
View File
@@ -914,6 +914,8 @@ interface gdObject {
[Const, Ref] DOMString GetAssetStoreId();
void SetType([Const] DOMString type);
[Const, Ref] DOMString GetType();
void SetResourcesPreloading([Const] DOMString value);
[Const, Ref] DOMString GetResourcesPreloading();
[Ref] ObjectConfiguration GetConfiguration();
+2
View File
@@ -791,6 +791,8 @@ export class gdObject extends EmscriptenObject {
getAssetStoreId(): string;
setType(type: string): void;
getType(): string;
setResourcesPreloading(value: string): void;
getResourcesPreloading(): string;
getConfiguration(): ObjectConfiguration;
getVariables(): VariablesContainer;
getEffects(): EffectsContainer;
+2
View File
@@ -8,6 +8,8 @@ declare class gdObject {
getAssetStoreId(): string;
setType(type: string): void;
getType(): string;
setResourcesPreloading(value: string): void;
getResourcesPreloading(): string;
getConfiguration(): gdObjectConfiguration;
getVariables(): gdVariablesContainer;
getEffects(): gdEffectsContainer;
@@ -156,7 +156,7 @@ export const CompactCollapsibleAdvancedSection = ({
uncollapsedByDefault
);
return showAdvancedOptions ? (
<React.Fragment>
<ColumnStackLayout noMargin expand>
{children}
<FlatButton
fullWidth
@@ -167,7 +167,7 @@ export const CompactCollapsibleAdvancedSection = ({
setShowAdvancedOptions(false);
}}
/>
</React.Fragment>
</ColumnStackLayout>
) : (
<FlatButton
fullWidth
@@ -61,6 +61,10 @@ import { CompactPropertiesEditorByVisibility } from '../../CompactPropertiesEdit
import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema';
import { useForceRecompute } from '../../Utils/UseForceUpdate';
import { exceptionallyGuardAgainstDeadObject } from '../../Utils/IsNullPtr';
import {
type Field,
type FieldChoices,
} from '../../PropertiesEditor/PropertiesEditorSchema';
const gd: libGDevelop = global.gd;
@@ -220,6 +224,38 @@ export const TopLevelCollapsibleSection = ({
</>
);
const resourcesPreloadingFieldChoices: Array<FieldChoices> = [
{
value: 'with-scene',
label: t`Preload with the scene`,
},
{
value: 'manually',
label: t`Preload with an action`,
},
];
const getResourcesPreloadingField = ({ i18n }: {| i18n: I18nType |}): Field => {
return {
name: 'LoadingSettings',
title: i18n._(t`Loading`),
type: 'column',
children: [
{
name: 'ResourcesPreloading',
getLabel: () => i18n._(t`Resources preloading`),
valueType: 'string',
getChoices: () => resourcesPreloadingFieldChoices,
getValue: ({ object }: { object: gdObject }): string =>
object.getResourcesPreloading(),
setValue: ({ object }: { object: gdObject }, newValue: string) =>
object.setResourcesPreloading(newValue),
defaultValue: 'with-scene',
visibility: 'advanced',
},
],
};
};
type Props = {|
project: gdProject,
resourceManagementProps: ResourceManagementProps,
@@ -508,7 +544,7 @@ export const CompactObjectPropertiesEditor = ({
// schemaRecomputeTrigger allows to invalidate the schema when required.
}
const properties = objectConfigurationAsGd.getProperties();
return propertiesMapToSchema({
const schema = propertiesMapToSchema({
properties,
defaultValueProperties: customObjectEventsBasedObject
? customObjectEventsBasedObject.getPropertyDescriptors()
@@ -526,13 +562,20 @@ export const CompactObjectPropertiesEditor = ({
object,
visibility: 'All',
});
if (layout && layout.getObjects().hasObjectNamed(object.getName())) {
schema.push(getResourcesPreloadingField({ i18n }));
}
return schema;
},
[
schemaRecomputeTrigger,
objectConfigurationAsGd,
object,
customObjectEventsBasedObject,
object,
layout,
onObjectsModified,
i18n,
]
);