Files
GDevelop/GDJS/Runtime/runtimeobject.ts
T
Harsimran Singh Virk 599ccb677f Add support for visual effects ("shaders") on objects. (#2901)
* This allows to generate interesting visual effects, which can be controlled by events. For example, you can use an outline on an object to highlight, make the player glow when launching a spell, blur objects, etc...
* A new "effects" tab is now present in the objects editor. From there, you can add visual effects, that were already available for layers, customize them from this editor.
* Actions and conditions are available to manipulate effects and change their parameters during the game.
* Learn more on the wiki: http://wiki.compilgames.net/doku.php/gdevelop5/objects/effects

Co-authored-by: Florian Rival <Florian.Rival@gmail.com>
2021-08-20 15:01:06 +02:00

2163 lines
67 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 {
/** An axis-aligned bounding box. Used to represents a box around an object for example. */
export type AABB = {
/** The [x,y] coordinates of the top left point */
min: FloatPoint;
/** The [x,y] coordinates of the bottom right point */
max: FloatPoint;
};
/**
* Return the squared bounding radius of an object given its width/height and its center of rotation
* (relative to the top-left of the object). The radius is relative to the center of rotation.
*/
const computeSqBoundingRadius = (
width: float,
height: float,
centerX: float,
centerY: float
) => {
const radiusX = Math.max(centerX, width - centerX);
const radiusY = Math.max(centerY, height - centerY);
return Math.pow(radiusX, 2) + Math.pow(radiusY, 2);
};
/**
* RuntimeObject represents an object being used on a RuntimeScene.
*
* A `gdjs.RuntimeObject` should not be instantiated directly, always a child class
* (because gdjs.RuntimeObject don't call onCreated at the end of its constructor).
*/
export class RuntimeObject implements EffectsTarget {
name: string;
type: string;
x: float = 0;
y: float = 0;
angle: float = 0;
zOrder: integer = 0;
hidden: boolean = false;
layer: string = '';
protected _nameId: integer;
protected _livingOnScene: boolean = true;
readonly id: integer;
_runtimeScene: gdjs.RuntimeScene;
/**
* An optional UUID associated to the object to be used
* for hot reload. Don't modify or use otherwise.
*/
persistentUuid: string | null = null;
/**
* A property to be used by external algorithms to indicate if the
* object is picked or not in an object selection. By construction, this is
* not "thread safe" or "re-entrant algorithm" safe.
*/
pick: boolean = false;
//Hit boxes:
protected _defaultHitBoxes: gdjs.Polygon[] = [];
protected hitBoxes: gdjs.Polygon[];
protected hitBoxesDirty: boolean = true;
protected aabb: AABB = { min: [0, 0], max: [0, 0] };
//Variables:
protected _variables: gdjs.VariablesContainer;
//Effects:
protected _rendererEffects: Record<string, PixiFiltersTools.Filter> = {};
//Forces:
protected _forces: gdjs.Force[] = [];
_averageForce: gdjs.Force;
/**
* Contains the behaviors of the object.
*/
protected _behaviors: gdjs.RuntimeBehavior[] = [];
protected _behaviorsTable: Hashtable<gdjs.RuntimeBehavior>;
protected _timers: Hashtable<gdjs.Timer>;
/**
* @param runtimeScene The scene the object belongs to..
* @param objectData The initial properties of the object.
*/
constructor(runtimeScene: gdjs.RuntimeScene, objectData: ObjectData) {
this.name = objectData.name || '';
this.type = objectData.type || '';
this._nameId = RuntimeObject.getNameIdentifier(this.name);
this.id = runtimeScene.createNewUniqueId();
this._runtimeScene = runtimeScene;
this._defaultHitBoxes.push(gdjs.Polygon.createRectangle(0, 0));
this.hitBoxes = this._defaultHitBoxes;
this._variables = new gdjs.VariablesContainer(
objectData ? objectData.variables : undefined
);
this._averageForce = new gdjs.Force(0, 0, 0);
this._behaviorsTable = new Hashtable();
for (let i = 0; i < objectData.effects.length; ++i) {
this._runtimeScene
.getGame()
.getEffectsManager()
.initializeEffect(objectData.effects[i], this._rendererEffects, this);
this.updateAllEffectParameters(objectData.effects[i]);
}
//Also contains the behaviors: Used when a behavior is accessed by its name ( see getBehavior ).
for (let i = 0, len = objectData.behaviors.length; i < len; ++i) {
const autoData = objectData.behaviors[i];
const Ctor = gdjs.getBehaviorConstructor(autoData.type);
this._behaviors.push(new Ctor(runtimeScene, autoData, this));
this._behaviorsTable.put(autoData.name, this._behaviors[i]);
}
this._timers = new Hashtable();
}
//Common members functions related to the object and its runtimeScene :
/**
* To be called by the child classes in their constructor, at the very end.
* Notify the behaviors that they have been constructed (this must be done when
* the object is ready, otherwise behaviors can do operations on the object which
* could be not initialized yet).
*
* If you redefine this function, **make sure to call the original method**
* (`RuntimeObject.prototype.onCreated.call(this);`).
*/
onCreated(): void {
for (const effectName in this._rendererEffects) {
this._runtimeScene
.getGame()
.getEffectsManager()
.applyEffect(
this.getRendererObject(),
this._rendererEffects[effectName]
);
}
for (let i = 0; i < this._behaviors.length; ++i) {
this._behaviors[i].onCreated();
}
}
/**
* Called to reset the object to its default state. This is used for objects that are
* "recycled": they are dismissed (at which point `onDestroyFromScene` is called) but still
* stored in a cache to be reused next time an object must be created. At this point,
* `reinitialize` will be called. The object must then work as if it was a newly constructed
* object.
*
* To implement this in your object:
* * Set `gdjs.YourRuntimeObject.supportsReinitialization = true;` to declare support for recycling.
* * Implement `reinitialize`. It **must** call the `reinitialize` of `gdjs.RuntimeObject`, and call `this.onCreated();`
* at the end of `reinitizalize`.
* * It must reset the object as if it was newly constructed (be careful about your renderers and any global state).
* * The `_runtimeScene`, `_nameId`, `name` and `type` are guaranteed to stay the same and do not
* need to be set again.
*
*/
reinitialize(objectData: ObjectData): void {
const runtimeScene = this._runtimeScene;
this.x = 0;
this.y = 0;
this.angle = 0;
this.zOrder = 0;
this.hidden = false;
this.layer = '';
this._livingOnScene = true;
//@ts-ignore Reinitialize is like a constructor, it can overwrite the readonly property.
this.id = runtimeScene.createNewUniqueId();
this.persistentUuid = null;
this.pick = false;
this.hitBoxesDirty = true;
this.aabb.min[0] = 0;
this.aabb.min[1] = 0;
this.aabb.max[0] = 0;
this.aabb.max[1] = 0;
this._variables = new gdjs.VariablesContainer(objectData.variables);
this.clearForces();
this._behaviorsTable.clear();
let i = 0;
for (const len = objectData.behaviors.length; i < len; ++i) {
const behaviorData = objectData.behaviors[i];
const Ctor = gdjs.getBehaviorConstructor(behaviorData.type);
if (i < this._behaviors.length) {
// TODO: Add support for behavior recycling with a `reinitialize` method.
this._behaviors[i] = new Ctor(runtimeScene, behaviorData, this);
} else {
this._behaviors.push(new Ctor(runtimeScene, behaviorData, this));
}
this._behaviorsTable.put(behaviorData.name, this._behaviors[i]);
}
this._behaviors.length = i;
// Make sure to delete already existing behaviors which are not used anymore.
this._timers.clear();
}
static supportsReinitialization = false;
/**
* Return the time elapsed since the last frame,
* in milliseconds, for the object.
*
* Objects can have different elapsed time if they are on layers with different time scales.
*
* @param runtimeScene The RuntimeScene the object belongs to (deprecated - can be omitted).
*/
getElapsedTime(runtimeScene?: gdjs.RuntimeScene): float {
const theLayer = this._runtimeScene.getLayer(this.layer);
return theLayer.getElapsedTime();
}
/**
* The gdjs.RuntimeScene the object belongs to.
*/
getRuntimeScene(): RuntimeScene {
return this._runtimeScene;
}
/**
* Called once during the game loop, before events and rendering.
* @param runtimeScene The gdjs.RuntimeScene the object belongs to.
*/
update(runtimeScene: gdjs.RuntimeScene): void {}
/**
* Called once during the game loop, after events and before rendering.
* @param runtimeScene The gdjs.RuntimeScene the object belongs to.
*/
updatePreRender(runtimeScene: gdjs.RuntimeScene): void {}
//Nothing to do.
/**
* Called when the object is created from an initial instance at the startup of the scene.<br>
* Note that common properties (position, angle, z order...) have already been setup.
*
* @param initialInstanceData The data of the initial instance.
*/
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {}
//Nothing to do.
/**
* Called when the object must be updated using the specified objectData. This is the
* case during hot-reload, and is only called if the object was modified.
*
* @param oldObjectData The previous data for the object.
* @param newObjectData The new data for the object.
* @returns true if the object was updated, false if it could not (i.e: hot-reload is not supported).
*/
updateFromObjectData(
oldObjectData: ObjectData,
newObjectData: ObjectData
): boolean {
// If not redefined, mark by default the hot-reload as failed.
return false;
}
/**
* Remove an object from a scene.
*
* Do not change/redefine this method. Instead, redefine the onDestroyFromScene method.
* @param runtimeScene The RuntimeScene owning the object.
*/
deleteFromScene(runtimeScene: gdjs.RuntimeScene): void {
if (this._livingOnScene) {
runtimeScene.markObjectForDeletion(this);
this._livingOnScene = false;
}
}
/**
* Called when the object is destroyed (because it is removed from a scene or the scene
* is being unloaded). If you redefine this function, **make sure to call the original method**
* (`RuntimeObject.prototype.onDestroyFromScene.call(this, runtimeScene);`).
*
* @param runtimeScene The scene owning the object.
*/
onDestroyFromScene(runtimeScene: gdjs.RuntimeScene): void {
const theLayer = runtimeScene.getLayer(this.layer);
const rendererObject = this.getRendererObject();
if (rendererObject) {
theLayer.getRenderer().removeRendererObject(rendererObject);
}
for (let j = 0, lenj = this._behaviors.length; j < lenj; ++j) {
this._behaviors[j].onDestroy();
}
}
//Rendering:
/**
* Called with a callback function that should be called with the internal
* object used for rendering by the object (PIXI.DisplayObject...)
*
* @return The internal rendered object (PIXI.DisplayObject...)
*/
getRendererObject(): any {
return undefined;
}
//Common properties:
/**
* Get the name of the object.
* @return The object's name.
*/
getName(): string {
return this.name;
}
/**
* Get the name identifier of the object.
* @return The object's name identifier.
*/
getNameId(): integer {
return this._nameId;
}
/**
* Get the unique identifier of the object.<br>
* The identifier is set by the runtimeScene owning the object.<br>
* You can also use the id property (this._object.id) for increased efficiency instead of
* calling this method.
*
* @return The object identifier
*/
getUniqueId(): integer {
return this.id;
}
/**
* Set the position of the object.
*
* @param x The new X position
* @param y The new Y position
*/
setPosition(x: float, y: float): void {
this.setX(x);
this.setY(y);
}
/**
* Set the X position of the object.
*
* @param x The new X position
*/
setX(x: float): void {
if (x === this.x) {
return;
}
this.x = x;
this.hitBoxesDirty = true;
}
/**
* Get the X position of the object.
*
* @return The X position of the object
*/
getX(): float {
return this.x;
}
/**
* Set the Y position of the object.
*
* @param y The new Y position
*/
setY(y: float): void {
if (y === this.y) {
return;
}
this.y = y;
this.hitBoxesDirty = true;
}
/**
* Get the Y position of the object.
*
* @return The Y position of the object
*/
getY(): float {
return this.y;
}
/**
* Get the X position of the rendered object.
*
* For most objects, this will returns the same value as getX(). But if the object
* has an origin that is not the same as the point (0,0) of the object displayed,
* getDrawableX will differ.
*
* @return The X position of the rendered object.
*/
getDrawableX(): float {
return this.getX();
}
/**
* Get the Y position of the rendered object.
*
* For most objects, this will returns the same value as getY(). But if the object
* has an origin that is not the same as the point (0,0) of the object displayed,
* getDrawableY will differ.
*
* @return The Y position of the rendered object.
*/
getDrawableY(): float {
return this.getY();
}
rotateTowardPosition(
x: float,
y: float,
speed: float,
scene: gdjs.RuntimeScene
): void {
this.rotateTowardAngle(
gdjs.toDegrees(
Math.atan2(
y - (this.getDrawableY() + this.getCenterY()),
x - (this.getDrawableX() + this.getCenterX())
)
),
speed,
scene
);
}
rotateTowardAngle(
angle: float,
speed: float,
runtimeScene: gdjs.RuntimeScene
): void {
if (speed === 0) {
this.setAngle(angle);
return;
}
const angularDiff = gdjs.evtTools.common.angleDifference(
this.getAngle(),
angle
);
const diffWasPositive = angularDiff >= 0;
let newAngle =
this.getAngle() +
((diffWasPositive ? -1.0 : 1.0) *
speed *
this.getElapsedTime(runtimeScene)) /
1000;
if (
// @ts-ignore
(gdjs.evtTools.common.angleDifference(newAngle, angle) > 0) ^
diffWasPositive
) {
newAngle = angle;
}
this.setAngle(newAngle);
if (
//Objects like sprite in 8 directions does not handle small increments...
this.getAngle() !== newAngle
) {
this.setAngle(
//...so force them to be in the path angle anyway.
angle
);
}
}
/**
* Rotate the object at the given speed
*
* @param speed The speed, in degrees per second.
* @param runtimeScene The scene where the object is displayed.
*/
rotate(speed: float, runtimeScene: gdjs.RuntimeScene): void {
this.setAngle(
this.getAngle() + (speed * this.getElapsedTime(runtimeScene)) / 1000
);
}
/**
* Set the angle of the object.
*
* @param angle The new angle of the object
*/
setAngle(angle: float): void {
if (this.angle === angle) {
return;
}
this.angle = angle;
this.hitBoxesDirty = true;
}
/**
* Get the rotation of the object.
*
* @return The rotation of the object, in degrees.
*/
getAngle(): float {
return this.angle;
}
/**
* Set the layer of the object.
*
* @param layer The new layer of the object
*/
setLayer(layer: string): void {
if (layer === this.layer) {
return;
}
const oldLayer = this._runtimeScene.getLayer(this.layer);
this.layer = layer;
const newLayer = this._runtimeScene.getLayer(this.layer);
const rendererObject = this.getRendererObject();
if (rendererObject) {
oldLayer.getRenderer().removeRendererObject(rendererObject);
newLayer.getRenderer().addRendererObject(rendererObject, this.zOrder);
}
}
/**
* Get the layer of the object.
*
* @return The layer of the object
*/
getLayer(): string {
return this.layer;
}
/**
* Return true if the object is on the specified layer
*
* @param layer The layer to be tested.
* @return true if the object is on the specified layer
*/
isOnLayer(layer: string): boolean {
return this.layer === layer;
}
/**
* Set the Z order of the object.
*
* @param z The new Z order position of the object
*/
setZOrder(z: integer): void {
if (z === this.zOrder) {
return;
}
this.zOrder = z;
if (this.getRendererObject()) {
const theLayer = this._runtimeScene.getLayer(this.layer);
theLayer
.getRenderer()
.changeRendererObjectZOrder(this.getRendererObject(), z);
}
}
/**
* Get the Z order of the object.
*
* @return The Z order of the object
*/
getZOrder(): float {
return this.zOrder;
}
/**
* Get the container of the object variables
* @return The variables of the object
*/
getVariables(): gdjs.VariablesContainer {
return this._variables;
}
/**
* Get the value of a variable considered as a number. Equivalent of variable.getAsNumber()
* @param variable The variable to be accessed
* @return The value of the specified variable
* @static
*/
static getVariableNumber(variable: gdjs.Variable): number {
return variable.getAsNumber();
}
/**
* Return the variable passed as argument without any change.
* Only for usage by events.
*
* @param variable The variable to be accessed
* @return The specified variable
* @static
*/
static returnVariable(variable: gdjs.Variable): gdjs.Variable {
return variable;
}
/**
* Get the value of a variable considered as a string. Equivalent of variable.getAsString()
* @param variable The variable to be accessed
* @return The string of the specified variable
* @static
*/
static getVariableString(variable: gdjs.Variable): string {
return variable.getAsString();
}
/**
* Shortcut to set the value of a variable considered as a boolean.
* This shortcut function is needed for events code generation.
*
* @private
* @param {gdjs.Variable} variable
* @param {boolean} newValue
*/
static setVariableBoolean = function (
variable: gdjs.Variable,
newValue: boolean
) {
variable.setBoolean(newValue);
};
/**
* Shortcut to compare the value of a variable considered as a boolean.
* This shortcut function is needed for events code generation.
*
* @private
* @param {gdjs.Variable} variable
* @param {boolean} compareWith
* @returns {boolean}
*/
static getVariableBoolean = function (
variable: gdjs.Variable,
compareWith: boolean
): boolean {
return gdjs.evtTools.common.getVariableBoolean(variable, compareWith);
};
/**
* Toggles a variable.
* This shortcut function is needed for events code generation.
*
* @private
* @param {gdjs.Variable} variable
* @see {gdjs.evtTools.common.toggleVariableBoolean}
*/
static toggleVariableBoolean = function (variable: gdjs.Variable) {
gdjs.evtTools.common.toggleVariableBoolean(variable);
};
/**
* Get the number of children from a variable
* @param variable The variable to be accessed
* @return The number of children
* @static
*/
static getVariableChildCount(variable: gdjs.Variable): integer {
return variable.getChildrenCount();
}
/**
* Shortcut to set the value of a variable considered as a number
* @param variable The variable to be changed
* @param newValue The value to be set
*/
static setVariableNumber(variable: gdjs.Variable, newValue: float): void {
variable.setNumber(newValue);
}
/**
* Shortcut to set the value of a variable considered as a string
* @param variable The variable to be changed
* @param newValue {String} The value to be set
*/
static setVariableString(variable: gdjs.Variable, newValue: string) {
variable.setString(newValue);
}
/**
* @static
* @param variable The variable to be tested
* @param childName The name of the child
*/
private static variableChildExists(
variable: gdjs.Variable,
childName: string
): boolean {
return variable.hasChild(childName);
}
/**
* @static
* @param variable The variable to be changed
* @param childName The name of the child
*/
private static variableRemoveChild(
variable: gdjs.Variable,
childName: string
): void {
variable.removeChild(childName);
}
/**
* @static
* @param variable The variable to be cleared
*/
private static variableClearChildren(variable: gdjs.Variable): void {
variable.clearChildren();
}
/**
* This shortcut function is needed for events code generation.
* @private
*/
static variablePushCopy = function (
array: gdjs.Variable,
variable: gdjs.Variable
) {
array.pushVariableCopy(variable);
};
/**
* This shortcut function is needed for events code generation.
* @private
*/
static valuePush = function (
array: gdjs.Variable,
value: string | float | boolean
) {
array.pushValue(value);
};
/**
* This shortcut function is needed for events code generation.
* @private
*/
static variableRemoveAt = function (array: gdjs.Variable, index: number) {
array.removeAtIndex(index);
};
/**
* Shortcut to test if a variable exists for the object.
* @param name The variable to be tested
* @return true if the variable exists.
*/
hasVariable(name: string): boolean {
return this._variables.has(name);
}
/**
* Returns the collection of effects to be rendered by the
* underlying renderer.
* @returns The renderer effects.
*/
getRendererEffects() {
return this._rendererEffects;
}
/**
* Add a new effect, or replace the one with the same name.
* @param effectData The data describing the effect to add.
*/
addEffect(effectData: EffectData): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.addEffect(
effectData,
this._rendererEffects,
this.getRendererObject(),
this
);
}
/**
* Remove the effect with the specified name
* @param effectName The name of the effect.
*/
removeEffect(effectName: string): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.removeEffect(
this._rendererEffects,
this.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
): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.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
): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.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
): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.setEffectBooleanParameter(
this._rendererEffects,
name,
parameterName,
value
);
}
/**
* Updates all the parameters of an effect.
* @param effectData The data describing the effect
*/
updateAllEffectParameters(effectData: EffectData): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.updateAllEffectParameters(this._rendererEffects, effectData);
}
/**
* 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._runtimeScene
.getGame()
.getEffectsManager()
.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._runtimeScene
.getGame()
.getEffectsManager()
.isEffectEnabled(this._rendererEffects, name);
}
/**
* Check if an effect exists on this object
* @param name The name of the effect
* @return true if the effect exists, false otherwise.
*/
hasEffect(name: string): boolean {
return this._runtimeScene
.getGame()
.getEffectsManager()
.hasEffect(this._rendererEffects, name);
}
/**
* Hide (or show) the object.
* @param enable Set it to true to hide the object, false to show it.
*/
hide(enable: boolean): void {
if (enable === undefined) {
enable = true;
}
this.hidden = enable;
}
/**
* Return true if the object is not hidden.
*
* Note: This is unrelated to the actual visibility of the object on the screen.
* For this, see `getVisibilityAABB` to get the bounding boxes of the object as displayed
* on the scene.
*
* @return true if the object is not hidden.
*/
isVisible(): boolean {
return !this.hidden;
}
/**
* Return true if the object is hidden.
* @return true if the object is hidden.
*/
isHidden(): boolean {
return this.hidden;
}
/**
* Set the width of the object, if applicable.
* @param width The new width in pixels.
*/
setWidth(width: float): void {}
/**
* Set the height of the object, if applicable.
* @param height The new height in pixels.
*/
setHeight(height: float): void {}
/**
* Return the width of the object.
* @return The width of the object
*/
getWidth(): float {
return 0;
}
/**
* Return the width of the object.
* @return The height of the object
*/
getHeight(): float {
return 0;
}
/**
* Return the X position of the object center, **relative to the object X position** (`getDrawableX`).
* Use `getCenterXInScene` to get the position of the center in the scene.
*
* @return the X position of the object center, relative to `getDrawableX()`.
*/
getCenterX(): float {
return this.getWidth() / 2;
}
/**
* Return the Y position of the object center, **relative to the object position** (`getDrawableY`).
* Use `getCenterYInScene` to get the position of the center in the scene.
*
* @return the Y position of the object center, relative to `getDrawableY()`.
*/
getCenterY(): float {
return this.getHeight() / 2;
}
/**
* Return the X position of the object center, **relative to the scene origin**.
* @returns the X position of the object center, **relative to the scene origin**.
*/
getCenterXInScene(): float {
return this.getDrawableX() + this.getCenterX();
}
/**
* Return the Y position of the object center, **relative to the scene origin**.
* @returns the Y position of the object center, **relative to the scene origin**.
*/
getCenterYInScene(): float {
return this.getDrawableY() + this.getCenterY();
}
/**
* Change the object center position in the scene.
* @param x The new X position of the center in the scene.
* @param y The new Y position of the center in the scene.
*/
setCenterPositionInScene(x: float, y: float): void {
this.setX(x + this.x - (this.getDrawableX() + this.getCenterX()));
this.setY(y + this.y - (this.getDrawableY() + this.getCenterY()));
}
/**
* Change the object center X position in the scene.
* @param x The new X position of the center in the scene.
*/
setCenterXInScene(x: float): void {
this.setX(x + this.x - (this.getDrawableX() + this.getCenterX()));
}
/**
* Change the object center Y position in the scene.
* @param x The new Y position of the center in the scene.
*/
setCenterYInScene(y: float): void {
this.setY(y + this.y - (this.getDrawableY() + this.getCenterY()));
}
//Forces :
/**
* Get a force from the garbage, or create a new force is garbage is empty.<br>
* To be used each time a force is created so as to avoid temporaries objects.
*
* @param x The x coordinates of the force
* @param y The y coordinates of the force
* @param multiplier Set the force multiplier
*/
private _getRecycledForce(
x: float,
y: float,
multiplier: integer
): gdjs.Force {
if (RuntimeObject.forcesGarbage.length === 0) {
return new gdjs.Force(x, y, multiplier);
} else {
const recycledForce = RuntimeObject.forcesGarbage.pop() as gdjs.Force;
recycledForce.setX(x);
recycledForce.setY(y);
recycledForce.setMultiplier(multiplier);
return recycledForce;
}
}
/**
* Add a force to the object to move it.
* @param x The x coordinates of the force
* @param y The y coordinates of the force
* @param multiplier Set the force multiplier
*/
addForce(x: float, y: float, multiplier: integer): void {
this._forces.push(this._getRecycledForce(x, y, multiplier));
}
/**
* Add a force using polar coordinates.
* @param angle The angle of the force, in degrees.
* @param len The length of the force, in pixels.
* @param multiplier Set the force multiplier
*/
addPolarForce(angle: float, len: float, multiplier: integer): void {
const angleInRadians = gdjs.toRad(angle);
//TODO: Benchmark with Math.PI
const forceX = Math.cos(angleInRadians) * len;
const forceY = Math.sin(angleInRadians) * len;
this._forces.push(this._getRecycledForce(forceX, forceY, multiplier));
}
/**
* Add a force oriented toward a position
* @param x The target x position
* @param y The target y position
* @param len The force length, in pixels.
* @param multiplier Set the force multiplier
*/
addForceTowardPosition(
x: float,
y: float,
len: float,
multiplier: integer
): void {
const angleInRadians = Math.atan2(
y - (this.getDrawableY() + this.getCenterY()),
x - (this.getDrawableX() + this.getCenterX())
);
const forceX = Math.cos(angleInRadians) * len;
const forceY = Math.sin(angleInRadians) * len;
this._forces.push(this._getRecycledForce(forceX, forceY, multiplier));
}
/**
* Add a force oriented toward another object.<br>
* (Shortcut for addForceTowardPosition)
* @param object The target object
* @param len The force length, in pixels.
* @param multiplier Set the force multiplier
*/
addForceTowardObject(
object: gdjs.RuntimeObject,
len: float,
multiplier: integer
): void {
if (object == null) {
return;
}
this.addForceTowardPosition(
object.getDrawableX() + object.getCenterX(),
object.getDrawableY() + object.getCenterY(),
len,
multiplier
);
}
/**
* Deletes all forces applied on the object
*/
clearForces(): void {
RuntimeObject.forcesGarbage.push.apply(
RuntimeObject.forcesGarbage,
this._forces
);
this._forces.length = 0;
}
/**
* Return true if no forces are applied on the object.
* @return true if no forces are applied on the object.
*/
hasNoForces(): boolean {
return this._forces.length === 0;
}
/**
* Called once a step by runtimeScene to update forces magnitudes and
* remove null ones.
*/
updateForces(elapsedTime: float): void {
for (let i = 0; i < this._forces.length; ) {
const force = this._forces[i];
const multiplier = force.getMultiplier();
if (multiplier === 1) {
// Permanent force
++i;
} else {
if (
multiplier === 0 ||
// Instant or force disappearing
force.getLength() <= 0.001
) {
RuntimeObject.forcesGarbage.push(force);
this._forces.splice(i, 1);
} else {
// Deprecated way of updating forces progressively.
force.setLength(
force.getLength() -
force.getLength() * (1 - multiplier) * elapsedTime
);
++i;
}
}
}
}
/**
* Return a force which is the sum of all forces applied on the object.
*
* @return A force object.
*/
getAverageForce(): gdjs.Force {
let averageX = 0;
let averageY = 0;
for (let i = 0, len = this._forces.length; i < len; ++i) {
averageX += this._forces[i].getX();
averageY += this._forces[i].getY();
}
this._averageForce.setX(averageX);
this._averageForce.setY(averageY);
return this._averageForce;
}
/**
* Return true if the average angle of the forces applied on the object
* is in a given range.
*
* @param angle The angle to be tested.
* @param toleranceInDegrees The length of the range :
* @return true if the difference between the average angle of the forces
* and the angle parameter is inferior to toleranceInDegrees parameter.
*/
averageForceAngleIs(angle: float, toleranceInDegrees: float): boolean {
let averageAngle = this.getAverageForce().getAngle();
if (averageAngle < 0) {
averageAngle += 360;
}
return Math.abs(angle - averageAngle) < toleranceInDegrees / 2;
}
//Hit boxes and collision :
/**
* Get the hit boxes for the object.<br>
* The default implementation returns a basic bouding box based the size (getWidth and
* getHeight) and the center point of the object (getCenterX and getCenterY).
*
* You should probably redefine updateHitBoxes instead of this function.
*
* @return An array composed of polygon.
*/
getHitBoxes(): gdjs.Polygon[] {
//Avoid a naive implementation requiring to recreate temporaries each time
//the function is called:
//(var rectangle = gdjs.Polygon.createRectangle(this.getWidth(), this.getHeight());
//...)
if (this.hitBoxesDirty) {
this.updateHitBoxes();
this.updateAABB();
this.hitBoxesDirty = false;
}
return this.hitBoxes;
}
/**
* Update the hit boxes for the object.
*
* The default implementation set a basic bounding box based on the size (getWidth and
* getHeight) and the center point of the object (getCenterX and getCenterY).
* Result is cached until invalidated (by a position change, angle change...).
*
* You should not call this function by yourself, it is called when necessary by getHitBoxes method.
* However, you can redefine it if your object need custom hit boxes.
*/
updateHitBoxes(): void {
this.hitBoxes = this._defaultHitBoxes;
const width = this.getWidth();
const height = this.getHeight();
const centerX = this.getCenterX();
const centerY = this.getCenterY();
if (centerX === width / 2 && centerY === height / 2) {
this.hitBoxes[0].vertices[0][0] = -centerX;
this.hitBoxes[0].vertices[0][1] = -centerY;
this.hitBoxes[0].vertices[1][0] = +centerX;
this.hitBoxes[0].vertices[1][1] = -centerY;
this.hitBoxes[0].vertices[2][0] = +centerX;
this.hitBoxes[0].vertices[2][1] = +centerY;
this.hitBoxes[0].vertices[3][0] = -centerX;
this.hitBoxes[0].vertices[3][1] = +centerY;
} else {
this.hitBoxes[0].vertices[0][0] = 0 - centerX;
this.hitBoxes[0].vertices[0][1] = 0 - centerY;
this.hitBoxes[0].vertices[1][0] = width - centerX;
this.hitBoxes[0].vertices[1][1] = 0 - centerY;
this.hitBoxes[0].vertices[2][0] = width - centerX;
this.hitBoxes[0].vertices[2][1] = height - centerY;
this.hitBoxes[0].vertices[3][0] = 0 - centerX;
this.hitBoxes[0].vertices[3][1] = height - centerY;
}
this.hitBoxes[0].rotate(gdjs.toRad(this.getAngle()));
this.hitBoxes[0].move(
this.getDrawableX() + centerX,
this.getDrawableY() + centerY
);
}
/**
* Get the AABB (axis aligned bounding box) for the object.
*
* The default implementation uses either the position/size of the object (when angle is 0) or
* hitboxes (when angle is not 0) to compute the bounding box.
* Result is cached until invalidated (by a position change, angle change...).
*
* You should probably redefine updateAABB instead of this function.
*
* @return The bounding box
*/
getAABB(): AABB {
if (this.hitBoxesDirty) {
this.updateHitBoxes();
this.updateAABB();
this.hitBoxesDirty = false;
}
return this.aabb;
}
/**
* Get the AABB (axis aligned bounding box) to be used to determine if the object
* is visible on screen. The gdjs.RuntimeScene will hide the renderer object if
* the object is not visible on screen ("culling").
*
* The default implementation uses the AABB returned by getAABB.
*
* If `null` is returned, the object is assumed to be always visible.
*
* @return The bounding box or `null`.
*/
getVisibilityAABB(): AABB | null {
return this.getAABB();
}
/**
* Update the AABB (axis aligned bounding box) for the object.
*
* Default implementation uses either the position/size of the object (when angle is 0) or
* hitboxes (when angle is not 0) to compute the bounding box.
*
* You should not call this function by yourself, it is called when necessary by getAABB method.
* However, you can redefine it if your object can have a faster implementation.
*/
updateAABB(): void {
if (this.getAngle() === 0) {
// Fast/simple computation of AABB for non rotated object
// (works even for object with non default center/origin
// because we're using getDrawableX/Y)
this.aabb.min[0] = this.getDrawableX();
this.aabb.min[1] = this.getDrawableY();
this.aabb.max[0] = this.aabb.min[0] + this.getWidth();
this.aabb.max[1] = this.aabb.min[1] + this.getHeight();
} else {
// Use hitboxes if object is rotated to ensure that the AABB
// is properly bounding the whole object.
// Slower (10-15% slower).
let first = true;
for (let i = 0; i < this.hitBoxes.length; i++) {
for (let j = 0; j < this.hitBoxes[i].vertices.length; j++) {
const vertex = this.hitBoxes[i].vertices[j];
if (first) {
this.aabb.min[0] = vertex[0];
this.aabb.max[0] = vertex[0];
this.aabb.min[1] = vertex[1];
this.aabb.max[1] = vertex[1];
first = false;
} else {
this.aabb.min[0] = Math.min(this.aabb.min[0], vertex[0]);
this.aabb.max[0] = Math.max(this.aabb.max[0], vertex[0]);
this.aabb.min[1] = Math.min(this.aabb.min[1], vertex[1]);
this.aabb.max[1] = Math.max(this.aabb.max[1], vertex[1]);
}
}
}
}
}
//Behaviors:
/**
* Call each behavior stepPreEvents method.
*/
stepBehaviorsPreEvents(runtimeScene): void {
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
this._behaviors[i].stepPreEvents(runtimeScene);
}
}
/**
* Call each behavior stepPostEvents method.
*/
stepBehaviorsPostEvents(runtimeScene): void {
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
this._behaviors[i].stepPostEvents(runtimeScene);
}
}
/**
* Called when the object was hot reloaded, to notify behaviors
* that the object was modified. Useful for behaviors that
*/
notifyBehaviorsObjectHotReloaded(): void {
for (let i = 0, len = this._behaviors.length; i < len; ++i) {
this._behaviors[i].onObjectHotReloaded();
}
}
/**
* Get a behavior from its name.
* If the behavior does not exists, `undefined` is returned.
*
* **Never keep a reference** to a behavior, as they can be hot-reloaded. Instead,
* always call getBehavior on the object.
*
* @param name {String} The behavior name.
* @return The behavior with the given name, or undefined.
*/
getBehavior(name: string): gdjs.RuntimeBehavior | null {
return this._behaviorsTable.get(name);
}
/**
* Check if a behavior is used by the object.
*
* @param name {String} The behavior name.
*/
hasBehavior(name: string): boolean {
return this._behaviorsTable.containsKey(name);
}
/**
* De/activate a behavior of the object.
*
* @param name {String} The behavior name.
* @param enable {boolean} true to activate the behavior
*/
activateBehavior(name: string, enable: boolean): void {
if (this._behaviorsTable.containsKey(name)) {
this._behaviorsTable.get(name).activate(enable);
}
}
/**
* Check if a behavior is activated
*
* @param name The behavior name.
* @return true if the behavior is activated.
*/
behaviorActivated(name: string): boolean {
if (this._behaviorsTable.containsKey(name)) {
return this._behaviorsTable.get(name).activated();
}
return false;
}
/**
* Remove the behavior with the given name. Usually only used by
* hot-reloading, as performance of this operation is not guaranteed
* (in the future, this could lead to re-organization of arrays
* holding behaviors).
*
* @param name The name of the behavior to remove.
* @returns true if the behavior was properly removed, false otherwise.
*/
removeBehavior(name: string): boolean {
const behavior = this._behaviorsTable.get(name);
if (!behavior) {
return false;
}
behavior.onDestroy();
const behaviorIndex = this._behaviors.indexOf(behavior);
if (behaviorIndex !== -1) {
this._behaviors.splice(behaviorIndex, 1);
}
this._behaviorsTable.remove(name);
return true;
}
/**
* Create the behavior decribed by the given BehaviorData
*
* @param behaviorData The data to be used to construct the behavior.
* @returns true if the behavior was properly created, false otherwise.
*/
addNewBehavior(behaviorData: BehaviorData): boolean {
const Ctor = gdjs.getBehaviorConstructor(behaviorData.type);
if (!Ctor) {
return false;
}
const newRuntimeBehavior = new Ctor(
this._runtimeScene,
behaviorData,
this
);
this._behaviors.push(newRuntimeBehavior);
this._behaviorsTable.put(behaviorData.name, newRuntimeBehavior);
return true;
}
//Timers:
/**
* Updates the object timers. Called once during the game loop, before events and rendering.
* @param elapsedTime The elapsed time since the previous frame in milliseconds.
*/
updateTimers(elapsedTime: float): void {
for (const name in this._timers.items) {
if (this._timers.items.hasOwnProperty(name)) {
this._timers.items[name].updateTime(elapsedTime);
}
}
}
/**
* Test a timer elapsed time, if the timer doesn't exist it is created
* @param timerName The timer name
* @param timeInSeconds The time value to check in seconds
* @return True if the timer exists and its value is greater than or equal than the given time, false otherwise
*/
timerElapsedTime(timerName: string, timeInSeconds: float): boolean {
if (!this._timers.containsKey(timerName)) {
this._timers.put(timerName, new gdjs.Timer(timerName));
return false;
}
return this.getTimerElapsedTimeInSeconds(timerName) >= timeInSeconds;
}
/**
* Test a if a timer is paused
* @param timerName The timer name
* @return True if the timer exists and is paused, false otherwise
*/
timerPaused(timerName: string): boolean {
if (!this._timers.containsKey(timerName)) {
return false;
}
return this._timers.get(timerName).isPaused();
}
/**
* Reset a timer, if the timer doesn't exist it is created
* @param timerName The timer name
*/
resetTimer(timerName: string): void {
if (!this._timers.containsKey(timerName)) {
this._timers.put(timerName, new gdjs.Timer(timerName));
}
this._timers.get(timerName).reset();
}
/**
* Pause a timer, if the timer doesn't exist it is created
* @param timerName The timer name
*/
pauseTimer(timerName: string): void {
if (!this._timers.containsKey(timerName)) {
this._timers.put(timerName, new gdjs.Timer(timerName));
}
this._timers.get(timerName).setPaused(true);
}
/**
* Unpause a timer, if the timer doesn't exist it is created
* @param timerName The timer name
*/
unpauseTimer(timerName: string): void {
if (!this._timers.containsKey(timerName)) {
this._timers.put(timerName, new gdjs.Timer(timerName));
}
this._timers.get(timerName).setPaused(false);
}
/**
* Remove a timer
* @param timerName The timer name
*/
removeTimer(timerName: string): void {
if (this._timers.containsKey(timerName)) {
this._timers.remove(timerName);
}
}
/**
* Get a timer elapsed time.
* @param timerName The timer name
* @return The timer elapsed time in seconds, 0 if the timer doesn't exist
*/
getTimerElapsedTimeInSeconds(timerName: string): float {
if (!this._timers.containsKey(timerName)) {
return 0;
}
return this._timers.get(timerName).getTime() / 1000.0;
}
//Other :
/**
* Separate the object from others objects, using their hitboxes.
* @param objects Objects
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
* @return true if the object was moved
*/
separateFromObjects(
objects: RuntimeObject[],
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
const hitBoxes = this.getHitBoxes();
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
}
/**
* Separate the object from others objects, using their hitboxes.
* @param objectsLists Tables of objects
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
* @return true if the object was moved
*/
separateFromObjectsList(
objectsLists: ObjectsLists,
ignoreTouchingEdges: boolean
): boolean {
let moved = false;
let xMove = 0;
let yMove = 0;
const hitBoxes = this.getHitBoxes();
for (const name in objectsLists.items) {
if (objectsLists.items.hasOwnProperty(name)) {
const objects = objectsLists.items[name];
//Check if their is a collision with each object
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
const otherHitBoxes = objects[i].getHitBoxes();
for (let k = 0, lenk = hitBoxes.length; k < lenk; ++k) {
for (let l = 0, lenl = otherHitBoxes.length; l < lenl; ++l) {
const result = gdjs.Polygon.collisionTest(
hitBoxes[k],
otherHitBoxes[l],
ignoreTouchingEdges
);
if (result.collision) {
xMove += result.move_axis[0];
yMove += result.move_axis[1];
moved = true;
}
}
}
}
}
}
}
//Move according to the results returned by the collision algorithm.
this.setPosition(this.getX() + xMove, this.getY() + yMove);
return moved;
}
/**
* Get the distance, in pixels, between *the center* of this object and another object.
* @param otherObject The other object
*/
getDistanceToObject(otherObject: gdjs.RuntimeObject): float {
return Math.sqrt(this.getSqDistanceToObject(otherObject));
}
/**
* Get the squared distance, in pixels, between *the center* of this object and another object.
* @param otherObject The other object
*/
getSqDistanceToObject(otherObject: gdjs.RuntimeObject): float {
if (otherObject === null) {
return 0;
}
const x =
this.getDrawableX() +
this.getCenterX() -
(otherObject.getDrawableX() + otherObject.getCenterX());
const y =
this.getDrawableY() +
this.getCenterY() -
(otherObject.getDrawableY() + otherObject.getCenterY());
return x * x + y * y;
}
/**
* Get the distance, in pixels, between *the center* of this object and a position.
* @param targetX Target X position
* @param targetY Target Y position
*/
getDistanceToPosition(targetX: float, targetY: float): float {
return Math.sqrt(this.getSqDistanceToPosition(targetX, targetY));
}
/**
* Get the squared distance, in pixels, between *the center* of this object and a position.
* @param targetX Target X position
* @param targetY Target Y position
*/
getSqDistanceToPosition(targetX: float, targetY: float): float {
const x = this.getDrawableX() + this.getCenterX() - targetX;
const y = this.getDrawableY() + this.getCenterY() - targetY;
return x * x + y * y;
}
/**
* Get the angle, in degrees, from the *object center* to another object.
* @param otherObject The other object
*/
getAngleToObject(otherObject: gdjs.RuntimeObject): float {
if (otherObject === null) {
return 0;
}
const x =
this.getDrawableX() +
this.getCenterX() -
(otherObject.getDrawableX() + otherObject.getCenterX());
const y =
this.getDrawableY() +
this.getCenterY() -
(otherObject.getDrawableY() + otherObject.getCenterY());
return gdjs.toDegrees(Math.atan2(-y, -x));
}
/**
* Compute the X position when given an angle and distance relative to the starting object.
* This is also known as getting the cartesian coordinates of a 2D vector, using its polar coordinates.
* @param angle The angle, in degrees.
* @param distance The distance from the object, in pixels
*/
getXFromAngleAndDistance(angle: float, distance: float): float {
return (
this.getDrawableX() +
this.getCenterX() +
distance * Math.cos(gdjs.toRad(angle))
);
}
/**
* Compute the Y position when given an angle and distance relative to the starting object.
* This is also known as getting the cartesian coordinates of a 2D vector, using its polar coordinates.
* @param angle The angle, in degrees.
* @param distance The distance from the object, in pixels
*/
getYFromAngleAndDistance(angle: float, distance: float): float {
return (
this.getDrawableY() +
this.getCenterY() +
distance * Math.sin(gdjs.toRad(angle))
);
}
/**
* Get the angle, in degrees, from the *object center* to a position.
* @param targetX Target X position
* @param targetY Target Y position
*/
getAngleToPosition(targetX: float, targetY: float): float {
const x = this.getDrawableX() + this.getCenterX() - targetX;
const y = this.getDrawableY() + this.getCenterY() - targetY;
return gdjs.toDegrees(Math.atan2(-y, -x));
}
/**
* Put the object around a position, with a specific distance and angle.
* The distance and angle are computed between the position and *the center of the object*.
*
* @param x The x position of the target
* @param y The y position of the target
* @param distance The distance between the object and the target, in pixels.
* @param angleInDegrees The angle between the object and the target, in degrees.
*/
putAround(
x: float,
y: float,
distance: float,
angleInDegrees: float
): void {
const angleInRadians = gdjs.toRad(angleInDegrees);
this.setCenterXInScene(x + Math.cos(angleInRadians) * distance);
this.setCenterYInScene(y + Math.sin(angleInRadians) * distance);
}
/**
* Put the object around another object, with a specific distance and angle.
* The distance and angle are computed between *the centers of the objects*.
*
* @param obj The target object
* @param distance The distance between the object and the target
* @param angleInDegrees The angle between the object and the target, in degrees.
*/
putAroundObject(
obj: gdjs.RuntimeObject,
distance: float,
angleInDegrees: float
): void {
this.putAround(
obj.getDrawableX() + obj.getCenterX(),
obj.getDrawableY() + obj.getCenterY(),
distance,
angleInDegrees
);
}
/**
* @deprecated
* @param objectsLists Tables of objects
*/
separateObjectsWithoutForces(objectsLists: ObjectsLists): void {
//Prepare the list of objects to iterate over.
const objects = gdjs.staticArray(
RuntimeObject.prototype.separateObjectsWithoutForces
);
objects.length = 0;
const lists = gdjs.staticArray2(
RuntimeObject.prototype.separateObjectsWithoutForces
);
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; ++i) {
objects.push.apply(objects, lists[i]);
}
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
if (this.getDrawableX() < objects[i].getDrawableX()) {
this.setX(objects[i].getDrawableX() - this.getWidth());
} else {
if (
this.getDrawableX() + this.getWidth() >
objects[i].getDrawableX() + objects[i].getWidth()
) {
this.setX(objects[i].getDrawableX() + objects[i].getWidth());
}
}
if (this.getDrawableY() < objects[i].getDrawableY()) {
this.setY(objects[i].getDrawableY() - this.getHeight());
} else {
if (
this.getDrawableY() + this.getHeight() >
objects[i].getDrawableY() + objects[i].getHeight()
) {
this.setY(objects[i].getDrawableY() + objects[i].getHeight());
}
}
}
}
}
/**
* @deprecated
* @param objectsLists Tables of objects
*/
separateObjectsWithForces(objectsLists: ObjectsLists): void {
//Prepare the list of objects to iterate over.
const objects = gdjs.staticArray(
RuntimeObject.prototype.separateObjectsWithForces
);
objects.length = 0;
const lists = gdjs.staticArray2(
RuntimeObject.prototype.separateObjectsWithForces
);
objectsLists.values(lists);
for (let i = 0, len = lists.length; i < len; ++i) {
objects.push.apply(objects, lists[i]);
}
for (let i = 0, len = objects.length; i < len; ++i) {
if (objects[i].id != this.id) {
if (
this.getDrawableX() + this.getCenterX() <
objects[i].getDrawableX() + objects[i].getCenterX()
) {
let av = this.hasNoForces() ? 0 : this.getAverageForce().getX();
this.addForce(-av - 10, 0, 0);
} else {
let av = this.hasNoForces() ? 0 : this.getAverageForce().getX();
this.addForce(-av + 10, 0, 0);
}
if (
this.getDrawableY() + this.getCenterY() <
objects[i].getDrawableY() + objects[i].getCenterY()
) {
let av = this.hasNoForces() ? 0 : this.getAverageForce().getY();
this.addForce(0, -av - 10, 0);
} else {
let av = this.hasNoForces() ? 0 : this.getAverageForce().getY();
this.addForce(0, -av + 10, 0);
}
}
}
}
/**
* Return true if the hitboxes of two objects are overlapping
* @static
* @param obj1 The first runtimeObject
* @param obj2 The second runtimeObject
* @param ignoreTouchingEdges If true, then edges that are touching each other, without the hitbox polygons actually overlapping, won't be considered in collision.
* @return true if obj1 and obj2 are in collision
*/
static collisionTest(
obj1: gdjs.RuntimeObject,
obj2: gdjs.RuntimeObject,
ignoreTouchingEdges: boolean
): boolean {
//First check if bounding circle are too far.
const o1centerX = obj1.getCenterX();
const o1centerY = obj1.getCenterY();
const obj1BoundingRadius = Math.sqrt(
computeSqBoundingRadius(
obj1.getWidth(),
obj1.getHeight(),
o1centerX,
o1centerY
)
);
const o2centerX = obj2.getCenterX();
const o2centerY = obj2.getCenterY();
const obj2BoundingRadius = Math.sqrt(
computeSqBoundingRadius(
obj2.getWidth(),
obj2.getHeight(),
o2centerX,
o2centerY
)
);
const diffX =
obj1.getDrawableX() + o1centerX - (obj2.getDrawableX() + o2centerX);
const diffY =
obj1.getDrawableY() + o1centerY - (obj2.getDrawableY() + o2centerY);
if (
Math.sqrt(diffX * diffX + diffY * diffY) >
obj1BoundingRadius + obj2BoundingRadius
) {
return false;
}
// Do a real check if necessary.
const hitBoxes1 = obj1.getHitBoxes();
const hitBoxes2 = obj2.getHitBoxes();
for (let k = 0, lenBoxes1 = hitBoxes1.length; k < lenBoxes1; ++k) {
for (let l = 0, lenBoxes2 = hitBoxes2.length; l < lenBoxes2; ++l) {
if (
gdjs.Polygon.collisionTest(
hitBoxes1[k],
hitBoxes2[l],
ignoreTouchingEdges
).collision
) {
return true;
}
}
}
return false;
}
/**
* @param x The raycast source X
* @param y The raycast source Y
* @param endX The raycast end position X
* @param endY The raycast end position Y
* @param closest Get the closest or farthest collision mask result?
* @return A raycast result with the contact points and distances
*/
raycastTest(
x: float,
y: float,
endX: float,
endY: float,
closest: boolean
): RaycastTestResult {
// First check if bounding circles are too far
const objCenterX = this.getCenterX();
const objCenterY = this.getCenterY();
const objSqBoundingRadius = computeSqBoundingRadius(
this.getWidth(),
this.getHeight(),
objCenterX,
objCenterY
);
const rayCenterWorldX = (x + endX) / 2;
const rayCenterWorldY = (y + endY) / 2;
const raySqBoundingRadius =
(endX - x) * (endX - x) + (endY - y) * (endY - y);
const diffX = this.getDrawableX() + objCenterX - rayCenterWorldX;
const diffY = this.getDrawableY() + objCenterY - rayCenterWorldY;
// @ts-ignore
let result = gdjs.Polygon.raycastTestStatics.result;
result.collision = false;
if (
// As an optimization, avoid computing the square root of the two boundings radius
// and the distance by comparing the squared values instead.
diffX * diffX + diffY * diffY >
objSqBoundingRadius +
raySqBoundingRadius +
2 * Math.sqrt(raySqBoundingRadius * objSqBoundingRadius)
) {
return result;
}
// Do a real check if necessary.
let testSqDist = closest ? raySqBoundingRadius : 0;
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < hitBoxes.length; i++) {
const res = gdjs.Polygon.raycastTest(hitBoxes[i], x, y, endX, endY);
if (res.collision) {
if (closest && res.closeSqDist < testSqDist) {
testSqDist = res.closeSqDist;
result = res;
} else {
if (
!closest &&
res.farSqDist > testSqDist &&
res.farSqDist <= raySqBoundingRadius
) {
testSqDist = res.farSqDist;
result = res;
}
}
}
}
return result;
}
/**
* Return true if the specified position is inside object bounding box.
*
* The position should be in "world" coordinates, i.e use gdjs.Layer.convertCoords
* if you need to pass the mouse or a touch position that you get from gdjs.InputManager.
*
*/
insideObject(x: float, y: float): boolean {
if (this.hitBoxesDirty) {
this.updateHitBoxes();
this.updateAABB();
this.hitBoxesDirty = false;
}
return (
this.aabb.min[0] <= x &&
this.aabb.max[0] >= x &&
this.aabb.min[1] <= y &&
this.aabb.max[1] >= y
);
}
/**
* Check the distance between two objects.
* @static
*/
static distanceTest(
obj1: RuntimeObject,
obj2: RuntimeObject,
distance: float
): boolean {
return obj1.getSqDistanceToObject(obj2) <= distance;
}
/**
* Return true if the cursor, or any touch, is on the object.
*
* @return true if the cursor, or any touch, is on the object.
*/
cursorOnObject(runtimeScene: RuntimeScene): boolean {
const inputManager = runtimeScene.getGame().getInputManager();
const layer = runtimeScene.getLayer(this.layer);
const mousePos = layer.convertCoords(
inputManager.getMouseX(),
inputManager.getMouseY()
);
if (this.insideObject(mousePos[0], mousePos[1])) {
return true;
}
const touchIds = inputManager.getAllTouchIdentifiers();
for (let i = 0; i < touchIds.length; ++i) {
const touchPos = layer.convertCoords(
inputManager.getTouchX(touchIds[i]),
inputManager.getTouchY(touchIds[i])
);
if (this.insideObject(touchPos[0], touchPos[1])) {
return true;
}
}
return false;
}
/**
* Check if a point is inside the object collision hitboxes.
* @param pointX The point x coordinate.
* @param pointY The point y coordinate.
* @return true if the point is inside the object collision hitboxes.
*/
isCollidingWithPoint(pointX: float, pointY: float): boolean {
const hitBoxes = this.getHitBoxes();
for (let i = 0; i < this.hitBoxes.length; ++i) {
if (gdjs.Polygon.isPointInside(hitBoxes[i], pointX, pointY)) {
return true;
}
}
return false;
}
/**
* Get the identifier associated to an object name.
* Some features may want to compare objects name a large number of time. In this case,
* it may be more efficient to compare objects name identifiers.
*
* @static
*/
static getNameIdentifier(name: string): integer {
if (RuntimeObject._identifiers.containsKey(name)) {
return RuntimeObject._identifiers.get(name);
}
RuntimeObject._newId = (RuntimeObject._newId || 0) + 1;
const newIdentifier = RuntimeObject._newId;
RuntimeObject._identifiers.put(name, newIdentifier);
return newIdentifier;
}
/**
* Table containing the id corresponding to an object name. Do not use directly or modify.
* @static
*/
static _identifiers = new Hashtable<integer>();
/**
* The next available unique identifier for an object. Do not use directly or modify.
* @static
*/
static _newId = 0;
/**
* Global container for unused forces, avoiding recreating forces each tick.
* @static
*/
static forcesGarbage: Array<gdjs.Force> = [];
getVariableNumber = RuntimeObject.getVariableNumber;
returnVariable = RuntimeObject.returnVariable;
getVariableString = RuntimeObject.getVariableString;
setVariableNumber = RuntimeObject.setVariableNumber;
setVariableString = RuntimeObject.setVariableString;
getVariableBoolean = RuntimeObject.getVariableBoolean;
setVariableBoolean = RuntimeObject.setVariableBoolean;
toggleVariableBoolean = RuntimeObject.toggleVariableBoolean;
variableChildExists = RuntimeObject.variableChildExists;
variableRemoveChild = RuntimeObject.variableRemoveChild;
variableClearChildren = RuntimeObject.variableClearChildren;
variablePushCopy = RuntimeObject.variablePushCopy;
valuePush = RuntimeObject.valuePush;
variableRemoveAt = RuntimeObject.variableRemoveAt;
/**
* Get the squared distance, in pixels, from the *object center* to a position.
* @param pointX X position
* @param pointY Y position
* @deprecated Use `getSqDistanceToPosition` instead.
*/
getSqDistanceTo = RuntimeObject.prototype.getSqDistanceToPosition;
}
gdjs.registerObject('', gdjs.RuntimeObject);
}