mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
975 lines
31 KiB
TypeScript
975 lines
31 KiB
TypeScript
/*
|
|
* GDevelop JS Platform
|
|
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
|
* This project is released under the MIT License.
|
|
*/
|
|
namespace gdjs {
|
|
const logger = new gdjs.Logger('RuntimeScene');
|
|
const setupWarningLogger = new gdjs.Logger('RuntimeScene (setup warnings)');
|
|
|
|
/**
|
|
* A scene being played, containing instances of objects rendered on screen.
|
|
*/
|
|
export class RuntimeScene extends gdjs.RuntimeInstanceContainer {
|
|
_eventsFunction: null | ((runtimeScene: RuntimeScene) => void) = null;
|
|
_idToCallbackMap: null | Map<
|
|
string,
|
|
(
|
|
runtimeScene: gdjs.RuntimeScene,
|
|
asyncObjectsList: gdjs.LongLivedObjectsList
|
|
) => void
|
|
> = null;
|
|
_renderer: RuntimeSceneRenderer;
|
|
_debuggerRenderer: gdjs.DebuggerRenderer;
|
|
_variables: gdjs.VariablesContainer;
|
|
_variablesByExtensionName: Map<string, gdjs.VariablesContainer>;
|
|
_runtimeGame: gdjs.RuntimeGame;
|
|
_lastId: integer = 0;
|
|
_name: string = '';
|
|
_timeManager: TimeManager;
|
|
_gameStopRequested: boolean = false;
|
|
_requestedScene: string = '';
|
|
_resourcesUnloading: 'at-scene-exit' | 'never' | 'inherit' = 'inherit';
|
|
private _asyncTasksManager = new gdjs.AsyncTasksManager();
|
|
|
|
/** True if loadFromScene was called and the scene is being played. */
|
|
_isLoaded: boolean = false;
|
|
/** True in the first frame after resuming the paused scene */
|
|
_isJustResumed: boolean = false;
|
|
|
|
_requestedChange: SceneChangeRequest;
|
|
/** Black background by default. */
|
|
_backgroundColor: integer = 0;
|
|
|
|
/** Should the canvas be cleared before this scene rendering. */
|
|
_clearCanvas: boolean = true;
|
|
|
|
_onceTriggers: OnceTriggers;
|
|
_profiler: gdjs.Profiler | null = null;
|
|
|
|
// Set to `new gdjs.Profiler()` to have profiling done on the scene.
|
|
_onProfilerStopped: null | ((oldProfiler: gdjs.Profiler) => void) = null;
|
|
|
|
_cachedGameResolutionWidth: integer;
|
|
_cachedGameResolutionHeight: integer;
|
|
|
|
/**
|
|
* A network ID associated to the scene to be used
|
|
* for multiplayer, to identify the scene across peers.
|
|
* A scene can have its networkId re-generated during the game, meaning
|
|
* that the scene is re-created on every peer.
|
|
*/
|
|
networkId: string | null = null;
|
|
|
|
/**
|
|
* @param runtimeGame The game associated to this scene.
|
|
*/
|
|
constructor(runtimeGame: gdjs.RuntimeGame) {
|
|
super();
|
|
this._runtimeGame = runtimeGame;
|
|
this._variables = new gdjs.VariablesContainer();
|
|
this._variablesByExtensionName = new Map<
|
|
string,
|
|
gdjs.VariablesContainer
|
|
>();
|
|
this._timeManager = new gdjs.TimeManager();
|
|
this._onceTriggers = new gdjs.OnceTriggers();
|
|
this._requestedChange = SceneChangeRequest.CONTINUE;
|
|
this._cachedGameResolutionWidth = runtimeGame
|
|
? runtimeGame.getGameResolutionWidth()
|
|
: 0;
|
|
this._cachedGameResolutionHeight = runtimeGame
|
|
? runtimeGame.getGameResolutionHeight()
|
|
: 0;
|
|
|
|
this._renderer = new gdjs.RuntimeSceneRenderer(
|
|
this,
|
|
// @ts-ignore This is needed because of test. They should mock RuntimeGame instead.
|
|
runtimeGame ? runtimeGame.getRenderer() : null
|
|
);
|
|
this._debuggerRenderer = new gdjs.DebuggerRenderer(this);
|
|
|
|
// What to do after the frame is rendered.
|
|
|
|
// The callback function to call when the profiler is stopped.
|
|
this.onGameResolutionResized();
|
|
}
|
|
|
|
addLayer(layerData: LayerData) {
|
|
const layer = new gdjs.Layer(layerData, this);
|
|
this._layers.put(layerData.name, layer);
|
|
this._orderedLayers.push(layer);
|
|
}
|
|
|
|
/**
|
|
* Should be called when the canvas where the scene is rendered has been resized.
|
|
* See gdjs.RuntimeGame.startGameLoop in particular.
|
|
*/
|
|
onGameResolutionResized() {
|
|
const oldGameResolutionOriginX = this.getViewportOriginX();
|
|
const oldGameResolutionOriginY = this.getViewportOriginY();
|
|
this._cachedGameResolutionWidth = this._runtimeGame
|
|
? this._runtimeGame.getGameResolutionWidth()
|
|
: 0;
|
|
this._cachedGameResolutionHeight = this._runtimeGame
|
|
? this._runtimeGame.getGameResolutionHeight()
|
|
: 0;
|
|
for (const name in this._layers.items) {
|
|
if (this._layers.items.hasOwnProperty(name)) {
|
|
const theLayer: gdjs.RuntimeLayer = this._layers.items[name];
|
|
theLayer.onGameResolutionResized(
|
|
oldGameResolutionOriginX,
|
|
oldGameResolutionOriginY
|
|
);
|
|
}
|
|
}
|
|
this._renderer.onGameResolutionResized();
|
|
}
|
|
|
|
/**
|
|
* Load the runtime scene from the given scene.
|
|
* @param sceneAndExtensionsData An object containing the scene data.
|
|
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
|
*/
|
|
loadFromScene(
|
|
sceneAndExtensionsData: SceneAndExtensionsData | null,
|
|
options?: {
|
|
excludedObjectNames?: Set<string>;
|
|
skipStoppingSoundsOnStartup?: boolean;
|
|
}
|
|
) {
|
|
if (!sceneAndExtensionsData) {
|
|
logger.error('loadFromScene was called without a scene');
|
|
return;
|
|
}
|
|
const { sceneData, usedExtensionsWithVariablesData } =
|
|
sceneAndExtensionsData;
|
|
|
|
if (this._isLoaded) {
|
|
this.unloadScene();
|
|
}
|
|
|
|
//Setup main properties
|
|
if (this._runtimeGame) {
|
|
this._runtimeGame.getRenderer().setWindowTitle(sceneData.title);
|
|
}
|
|
this._name = sceneData.name;
|
|
this._resourcesUnloading = sceneData.resourcesUnloading || 'inherit';
|
|
this.setBackgroundColor(sceneData.r, sceneData.v, sceneData.b);
|
|
|
|
//Load layers
|
|
for (let i = 0, len = sceneData.layers.length; i < len; ++i) {
|
|
this.addLayer(sceneData.layers[i]);
|
|
}
|
|
|
|
// Load variables
|
|
this._variables = new gdjs.VariablesContainer(sceneData.variables);
|
|
for (const extensionData of usedExtensionsWithVariablesData) {
|
|
this._variablesByExtensionName.set(
|
|
extensionData.name,
|
|
new gdjs.VariablesContainer(extensionData.sceneVariables)
|
|
);
|
|
}
|
|
|
|
//Cache the initial shared data of the behaviors
|
|
for (
|
|
let i = 0, len = sceneData.behaviorsSharedData.length;
|
|
i < len;
|
|
++i
|
|
) {
|
|
const behaviorSharedData = sceneData.behaviorsSharedData[i];
|
|
this.setInitialSharedDataForBehavior(
|
|
behaviorSharedData.name,
|
|
behaviorSharedData
|
|
);
|
|
}
|
|
|
|
//Registering objects: Global objects first...
|
|
const initialGlobalObjectsData = this.getGame().getInitialObjectsData();
|
|
for (let i = 0, len = initialGlobalObjectsData.length; i < len; ++i) {
|
|
this.registerObject(initialGlobalObjectsData[i]);
|
|
}
|
|
|
|
//...then the scene objects
|
|
for (let i = 0, len = sceneData.objects.length; i < len; ++i) {
|
|
this.registerObject(sceneData.objects[i]);
|
|
}
|
|
|
|
// Create initial instances of objects.
|
|
this.createObjectsFrom(
|
|
sceneData.instances,
|
|
0,
|
|
0,
|
|
0,
|
|
/*trackByPersistentUuid=*/
|
|
true,
|
|
{
|
|
excludedObjectNames: options?.excludedObjectNames,
|
|
}
|
|
);
|
|
|
|
// Set up the default z order (for objects created from events)
|
|
this._setLayerDefaultZOrders();
|
|
|
|
//Set up the function to be executed at each tick
|
|
this.setEventsGeneratedCodeFunction(sceneData);
|
|
this._onceTriggers = new gdjs.OnceTriggers();
|
|
|
|
// Notify the global callbacks
|
|
if (this._runtimeGame && !this._runtimeGame.wasFirstSceneLoaded()) {
|
|
for (let i = 0; i < gdjs.callbacksFirstRuntimeSceneLoaded.length; ++i) {
|
|
gdjs.callbacksFirstRuntimeSceneLoaded[i](this);
|
|
}
|
|
}
|
|
for (let i = 0; i < gdjs.callbacksRuntimeSceneLoaded.length; ++i) {
|
|
gdjs.callbacksRuntimeSceneLoaded[i](this);
|
|
}
|
|
if (
|
|
sceneData.stopSoundsOnStartup &&
|
|
(!options || !options.skipStoppingSoundsOnStartup) &&
|
|
this._runtimeGame
|
|
) {
|
|
this._runtimeGame.getSoundManager().clearAll();
|
|
}
|
|
this._isLoaded = true;
|
|
this._timeManager.reset();
|
|
}
|
|
|
|
getInitialSharedDataForBehavior(name: string): BehaviorSharedData | null {
|
|
// TODO Move this error in RuntimeInstanceContainer after deciding
|
|
// what to do with shared data in custom object.
|
|
const behaviorSharedData = super.getInitialSharedDataForBehavior(name);
|
|
if (!behaviorSharedData) {
|
|
logger.error("Can't find shared data for behavior with name: " + name);
|
|
}
|
|
return behaviorSharedData;
|
|
}
|
|
|
|
/**
|
|
* Called when a scene is "paused", i.e it will be not be rendered again
|
|
* for some time, until it's resumed or unloaded.
|
|
*/
|
|
onPause() {
|
|
// Notify the objects that the scene is being paused. Objects should not
|
|
// do anything special, but some object renderers might want to know about this.
|
|
const allInstancesList = this.getAdhocListOfAllInstances();
|
|
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
|
const object = allInstancesList[i];
|
|
object.onScenePaused(this);
|
|
}
|
|
|
|
for (let i = 0; i < gdjs.callbacksRuntimeScenePaused.length; ++i) {
|
|
gdjs.callbacksRuntimeScenePaused[i](this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a scene is "resumed", i.e it will be rendered again
|
|
* on screen after having being paused.
|
|
*/
|
|
onResume() {
|
|
this._isJustResumed = true;
|
|
|
|
// Notify the objects that the scene is being resumed. Objects should not
|
|
// do anything special, but some object renderers might want to know about this.
|
|
const allInstancesList = this.getAdhocListOfAllInstances();
|
|
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
|
const object = allInstancesList[i];
|
|
object.onSceneResumed(this);
|
|
}
|
|
|
|
for (let i = 0; i < gdjs.callbacksRuntimeSceneResumed.length; ++i) {
|
|
gdjs.callbacksRuntimeSceneResumed[i](this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called before a scene is removed from the stack of scenes
|
|
* rendered on the screen.
|
|
*/
|
|
unloadScene() {
|
|
if (!this._isLoaded) {
|
|
return;
|
|
}
|
|
if (this._profiler) {
|
|
this.stopProfiler();
|
|
}
|
|
|
|
// Notify the global callbacks (which should not release resources yet,
|
|
// as other callbacks might still refer to the objects/scene).
|
|
for (let i = 0; i < gdjs.callbacksRuntimeSceneUnloading.length; ++i) {
|
|
gdjs.callbacksRuntimeSceneUnloading[i](this);
|
|
}
|
|
|
|
// Notify the objects they are being destroyed
|
|
const allInstancesList = this.getAdhocListOfAllInstances();
|
|
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
|
const object = allInstancesList[i];
|
|
object.onDeletedFromScene();
|
|
object.onDestroyed();
|
|
}
|
|
|
|
// Notify the renderer
|
|
if (this._renderer) {
|
|
this._renderer.onSceneUnloaded();
|
|
}
|
|
|
|
// Notify the global callbacks (after notifying objects and renderer, because
|
|
// callbacks from extensions might want to free resources - which can't be done
|
|
// safely before destroying objects and the renderer).
|
|
for (let i = 0; i < gdjs.callbacksRuntimeSceneUnloaded.length; ++i) {
|
|
gdjs.callbacksRuntimeSceneUnloaded[i](this);
|
|
}
|
|
|
|
this._destroy();
|
|
|
|
this._isLoaded = false;
|
|
this.onGameResolutionResized();
|
|
}
|
|
|
|
override _destroy() {
|
|
// It should not be necessary to reset these variables, but this help
|
|
// ensuring that all memory related to the RuntimeScene is released immediately.
|
|
super._destroy();
|
|
this._variables = new gdjs.VariablesContainer();
|
|
this._variablesByExtensionName = new Map<
|
|
string,
|
|
gdjs.VariablesContainer
|
|
>();
|
|
this._initialBehaviorSharedData = new Hashtable();
|
|
this._eventsFunction = null;
|
|
this._lastId = 0;
|
|
this.networkId = null;
|
|
// @ts-ignore We are deleting the object
|
|
this._onceTriggers = null;
|
|
}
|
|
|
|
/**
|
|
* Set the function called each time the scene is stepped to be the events generated code,
|
|
* which is by convention assumed to be a function in `gdjs` with a name based on the scene
|
|
* mangled name.
|
|
*
|
|
* @param sceneData The scene data, used to find where the code was generated.
|
|
*/
|
|
setEventsGeneratedCodeFunction(sceneData: LayoutData): void {
|
|
const module = gdjs[sceneData.mangledName + 'Code'];
|
|
if (module && module.func) {
|
|
this._eventsFunction = module.func;
|
|
this._idToCallbackMap =
|
|
gdjs[sceneData.mangledName + 'Code'].idToCallbackMap;
|
|
} else {
|
|
setupWarningLogger.warn(
|
|
'No function found for running logic of scene ' + this._name
|
|
);
|
|
this._eventsFunction = function () {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the function called each time the scene is stepped.
|
|
* The function will be passed the `runtimeScene` as argument.
|
|
*
|
|
* Note that this is already set up by the gdjs.RuntimeScene constructor and that you should
|
|
* not need to use this method.
|
|
*
|
|
* @param func The function to be called.
|
|
*/
|
|
setEventsFunction(func: () => void): void {
|
|
this._eventsFunction = func;
|
|
}
|
|
|
|
/**
|
|
* Step and render the scene.
|
|
* @param elapsedTime In milliseconds
|
|
* @return true if the game loop should continue, false if a scene change/push/pop
|
|
* or a game stop was requested.
|
|
*/
|
|
renderAndStep(elapsedTime: float): boolean {
|
|
if (this._profiler) {
|
|
this._profiler.beginFrame();
|
|
}
|
|
this._requestedChange = SceneChangeRequest.CONTINUE;
|
|
this._timeManager.update(
|
|
elapsedTime,
|
|
this._runtimeGame.getMinimalFramerate()
|
|
);
|
|
if (this._profiler) {
|
|
this._profiler.begin('asynchronous actions (wait action, etc...)');
|
|
}
|
|
this._asyncTasksManager.processTasks(this);
|
|
if (this._profiler) {
|
|
this._profiler.end('asynchronous actions (wait action, etc...)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('objects (pre-events)');
|
|
}
|
|
this._updateObjectsPreEvents();
|
|
if (this._profiler) {
|
|
this._profiler.end('objects (pre-events)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('callbacks and extensions (pre-events)');
|
|
}
|
|
for (let i = 0; i < gdjs.callbacksRuntimeScenePreEvents.length; ++i) {
|
|
gdjs.callbacksRuntimeScenePreEvents[i](this);
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.end('callbacks and extensions (pre-events)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('events');
|
|
}
|
|
if (this._eventsFunction !== null) this._eventsFunction(this);
|
|
if (this._profiler) {
|
|
this._profiler.end('events');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('objects (post-events)');
|
|
}
|
|
this._stepBehaviorsPostEvents();
|
|
if (this._profiler) {
|
|
this._profiler.end('objects (post-events)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('callbacks and extensions (post-events)');
|
|
}
|
|
for (let i = 0; i < gdjs.callbacksRuntimeScenePostEvents.length; ++i) {
|
|
gdjs.callbacksRuntimeScenePostEvents[i](this);
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.end('callbacks and extensions (post-events)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('objects (pre-render, effects update)');
|
|
}
|
|
this._updateObjectsPreRender();
|
|
if (this._profiler) {
|
|
this._profiler.end('objects (pre-render, effects update)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('layers (effects update)');
|
|
}
|
|
this._updateLayersPreRender();
|
|
if (this._profiler) {
|
|
this._profiler.end('layers (effects update)');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.begin('render');
|
|
}
|
|
|
|
// Set to true to enable debug rendering (look for the implementation in the renderer
|
|
// to see what is rendered).
|
|
if (this._debugDrawEnabled) {
|
|
this._debuggerRenderer.renderDebugDraw(
|
|
this.getAdhocListOfAllInstances(),
|
|
this._debugDrawShowHiddenInstances,
|
|
this._debugDrawShowPointsNames,
|
|
this._debugDrawShowCustomPoints
|
|
);
|
|
}
|
|
|
|
this._isJustResumed = false;
|
|
this.render();
|
|
if (this._profiler) {
|
|
this._profiler.end('render');
|
|
}
|
|
if (this._profiler) {
|
|
this._profiler.endFrame();
|
|
}
|
|
return !!this.getRequestedChange();
|
|
}
|
|
|
|
/**
|
|
* Render the PIXI container associated to the runtimeScene.
|
|
*/
|
|
render() {
|
|
this._renderer.render();
|
|
}
|
|
|
|
/**
|
|
* Called to update visibility of the renderers of objects
|
|
* rendered on the scene ("culling"), update effects (of visible objects)
|
|
* and give a last chance for objects to update before rendering.
|
|
*
|
|
* Visibility is set to false if object is hidden, or if
|
|
* object is too far from the camera of its layer ("culling").
|
|
*/
|
|
_updateObjectsPreRender() {
|
|
if (this._timeManager.isFirstFrame()) {
|
|
super._updateObjectsPreRender();
|
|
return;
|
|
} else {
|
|
// After first frame, optimise rendering by setting only objects
|
|
// near camera as visible.
|
|
// TODO: For compatibility, pass a scale of `2`,
|
|
// meaning that size of cameras will be multiplied by 2 and so objects
|
|
// will be hidden if they are outside of this *larger* camera area.
|
|
// This is useful for:
|
|
// - objects not properly reporting their visibility AABB,
|
|
// (so we have a "safety margin") but these objects should be fixed
|
|
// instead.
|
|
// - objects having effects rendering outside of their visibility AABB.
|
|
|
|
// TODO (3D) culling - add support for 3D object culling?
|
|
this._updateLayersCameraCoordinates(2);
|
|
const allInstancesList = this.getAdhocListOfAllInstances();
|
|
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
|
|
const object = allInstancesList[i];
|
|
const rendererObject = object.getRendererObject();
|
|
if (rendererObject) {
|
|
if (object.isHidden()) {
|
|
rendererObject.visible = false;
|
|
} else {
|
|
const cameraCoords =
|
|
this._layersCameraCoordinates[object.getLayer()];
|
|
if (!cameraCoords) {
|
|
continue;
|
|
}
|
|
const aabb = object.getVisibilityAABB();
|
|
rendererObject.visible =
|
|
// If no AABB is returned, the object should always be visible
|
|
!aabb ||
|
|
// If an AABB is there, it must be at least partially inside
|
|
// the camera bounds.
|
|
!(
|
|
aabb.min[0] > cameraCoords[2] ||
|
|
aabb.min[1] > cameraCoords[3] ||
|
|
aabb.max[0] < cameraCoords[0] ||
|
|
aabb.max[1] < cameraCoords[1]
|
|
);
|
|
}
|
|
|
|
// Update effects, only for visible objects.
|
|
if (rendererObject.visible) {
|
|
this._runtimeGame
|
|
.getEffectsManager()
|
|
.updatePreRender(object.getRendererEffects(), object);
|
|
|
|
// Perform pre-render update only if the object is visible
|
|
// (including if there is no visibility AABB returned previously).
|
|
object.updatePreRender(this);
|
|
}
|
|
} else {
|
|
// Perform pre-render update, always for objects not having an
|
|
// associated renderer object (so it must handle visibility on its own).
|
|
object.updatePreRender(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the background color, by setting the RGB components.
|
|
* Internally, the color is stored as an hexadecimal number.
|
|
*
|
|
* @param r The color red component (0-255).
|
|
* @param g The color green component (0-255).
|
|
* @param b The color blue component (0-255).
|
|
*/
|
|
setBackgroundColor(r: integer, g: integer, b: integer): void {
|
|
this._backgroundColor = parseInt(gdjs.rgbToHex(r, g, b), 16);
|
|
}
|
|
|
|
/**
|
|
* Get the background color, as an hexadecimal number.
|
|
* @returns The current background color.
|
|
*/
|
|
getBackgroundColor(): number {
|
|
return this._backgroundColor;
|
|
}
|
|
|
|
/**
|
|
* Set whether the canvas should be cleared before this scene rendering.
|
|
* This is experimental: if possible, try to avoid relying on this and use
|
|
* custom objects to build complex scenes.
|
|
*/
|
|
setClearCanvas(shouldClearCanvas: boolean): void {
|
|
this._clearCanvas = shouldClearCanvas;
|
|
}
|
|
|
|
/**
|
|
* Get whether the canvas should be cleared before this scene rendering.
|
|
*/
|
|
getClearCanvas(): boolean {
|
|
return this._clearCanvas;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the scene.
|
|
*/
|
|
getName(): string {
|
|
return this._name;
|
|
}
|
|
|
|
/**
|
|
* Get the strategy to unload resources of this scene.
|
|
*/
|
|
getResourcesUnloading(): 'at-scene-exit' | 'never' | 'inherit' {
|
|
return this._resourcesUnloading;
|
|
}
|
|
|
|
/**
|
|
* Create an identifier for a new object of the scene.
|
|
*/
|
|
createNewUniqueId(): integer {
|
|
this._lastId++;
|
|
return this._lastId;
|
|
}
|
|
|
|
getRenderer(): gdjs.RuntimeScenePixiRenderer {
|
|
return this._renderer;
|
|
}
|
|
|
|
getDebuggerRenderer() {
|
|
return this._debuggerRenderer;
|
|
}
|
|
|
|
getGame() {
|
|
return this._runtimeGame;
|
|
}
|
|
|
|
getScene() {
|
|
return this;
|
|
}
|
|
|
|
getUnrotatedViewportMinX(): float {
|
|
return 0;
|
|
}
|
|
|
|
getUnrotatedViewportMinY(): float {
|
|
return 0;
|
|
}
|
|
|
|
getUnrotatedViewportMaxX(): float {
|
|
return this._cachedGameResolutionWidth;
|
|
}
|
|
|
|
getUnrotatedViewportMaxY(): float {
|
|
return this._cachedGameResolutionHeight;
|
|
}
|
|
|
|
getInitialUnrotatedViewportMinX(): float {
|
|
return 0;
|
|
}
|
|
|
|
getInitialUnrotatedViewportMinY(): float {
|
|
return 0;
|
|
}
|
|
|
|
getInitialUnrotatedViewportMaxX(): float {
|
|
return this.getGame().getOriginalWidth();
|
|
}
|
|
|
|
getInitialUnrotatedViewportMaxY(): float {
|
|
return this.getGame().getOriginalHeight();
|
|
}
|
|
|
|
getViewportWidth(): float {
|
|
return this._cachedGameResolutionWidth;
|
|
}
|
|
|
|
getViewportHeight(): float {
|
|
return this._cachedGameResolutionHeight;
|
|
}
|
|
|
|
getViewportOriginX(): float {
|
|
return this._cachedGameResolutionWidth / 2;
|
|
}
|
|
|
|
getViewportOriginY(): float {
|
|
return this._cachedGameResolutionHeight / 2;
|
|
}
|
|
|
|
convertCoords(x: float, y: float, result: FloatPoint): FloatPoint {
|
|
// The result parameter used to be optional.
|
|
const point = result || [0, 0];
|
|
point[0] = x;
|
|
point[1] = y;
|
|
return point;
|
|
}
|
|
|
|
convertInverseCoords(
|
|
sceneX: float,
|
|
sceneY: float,
|
|
result: FloatPoint
|
|
): FloatPoint {
|
|
const point = result || [0, 0];
|
|
point[0] = sceneX;
|
|
point[1] = sceneY;
|
|
return point;
|
|
}
|
|
|
|
onChildrenLocationChanged(): void {
|
|
// Scenes don't maintain bounds.
|
|
}
|
|
|
|
/**
|
|
* Get the variables of the runtimeScene.
|
|
* @return The container holding the variables of the scene.
|
|
*/
|
|
getVariables() {
|
|
return this._variables;
|
|
}
|
|
|
|
/**
|
|
* Get the extension's variables for this scene.
|
|
* @param extensionName The extension name.
|
|
* @returns The extension's variables for this scene.
|
|
*/
|
|
getVariablesForExtension(extensionName: string) {
|
|
return this._variablesByExtensionName.get(extensionName) || null;
|
|
}
|
|
|
|
/**
|
|
* Get the TimeManager of the scene.
|
|
* @return The gdjs.TimeManager of the scene.
|
|
*/
|
|
getTimeManager(): gdjs.TimeManager {
|
|
return this._timeManager;
|
|
}
|
|
|
|
/**
|
|
* Return the time elapsed since the last frame,
|
|
* in milliseconds, for objects on the layer.
|
|
*/
|
|
getElapsedTime(): float {
|
|
return this._timeManager.getElapsedTime();
|
|
}
|
|
|
|
/**
|
|
* Shortcut to get the SoundManager of the game.
|
|
* @return The gdjs.SoundManager of the game.
|
|
*/
|
|
getSoundManager(): gdjs.SoundManager {
|
|
return this._runtimeGame.getSoundManager();
|
|
}
|
|
|
|
/**
|
|
* @returns The scene's async tasks manager.
|
|
*/
|
|
getAsyncTasksManager() {
|
|
return this._asyncTasksManager;
|
|
}
|
|
|
|
/**
|
|
* Return the value of the scene change that is requested.
|
|
*/
|
|
getRequestedChange(): SceneChangeRequest {
|
|
return this._requestedChange;
|
|
}
|
|
|
|
/**
|
|
* Return the name of the new scene to be launched.
|
|
*
|
|
* See requestChange.
|
|
*/
|
|
getRequestedScene(): string {
|
|
return this._requestedScene;
|
|
}
|
|
|
|
/**
|
|
* Request a scene change to be made. The change is handled externally (see gdjs.SceneStack)
|
|
* thanks to getRequestedChange and getRequestedScene methods.
|
|
* @param change One of RuntimeScene.CONTINUE|PUSH_SCENE|POP_SCENE|REPLACE_SCENE|CLEAR_SCENES|STOP_GAME.
|
|
* @param sceneName The name of the new scene to launch, if applicable.
|
|
*/
|
|
requestChange(change: SceneChangeRequest, sceneName?: string) {
|
|
this._requestedChange = change;
|
|
if (sceneName) this._requestedScene = sceneName;
|
|
}
|
|
|
|
/**
|
|
* Get the profiler associated with the scene, or null if none.
|
|
*/
|
|
getProfiler(): gdjs.Profiler | null {
|
|
return this._profiler;
|
|
}
|
|
|
|
/**
|
|
* Start a new profiler to measures the time passed in sections of the engine
|
|
* in the scene.
|
|
* @param onProfilerStopped Function to be called when the profiler is stopped. Will be passed the profiler as argument.
|
|
*/
|
|
startProfiler(onProfilerStopped: (oldProfiler: gdjs.Profiler) => void) {
|
|
if (this._profiler) {
|
|
return;
|
|
}
|
|
this._profiler = new gdjs.Profiler();
|
|
this._onProfilerStopped = onProfilerStopped;
|
|
}
|
|
|
|
/**
|
|
* Stop the profiler being run on the scene.
|
|
*/
|
|
stopProfiler() {
|
|
if (!this._profiler) {
|
|
return;
|
|
}
|
|
const oldProfiler = this._profiler;
|
|
const onProfilerStopped = this._onProfilerStopped;
|
|
this._profiler = null;
|
|
this._onProfilerStopped = null;
|
|
if (onProfilerStopped) {
|
|
onProfilerStopped(oldProfiler);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the structure containing the triggers for "Trigger once" conditions.
|
|
*/
|
|
getOnceTriggers() {
|
|
return this._onceTriggers;
|
|
}
|
|
|
|
/**
|
|
* Check if the scene was just resumed.
|
|
* This is true during the first frame after the scene has been unpaused.
|
|
*
|
|
* @returns true if the scene was just resumed
|
|
*/
|
|
sceneJustResumed(): boolean {
|
|
return this._isJustResumed;
|
|
}
|
|
|
|
getNetworkSyncData(
|
|
syncOptions: GetNetworkSyncDataOptions
|
|
): LayoutNetworkSyncData | null {
|
|
const syncedPlayerNumber = syncOptions.playerNumber;
|
|
const variablesNetworkSyncData =
|
|
this._variables.getNetworkSyncData(syncOptions);
|
|
const extensionsVariablesSyncData = {};
|
|
this._variablesByExtensionName.forEach((variables, extensionName) => {
|
|
const extensionVariablesSyncData =
|
|
variables.getNetworkSyncData(syncOptions);
|
|
// If there is no variables to sync, don't include the extension in the sync data.
|
|
if (extensionVariablesSyncData) {
|
|
extensionsVariablesSyncData[extensionName] =
|
|
extensionVariablesSyncData;
|
|
}
|
|
});
|
|
|
|
if (
|
|
syncedPlayerNumber !== undefined &&
|
|
syncedPlayerNumber !== 1 &&
|
|
!this.networkId
|
|
) {
|
|
// If we are getting sync data for a specific player,
|
|
// and they are not the host, there is no sync data to send if
|
|
// the scene has no networkId (it's either not a multiplayer scene or the scene is not yet networked).
|
|
return null;
|
|
}
|
|
|
|
const networkSyncData: LayoutNetworkSyncData = {
|
|
var: variablesNetworkSyncData,
|
|
extVar: extensionsVariablesSyncData,
|
|
id: this.getOrCreateNetworkId(),
|
|
};
|
|
if (syncOptions.syncSceneVisualProps) {
|
|
networkSyncData.color = this._backgroundColor;
|
|
}
|
|
if (syncOptions.syncLayers) {
|
|
const layersSyncData = {};
|
|
for (const layerName in this._layers.items) {
|
|
layersSyncData[layerName] =
|
|
this._layers.items[layerName].getNetworkSyncData();
|
|
}
|
|
networkSyncData.layers = layersSyncData;
|
|
}
|
|
if (syncOptions.syncSceneTimers) {
|
|
networkSyncData.time = this._timeManager.getNetworkSyncData();
|
|
}
|
|
if (syncOptions.syncOnceTriggers) {
|
|
networkSyncData.once = this._onceTriggers.getNetworkSyncData();
|
|
}
|
|
|
|
gdjs.callbacksRuntimeSceneGetSyncData.forEach((callback) => {
|
|
callback(this, networkSyncData, syncOptions);
|
|
});
|
|
|
|
if (syncOptions.syncAsyncTasks) {
|
|
networkSyncData.async =
|
|
this._asyncTasksManager.getNetworkSyncData(syncOptions);
|
|
}
|
|
|
|
return networkSyncData;
|
|
}
|
|
|
|
updateFromNetworkSyncData(
|
|
syncData: LayoutNetworkSyncData,
|
|
options: UpdateFromNetworkSyncDataOptions
|
|
) {
|
|
if (syncData.color !== undefined) {
|
|
this._backgroundColor = syncData.color;
|
|
}
|
|
if (syncData.layers) {
|
|
for (const layerName in syncData.layers) {
|
|
const layerData = syncData.layers[layerName];
|
|
if (this.hasLayer(layerName)) {
|
|
const layer = this.getLayer(layerName);
|
|
layer.updateFromNetworkSyncData(layerData);
|
|
}
|
|
}
|
|
}
|
|
// Update variables before anything else, as they might be used
|
|
// in other sync data (for instance in tweens).
|
|
if (syncData.var) {
|
|
this._variables.updateFromNetworkSyncData(syncData.var, options);
|
|
}
|
|
if (syncData.extVar) {
|
|
for (const extensionName in syncData.extVar) {
|
|
if (!syncData.extVar.hasOwnProperty(extensionName)) {
|
|
continue;
|
|
}
|
|
const extensionVariablesData = syncData.extVar[extensionName];
|
|
const extensionVariables =
|
|
this._variablesByExtensionName.get(extensionName);
|
|
if (extensionVariables) {
|
|
extensionVariables.updateFromNetworkSyncData(
|
|
extensionVariablesData,
|
|
options
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (syncData.time) {
|
|
this._timeManager.updateFromNetworkSyncData(syncData.time);
|
|
}
|
|
if (syncData.once) {
|
|
this._onceTriggers.updateNetworkSyncData(syncData.once);
|
|
}
|
|
|
|
gdjs.callbacksRuntimeSceneUpdateFromSyncData.forEach((callback) => {
|
|
callback(this, syncData, options);
|
|
});
|
|
|
|
// Sync Async last, as it might depend on other data.
|
|
if (syncData.async && this._idToCallbackMap) {
|
|
this._asyncTasksManager.updateFromNetworkSyncData(
|
|
syncData.async,
|
|
this._idToCallbackMap,
|
|
this,
|
|
options
|
|
);
|
|
}
|
|
}
|
|
|
|
getOrCreateNetworkId(): string {
|
|
if (!this.networkId) {
|
|
const newNetworkId = gdjs.makeUuid().substring(0, 8);
|
|
this.networkId = newNetworkId;
|
|
}
|
|
return this.networkId;
|
|
}
|
|
}
|
|
|
|
//The flags to describe the change request by a scene:
|
|
export enum SceneChangeRequest {
|
|
CONTINUE,
|
|
PUSH_SCENE,
|
|
POP_SCENE,
|
|
REPLACE_SCENE,
|
|
CLEAR_SCENES,
|
|
STOP_GAME,
|
|
}
|
|
}
|