Files
GDevelop/GDJS/Runtime/CustomRuntimeObject.ts
T
D8H 07ab4c938b Fix event-based objects properties that were changing for all instances at once (#4717)
* It was only affecting properties defined in the event-based objects not the one from child-object configurations.
2022-12-20 14:20:19 +01:00

589 lines
17 KiB
TypeScript

/*
* GDevelop JS Platform
* Copyright 2013-2022 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
export type ObjectConfiguration = {
content: any;
};
export type CustomObjectConfiguration = ObjectConfiguration & {
childrenContent: { [objectName: string]: ObjectConfiguration & any };
};
/**
* An object that contains other object.
*
* This is the base class for objects generated from EventsBasedObject.
*
* @see gdjs.CustomRuntimeObjectInstanceContainer
*/
export class CustomRuntimeObject extends gdjs.RuntimeObject {
/** It contains the children of this object. */
_instanceContainer: gdjs.CustomRuntimeObjectInstanceContainer;
_isUntransformedHitBoxesDirty: boolean = true;
/** It contains shallow copies of the children hitboxes */
_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;
_flippedX: boolean = false;
_flippedY: boolean = false;
opacity: float = 255;
/**
* @param parent The container the object belongs to
* @param objectData The object data used to initialize the object
*/
constructor(
parent: gdjs.RuntimeInstanceContainer,
objectData: ObjectData & CustomObjectConfiguration
) {
super(parent, objectData);
this._instanceContainer = new gdjs.CustomRuntimeObjectInstanceContainer(
parent,
this
);
this._instanceContainer.loadFrom(objectData);
this.getRenderer().reinitialize(this, parent);
// The generated code calls onCreated at the constructor end
// and onCreated calls its super implementation at its end.
}
reinitialize(objectData: ObjectData & CustomObjectConfiguration) {
super.reinitialize(objectData);
this._instanceContainer.loadFrom(objectData);
this.getRenderer().reinitialize(this, this.getParent());
// The generated code calls the onCreated super implementation at the end.
this.onCreated();
}
updateFromObjectData(
oldObjectData: ObjectData & CustomObjectConfiguration,
newObjectData: ObjectData & CustomObjectConfiguration
): boolean {
return this._instanceContainer.updateFrom(oldObjectData, newObjectData);
}
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
}
onDestroyFromScene(parent: gdjs.RuntimeInstanceContainer): void {
// Let subclasses do something before the object is destroyed.
this.onDestroy(parent);
// Let behaviors do something before the object is destroyed.
super.onDestroyFromScene(parent);
// Destroy the children.
this._instanceContainer.onDestroyFromScene(parent);
}
update(parent: gdjs.RuntimeInstanceContainer): void {
this._instanceContainer._updateObjectsPreEvents();
this.doStepPreEvents(parent);
const profiler = this.getRuntimeScene().getProfiler();
if (profiler) {
profiler.begin(this.type);
}
// This is a bit like the "scene" events for custom objects.
this.doStepPostEvents(parent);
if (profiler) {
profiler.end(this.type);
}
this._instanceContainer._updateObjectsPostEvents();
}
/**
* This method is called when the preview is being hot-reloaded.
*/
onHotReloading(parent: gdjs.RuntimeInstanceContainer) {}
// This is only to handle trigger once.
doStepPreEvents(parent: gdjs.RuntimeInstanceContainer) {}
/**
* This method is called each tick after events are done.
* @param parent The instanceContainer owning the object
*/
doStepPostEvents(parent: gdjs.RuntimeInstanceContainer) {}
/**
* This method is called when the object is being removed from its parent
* container and is about to be destroyed/reused later.
*/
onDestroy(parent: gdjs.RuntimeInstanceContainer) {}
updatePreRender(parent: gdjs.RuntimeInstanceContainer): void {
this._instanceContainer._updateObjectsPreRender();
this.getRenderer().ensureUpToDate();
}
getRendererObject() {
return this.getRenderer().getRendererObject();
}
getRenderer() {
return this._instanceContainer.getRenderer();
}
onChildrenLocationChanged() {
this._isUntransformedHitBoxesDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
updateHitBoxes(): void {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
//Update the current hitboxes with the frame custom hit boxes
//and apply transformations.
for (let i = 0; i < this._untransformedHitBoxes.length; ++i) {
if (i >= this.hitBoxes.length) {
this.hitBoxes.push(new gdjs.Polygon());
}
for (
let j = 0;
j < this._untransformedHitBoxes[i].vertices.length;
++j
) {
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],
this.hitBoxes[i].vertices[j]
);
}
this.hitBoxes[i].vertices.length = this._untransformedHitBoxes[
i
].vertices.length;
}
}
/**
* Merge the hitboxes of the children.
*/
_updateUntransformedHitBoxes() {
this._isUntransformedHitBoxesDirty = false;
const oldUnscaledCenterX =
(this._unrotatedAABB.max[0] + this._unrotatedAABB.min[0]) / 2;
const oldUnscaledCenterY =
(this._unrotatedAABB.max[1] + this._unrotatedAABB.min[1]) / 2;
this._untransformedHitBoxes.length = 0;
if (this._instanceContainer.getAdhocListOfAllInstances().length === 0) {
this._unrotatedAABB.min[0] = 0;
this._unrotatedAABB.min[1] = 0;
this._unrotatedAABB.max[0] = 0;
this._unrotatedAABB.max[1] = 0;
} else {
let minX = Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let maxY = -Number.MAX_VALUE;
for (const childInstance of this._instanceContainer.getAdhocListOfAllInstances()) {
Array.prototype.push.apply(
this._untransformedHitBoxes,
childInstance.getHitBoxes()
);
const childAABB = childInstance.getAABB();
minX = Math.min(minX, childAABB.min[0]);
minY = Math.min(minY, childAABB.min[1]);
maxX = Math.max(maxX, childAABB.max[0]);
maxY = Math.max(maxY, childAABB.max[1]);
}
this._unrotatedAABB.min[0] = minX;
this._unrotatedAABB.min[1] = minY;
this._unrotatedAABB.max[0] = maxX;
this._unrotatedAABB.max[1] = maxY;
while (this.hitBoxes.length < this._untransformedHitBoxes.length) {
this.hitBoxes.push(new gdjs.Polygon());
}
this.hitBoxes.length = this._untransformedHitBoxes.length;
}
if (
this.getUnscaledCenterX() !== oldUnscaledCenterX ||
this.getUnscaledCenterY() !== oldUnscaledCenterY
) {
this._instanceContainer.onObjectUnscaledCenterChanged(
oldUnscaledCenterX,
oldUnscaledCenterY
);
}
}
// Position:
/**
* Return an array containing the coordinates of the point passed as parameter
* in parent coordinate coordinates (as opposed to the object local coordinates).
*
* All transformations (flipping, scale, rotation) are supported.
*
* @param x The X position of the point, in object coordinates.
* @param y The Y position of the point, in object coordinates.
* @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();
// Flipping
if (this._flippedX) {
x = x + (cx - x) * 2;
}
if (this._flippedY) {
y = y + (cy - y) * 2;
}
// Scale
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;
}
/**
* Return an array containing the coordinates of the point passed as parameter
* in object local coordinates (as opposed to the parent coordinate coordinates).
*
* All transformations (flipping, 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
* (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;
}
getDrawableX(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this.x + this._unrotatedAABB.min[0] * this._scaleX;
}
getDrawableY(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this.y + this._unrotatedAABB.min[1] * this._scaleY;
}
/**
* @return the internal width of the object according to its children.
*/
getUnscaledWidth(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[0] - this._unrotatedAABB.min[0];
}
/**
* @return the internal height of the object according to its children.
*/
getUnscaledHeight(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this._unrotatedAABB.max[1] - this._unrotatedAABB.min[1];
}
/**
* @returns the center X from the local origin (0;0).
*/
getUnscaledCenterX(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (this._unrotatedAABB.min[0] + this._unrotatedAABB.max[0]) / 2;
}
/**
* @returns the center Y from the local origin (0;0).
*/
getUnscaledCenterY(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (this._unrotatedAABB.min[1] + this._unrotatedAABB.max[1]) / 2;
}
getWidth(): float {
return this.getUnscaledWidth() * this.getScaleX();
}
getHeight(): float {
return this.getUnscaledHeight() * this.getScaleY();
}
setWidth(newWidth: float): void {
const unscaledWidth = this.getUnscaledWidth();
if (unscaledWidth !== 0) {
this.setScaleX(newWidth / unscaledWidth);
}
}
setHeight(newHeight: float): void {
const unscaledHeight = this.getUnscaledHeight();
if (unscaledHeight !== 0) {
this.setScaleY(newHeight / unscaledHeight);
}
}
/**
* Change the size of the object.
*
* @param newWidth The new width of the object, in pixels.
* @param newHeight The new height of the object, in pixels.
*/
setSize(newWidth: float, newHeight: float): void {
this.setWidth(newWidth);
this.setHeight(newHeight);
}
setX(x: float): void {
if (x === this.x) {
return;
}
this.x = x;
this.invalidateHitboxes();
this.getRenderer().updateX();
}
setY(y: float): void {
if (y === this.y) {
return;
}
this.y = y;
this.invalidateHitboxes();
this.getRenderer().updateY();
}
setAngle(angle: float): void {
if (this.angle === angle) {
return;
}
this.angle = angle;
this.invalidateHitboxes();
this.getRenderer().updateAngle();
}
/**
* Change the scale on X and Y axis of the object.
*
* @param newScale The new scale (must be greater than 0).
*/
setScale(newScale: number): void {
if (newScale < 0) {
newScale = 0;
}
if (
newScale === Math.abs(this._scaleX) &&
newScale === Math.abs(this._scaleY)
) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Change the scale on X axis of the object (changing its width).
*
* @param newScale The new scale (must be greater than 0).
*/
setScaleX(newScale: number): void {
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleX)) {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Change the scale on Y axis of the object (changing its height).
*
* @param newScale The new scale (must be greater than 0).
*/
setScaleY(newScale: number): void {
if (newScale < 0) {
newScale = 0;
}
if (newScale === Math.abs(this._scaleY)) {
return;
}
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this.invalidateHitboxes();
this.getRenderer().update();
}
/**
* Get the scale of the object (or the average of the X and Y scale in case
* they are different).
*
* @return the scale of the object (or the average of the X and Y scale in
* case they are different).
*/
getScale(): number {
return (Math.abs(this._scaleX) + Math.abs(this._scaleY)) / 2.0;
}
/**
* Get the scale of the object on Y axis.
*
* @return the scale of the object on Y axis
*/
getScaleY(): float {
return Math.abs(this._scaleY);
}
/**
* Get the scale of the object on X axis.
*
* @return the scale of the object on X axis
*/
getScaleX(): float {
return Math.abs(this._scaleX);
}
// Visibility and display :
/**
* Change the transparency of the object.
* @param opacity The new opacity, between 0 (transparent) and 255 (opaque).
*/
setOpacity(opacity: float): void {
if (opacity < 0) {
opacity = 0;
}
if (opacity > 255) {
opacity = 255;
}
this.opacity = opacity;
this.getRenderer().updateOpacity();
}
/**
* Get the transparency of the object.
* @return The opacity, between 0 (transparent) and 255 (opaque).
*/
getOpacity(): number {
return this.opacity;
}
/**
* Hide (or show) the object
* @param enable true to hide the object, false to show it again.
*/
hide(enable: boolean): void {
if (enable === undefined) {
enable = true;
}
this.hidden = enable;
this.getRenderer().updateVisibility();
}
flipX(enable: boolean) {
if (enable !== this._flippedX) {
this._scaleX *= -1;
this._flippedX = enable;
this.invalidateHitboxes();
this.getRenderer().update();
}
}
flipY(enable: boolean) {
if (enable !== this._flippedY) {
this._scaleY *= -1;
this._flippedY = enable;
this.invalidateHitboxes();
this.getRenderer().update();
}
}
isFlippedX(): boolean {
return this._flippedX;
}
isFlippedY(): boolean {
return this._flippedY;
}
}
// Others initialization and internal state management :
// TODO EBO Activate and test instance recycling.
CustomRuntimeObject.supportsReinitialization = false;
}