diff --git a/Extensions/PrimitiveDrawing/Extension.cpp b/Extensions/PrimitiveDrawing/Extension.cpp index 55164cd01e23..56b752994904 100644 --- a/Extensions/PrimitiveDrawing/Extension.cpp +++ b/Extensions/PrimitiveDrawing/Extension.cpp @@ -722,7 +722,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) { _("Center of rotation"), _("Change the center of rotation of an object relatively to the " "object origin."), - _("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"), + _("Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_"), _("Angle"), "res/actions/position24_black.png", "res/actions/position_black.png") diff --git a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts index 345ce1ff8c66..ef0b168b0866 100644 --- a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts +++ b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts @@ -474,7 +474,7 @@ namespace gdjs { /** * The center of rotation is defined relatively * to the drawing origin (the object position). - * This avoid the center to move on the drawing + * This avoids the center to move on the drawing * when new shapes push the bounds. * * When no custom center is defined, it will move diff --git a/GDJS/GDJS/IDE/ExporterHelper.cpp b/GDJS/GDJS/IDE/ExporterHelper.cpp index 3c913e400d6a..9242688512cc 100644 --- a/GDJS/GDJS/IDE/ExporterHelper.cpp +++ b/GDJS/GDJS/IDE/ExporterHelper.cpp @@ -589,8 +589,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers, InsertUnique(includesFiles, "runtimescene.js"); InsertUnique(includesFiles, "scenestack.js"); InsertUnique(includesFiles, "force.js"); + InsertUnique(includesFiles, "RuntimeLayer.js"); InsertUnique(includesFiles, "layer.js"); - InsertUnique(includesFiles, "RuntimeSceneLayer.js"); + InsertUnique(includesFiles, "RuntimeCustomObjectLayer.js"); InsertUnique(includesFiles, "timer.js"); InsertUnique(includesFiles, "runtimewatermark.js"); InsertUnique(includesFiles, "runtimegame.js"); diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index faba9bfb6ad6..b55c687986ff 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -27,11 +27,15 @@ namespace gdjs { _untransformedHitBoxes: gdjs.Polygon[] = []; /** The dimension of this object is calculated from its children AABBs. */ _unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] }; - _scaleX: number = 1; - _scaleY: number = 1; + _scaleX: float = 1; + _scaleY: float = 1; _flippedX: boolean = false; _flippedY: boolean = false; opacity: float = 255; + _customCenter: FloatPoint | null = null; + _localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation(); + _localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation(); + _isLocalTransformationDirty: boolean = true; /** * @param parent The container the object belongs to @@ -149,8 +153,9 @@ namespace gdjs { this._updateUntransformedHitBoxes(); } - //Update the current hitboxes with the frame custom hit boxes - //and apply transformations. + // Update the current hitboxes with the frame custom hit boxes + // and apply transformations. + const localTransformation = this.getLocalTransformation(); for (let i = 0; i < this._untransformedHitBoxes.length; ++i) { if (i >= this.hitBoxes.length) { this.hitBoxes.push(new gdjs.Polygon()); @@ -163,9 +168,8 @@ namespace gdjs { if (j >= this.hitBoxes[i].vertices.length) { this.hitBoxes[i].vertices.push([0, 0]); } - this.applyObjectTransformation( - this._untransformedHitBoxes[i].vertices[j][0], - this._untransformedHitBoxes[i].vertices[j][1], + localTransformation.transform( + this._untransformedHitBoxes[i].vertices[j], this.hitBoxes[i].vertices[j] ); } @@ -181,11 +185,6 @@ namespace gdjs { _updateUntransformedHitBoxes() { this._isUntransformedHitBoxesDirty = false; - const oldUnrotatedMinX = this._unrotatedAABB.min[0]; - const oldUnrotatedMinY = this._unrotatedAABB.min[1]; - const oldUnrotatedMaxX = this._unrotatedAABB.max[0]; - const oldUnrotatedMaxY = this._unrotatedAABB.max[1]; - this._untransformedHitBoxes.length = 0; if (this._instanceContainer.getAdhocListOfAllInstances().length === 0) { this._unrotatedAABB.min[0] = 0; @@ -218,20 +217,6 @@ namespace gdjs { } this.hitBoxes.length = this._untransformedHitBoxes.length; } - - // The default camera center depends on the object dimensions so checking - // the AABB center is not enough. - if ( - this._unrotatedAABB.min[0] !== oldUnrotatedMinX || - this._unrotatedAABB.min[1] !== oldUnrotatedMinY || - this._unrotatedAABB.max[0] !== oldUnrotatedMaxX || - this._unrotatedAABB.max[1] !== oldUnrotatedMaxY - ) { - this._instanceContainer.onObjectUnscaledCenterChanged( - (oldUnrotatedMinX + oldUnrotatedMaxX) / 2, - (oldUnrotatedMinY + oldUnrotatedMaxY) / 2 - ); - } } // Position: @@ -246,37 +231,52 @@ namespace gdjs { * @param result Array that will be updated with the result * (x and y position of the point in parent coordinates). */ - applyObjectTransformation(x: float, y: float, result: number[]) { - let cx = this.getCenterX(); - let cy = this.getCenterY(); + applyObjectTransformation(x: float, y: float, destination: FloatPoint) { + const source = destination; + source[0] = x; + source[1] = y; + this.getLocalTransformation().transform(source, destination); + } - // Flipping - if (this._flippedX) { - x = x + (cx - x) * 2; + /** + * Return the affine transformation that represents + * flipping, scale, rotation and translation of the object. + * @returns the affine transformation. + */ + getLocalTransformation(): gdjs.AffineTransformation { + if (this._isLocalTransformationDirty) { + this._updateLocalTransformation(); } - if (this._flippedY) { - y = y + (cy - y) * 2; + return this._localTransformation; + } + + getLocalInverseTransformation(): gdjs.AffineTransformation { + if (this._isLocalTransformationDirty) { + this._updateLocalTransformation(); } + return this._localInverseTransformation; + } - // Scale + _updateLocalTransformation() { const absScaleX = Math.abs(this._scaleX); const absScaleY = Math.abs(this._scaleY); - x *= absScaleX; - y *= absScaleY; - cx *= absScaleX; - cy *= absScaleY; - - // Rotation - const angleInRadians = (this.angle / 180) * Math.PI; - const cosValue = Math.cos(angleInRadians); - const sinValue = Math.sin(angleInRadians); - const xToCenterXDelta = x - cx; - const yToCenterYDelta = y - cy; - x = cx + cosValue * xToCenterXDelta - sinValue * yToCenterYDelta; - y = cy + sinValue * xToCenterXDelta + cosValue * yToCenterYDelta; - result.length = 2; - result[0] = x + this.x; - result[1] = y + this.y; + const centerX = this.getUnscaledCenterX() * absScaleX; + const centerY = this.getUnscaledCenterY() * absScaleY; + const angleInRadians = (this.angle * Math.PI) / 180; + + this._localTransformation.setToTranslation(this.x, this.y); + this._localTransformation.rotateAround(angleInRadians, centerX, centerY); + if (this._flippedX) { + this._localTransformation.flipX(centerX); + } + if (this._flippedY) { + this._localTransformation.flipY(centerY); + } + this._localTransformation.scale(absScaleX, absScaleY); + + this._localInverseTransformation.copyFrom(this._localTransformation); + this._localInverseTransformation.invert(); + this._isLocalTransformationDirty = false; } /** @@ -290,53 +290,51 @@ namespace gdjs { * @param result Array that will be updated with the result * (x and y position of the point in object coordinates). */ - applyObjectInverseTransformation(x: float, y: float, result: number[]) { - x -= this.getCenterXInScene(); - y -= this.getCenterYInScene(); - - const absScaleX = Math.abs(this._scaleX); - const absScaleY = Math.abs(this._scaleY); - - // Rotation - const angleInRadians = (this.angle / 180) * Math.PI; - const cosValue = Math.cos(-angleInRadians); - const sinValue = Math.sin(-angleInRadians); - const oldX = x; - x = cosValue * x - sinValue * y; - y = sinValue * oldX + cosValue * y; - - // Scale - x /= absScaleX; - y /= absScaleY; - - // Flipping - if (this._flippedX) { - x = -x; - } - if (this._flippedY) { - y = -y; - } - - const positionToCenterX = - this.getUnscaledWidth() / 2 + this._unrotatedAABB.min[0]; - const positionToCenterY = - this.getUnscaledHeight() / 2 + this._unrotatedAABB.min[1]; - result[0] = x + positionToCenterX; - result[1] = y + positionToCenterY; + applyObjectInverseTransformation( + x: float, + y: float, + destination: FloatPoint + ) { + const source = destination; + source[0] = x; + source[1] = y; + this.getLocalInverseTransformation().transform(source, destination); } getDrawableX(): float { if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } - return this.x + this._unrotatedAABB.min[0] * this._scaleX; + const absScaleX = this.getScaleX(); + if (!this._flippedX) { + return this.x + this._unrotatedAABB.min[0] * absScaleX; + } else { + return ( + this.x + + (-this._unrotatedAABB.min[0] - + this.getUnscaledWidth() + + 2 * this.getUnscaledCenterX()) * + absScaleX + ); + } } getDrawableY(): float { if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } - return this.y + this._unrotatedAABB.min[1] * this._scaleY; + const absScaleY = this.getScaleY(); + if (!this._flippedY) { + return this.y + this._unrotatedAABB.min[1] * absScaleY; + } else { + return ( + this.y + + (-this._unrotatedAABB.min[1] - + this.getUnscaledHeight() + + 2 * this.getUnscaledCenterY()) * + absScaleY + ); + } } /** @@ -363,6 +361,9 @@ namespace gdjs { * @returns the center X from the local origin (0;0). */ getUnscaledCenterX(): float { + if (this._customCenter) { + return this._customCenter[0]; + } if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } @@ -373,12 +374,57 @@ namespace gdjs { * @returns the center Y from the local origin (0;0). */ getUnscaledCenterY(): float { + if (this._customCenter) { + return this._customCenter[1]; + } if (this._isUntransformedHitBoxesDirty) { this._updateUntransformedHitBoxes(); } return (this._unrotatedAABB.min[1] + this._unrotatedAABB.max[1]) / 2; } + /** + * The center of rotation is defined relatively to the origin (the object + * position). + * This avoids the center to move when children push the bounds. + * + * When no custom center is defined, it will move + * to stay at the center of the children bounds. + * + * @param x coordinate of the custom center + * @param y coordinate of the custom center + */ + setRotationCenter(x: float, y: float) { + if (!this._customCenter) { + this._customCenter = [0, 0]; + } + this._customCenter[0] = x; + this._customCenter[1] = y; + + this._isLocalTransformationDirty = true; + this.invalidateHitboxes(); + } + + getCenterX(): float { + if (this._isUntransformedHitBoxesDirty) { + this._updateUntransformedHitBoxes(); + } + return ( + (this.getUnscaledCenterX() - this._unrotatedAABB.min[0]) * + this.getScaleX() + ); + } + + getCenterY(): float { + if (this._isUntransformedHitBoxesDirty) { + this._updateUntransformedHitBoxes(); + } + return ( + (this.getUnscaledCenterY() - this._unrotatedAABB.min[1]) * + this.getScaleY() + ); + } + getWidth(): float { return this.getUnscaledWidth() * this.getScaleX(); } @@ -417,6 +463,7 @@ namespace gdjs { return; } this.x = x; + this._isLocalTransformationDirty = true; this.invalidateHitboxes(); this.getRenderer().updateX(); } @@ -426,6 +473,7 @@ namespace gdjs { return; } this.y = y; + this._isLocalTransformationDirty = true; this.invalidateHitboxes(); this.getRenderer().updateY(); } @@ -435,6 +483,7 @@ namespace gdjs { return; } this.angle = angle; + this._isLocalTransformationDirty = true; this.invalidateHitboxes(); this.getRenderer().updateAngle(); } @@ -456,6 +505,7 @@ namespace gdjs { } this._scaleX = newScale * (this._flippedX ? -1 : 1); this._scaleY = newScale * (this._flippedY ? -1 : 1); + this._isLocalTransformationDirty = true; this.invalidateHitboxes(); this.getRenderer().update(); } @@ -473,6 +523,7 @@ namespace gdjs { return; } this._scaleX = newScale * (this._flippedX ? -1 : 1); + this._isLocalTransformationDirty = true; this.invalidateHitboxes(); this.getRenderer().update(); } diff --git a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts index 1d1d0b06b7f6..42e92ec9769d 100644 --- a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts +++ b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts @@ -43,6 +43,13 @@ namespace gdjs { this._debuggerRenderer = new gdjs.DebuggerRenderer(this); } + addLayer(layerData: LayerData) { + this._layers.put( + layerData.name, + new gdjs.RuntimeCustomObjectLayer(layerData, this) + ); + } + createObject(objectName: string): gdjs.RuntimeObject | null { const result = super.createObject(objectName); this._customObject.onChildrenLocationChanged(); @@ -293,21 +300,6 @@ namespace gdjs { this._customObject.onChildrenLocationChanged(); } - /** - * Triggered when the object dimensions are changed. - * - * It adapts the layers camera positions. - */ - onObjectUnscaledCenterChanged(oldOriginX: float, oldOriginY: float): void { - for (const name in this._layers.items) { - if (this._layers.items.hasOwnProperty(name)) { - /** @type gdjs.Layer */ - const theLayer: gdjs.Layer = this._layers.items[name]; - theLayer.onGameResolutionResized(oldOriginX, oldOriginY); - } - } - } - convertCoords(x: float, y: float, result: FloatPoint): FloatPoint { // The result parameter used to be optional. let position = result || [0, 0]; diff --git a/GDJS/Runtime/RuntimeCustomObjectLayer.ts b/GDJS/Runtime/RuntimeCustomObjectLayer.ts new file mode 100644 index 000000000000..f35c888e2346 --- /dev/null +++ b/GDJS/Runtime/RuntimeCustomObjectLayer.ts @@ -0,0 +1,104 @@ +/* + * 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 { + /** + * Represents a layer of a custom object. It doesn't allow to move any camera + * because it doesn't make sense inside an object. + */ + export class RuntimeCustomObjectLayer extends gdjs.RuntimeLayer { + /** + * @param layerData The data used to initialize the layer + * @param instanceContainer The container in which the layer is used + */ + constructor( + layerData: LayerData, + instanceContainer: gdjs.RuntimeInstanceContainer + ) { + super(layerData, instanceContainer); + } + + onGameResolutionResized( + oldGameResolutionOriginX: float, + oldGameResolutionOriginY: float + ): void {} + + getCameraX(cameraId?: integer): float { + return 0; + } + + getCameraY(cameraId?: integer): float { + return 0; + } + + setCameraX(x: float, cameraId?: integer): void {} + + setCameraY(y: float, cameraId?: integer): void {} + + getCameraWidth(cameraId?: integer): float { + return 0; + } + + getCameraHeight(cameraId?: integer): float { + return 0; + } + + setCameraZoom(newZoom: float, cameraId?: integer): void {} + + getCameraZoom(cameraId?: integer): float { + return 0; + } + + getCameraRotation(cameraId?: integer): float { + return 0; + } + + setCameraRotation(rotation: float, cameraId?: integer): void {} + + convertCoords( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + // TODO EBO use an AffineTransformation to avoid chained calls. + // The result parameter used to be optional. + return this._runtimeScene.convertCoords(x, y, result || [0, 0]); + } + + convertInverseCoords( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + // TODO EBO use an AffineTransformation to avoid chained calls. + // The result parameter used to be optional. + return this._runtimeScene.convertInverseCoords(x, y, result || [0, 0]); + } + + applyLayerInverseTransformation( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + result[0] = x; + result[1] = y; + return result; + } + + applyLayerTransformation( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint { + result[0] = x; + result[1] = y; + return result; + } + } +} diff --git a/GDJS/Runtime/RuntimeInstanceContainer.ts b/GDJS/Runtime/RuntimeInstanceContainer.ts index 69e6749d4213..ef0e4d0695f2 100644 --- a/GDJS/Runtime/RuntimeInstanceContainer.ts +++ b/GDJS/Runtime/RuntimeInstanceContainer.ts @@ -32,7 +32,7 @@ namespace gdjs { _objects: Hashtable; _objectsCtor: Hashtable; - _layers: Hashtable; + _layers: Hashtable; _layersCameraCoordinates: Record = {}; // Options for the debug draw: @@ -601,7 +601,7 @@ namespace gdjs { * @param name The name of the layer * @returns The layer, or the base layer if not found */ - getLayer(name: string): gdjs.Layer { + getLayer(name: string): gdjs.RuntimeLayer { if (this._layers.containsKey(name)) { return this._layers.get(name); } @@ -620,9 +620,7 @@ namespace gdjs { * Add a layer. * @param layerData The data to construct the layer */ - addLayer(layerData: LayerData) { - this._layers.put(layerData.name, new gdjs.Layer(layerData, this)); - } + abstract addLayer(layerData: LayerData); /** * Remove a layer. All {@link gdjs.RuntimeObject} on this layer will @@ -647,7 +645,7 @@ namespace gdjs { * @param index The new position in the list of layers */ setLayerIndex(layerName: string, index: integer): void { - const layer: gdjs.Layer = this._layers.get(layerName); + const layer: gdjs.RuntimeLayer = this._layers.get(layerName); if (!layer) { return; } diff --git a/GDJS/Runtime/RuntimeLayer.ts b/GDJS/Runtime/RuntimeLayer.ts new file mode 100644 index 000000000000..285fb0e486ef --- /dev/null +++ b/GDJS/Runtime/RuntimeLayer.ts @@ -0,0 +1,490 @@ +/* + * 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 { + /** + * Represents a layer of a container, used to display objects. + * + * Viewports and multiple cameras are not supported. + */ + export abstract class RuntimeLayer implements EffectsTarget { + _name: string; + _timeScale: float = 1; + _defaultZOrder: integer = 0; + _hidden: boolean; + _initialEffectsData: Array; + + _runtimeScene: gdjs.RuntimeInstanceContainer; + _effectsManager: gdjs.EffectsManager; + + // Lighting layer properties. + _isLightingLayer: boolean; + _followBaseLayerCamera: boolean; + _clearColor: Array; + + _rendererEffects: Record = {}; + _renderer: gdjs.LayerRenderer; + + /** + * @param layerData The data used to initialize the layer + * @param instanceContainer The container in which the layer is used + */ + constructor( + layerData: LayerData, + instanceContainer: gdjs.RuntimeInstanceContainer + ) { + this._name = layerData.name; + this._hidden = !layerData.visibility; + this._initialEffectsData = layerData.effects || []; + this._runtimeScene = instanceContainer; + this._effectsManager = instanceContainer.getGame().getEffectsManager(); + this._isLightingLayer = layerData.isLightingLayer; + this._followBaseLayerCamera = layerData.followBaseLayerCamera; + this._clearColor = [ + layerData.ambientLightColorR / 255, + layerData.ambientLightColorG / 255, + layerData.ambientLightColorB / 255, + 1.0, + ]; + this._renderer = new gdjs.LayerRenderer( + this, + instanceContainer.getRenderer(), + instanceContainer.getGame().getRenderer().getPIXIRenderer() + ); + this.show(!this._hidden); + for (let i = 0; i < layerData.effects.length; ++i) { + this.addEffect(layerData.effects[i]); + } + } + + getRenderer(): gdjs.LayerRenderer { + return this._renderer; + } + + /** + * Get the default Z order to be attributed to objects created on this layer + * (usually from events generated code). + */ + getDefaultZOrder(): float { + return this._defaultZOrder; + } + + /** + * Set the default Z order to be attributed to objects created on this layer. + * @param defaultZOrder The Z order to use when creating a new object from events. + */ + setDefaultZOrder(defaultZOrder: integer): void { + this._defaultZOrder = defaultZOrder; + } + + /** + * Called by the RuntimeScene whenever the game resolution size is changed. + * Updates the layer width/height and position. + */ + abstract onGameResolutionResized( + oldGameResolutionOriginX: float, + oldGameResolutionOriginY: float + ): void; + + /** + * Returns the scene the layer belongs to directly or indirectly + * @returns the scene the layer belongs to directly or indirectly + */ + getRuntimeScene(): gdjs.RuntimeScene { + return this._runtimeScene.getScene(); + } + + /** + * Called at each frame, after events are run and before rendering. + */ + updatePreRender(instanceContainer?: gdjs.RuntimeInstanceContainer): void { + if (this._followBaseLayerCamera) { + this.followBaseLayer(); + } + this._renderer.updatePreRender(); + this._effectsManager.updatePreRender(this._rendererEffects, this); + } + + /** + * Get the name of the layer + * @return The name of the layer + */ + getName(): string { + return this._name; + } + + /** + * Change the camera center X position. + * + * @param cameraId The camera number. Currently ignored. + * @return The x position of the camera + */ + abstract getCameraX(cameraId?: integer): float; + + /** + * Change the camera center Y position. + * + * @param cameraId The camera number. Currently ignored. + * @return The y position of the camera + */ + abstract getCameraY(cameraId?: integer): float; + + /** + * Set the camera center X position. + * + * @param x The new x position + * @param cameraId The camera number. Currently ignored. + */ + abstract setCameraX(x: float, cameraId?: integer): void; + + /** + * Set the camera center Y position. + * + * @param y The new y position + * @param cameraId The camera number. Currently ignored. + */ + abstract setCameraY(y: float, cameraId?: integer): void; + + /** + * Get the camera width (which can be different than the game resolution width + * if the camera is zoomed). + * + * @param cameraId The camera number. Currently ignored. + * @return The width of the camera + */ + abstract getCameraWidth(cameraId?: integer): float; + + /** + * Get the camera height (which can be different than the game resolution height + * if the camera is zoomed). + * + * @param cameraId The camera number. Currently ignored. + * @return The height of the camera + */ + abstract getCameraHeight(cameraId?: integer): float; + + /** + * Show (or hide) the layer. + * @param enable true to show the layer, false to hide it. + */ + show(enable: boolean): void { + this._hidden = !enable; + this._renderer.updateVisibility(enable); + } + + /** + * Check if the layer is visible. + * + * @return true if the layer is visible. + */ + isVisible(): boolean { + return !this._hidden; + } + + /** + * Set the zoom of a camera. + * + * @param newZoom The new zoom. Must be superior to 0. 1 is the default zoom. + * @param cameraId The camera number. Currently ignored. + */ + abstract setCameraZoom(newZoom: float, cameraId?: integer): void; + + /** + * Get the zoom of a camera. + * + * @param cameraId The camera number. Currently ignored. + * @return The zoom. + */ + abstract getCameraZoom(cameraId?: integer): float; + + /** + * Get the rotation of the camera, expressed in degrees. + * + * @param cameraId The camera number. Currently ignored. + * @return The rotation, in degrees. + */ + abstract getCameraRotation(cameraId?: integer): float; + + /** + * Set the rotation of the camera, expressed in degrees. + * The rotation is made around the camera center. + * + * @param rotation The new rotation, in degrees. + * @param cameraId The camera number. Currently ignored. + */ + abstract setCameraRotation(rotation: float, cameraId?: integer): void; + + /** + * Convert a point from the canvas coordinates (for example, + * the mouse position) to the container coordinates. + * + * @param x The x position, in canvas coordinates. + * @param y The y position, in canvas coordinates. + * @param cameraId The camera number. Currently ignored. + * @param result The point instance that is used to return the result. + */ + abstract convertCoords( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint; + + /** + * Return an array containing the coordinates of the point passed as parameter + * in parent coordinate coordinates (as opposed to the layer local coordinates). + * + * All transformations (scale, rotation) are supported. + * + * @param x The X position of the point, in layer coordinates. + * @param y The Y position of the point, in layer coordinates. + * @param result Array that will be updated with the result + * (x and y position of the point in parent coordinates). + */ + abstract applyLayerTransformation( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint; + + /** + * Convert a point from the container coordinates (for example, + * an object position) to the canvas coordinates. + * + * @param x The x position, in container coordinates. + * @param y The y position, in container coordinates. + * @param cameraId The camera number. Currently ignored. + * @param result The point instance that is used to return the result. + */ + abstract convertInverseCoords( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint; + + /** + * Return an array containing the coordinates of the point passed as parameter + * in layer local coordinates (as opposed to the parent coordinates). + * + * All transformations (scale, rotation) are supported. + * + * @param x The X position of the point, in parent coordinates. + * @param y The Y position of the point, in parent coordinates. + * @param result Array that will be updated with the result + * @param result The point instance that is used to return the result. + * (x and y position of the point in layer coordinates). + */ + abstract applyLayerInverseTransformation( + x: float, + y: float, + cameraId: integer, + result: FloatPoint + ): FloatPoint; + + getWidth(): float { + return this._runtimeScene.getViewportWidth(); + } + + getHeight(): float { + return this._runtimeScene.getViewportHeight(); + } + + /** + * Return the initial effects data for the layer. Only to + * be used by renderers. + * @deprecated + */ + getInitialEffectsData(): EffectData[] { + return this._initialEffectsData; + } + + /** + * Add a new effect, or replace the one with the same name. + * @param effectData The data of the effect to add. + */ + addEffect(effectData: EffectData): void { + this._effectsManager.addEffect( + effectData, + this._rendererEffects, + this._renderer.getRendererObject(), + this + ); + } + + /** + * Remove the effect with the specified name + * @param effectName The name of the effect. + */ + removeEffect(effectName: string): void { + this._effectsManager.removeEffect( + this._rendererEffects, + this._renderer.getRendererObject(), + effectName + ); + } + + /** + * Change an effect parameter value (for parameters that are numbers). + * @param name The name of the effect to update. + * @param parameterName The name of the parameter to update. + * @param value The new value (number). + */ + setEffectDoubleParameter( + name: string, + parameterName: string, + value: float + ): void { + this._effectsManager.setEffectDoubleParameter( + this._rendererEffects, + name, + parameterName, + value + ); + } + + /** + * Change an effect parameter value (for parameters that are strings). + * @param name The name of the effect to update. + * @param parameterName The name of the parameter to update. + * @param value The new value (string). + */ + setEffectStringParameter( + name: string, + parameterName: string, + value: string + ): void { + this._effectsManager.setEffectStringParameter( + this._rendererEffects, + name, + parameterName, + value + ); + } + + /** + * Change an effect parameter value (for parameters that are booleans). + * @param name The name of the effect to update. + * @param parameterName The name of the parameter to update. + * @param value The new value (boolean). + */ + setEffectBooleanParameter( + name: string, + parameterName: string, + value: boolean + ): void { + this._effectsManager.setEffectBooleanParameter( + this._rendererEffects, + name, + parameterName, + value + ); + } + + /** + * Enable or disable an effect. + * @param name The name of the effect to enable or disable. + * @param enable true to enable, false to disable + */ + enableEffect(name: string, enable: boolean): void { + this._effectsManager.enableEffect(this._rendererEffects, name, enable); + } + + /** + * Check if an effect is enabled + * @param name The name of the effect + * @return true if the effect is enabled, false otherwise. + */ + isEffectEnabled(name: string): boolean { + return this._effectsManager.isEffectEnabled(this._rendererEffects, name); + } + + /** + * Check if an effect exists on this layer + * @param name The name of the effect + * @return true if the effect exists, false otherwise. + */ + hasEffect(name: string): boolean { + return this._effectsManager.hasEffect(this._rendererEffects, name); + } + + /** + * Set the time scale for the objects on the layer: + * time will be slower if time scale is < 1, faster if > 1. + * @param timeScale The new time scale (must be positive). + */ + setTimeScale(timeScale: float): void { + if (timeScale >= 0) { + this._timeScale = timeScale; + } + } + + /** + * Get the time scale for the objects on the layer. + */ + getTimeScale(): float { + return this._timeScale; + } + + /** + * Return the time elapsed since the last frame, + * in milliseconds, for objects on the layer. + * + * @param instanceContainer The instance container the layer belongs to (deprecated - can be omitted). + */ + getElapsedTime(instanceContainer?: gdjs.RuntimeInstanceContainer): float { + const container = instanceContainer || this._runtimeScene; + return container.getElapsedTime() * this._timeScale; + } + + /** + * Change the position, rotation and scale (zoom) of the layer camera to be the same as the base layer camera. + */ + followBaseLayer(): void { + const baseLayer = this._runtimeScene.getLayer(''); + this.setCameraX(baseLayer.getCameraX()); + this.setCameraY(baseLayer.getCameraY()); + this.setCameraRotation(baseLayer.getCameraRotation()); + this.setCameraZoom(baseLayer.getCameraZoom()); + } + + /** + * The clear color is defined in the format [r, g, b], with components in the range of 0 to 1. + * @return the clear color of layer in the range of [0, 1]. + */ + getClearColor(): Array { + return this._clearColor; + } + + /** + * Set the clear color in format [r, g, b], with components in the range of 0 to 1.; + * @param r Red color component in the range 0-255. + * @param g Green color component in the range 0-255. + * @param b Blue color component in the range 0-255. + */ + setClearColor(r: integer, g: integer, b: integer): void { + this._clearColor[0] = r / 255; + this._clearColor[1] = g / 255; + this._clearColor[2] = b / 255; + this._renderer.updateClearColor(); + } + + /** + * Set whether layer's camera follows base layer's camera or not. + */ + setFollowBaseLayerCamera(follow: boolean): void { + this._followBaseLayerCamera = follow; + } + + /** + * Return true if the layer is a lighting layer, false otherwise. + * @return true if it is a lighting layer, false otherwise. + */ + isLightingLayer(): boolean { + return this._isLightingLayer; + } + } +} diff --git a/GDJS/Runtime/RuntimeSceneLayer.ts b/GDJS/Runtime/RuntimeSceneLayer.ts deleted file mode 100644 index 9aa514065ca7..000000000000 --- a/GDJS/Runtime/RuntimeSceneLayer.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { - /** - * Represents a layer of a scene, used to display objects. - * - * Viewports and multiple cameras are not supported. - * - * It does some optimizations but works exactly the same as - * {@link gdjs.Layer}. - */ - export class RuntimeSceneLayer extends gdjs.Layer { - /** - * @param layerData The data used to initialize the layer - * @param scene The scene in which the layer is used - */ - constructor(layerData: LayerData, scene: gdjs.RuntimeScene) { - super(layerData, scene); - } - - convertCoords( - x: float, - y: float, - cameraId: integer = 0, - result: FloatPoint - ): FloatPoint { - // The result parameter used to be optional. - let position = result || [0, 0]; - x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2; - y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2; - x /= Math.abs(this._zoomFactor); - y /= Math.abs(this._zoomFactor); - - // Only compute angle and cos/sin once (allow heavy optimization from JS engines). - const angleInRadians = (this._cameraRotation / 180) * Math.PI; - const tmp = x; - const cosValue = Math.cos(angleInRadians); - const sinValue = Math.sin(angleInRadians); - x = cosValue * x - sinValue * y; - y = sinValue * tmp + cosValue * y; - position[0] = x + this.getCameraX(cameraId); - position[1] = y + this.getCameraY(cameraId); - return position; - } - - convertInverseCoords( - x: float, - y: float, - cameraId: integer = 0, - result: FloatPoint - ): FloatPoint { - // The result parameter used to be optional. - let position = result || [0, 0]; - x -= this.getCameraX(cameraId); - y -= this.getCameraY(cameraId); - - // Only compute angle and cos/sin once (allow heavy optimization from JS engines). - const angleInRadians = (this._cameraRotation / 180) * Math.PI; - const tmp = x; - const cosValue = Math.cos(-angleInRadians); - const sinValue = Math.sin(-angleInRadians); - x = cosValue * x - sinValue * y; - y = sinValue * tmp + cosValue * y; - x *= Math.abs(this._zoomFactor); - y *= Math.abs(this._zoomFactor); - position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2; - position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2; - return position; - } - } -} diff --git a/GDJS/Runtime/debugger-client/hot-reloader.ts b/GDJS/Runtime/debugger-client/hot-reloader.ts index e3305dd6b59e..ef5025fcc373 100644 --- a/GDJS/Runtime/debugger-client/hot-reloader.ts +++ b/GDJS/Runtime/debugger-client/hot-reloader.ts @@ -1037,7 +1037,7 @@ namespace gdjs { _hotReloadRuntimeLayer( oldLayer: LayerData, newLayer: LayerData, - runtimeLayer: gdjs.Layer + runtimeLayer: gdjs.RuntimeLayer ): void { // Properties if (oldLayer.visibility !== newLayer.visibility) { @@ -1073,7 +1073,7 @@ namespace gdjs { _hotReloadRuntimeLayerEffects( oldEffectsData: EffectData[], newEffectsData: EffectData[], - runtimeLayer: gdjs.Layer + runtimeLayer: gdjs.RuntimeLayer ): void { oldEffectsData.forEach((oldEffectData) => { const name = oldEffectData.name; @@ -1115,7 +1115,7 @@ namespace gdjs { _hotReloadRuntimeLayerEffect( oldEffectData: EffectData, newEffectData: EffectData, - runtimeLayer: gdjs.Layer, + runtimeLayer: gdjs.RuntimeLayer, effectName: string ): void { // We consider oldEffectData.effectType and newEffectData.effectType diff --git a/GDJS/Runtime/layer.ts b/GDJS/Runtime/layer.ts index c5584be7efce..b9f076b0323f 100644 --- a/GDJS/Runtime/layer.ts +++ b/GDJS/Runtime/layer.ts @@ -5,32 +5,16 @@ */ namespace gdjs { /** - * Represents a layer of a container, used to display objects. + * Represents a layer of a scene, used to display objects. * * Viewports and multiple cameras are not supported. */ - export class Layer implements EffectsTarget { - _name: string; + export class Layer extends gdjs.RuntimeLayer { _cameraRotation: float = 0; _zoomFactor: float = 1; - _timeScale: float = 1; - _defaultZOrder: integer = 0; - _hidden: boolean; - _initialEffectsData: Array; _cameraX: float; _cameraY: float; - _runtimeScene: gdjs.RuntimeInstanceContainer; - _effectsManager: gdjs.EffectsManager; - - // Lighting layer properties. - _isLightingLayer: boolean; - _followBaseLayerCamera: boolean; - _clearColor: Array; - - _rendererEffects: Record = {}; - _renderer: gdjs.LayerRenderer; - /** * @param layerData The data used to initialize the layer * @param instanceContainer The container in which the layer is used @@ -39,50 +23,10 @@ namespace gdjs { layerData: LayerData, instanceContainer: gdjs.RuntimeInstanceContainer ) { - this._name = layerData.name; - this._hidden = !layerData.visibility; - this._initialEffectsData = layerData.effects || []; - this._runtimeScene = instanceContainer; + super(layerData, instanceContainer); + this._cameraX = this.getWidth() / 2; this._cameraY = this.getHeight() / 2; - this._effectsManager = instanceContainer.getGame().getEffectsManager(); - this._isLightingLayer = layerData.isLightingLayer; - this._followBaseLayerCamera = layerData.followBaseLayerCamera; - this._clearColor = [ - layerData.ambientLightColorR / 255, - layerData.ambientLightColorG / 255, - layerData.ambientLightColorB / 255, - 1.0, - ]; - this._renderer = new gdjs.LayerRenderer( - this, - instanceContainer.getRenderer(), - instanceContainer.getGame().getRenderer().getPIXIRenderer() - ); - this.show(!this._hidden); - for (let i = 0; i < layerData.effects.length; ++i) { - this.addEffect(layerData.effects[i]); - } - } - - getRenderer(): gdjs.LayerRenderer { - return this._renderer; - } - - /** - * Get the default Z order to be attributed to objects created on this layer - * (usually from events generated code). - */ - getDefaultZOrder(): float { - return this._defaultZOrder; - } - - /** - * Set the default Z order to be attributed to objects created on this layer. - * @param defaultZOrder The Z order to use when creating a new object from events. - */ - setDefaultZOrder(defaultZOrder: integer): void { - this._defaultZOrder = defaultZOrder; } /** @@ -107,33 +51,6 @@ namespace gdjs { this._renderer.updatePosition(); } - /** - * Returns the scene the layer belongs to directly or indirectly - * @returns the scene the layer belongs to directly or indirectly - */ - getRuntimeScene(): gdjs.RuntimeScene { - return this._runtimeScene.getScene(); - } - - /** - * Called at each frame, after events are run and before rendering. - */ - updatePreRender(instanceContainer?: gdjs.RuntimeInstanceContainer): void { - if (this._followBaseLayerCamera) { - this.followBaseLayer(); - } - this._renderer.updatePreRender(); - this._effectsManager.updatePreRender(this._rendererEffects, this); - } - - /** - * Get the name of the layer - * @return The name of the layer - */ - getName(): string { - return this._name; - } - /** * Change the camera center X position. * @@ -202,24 +119,6 @@ namespace gdjs { return this.getHeight() / this._zoomFactor; } - /** - * Show (or hide) the layer. - * @param enable true to show the layer, false to hide it. - */ - show(enable: boolean): void { - this._hidden = !enable; - this._renderer.updateVisibility(enable); - } - - /** - * Check if the layer is visible. - * - * @return true if the layer is visible. - */ - isVisible(): boolean { - return !this._hidden; - } - /** * Set the zoom of a camera. * @@ -278,16 +177,25 @@ namespace gdjs { cameraId: integer = 0, result: FloatPoint ): FloatPoint { + // This code duplicates applyLayerInverseTransformation for performance reasons; + // The result parameter used to be optional. let position = result || [0, 0]; - // TODO EBO use an AffineTransformation to avoid chained calls. - position = this._runtimeScene.convertCoords(x, y, position); - return this.applyLayerInverseTransformation( - position[0], - position[1], - cameraId, - position - ); + x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2; + y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2; + x /= Math.abs(this._zoomFactor); + y /= Math.abs(this._zoomFactor); + + // Only compute angle and cos/sin once (allow heavy optimization from JS engines). + const angleInRadians = (this._cameraRotation / 180) * Math.PI; + const tmp = x; + const cosValue = Math.cos(angleInRadians); + const sinValue = Math.sin(angleInRadians); + x = cosValue * x - sinValue * y; + y = sinValue * tmp + cosValue * y; + position[0] = x + this.getCameraX(cameraId); + position[1] = y + this.getCameraY(cameraId); + return position; } /** @@ -341,14 +249,25 @@ namespace gdjs { cameraId: integer = 0, result: FloatPoint ): FloatPoint { + // This code duplicates applyLayerTransformation for performance reasons; + + // The result parameter used to be optional. let position = result || [0, 0]; - // TODO EBO use an AffineTransformation to avoid chained calls. - this.applyLayerTransformation(x, y, cameraId, position); - return this._runtimeScene.convertInverseCoords( - position[0], - position[1], - position - ); + x -= this.getCameraX(cameraId); + y -= this.getCameraY(cameraId); + + // Only compute angle and cos/sin once (allow heavy optimization from JS engines). + const angleInRadians = (this._cameraRotation / 180) * Math.PI; + const tmp = x; + const cosValue = Math.cos(-angleInRadians); + const sinValue = Math.sin(-angleInRadians); + x = cosValue * x - sinValue * y; + y = sinValue * tmp + cosValue * y; + x *= Math.abs(this._zoomFactor); + y *= Math.abs(this._zoomFactor); + position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2; + position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2; + return position; } /** @@ -388,14 +307,6 @@ namespace gdjs { return result; } - getWidth(): float { - return this._runtimeScene.getViewportWidth(); - } - - getHeight(): float { - return this._runtimeScene.getViewportHeight(); - } - /** * This ensure that the viewport dimensions are up to date. * @@ -406,199 +317,5 @@ namespace gdjs { // This will update dimensions. this._runtimeScene.getViewportWidth(); } - - /** - * Return the initial effects data for the layer. Only to - * be used by renderers. - * @deprecated - */ - getInitialEffectsData(): EffectData[] { - return this._initialEffectsData; - } - - /** - * Add a new effect, or replace the one with the same name. - * @param effectData The data of the effect to add. - */ - addEffect(effectData: EffectData): void { - this._effectsManager.addEffect( - effectData, - this._rendererEffects, - this._renderer.getRendererObject(), - this - ); - } - - /** - * Remove the effect with the specified name - * @param effectName The name of the effect. - */ - removeEffect(effectName: string): void { - this._effectsManager.removeEffect( - this._rendererEffects, - this._renderer.getRendererObject(), - effectName - ); - } - - /** - * Change an effect parameter value (for parameters that are numbers). - * @param name The name of the effect to update. - * @param parameterName The name of the parameter to update. - * @param value The new value (number). - */ - setEffectDoubleParameter( - name: string, - parameterName: string, - value: float - ): void { - this._effectsManager.setEffectDoubleParameter( - this._rendererEffects, - name, - parameterName, - value - ); - } - - /** - * Change an effect parameter value (for parameters that are strings). - * @param name The name of the effect to update. - * @param parameterName The name of the parameter to update. - * @param value The new value (string). - */ - setEffectStringParameter( - name: string, - parameterName: string, - value: string - ): void { - this._effectsManager.setEffectStringParameter( - this._rendererEffects, - name, - parameterName, - value - ); - } - - /** - * Change an effect parameter value (for parameters that are booleans). - * @param name The name of the effect to update. - * @param parameterName The name of the parameter to update. - * @param value The new value (boolean). - */ - setEffectBooleanParameter( - name: string, - parameterName: string, - value: boolean - ): void { - this._effectsManager.setEffectBooleanParameter( - this._rendererEffects, - name, - parameterName, - value - ); - } - - /** - * Enable or disable an effect. - * @param name The name of the effect to enable or disable. - * @param enable true to enable, false to disable - */ - enableEffect(name: string, enable: boolean): void { - this._effectsManager.enableEffect(this._rendererEffects, name, enable); - } - - /** - * Check if an effect is enabled - * @param name The name of the effect - * @return true if the effect is enabled, false otherwise. - */ - isEffectEnabled(name: string): boolean { - return this._effectsManager.isEffectEnabled(this._rendererEffects, name); - } - - /** - * Check if an effect exists on this layer - * @param name The name of the effect - * @return true if the effect exists, false otherwise. - */ - hasEffect(name: string): boolean { - return this._effectsManager.hasEffect(this._rendererEffects, name); - } - - /** - * Set the time scale for the objects on the layer: - * time will be slower if time scale is < 1, faster if > 1. - * @param timeScale The new time scale (must be positive). - */ - setTimeScale(timeScale: float): void { - if (timeScale >= 0) { - this._timeScale = timeScale; - } - } - - /** - * Get the time scale for the objects on the layer. - */ - getTimeScale(): float { - return this._timeScale; - } - - /** - * Return the time elapsed since the last frame, - * in milliseconds, for objects on the layer. - * - * @param instanceContainer The instance container the layer belongs to (deprecated - can be omitted). - */ - getElapsedTime(instanceContainer?: gdjs.RuntimeInstanceContainer): float { - const container = instanceContainer || this._runtimeScene; - return container.getElapsedTime() * this._timeScale; - } - - /** - * Change the position, rotation and scale (zoom) of the layer camera to be the same as the base layer camera. - */ - followBaseLayer(): void { - const baseLayer = this._runtimeScene.getLayer(''); - this.setCameraX(baseLayer.getCameraX()); - this.setCameraY(baseLayer.getCameraY()); - this.setCameraRotation(baseLayer.getCameraRotation()); - this.setCameraZoom(baseLayer.getCameraZoom()); - } - - /** - * The clear color is defined in the format [r, g, b], with components in the range of 0 to 1. - * @return the clear color of layer in the range of [0, 1]. - */ - getClearColor(): Array { - return this._clearColor; - } - - /** - * Set the clear color in format [r, g, b], with components in the range of 0 to 1.; - * @param r Red color component in the range 0-255. - * @param g Green color component in the range 0-255. - * @param b Blue color component in the range 0-255. - */ - setClearColor(r: integer, g: integer, b: integer): void { - this._clearColor[0] = r / 255; - this._clearColor[1] = g / 255; - this._clearColor[2] = b / 255; - this._renderer.updateClearColor(); - } - - /** - * Set whether layer's camera follows base layer's camera or not. - */ - setFollowBaseLayerCamera(follow: boolean): void { - this._followBaseLayerCamera = follow; - } - - /** - * Return true if the layer is a lighting layer, false otherwise. - * @return true if it is a lighting layer, false otherwise. - */ - isLightingLayer(): boolean { - return this._isLightingLayer; - } } } diff --git a/GDJS/Runtime/pixi-renderers/CustomObjectPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/CustomObjectPixiRenderer.ts index bec1e2fdea2f..436c5cf3628c 100644 --- a/GDJS/Runtime/pixi-renderers/CustomObjectPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/CustomObjectPixiRenderer.ts @@ -67,11 +67,12 @@ namespace gdjs { this._pixiContainer.pivot.x = this._object.getUnscaledCenterX(); this._pixiContainer.pivot.y = this._object.getUnscaledCenterY(); this._pixiContainer.position.x = - this._object.getDrawableX() + + this._object.getX() + this._pixiContainer.pivot.x * Math.abs(this._object._scaleX); this._pixiContainer.position.y = - this._object.getDrawableY() + + this._object.getY() + this._pixiContainer.pivot.y * Math.abs(this._object._scaleY); + this._pixiContainer.rotation = gdjs.toRad(this._object.angle); this._pixiContainer.scale.x = this._object._scaleX; this._pixiContainer.scale.y = this._object._scaleY; @@ -126,7 +127,7 @@ namespace gdjs { return null; } - setLayerIndex(layer: gdjs.Layer, index: float): void { + setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void { const layerPixiRenderer: gdjs.LayerPixiRenderer = layer.getRenderer(); let layerPixiObject: | PIXI.Container diff --git a/GDJS/Runtime/pixi-renderers/RuntimeInstanceContainerPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/RuntimeInstanceContainerPixiRenderer.ts index 6b8cd25fac70..62845a336157 100644 --- a/GDJS/Runtime/pixi-renderers/RuntimeInstanceContainerPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/RuntimeInstanceContainerPixiRenderer.ts @@ -20,7 +20,7 @@ namespace gdjs { * * @see gdjs.RuntimeInstanceContainer.setLayerIndex */ - setLayerIndex(layer: gdjs.Layer, index: integer): void; + setLayerIndex(layer: gdjs.RuntimeLayer, index: integer): void; getRendererObject(): PIXI.Container; } diff --git a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts index 532c0d6ed5c9..bbfd3b3914ec 100644 --- a/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/layer-pixi-renderer.ts @@ -13,7 +13,7 @@ namespace gdjs { export class LayerPixiRenderer { _pixiContainer: PIXI.Container; - _layer: gdjs.Layer; + _layer: gdjs.RuntimeLayer; _renderTexture: PIXI.RenderTexture | null = null; _lightingSprite: PIXI.Sprite | null = null; _runtimeSceneRenderer: gdjs.RuntimeInstanceContainerRenderer; @@ -35,7 +35,7 @@ namespace gdjs { * @param runtimeInstanceContainerRenderer The scene renderer */ constructor( - layer: gdjs.Layer, + layer: gdjs.RuntimeLayer, runtimeInstanceContainerRenderer: gdjs.RuntimeInstanceContainerRenderer, pixiRenderer: PIXI.Renderer | null ) { diff --git a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts index 2996f97f2130..6e7d82ffc05f 100644 --- a/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimescene-pixi-renderer.ts @@ -104,7 +104,7 @@ namespace gdjs { return this._pixiRenderer; } - setLayerIndex(layer: gdjs.Layer, index: float): void { + setLayerIndex(layer: gdjs.RuntimeLayer, index: float): void { const layerPixiRenderer: gdjs.LayerPixiRenderer = layer.getRenderer(); let layerPixiObject: | PIXI.Container diff --git a/GDJS/Runtime/runtimescene.ts b/GDJS/Runtime/runtimescene.ts index 653a8498b00c..545446337f2f 100644 --- a/GDJS/Runtime/runtimescene.ts +++ b/GDJS/Runtime/runtimescene.ts @@ -71,6 +71,10 @@ namespace gdjs { this.onGameResolutionResized(); } + addLayer(layerData: LayerData) { + this._layers.put(layerData.name, new gdjs.Layer(layerData, this)); + } + /** * Should be called when the canvas where the scene is rendered has been resized. * See gdjs.RuntimeGame.startGameLoop in particular. @@ -86,8 +90,7 @@ namespace gdjs { : 0; for (const name in this._layers.items) { if (this._layers.items.hasOwnProperty(name)) { - /** @type gdjs.Layer */ - const theLayer: gdjs.Layer = this._layers.items[name]; + const theLayer: gdjs.RuntimeLayer = this._layers.items[name]; theLayer.onGameResolutionResized( oldGameResolutionOriginX, oldGameResolutionOriginY @@ -193,13 +196,6 @@ namespace gdjs { return behaviorSharedData; } - addLayer(layerData: LayerData) { - this._layers.put( - layerData.name, - new gdjs.RuntimeSceneLayer(layerData, this) - ); - } - /** * Called when a scene is "paused", i.e it will be not be rendered again * for some time, until it's resumed or unloaded. diff --git a/GDJS/tests/benchmarks/layer.js b/GDJS/tests/benchmarks/layer.js index 901ba831244c..83cba55b2d6a 100644 --- a/GDJS/tests/benchmarks/layer.js +++ b/GDJS/tests/benchmarks/layer.js @@ -6,7 +6,7 @@ describe('gdjs.Layer', function() { it('benchmark convertCoords and convertInverseCoords', function() { this.timeout(30000); - var layer = new gdjs.RuntimeSceneLayer( + var layer = new gdjs.Layer( { name: 'My layer', visibility: true, effects: [], diff --git a/GDJS/tests/karma.conf.js b/GDJS/tests/karma.conf.js index d1109f32c298..9bf1bc8dd919 100644 --- a/GDJS/tests/karma.conf.js +++ b/GDJS/tests/karma.conf.js @@ -55,8 +55,9 @@ module.exports = function (config) { './newIDE/app/resources/GDJS/Runtime/scenestack.js', './newIDE/app/resources/GDJS/Runtime/profiler.js', './newIDE/app/resources/GDJS/Runtime/force.js', + './newIDE/app/resources/GDJS/Runtime/RuntimeLayer.js', './newIDE/app/resources/GDJS/Runtime/layer.js', - './newIDE/app/resources/GDJS/Runtime/RuntimeSceneLayer.js', + './newIDE/app/resources/GDJS/Runtime/RuntimeCustomObjectLayer.js', './newIDE/app/resources/GDJS/Runtime/timer.js', './newIDE/app/resources/GDJS/Runtime/inputmanager.js', './newIDE/app/resources/GDJS/Runtime/runtimegame.js', diff --git a/GDJS/tests/tests/CustomRuntimeObject.js b/GDJS/tests/tests/CustomRuntimeObject.js index ff3af65eb3b5..3e5de044b4b4 100644 --- a/GDJS/tests/tests/CustomRuntimeObject.js +++ b/GDJS/tests/tests/CustomRuntimeObject.js @@ -198,8 +198,8 @@ describe('gdjs.CustomRuntimeObject', function () { expect(customObject.getHitBoxes().length).to.be(2); expect(customObject.getHitBoxes()[0].vertices).to.eql([ [32, 32], - [31.999999999999993, -31.999999999999996], - [96, 31.999999999999996], + [32, -32], + [96, 32], ]); expect(customObject.getHitBoxes()[1].vertices).to.eql([ [32, 96], @@ -245,6 +245,12 @@ describe('gdjs.CustomRuntimeObject', function () { customObject.setWidth(32); customObject.setHeight(96); + // To draw the transformed shapes: + // - draw 2 squares side-by-side + // - scale them and keep the top-left corner in place + // - rotate the shape keeping the center of the scaled drawing in place + // - translate it according to the object new position. + expect(customObject.getWidth()).to.be(32); expect(customObject.getHeight()).to.be(96); @@ -253,14 +259,14 @@ describe('gdjs.CustomRuntimeObject', function () { expect(customObject.getHitBoxes().length).to.be(2); expect(customObject.getHitBoxes()[0].vertices).to.eql([ - [-12, 100], - [-12, 84], - [84, 100], + [-24, 64], + [-24, 48], + [72, 64], ]); expect(customObject.getHitBoxes()[1].vertices).to.eql([ - [-12, 116], - [-12, 100], - [84, 116], + [-24, 80], + [-24, 64], + [72, 80], ]); }); diff --git a/GDJS/tests/tests/layer.js b/GDJS/tests/tests/layer.js index 434f2304fd3a..ccacebdadf8e 100644 --- a/GDJS/tests/tests/layer.js +++ b/GDJS/tests/tests/layer.js @@ -9,7 +9,7 @@ describe('gdjs.Layer', function() { var runtimeScene = new gdjs.RuntimeScene(runtimeGame); it('can convert coordinates', function(){ - var layer = new gdjs.RuntimeSceneLayer({name: 'My layer', visibility: true, effects:[]}, runtimeScene) + var layer = new gdjs.Layer({name: 'My layer', visibility: true, effects:[]}, runtimeScene) layer.setCameraX(100, 0); layer.setCameraY(200, 0); layer.setCameraRotation(90, 0); @@ -18,7 +18,7 @@ describe('gdjs.Layer', function() { expect(layer.convertCoords(350, 450, 0)[1]).to.be.within(149.9999, 150.001); }); it('can convert inverse coordinates', function(){ - var layer = new gdjs.RuntimeSceneLayer({name: 'My layer', visibility: true, effects:[]}, runtimeScene) + var layer = new gdjs.Layer({name: 'My layer', visibility: true, effects:[]}, runtimeScene) layer.setCameraX(100, 0); layer.setCameraY(200, 0); layer.setCameraRotation(90, 0); diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/MetadataDeclarationHelpers.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/MetadataDeclarationHelpers.js index 3566fef5c3d2..6afe3684963d 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/MetadataDeclarationHelpers.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/MetadataDeclarationHelpers.js @@ -1171,6 +1171,42 @@ export const declareObjectPropertiesInstructionAndExpressions = ( }); }; +/** + * Declare the instructions (actions/conditions) and expressions for the + * properties of the given events based object. + * This is akin to what would happen by manually declaring a JS extension + * (see `JsExtension.js` files of extensions). + */ +export const declareObjectInternalInstructions = ( + i18n: I18nType, + extension: gdPlatformExtension, + objectMetadata: gdObjectMetadata, + eventsBasedObject: gdEventsBasedObject +): void => { + // TODO EBO Use full type to identify object to avoid collision. + // Objects are identified by their name alone. + const objectType = eventsBasedObject.getName(); + + objectMetadata + .addScopedAction( + 'SetRotationCenter', + i18n._('Center of rotation'), + i18n._( + 'Change the center of rotation of an object relatively to the object origin.' + ), + i18n._('Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_'), + i18n._('Angle'), + 'res/actions/position24_black.png', + 'res/actions/position_black.png' + ) + .addParameter('object', i18n._('Object'), objectType) + .addParameter('number', i18n._('X position')) + .addParameter('number', i18n._('Y position')) + .markAsAdvanced() + .setPrivate() + .setFunctionName('setRotationCenter'); +}; + /** * Add to the instruction (action/condition) or expression the parameters * expected by the events function. diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js index f98fbe49793a..148800838d37 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/index.js @@ -15,6 +15,7 @@ import { isExtensionLifecycleEventsFunction, declareBehaviorPropertiesInstructionAndExpressions, declareObjectPropertiesInstructionAndExpressions, + declareObjectInternalInstructions, } from './MetadataDeclarationHelpers'; const gd: libGDevelop = global.gd; @@ -524,6 +525,12 @@ function generateObject( objectMetadata, eventsBasedObject ); + declareObjectInternalInstructions( + options.i18n, + extension, + objectMetadata, + eventsBasedObject + ); // Declare all the object functions mapFor(0, eventsFunctionsContainer.getEventsFunctionsCount(), i => { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js index 0fd2f59ce51c..5c8cf86be45b 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js @@ -186,12 +186,12 @@ export default class RenderedCustomObjectInstance extends RenderedInstance update() { applyChildLayouts(this); - const defaultWidth = this.getDefaultWidth(); - const defaultHeight = this.getDefaultHeight(); - const originX = this._proportionalOriginX * defaultWidth; - const originY = this._proportionalOriginY * defaultHeight; - const centerX = defaultWidth / 2; - const centerY = defaultHeight / 2; + const width = this.getWidth(); + const height = this.getHeight(); + const originX = this._proportionalOriginX * width; + const originY = this._proportionalOriginY * height; + const centerX = width / 2; + const centerY = height / 2; this._pixiObject.pivot.x = centerX; this._pixiObject.pivot.y = centerY; @@ -200,12 +200,8 @@ export default class RenderedCustomObjectInstance extends RenderedInstance ); this._pixiObject.scale.x = 1; this._pixiObject.scale.y = 1; - this._pixiObject.position.x = - this._instance.getX() + - (centerX - originX) * Math.abs(this._pixiObject.scale.x); - this._pixiObject.position.y = - this._instance.getY() + - (centerY - originY) * Math.abs(this._pixiObject.scale.y); + this._pixiObject.position.x = this._instance.getX() + centerX - originX; + this._pixiObject.position.y = this._instance.getY() + centerY - originY; } getWidth() { @@ -233,18 +229,10 @@ export default class RenderedCustomObjectInstance extends RenderedInstance } getOriginX(): number { - return ( - this._proportionalOriginX * - this.getDefaultWidth() * - this._pixiObject.scale.x - ); + return this._proportionalOriginX * this.getWidth(); } getOriginY(): number { - return ( - this._proportionalOriginY * - this.getDefaultHeight() * - this._pixiObject.scale.y - ); + return this._proportionalOriginY * this.getHeight(); } }