Skip to content

Commit

Permalink
Add support for external layouts and reload when a change is made in …
Browse files Browse the repository at this point in the history
…the runtime
  • Loading branch information
4ian committed Jan 1, 2025
1 parent 0aea8df commit fa23712
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 127 deletions.
20 changes: 15 additions & 5 deletions GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,30 @@ bool ExporterHelper::ExportProjectForPixiPreview(
// Strip the project (*after* generating events as the events may use stripped
// things (objects groups...))
gd::ProjectStripper::StripProjectForExport(exportedProject);
exportedProject.SetFirstLayout(options.layoutName);

previousTime = LogTimeSpent("Data stripping", previousTime);

// Create the setup options passed to the gdjs.RuntimeGame
gd::SerializerElement runtimeGameOptions;
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);

auto &initialRuntimeGameStatus =
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
initialRuntimeGameStatus.AddChild("sceneName")
.SetStringValue(options.layoutName);
if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
}
if (!options.externalLayoutName.empty()) {
runtimeGameOptions.AddChild("injectExternalLayout")
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
.SetValue(options.externalLayoutName);

if (options.isInGameEdition) {
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
.SetBoolValue(true);
}
}

runtimeGameOptions.AddChild("projectDataOnlyExport")
.SetBoolValue(options.projectDataOnlyExport);
runtimeGameOptions.AddChild("nativeMobileApp")
Expand All @@ -240,9 +253,6 @@ bool ExporterHelper::ExportProjectForPixiPreview(
if (options.isDevelopmentEnvironment) {
runtimeGameOptions.AddChild("environment").SetStringValue("dev");
}
if (options.isInGameEdition) {
runtimeGameOptions.AddChild("isInGameEdition").SetBoolValue(true);
}
if (!options.gdevelopResourceToken.empty()) {
runtimeGameOptions.AddChild("gdevelopResourceToken")
.SetStringValue(options.gdevelopResourceToken);
Expand Down
74 changes: 62 additions & 12 deletions GDJS/Runtime/debugger-client/abstract-debugger-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,31 +268,81 @@ namespace gdjs {
// TODO: if fatal error, should probably reload. The editor should handle this
// as it knows the current scene to show.
});
} else if (data.command === 'requestSceneReplace') {
} else if (data.command === 'switchForInGameEdition') {
if (!this._runtimegame.isInGameEdition()) return;

const sceneName = data.sceneName || null;
const externalLayoutName = data.externalLayoutName || null;
if (!sceneName) {
logger.warn('No scene name specified, requestSceneReplace aborted');
logger.warn('No scene name specified, switchForInGameEdition aborted');
return;
}

const currentScene = runtimeGame.getSceneStack().getCurrentScene();
if (currentScene && currentScene.getName() === sceneName) {
return;
const runtimeGameOptions = this._runtimegame.getAdditionalOptions();
if (runtimeGameOptions.initialRuntimeGameStatus) {
// Skip changing the scene if we're already on the state that is being requested.
if (
runtimeGameOptions.initialRuntimeGameStatus.sceneName ===
sceneName &&
runtimeGameOptions.initialRuntimeGameStatus
.injectedExternalLayoutName === externalLayoutName
) {
return;
}
}

runtimeGame.getSceneStack().replace(sceneName, true);
// TODO: handle external layouts.

// TODO: if fatal error, should probably reload. The editor should handle this
// as it knows the current scene to show.
runtimeGame
.getSceneStack()
.replace({
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene: !!externalLayoutName,
clear: true,
});

// Update initialRuntimeGameStatus so that a hard reload
// will come back to the same state, and so that we can check later
// if the game is already on the state that is being requested.
runtimeGameOptions.initialRuntimeGameStatus = {
isPaused: runtimeGame.isPaused(),
isInGameEdition: runtimeGame.isInGameEdition(),
sceneName: sceneName,
injectedExternalLayoutName: externalLayoutName,
skipCreatingInstancesFromScene: !!externalLayoutName,
};
} else if (data.command === 'updateInstances') {
// TODO: do an update/partial hot reload of the instances
} else if (data.command === 'hardReload') {
// This usually means that the preview was modified so much that an entire reload
// is needed, or that the runtime itself could have been modified.
location.reload();
try {
const reloadUrl = new URL(location.href);

// Construct the initial status to be restored.
const initialRuntimeGameStatus = this._runtimegame.getAdditionalOptions()
.initialRuntimeGameStatus;
const runtimeGameStatus: RuntimeGameStatus = {
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
sceneName: initialRuntimeGameStatus?.sceneName || null,
injectedExternalLayoutName:
initialRuntimeGameStatus?.injectedExternalLayoutName || null,
skipCreatingInstancesFromScene:
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
};

reloadUrl.searchParams.set(
'runtimeGameStatus',
JSON.stringify(runtimeGameStatus)
);
location.replace(reloadUrl);
} catch (error) {
logger.error(
'Could not reload the game with the new initial status',
error
);
location.reload();
}
} else {
logger.info(
'Unknown command "' + data.command + '" received by the debugger.'
Expand Down Expand Up @@ -471,7 +521,7 @@ namespace gdjs {
payload: {
isPaused: this._runtimegame.isPaused(),
isInGameEdition: this._runtimegame.isInGameEdition(),
currentSceneName: currentScene ? currentScene.getName() : null,
sceneName: currentScene ? currentScene.getName() : null,
},
})
);
Expand Down
97 changes: 73 additions & 24 deletions GDJS/Runtime/runtimegame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,54 @@ namespace gdjs {
return supportedCompressionMethods;
};

/**
* The desired status of the game, used for previews or in-game edition.
* Either stored in the options generated by the preview or in the URL
* in case of a hard reload.
*/
type RuntimeGameStatus = {
isPaused: boolean;
isInGameEdition: boolean;
sceneName: string | null;
injectedExternalLayoutName: string | null;
skipCreatingInstancesFromScene: boolean;
};

/**
* Read the desired status of the game from the URL. Only useful for previews
* when hard reloaded.
*/
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
try {
const url = new URL(location.href);
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
if (!runtimeGameStatus) return null;

const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
return {
isPaused: !!parsedRuntimeGameStatus.isPaused,
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
sceneName: '' + parsedRuntimeGameStatus.sceneName,
injectedExternalLayoutName:
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
skipCreatingInstancesFromScene: !!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
};
} catch (e) {
return null;
}
};

