Skip to content

Commit

Permalink
Make culling more efficient
Browse files Browse the repository at this point in the history
  • Loading branch information
D8H committed Nov 23, 2023
1 parent a848764 commit 253ce76
Show file tree
Hide file tree
Showing 13 changed files with 1,001 additions and 665 deletions.
18 changes: 15 additions & 3 deletions Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down
3 changes: 2 additions & 1 deletion Extensions/TextInput/textinputruntimeobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ namespace gdjs {
}

getRendererObject() {
return null;
// The renderer is not a Pixi Object but it implements visible.
return this._renderer;
}

updateFromObjectData(
Expand Down
2 changes: 2 additions & 0 deletions GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
122 changes: 122 additions & 0 deletions GDJS/Runtime/ObjectManager.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
112 changes: 112 additions & 0 deletions GDJS/Runtime/ObjectSleepState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* GDevelop JS Platform
* Copyright 2023-2023 Florian Rival ([email protected]). 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,
}
}
}
23 changes: 2 additions & 21 deletions GDJS/Runtime/RuntimeInstanceContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion GDJS/Runtime/events-tools/inputtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ namespace gdjs {
};

export const cursorOnObject = function (
objectsLists: Hashtable<gdjs.RuntimeObject[]>,
objectsLists: ObjectsLists,
instanceContainer: gdjs.RuntimeInstanceContainer,
accurate: boolean,
inverted: boolean
Expand Down
Loading

0 comments on commit 253ce76

Please sign in to comment.