Compare commits

...

4 Commits

Author SHA1 Message Date
Davy Hélard
8a55078119 Fix types. 2023-11-24 15:57:08 +01:00
Davy Hélard
4417d7e958 Add some comments. 2023-11-24 15:55:06 +01:00
Davy Hélard
79cd327c68 Add tests. 2023-11-24 15:49:43 +01:00
Davy Hélard
c3580fd207 Make culling more efficient 2023-11-24 12:51:26 +01:00
14 changed files with 1145 additions and 680 deletions

View File

@@ -26,11 +26,12 @@ namespace gdjs {
);
};
class TextInputRuntimeObjectPixiRenderer {
class TextInputRuntimeObjectPixiRenderer implements RendererObjectInterface {
private _object: gdjs.TextInputRuntimeObject;
private _input: HTMLInputElement | HTMLTextAreaElement | null = null;
private _instanceContainer: gdjs.RuntimeInstanceContainer;
private _runtimeGame: gdjs.RuntimeGame;
private _isVisible = false;
constructor(
runtimeObject: gdjs.TextInputRuntimeObject,
@@ -113,14 +114,25 @@ namespace gdjs {
this._destroyElement();
}
//@ts-ignore
set visible(isVisible: boolean) {
this._isVisible = isVisible;
if (!this._input) return;
this._input.style.display = isVisible ? 'initial' : 'none';
}
//@ts-ignore
get visible(): boolean {
return this._isVisible;
}
updatePreRender() {
if (!this._input) return;
// Hide the input entirely if the object is hidden.
// Because this object is rendered as a DOM element (and not part of the PixiJS
// scene graph), we have to do this manually.
if (this._object.isHidden()) {
this._input.style.display = 'none';
if (!this._isVisible) {
return;
}

View File

@@ -102,7 +102,8 @@ namespace gdjs {
}
getRendererObject() {
return null;
// The renderer is not a Pixi Object but it implements visible.
return this._renderer;
}
updateFromObjectData(

View File

@@ -670,6 +670,8 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "ResourceCache.js");
InsertUnique(includesFiles, "timemanager.js");
InsertUnique(includesFiles, "polygon.js");
InsertUnique(includesFiles, "ObjectSleepState.js");
InsertUnique(includesFiles, "ObjectManager.js");
InsertUnique(includesFiles, "runtimeobject.js");
InsertUnique(includesFiles, "profiler.js");
InsertUnique(includesFiles, "RuntimeInstanceContainer.js");

View File

@@ -0,0 +1,122 @@
namespace gdjs {
/**
* Allow to do spacial searches on objects as fast as possible.
*
* Objects are put in an R-Tree only if they didn't move recently to avoid to
* update the R-Tree too often.
*/
export class ObjectManager {
private _allInstances: Array<RuntimeObject> = [];
private _awakeInstances: Array<RuntimeObject> = [];
private _rbush: RBush<RuntimeObject>;
constructor() {
this._rbush = new RBush<RuntimeObject>();
}
_destroy(): void {
this._allInstances = [];
this._awakeInstances = [];
this._rbush.clear();
}
search(
searchArea: SearchArea,
results: Array<RuntimeObject>
): Array<RuntimeObject> {
let instances = this._allInstances;
if (instances.length >= 8) {
this._rbush.search(searchArea, results);
instances = this._awakeInstances;
}
for (const instance of instances) {
// TODO Allow to use getAABB to optimize collision conditions
const aabb = instance.getVisibilityAABB();
if (
!aabb ||
(aabb.min[0] <= searchArea.maxX &&
aabb.max[0] >= searchArea.minX &&
aabb.min[1] <= searchArea.maxY &&
aabb.max[1] >= searchArea.minY)
) {
results.push(instance);
}
}
return results;
}
private _onWakingUp(object: RuntimeObject): void {
this._rbush.remove(object._rtreeAABB);
this._awakeInstances.push(object);
}
private _onFallenAsleep(object: RuntimeObject): void {
// TODO Allow to use getAABB to optimize collision conditions
const objectAABB = object.getVisibilityAABB();
if (!objectAABB) {
return;
}
this._rbush.remove(object._rtreeAABB);
object._rtreeAABB.minX = objectAABB.min[0];
object._rtreeAABB.minY = objectAABB.min[1];
object._rtreeAABB.maxX = objectAABB.max[0];
object._rtreeAABB.maxY = objectAABB.max[1];
this._rbush.insert(object._rtreeAABB);
}
updateAwakeObjects(): void {
gdjs.ObjectSleepState.updateAwakeObjects(
this._awakeInstances,
(object) => object.getSpatialSearchSleepState(),
(object) => this._onFallenAsleep(object),
(object) => this._onWakingUp(object)
);
}
getAllInstances(): Array<RuntimeObject> {
return this._allInstances;
}
getAwakeInstances(): Array<RuntimeObject> {
return this._awakeInstances;
}
/**
* Add an object to the instances living in the container.
* @param obj The object to be added.
*/
addObject(object: gdjs.RuntimeObject): void {
this._allInstances.push(object);
this._awakeInstances.push(object);
}
/**
* Must be called whenever an object must be removed from the container.
* @param object The object to be removed.
*/
deleteObject(object: gdjs.RuntimeObject): boolean {
const objId = object.id;
let isObjectDeleted = false;
for (let i = 0, len = this._allInstances.length; i < len; ++i) {
if (this._allInstances[i].id == objId) {
this._allInstances.splice(i, 1);
isObjectDeleted = true;
break;
}
}
// TODO Maybe the state could be used but it would be more prone to errors.
let isAwake = false;
for (let i = 0, len = this._awakeInstances.length; i < len; ++i) {
if (this._awakeInstances[i].id == objId) {
this._awakeInstances.splice(i, 1);
isAwake = true;
break;
}
}
if (!isAwake) {
this._rbush.remove(object._rtreeAABB);
}
return isObjectDeleted;
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* GDevelop JS Platform
* Copyright 2023-2023 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
export class ObjectSleepState {
private static readonly framesBeforeSleep = 60;
private _object: RuntimeObject;
private _isNeedingToBeAwake: () => boolean;
private _state: ObjectSleepState.State;
private _lastActivityFrameIndex: integer;
private _onWakingUpCallbacks: Array<(object: RuntimeObject) => void> = [];
constructor(
object: RuntimeObject,
isNeedingToBeAwake: () => boolean,
initialSleepState: ObjectSleepState.State
) {
this._object = object;
this._isNeedingToBeAwake = isNeedingToBeAwake;
this._state = initialSleepState;
this._lastActivityFrameIndex = this._object
.getRuntimeScene()
.getFrameIndex();
}
canSleep(): boolean {
return (
this._state === gdjs.ObjectSleepState.State.CanSleepThisFrame ||
this._object.getRuntimeScene().getFrameIndex() -
this._lastActivityFrameIndex >=
ObjectSleepState.framesBeforeSleep
);
}
isAwake(): boolean {
return this._state !== gdjs.ObjectSleepState.State.ASleep;
}
_forceToSleep(): void {
if (!this.isAwake()) {
return;
}
this._lastActivityFrameIndex = Number.NEGATIVE_INFINITY;
}
wakeUp() {
const object = this._object;
this._lastActivityFrameIndex = object.getRuntimeScene().getFrameIndex();
if (this.isAwake()) {
return;
}
this._state = gdjs.ObjectSleepState.State.AWake;
for (const onWakingUp of this._onWakingUpCallbacks) {
onWakingUp(object);
}
}
registerOnWakingUp(onWakingUp: (object: RuntimeObject) => void) {
this._onWakingUpCallbacks.push(onWakingUp);
}
tryToSleep(): void {
if (
this._lastActivityFrameIndex !== Number.NEGATIVE_INFINITY &&
this._isNeedingToBeAwake()
) {
this._lastActivityFrameIndex = this._object
.getRuntimeScene()
.getFrameIndex();
}
}
static updateAwakeObjects(
awakeObjects: Array<RuntimeObject>,
getSleepState: (object: RuntimeObject) => ObjectSleepState,
onFallenAsleep: (object: RuntimeObject) => void,
onWakingUp: (object: RuntimeObject) => void
) {
let writeIndex = 0;
for (let readIndex = 0; readIndex < awakeObjects.length; readIndex++) {
const object = awakeObjects[readIndex];
const sleepState = getSleepState(object);
sleepState.tryToSleep();
if (sleepState.canSleep() || !sleepState.isAwake()) {
if (sleepState.isAwake()) {
// Avoid onWakingUp to be called if some managers didn't have time
// to update their awake object list.
sleepState._onWakingUpCallbacks.length = 0;
}
sleepState._state = gdjs.ObjectSleepState.State.ASleep;
onFallenAsleep(object);
sleepState._onWakingUpCallbacks.push(onWakingUp);
} else {
awakeObjects[writeIndex] = object;
writeIndex++;
}
}
awakeObjects.length = writeIndex;
return awakeObjects;
}
}
export namespace ObjectSleepState {
export enum State {
ASleep,
CanSleepThisFrame,
AWake,
}
}
}

View File

@@ -34,7 +34,6 @@ namespace gdjs {
_layers: Hashtable<RuntimeLayer>;
_orderedLayers: RuntimeLayer[]; // TODO: should this be a single structure with _layers, to enforce its usage?
_layersCameraCoordinates: Record<string, [float, float, float, float]> = {};
// Options for the debug draw:
_debugDrawEnabled: boolean = false;
@@ -351,26 +350,6 @@ namespace gdjs {
}
}
_updateLayersCameraCoordinates(scale: float) {
this._layersCameraCoordinates = this._layersCameraCoordinates || {};
for (const name in this._layers.items) {
if (this._layers.items.hasOwnProperty(name)) {
const theLayer = this._layers.items[name];
this._layersCameraCoordinates[name] = this._layersCameraCoordinates[
name
] || [0, 0, 0, 0];
this._layersCameraCoordinates[name][0] =
theLayer.getCameraX() - (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name][1] =
theLayer.getCameraY() - (theLayer.getCameraHeight() / 2) * scale;
this._layersCameraCoordinates[name][2] =
theLayer.getCameraX() + (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name][3] =
theLayer.getCameraY() + (theLayer.getCameraHeight() / 2) * scale;
}
}
}
/**
* Called to update effects of layers before rendering.
*/
@@ -625,6 +604,8 @@ namespace gdjs {
return;
}
onObjectChangedOfLayer(object: RuntimeObject, oldLayer: RuntimeLayer) {}
/**
* Get the layer with the given name
* @param name The name of the layer

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,8 @@ namespace gdjs {
return true;
};
type RuntimeObjectCallback = (object: gdjs.RuntimeObject) => void;
/**
* RuntimeObject represents an object being used on a RuntimeScene.
*
@@ -164,9 +166,12 @@ namespace gdjs {
layer: string = '';
protected _nameId: integer;
protected _livingOnScene: boolean = true;
protected _spatialSearchSleepState: ObjectSleepState;
readonly id: integer;
private destroyCallbacks = new Set<() => void>();
// HitboxChanges happen a lot, an Array is faster to iterate.
private hitBoxChangedCallbacks: Array<RuntimeObjectCallback> = [];
_runtimeScene: gdjs.RuntimeInstanceContainer;
/**
@@ -181,12 +186,16 @@ namespace gdjs {
* not "thread safe" or "re-entrant algorithm" safe.
*/
pick: boolean = false;
pickingId: integer = 0;
//Hit boxes:
protected _defaultHitBoxes: gdjs.Polygon[] = [];
protected hitBoxes: gdjs.Polygon[];
protected hitBoxesDirty: boolean = true;
// TODO use a different AABB for collision mask and rendered image.
protected aabb: AABB = { min: [0, 0], max: [0, 0] };
_rtreeAABB: SearchedItem<RuntimeObject>;
protected _isIncludedInParentCollisionMask = true;
//Variables:
@@ -229,10 +238,11 @@ namespace gdjs {
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: ObjectData & any
) {
const scene = instanceContainer.getScene();
this.name = objectData.name || '';
this.type = objectData.type || '';
this._nameId = RuntimeObject.getNameIdentifier(this.name);
this.id = instanceContainer.getScene().createNewUniqueId();
this.id = scene.createNewUniqueId();
this._runtimeScene = instanceContainer;
this._defaultHitBoxes.push(gdjs.Polygon.createRectangle(0, 0));
this.hitBoxes = this._defaultHitBoxes;
@@ -241,8 +251,20 @@ namespace gdjs {
);
this._totalForce = new gdjs.Force(0, 0, 0);
this._behaviorsTable = new Hashtable();
this._rtreeAABB = {
source: this,
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
};
this._spatialSearchSleepState = new gdjs.ObjectSleepState(
this,
() => !this.getVisibilityAABB(),
gdjs.ObjectSleepState.State.CanSleepThisFrame
);
for (let i = 0; i < objectData.effects.length; ++i) {
this._runtimeScene
scene
.getGame()
.getEffectsManager()
.initializeEffect(objectData.effects[i], this._rendererEffects, this);
@@ -439,6 +461,14 @@ namespace gdjs {
return false;
}
getSpatialSearchSleepState(): ObjectSleepState {
return this._spatialSearchSleepState;
}
isAlive(): boolean {
return this._livingOnScene;
}
/**
* Remove an object from a scene.
*
@@ -486,6 +516,31 @@ namespace gdjs {
onDestroyed(): void {}
registerHitboxChangedCallback(callback: RuntimeObjectCallback) {
if (this.hitBoxChangedCallbacks.includes(callback)) {
return;
}
this.hitBoxChangedCallbacks.push(callback);
}
/**
* Send a signal that the object hitboxes are no longer up to date.
*
* The signal is propagated to parents so
* {@link gdjs.RuntimeObject.hitBoxesDirty} should never be modified
* directly.
*/
invalidateHitboxes(): void {
// TODO EBO Check that no community extension set hitBoxesDirty to true
// directly.
this.hitBoxesDirty = true;
this._spatialSearchSleepState.wakeUp();
this._runtimeScene.onChildrenLocationChanged();
for (const callback of this.hitBoxChangedCallbacks) {
callback(this);
}
}
/**
* Called whenever the scene owning the object is paused.
* This should *not* impact objects, but some may need to inform their renderer.
@@ -570,20 +625,6 @@ namespace gdjs {
this.invalidateHitboxes();
}
/**
* Send a signal that the object hitboxes are no longer up to date.
*
* The signal is propagated to parents so
* {@link gdjs.RuntimeObject.hitBoxesDirty} should never be modified
* directly.
*/
invalidateHitboxes(): void {
// TODO EBO Check that no community extension set hitBoxesDirty to true
// directly.
this.hitBoxesDirty = true;
this._runtimeScene.onChildrenLocationChanged();
}
/**
* Get the X position of the object.
*
@@ -758,6 +799,7 @@ namespace gdjs {
oldLayer.getRenderer().remove3DRendererObject(rendererObject3D);
newLayer.getRenderer().add3DRendererObject(rendererObject3D);
}
this._runtimeScene.onObjectChangedOfLayer(this, oldLayer);
}
/**

View File

@@ -6,6 +6,7 @@
namespace gdjs {
const logger = new gdjs.Logger('RuntimeScene');
const setupWarningLogger = new gdjs.Logger('RuntimeScene (setup warnings)');
type SearchArea = { minX: float; minY: float; maxX: float; maxY: float };
/**
* A scene being played, containing instances of objects rendered on screen.
@@ -45,6 +46,17 @@ namespace gdjs {
_cachedGameResolutionWidth: integer;
_cachedGameResolutionHeight: integer;
private _frameIndex: integer = 0;
_layersCameraCoordinates: Record<string, SearchArea> = {};
private _layerObjectManagers = new Map<string, ObjectManager>();
/**
* Objects that were rendered for the last frame.
*
* They keep to be hide back without iterating every objects from the scene.
*/
private _objectsInsideCamera: Record<string, Array<RuntimeObject>> = {};
/**
* @param runtimeGame The game associated to this scene.
*/
@@ -81,6 +93,43 @@ namespace gdjs {
this._orderedLayers.push(layer);
}
addObject(object: gdjs.RuntimeObject): void {
super.addObject(object);
this._addObjectToLayerObjectManager(object);
}
onObjectChangedOfLayer(object: RuntimeObject, oldLayer: RuntimeLayer) {
this._removeObjectFromLayerObjectManager(object, oldLayer.getName());
this._addObjectToLayerObjectManager(object);
}
private _addObjectToLayerObjectManager(object: gdjs.RuntimeObject): void {
const layerName = object.getLayer();
let objectManager = this._layerObjectManagers.get(layerName);
if (!objectManager) {
objectManager = new gdjs.ObjectManager();
this._layerObjectManagers.set(layerName, objectManager);
}
objectManager.addObject(object);
}
markObjectForDeletion(object: gdjs.RuntimeObject): void {
super.markObjectForDeletion(object);
const layerName = object.getLayer();
this._removeObjectFromLayerObjectManager(object, layerName);
}
private _removeObjectFromLayerObjectManager(
object: gdjs.RuntimeObject,
layerName: string
): void {
let objectManager = this._layerObjectManagers.get(layerName);
if (!objectManager) {
return;
}
objectManager.deleteObject(object);
}
/**
* Should be called when the canvas where the scene is rendered has been resized.
* See gdjs.RuntimeGame.startGameLoop in particular.
@@ -427,6 +476,7 @@ namespace gdjs {
if (this._profiler) {
this._profiler.endFrame();
}
this._frameIndex++;
return !!this.getRequestedChange();
}
@@ -437,6 +487,26 @@ namespace gdjs {
this._renderer.render();
}
_updateLayersCameraCoordinates(scale: float) {
this._layersCameraCoordinates = this._layersCameraCoordinates || {};
for (const name in this._layers.items) {
if (this._layers.items.hasOwnProperty(name)) {
const theLayer = this._layers.items[name];
this._layersCameraCoordinates[name] = this._layersCameraCoordinates[
name
] || { minX: 0, minY: 0, maxX: 0, maxY: 0 };
this._layersCameraCoordinates[name].minX =
theLayer.getCameraX() - (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name].minY =
theLayer.getCameraY() - (theLayer.getCameraHeight() / 2) * scale;
this._layersCameraCoordinates[name].maxX =
theLayer.getCameraX() + (theLayer.getCameraWidth() / 2) * scale;
this._layersCameraCoordinates[name].maxY =
theLayer.getCameraY() + (theLayer.getCameraHeight() / 2) * scale;
}
}
}
/**
* Called to update visibility of the renderers of objects
* rendered on the scene ("culling"), update effects (of visible objects)
@@ -446,50 +516,68 @@ namespace gdjs {
* object is too far from the camera of its layer ("culling").
*/
_updateObjectsPreRender() {
// Check awake objects only once every 64 frames.
if ((this._frameIndex & 63) === 0) {
for (const objectManager of this._layerObjectManagers.values()) {
objectManager.updateAwakeObjects();
}
}
if (this._timeManager.isFirstFrame()) {
super._updateObjectsPreRender();
return;
} else {
// After first frame, optimise rendering by setting only objects
// near camera as visible.
// TODO: For compatibility, pass a scale of `2`,
// meaning that size of cameras will be multiplied by 2 and so objects
// will be hidden if they are outside of this *larger* camera area.
// This is useful for:
// - objects not properly reporting their visibility AABB,
// (so we have a "safety margin") but these objects should be fixed
// instead.
// - objects having effects rendering outside of their visibility AABB.
// TODO (3D) culling - add support for 3D object culling?
this._updateLayersCameraCoordinates(2);
const allInstancesList = this.getAdhocListOfAllInstances();
for (let i = 0, len = allInstancesList.length; i < len; ++i) {
const object = allInstancesList[i];
const rendererObject = object.getRendererObject();
if (rendererObject) {
if (object.isHidden()) {
rendererObject.visible = false;
} else {
const cameraCoords = this._layersCameraCoordinates[
object.getLayer()
];
if (!cameraCoords) {
continue;
}
const aabb = object.getVisibilityAABB();
rendererObject.visible =
// If no AABB is returned, the object should always be visible
!aabb ||
// If an AABB is there, it must be at least partially inside
// the camera bounds.
!(
aabb.min[0] > cameraCoords[2] ||
aabb.min[1] > cameraCoords[3] ||
aabb.max[0] < cameraCoords[0] ||
aabb.max[1] < cameraCoords[1]
);
}
rendererObject.visible = false;
}
}
}
// After first frame, optimise rendering by setting only objects
// near camera as visible.
// TODO: For compatibility, pass a scale of `2`,
// meaning that size of cameras will be multiplied by 2 and so objects
// will be hidden if they are outside of this *larger* camera area.
// This is useful for:
// - objects not properly reporting their visibility AABB,
// (so we have a "safety margin") but these objects should be fixed
// instead.
// - objects having effects rendering outside of their visibility AABB.
// TODO (3D) culling - add support for 3D object culling?
this._updateLayersCameraCoordinates(2);
// Reset objects that were visible last frame.
for (const layerName in this._objectsInsideCamera) {
for (const object of this._objectsInsideCamera[layerName]) {
const rendererObject = object.getRendererObject();
if (rendererObject) {
rendererObject.visible = false;
}
}
}
for (const layerName in this._layers.items) {
const cameraAABB = this._layersCameraCoordinates[layerName];
let objectsInsideCamera = this._objectsInsideCamera[layerName];
if (objectsInsideCamera === undefined) {
objectsInsideCamera = [];
this._objectsInsideCamera[layerName] = objectsInsideCamera;
}
if (!cameraAABB) {
continue;
}
const layerObjectManager = this._layerObjectManagers.get(layerName);
if (!layerObjectManager) {
continue;
}
// Find objects that are visible this frame.
objectsInsideCamera.length = 0;
layerObjectManager.search(cameraAABB, objectsInsideCamera);
for (const object of objectsInsideCamera) {
const rendererObject = object.getRendererObject();
if (rendererObject) {
rendererObject.visible = !object.isHidden();
// Update effects, only for visible objects.
if (rendererObject.visible) {
@@ -734,6 +822,10 @@ namespace gdjs {
sceneJustResumed(): boolean {
return this._isJustResumed;
}
getFrameIndex(): integer {
return this._frameIndex;
}
}
//The flags to describe the change request by a scene:

19
GDJS/Runtime/types/rbush.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
type SearchArea = { minX: float; minY: float; maxX: float; maxY: float };
type SearchedItem<T> = {
source: T;
minX: float;
minY: float;
maxX: float;
maxY: float;
};
declare class RBush<T> {
constructor(maxEntries?: number);
search(bbox: SearchArea, result?: Array<T>): Array<T>;
insert(item: SearchedItem<T>): RBush<T>;
clear(): RBush<T>;
remove(
item: SearchedItem<T>,
equalsFn?: (item: SearchedItem<T>, otherItem: SearchedItem<T>) => boolean
): RBush<T>;
}

View File

@@ -53,6 +53,8 @@ module.exports = function (config) {
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',
'./newIDE/app/resources/GDJS/Runtime/polygon.js',
'./newIDE/app/resources/GDJS/Runtime/ObjectSleepState.js',
'./newIDE/app/resources/GDJS/Runtime/ObjectManager.js',
'./newIDE/app/resources/GDJS/Runtime/runtimeobject.js',
'./newIDE/app/resources/GDJS/Runtime/RuntimeInstanceContainer.js',
'./newIDE/app/resources/GDJS/Runtime/runtimescene.js',

View File

@@ -3,12 +3,15 @@
*/
gdjs.TestRuntimeScene = class TestRuntimeScene extends gdjs.RuntimeScene {
/**
* @param {gdjs.RuntimeGame} runtimeGame
* @param {gdjs.RuntimeGame} runtimeGame
*/
constructor(runtimeGame) {
constructor(runtimeGame, layerNames = ['']) {
super(runtimeGame);
this.addLayer({ name: '', cameras: [], effects: [] });
for (const layerName of layerNames) {
this.addLayer({ name: layerName, cameras: [], effects: [] });
}
}
/**

View File

@@ -20,7 +20,32 @@
/** @type {?float} */
_customCenterY = null;
constructor(runtimeScene, objectData) {
constructor(runtimeScene, objectData = {
name: 'MySprite',
type: '',
behaviors: [],
effects: [],
animations: [
{
name: 'animation',
directions: [
{
sprites: [
{
originPoint: { x: 0, y: 0 },
centerPoint: { x: 0, y: 0 },
points: [
{ name: 'Center', x: 0, y: 0 },
{ name: 'Origin', x: 0, y: 0 },
],
hasCustomCollisionMask: false,
},
],
},
],
},
],
}) {
// *ALWAYS* call the base gdjs.RuntimeObject constructor.
super(runtimeScene, objectData);
@@ -50,10 +75,6 @@
this.invalidateHitboxes();
}
getRendererObject() {
return null;
}
getWidth() {
return this._customWidth;
}

View File

@@ -0,0 +1,105 @@
// @ts-check
describe('gdjs.RuntimeScene culling tests', () => {
it('should hide objects moving outside of the screen', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game, ['', 'MyLayer']);
scene.getLayer('MyLayer').setCameraX(8000);
const object = new gdjs.TestSpriteRuntimeObject(scene);
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(100, 100);
scene.addObject(object);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(true);
object.setY(8000);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(false);
});
it('should show objects moving back in the screen', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game, ['']);
const object = new gdjs.TestSpriteRuntimeObject(scene);
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(100, 100);
scene.addObject(object);
object.setY(8000);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(false);
object.setY(200);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(true);
});
it('should handle objects changing of layer', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game, ['']);
const object = new gdjs.TestSpriteRuntimeObject(scene);
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(100, 100);
scene.addObject(object);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(true);
object.setLayer('MyLayer');
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(false);
object.setLayer('');
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(true);
});
// This is important to avoid games with big levels to be CPU limited during
// the 2 first seconds.
it('should allow objects to sleep at the end of the 1st frame', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game, ['']);
const object = new gdjs.TestSpriteRuntimeObject(scene);
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(100, 100);
scene.addObject(object);
scene.renderAndStep(1000 / 60);
expect(object.getRendererObject().visible).to.be(true);
expect(object.getSpatialSearchSleepState().isAwake()).to.be(false);
object.setY(200);
expect(object.getSpatialSearchSleepState().isAwake()).to.be(true);
});
it('should put objects to sleep when they don\'t move for 2 seconds', () => {
const game = gdjs.getPixiRuntimeGame();
const scene = new gdjs.TestRuntimeScene(game, ['']);
const object = new gdjs.TestSpriteRuntimeObject(scene);
object.setUnscaledWidthAndHeight(100, 100);
object.setCustomWidthAndHeight(100, 100);
scene.addObject(object);
scene.renderAndStep(1000 / 60);
object.setY(200);
expect(object.getSpatialSearchSleepState().isAwake()).to.be(true);
// Objects can sleep every 64 frames if they haven't moved for 1 second.
for (let index = 0; index < 60 + 64; index++) {
scene.renderAndStep(1000 / 60);
}
expect(object.getSpatialSearchSleepState().isAwake()).to.be(false);
expect(object.getRendererObject().visible).to.be(true);
});
});