/** Options given to the game at startup. */
export type RuntimeGameOptions = {
/** if true, force fullscreen. */
forceFullscreen?: boolean;

/** if true, game is run as a preview launched from an editor. */
isPreview?: boolean;
/** if true, game is run for being edited from the editor. */
isInGameEdition?: boolean;

/** The name of the external layout to create in the scene at position 0;0. */
injectExternalLayout?: string;
/** if set, the status of the game to be restored. */
initialRuntimeGameStatus?: RuntimeGameStatus;

/** Script files, used for hot-reloading. */
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
/** if true, export is a partial preview without events. */
Expand Down Expand Up @@ -173,10 +209,6 @@ namespace gdjs {
//Inputs :
_inputManager: InputManager;

/**
* Allow to specify an external layout to insert in the first scene.
*/
_injectExternalLayout: any;
_options: RuntimeGameOptions;

/**
Expand Down Expand Up @@ -207,6 +239,19 @@ namespace gdjs {
*/
constructor(data: ProjectData, options?: RuntimeGameOptions) {
this._options = options || {};

this._isPreview = this._options.isPreview || false;
if (this._isPreview) {
// Check if we need to restore the state from the URL, which is used
// when a preview is hard reloaded (search for `hardReload`).
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
if (runtimeGameStatusFromUrl) {
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
}
}
this._isInGameEdition =
this._options.initialRuntimeGameStatus?.isInGameEdition || false;

this._variables = new gdjs.VariablesContainer(data.variables);
this._variablesByExtensionName = new Map<
string,
Expand Down Expand Up @@ -255,7 +300,6 @@ namespace gdjs {
);
this._sceneStack = new gdjs.SceneStack(this);
this._inputManager = new gdjs.InputManager();
this._injectExternalLayout = this._options.injectExternalLayout || '';
this._debuggerClient = gdjs.DebuggerClient
? new gdjs.DebuggerClient(this)
: null;
Expand All @@ -265,8 +309,6 @@ namespace gdjs {
this._options.captureOptions || {}
)
: null;
this._isPreview = this._options.isPreview || false;
this._isInGameEdition = this._options.isInGameEdition || false;
this._sessionId = null;
this._playerId = null;

Expand Down Expand Up @@ -884,7 +926,9 @@ namespace gdjs {
}

private _getFirstSceneName(): string {
const firstSceneName = this._data.firstLayout;
const firstSceneName =
this._options.initialRuntimeGameStatus?.sceneName ||
this._data.firstLayout;
return this.hasScene(firstSceneName)
? firstSceneName
: // There is always at least a scene
Expand All @@ -904,10 +948,15 @@ namespace gdjs {
this._forceGameResolutionUpdate();

// Load the first scene
this._sceneStack.push(
this._getFirstSceneName(),
this._injectExternalLayout
);
this._sceneStack.push({
sceneName: this._getFirstSceneName(),
externalLayoutName:
this._options.initialRuntimeGameStatus
?.injectedExternalLayoutName || undefined,
skipCreatingInstancesFromScene:
this._options.initialRuntimeGameStatus
?.skipCreatingInstancesFromScene || false,
});
this._watermark.displayAtStartup();

//Uncomment to profile the first x frames of the game.
Expand Down Expand Up @@ -1302,14 +1351,6 @@ namespace gdjs {
return this._isPreview;
}

/**
* Check if the game should display in-game edition tools or not.
* @returns true if the current game is being edited.
*/
isInGameEdition(): boolean {
return this._isInGameEdition;
}

/**
* Check if the game loop is paused, for debugging/edition purposes.
* @returns true if the current game is paused
Expand All @@ -1318,6 +1359,14 @@ namespace gdjs {
return this._paused;
}

/**
* Check if the game should display in-game edition tools or not.
* @returns true if the current game is being edited.
*/
isInGameEdition(): boolean {
return this._isInGameEdition;
}

/**
* Check if the game should call GDevelop development APIs or not.
*
Expand Down
27 changes: 17 additions & 10 deletions GDJS/Runtime/runtimescene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,15 @@ namespace gdjs {

/**
* Load the runtime scene from the given scene.
* @param sceneData An object containing the scene data.
*
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
* @param options Options to change what is loaded.
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
*/
loadFromScene(sceneAndExtensionsData: SceneAndExtensionsData | null) {
loadFromScene(
sceneAndExtensionsData: SceneAndExtensionsData | null,
options?: { skipCreatingInstances?: boolean }
) {
if (!sceneAndExtensionsData) {
logger.error('loadFromScene was called without a scene');
return;
Expand Down Expand Up @@ -184,14 +189,16 @@ namespace gdjs {
}

//Create initial instances of objects
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true
);
if (!options || !options.skipCreatingInstances) {
this.createObjectsFrom(
sceneData.instances,
0,
0,
0,
/*trackByPersistentUuid=*/
true
);
}

// Set up the default z order (for objects created from events)
this._setLayerDefaultZOrders();
Expand Down
Loading

0 comments on commit fa23712

Please sign in to comment.