diff --git a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts index 030c7c32e548..8f5cb01aecb2 100644 --- a/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts +++ b/Extensions/TextInput/textinputruntimeobject-pixi-renderer.ts @@ -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; } diff --git a/Extensions/TextInput/textinputruntimeobject.ts b/Extensions/TextInput/textinputruntimeobject.ts index 9233b00b2c12..d51ae5a3cf2b 100644 --- a/Extensions/TextInput/textinputruntimeobject.ts +++ b/Extensions/TextInput/textinputruntimeobject.ts @@ -102,7 +102,8 @@ namespace gdjs { } getRendererObject() { - return null; + // The renderer is not a Pixi Object but it implements visible. + return this._renderer; } updateFromObjectData( diff --git a/GDJS/GDJS/IDE/ExporterHelper.cpp b/GDJS/GDJS/IDE/ExporterHelper.cpp index bcf86307b965..781565ceac6b 100644 --- a/GDJS/GDJS/IDE/ExporterHelper.cpp +++ b/GDJS/GDJS/IDE/ExporterHelper.cpp @@ -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"); diff --git a/GDJS/Runtime/ObjectManager.ts b/GDJS/Runtime/ObjectManager.ts new file mode 100644 index 000000000000..ca9d3d98d9ee --- /dev/null +++ b/GDJS/Runtime/ObjectManager.ts @@ -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 = []; + private _awakeInstances: Array = []; + private _rbush: RBush; + + constructor() { + this._rbush = new RBush(); + } + + _destroy(): void { + this._allInstances = []; + this._awakeInstances = []; + this._rbush.clear(); + } + + search( + searchArea: SearchArea, + results: Array + ): Array { + 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 { + return this._allInstances; + } + + getAwakeInstances(): Array { + 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; + } + } +} diff --git a/GDJS/Runtime/ObjectSleepState.ts b/GDJS/Runtime/ObjectSleepState.ts new file mode 100644 index 000000000000..59ebaa238a56 --- /dev/null +++ b/GDJS/Runtime/ObjectSleepState.ts @@ -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, + 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, + } + } +} diff --git a/GDJS/Runtime/RuntimeInstanceContainer.ts b/GDJS/Runtime/RuntimeInstanceContainer.ts index 1b65020907f9..e9c5799d79bc 100644 --- a/GDJS/Runtime/RuntimeInstanceContainer.ts +++ b/GDJS/Runtime/RuntimeInstanceContainer.ts @@ -34,7 +34,6 @@ namespace gdjs { _layers: Hashtable; _orderedLayers: RuntimeLayer[]; // TODO: should this be a single structure with _layers, to enforce its usage? - _layersCameraCoordinates: Record = {}; // 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 diff --git a/GDJS/Runtime/libs/rbush.js b/GDJS/Runtime/libs/rbush.js index 96c70f1a4354..cba95bc1bf0d 100644 --- a/GDJS/Runtime/libs/rbush.js +++ b/GDJS/Runtime/libs/rbush.js @@ -1,624 +1,575 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.rbush = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o left) { + if (right - left > 600) { + var n = right - left + 1; + var m = k - left + 1; + var z = Math.log(n); + var s = 0.5 * Math.exp(2 * z / 3); + var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); + var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); + var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); + quickselectStep(arr, k, newLeft, newRight, compare); + } + + var t = arr[k]; + var i = left; + var j = right; + + swap(arr, left, k); + if (compare(arr[right], t) > 0) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (compare(arr[i], t) < 0) i++; + while (compare(arr[j], t) > 0) j--; + } + + if (compare(arr[left], t) === 0) swap(arr, left, j); + else { + j++; + swap(arr, j, right); } - node = nodesToSearch.pop(); + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } + } + + function swap(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + + function defaultCompare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; + } + + class RBush { + constructor(maxEntries = 9) { + // max entries in a node is 9 by default; min node fill is 40% for best performance + this._maxEntries = Math.max(4, maxEntries); + this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); + this.clear(); } - - return result; - }, - - collides: function (bbox) { - - var node = this.data, - toBBox = this.toBBox; - - if (!intersects(bbox, node)) return false; - - var nodesToSearch = [], - i, len, child, childBBox; - - while (node) { - for (i = 0, len = node.children.length; i < len; i++) { - - child = node.children[i]; - childBBox = node.leaf ? toBBox(child) : child; - - if (intersects(bbox, childBBox)) { - if (node.leaf || contains(bbox, childBBox)) return true; - nodesToSearch.push(child); + + all() { + return this._all(this.data, []); + } + + search(bbox, result = []) { + let node = this.data; + + if (!intersects(bbox, node)) return result; + + const toBBox = this.toBBox; + const nodesToSearch = []; + + while (node) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const childBBox = node.leaf ? toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf) result.push(child.source || child); + else if (contains(bbox, childBBox)) this._all(child, result); + else nodesToSearch.push(child); + } } + node = nodesToSearch.pop(); } - node = nodesToSearch.pop(); + + return result; } - - return false; - }, - - load: function (data) { - if (!(data && data.length)) return this; - - if (data.length < this._minEntries) { - for (var i = 0, len = data.length; i < len; i++) { - this.insert(data[i]); + + collides(bbox) { + let node = this.data; + + if (!intersects(bbox, node)) return false; + + const nodesToSearch = []; + while (node) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const childBBox = node.leaf ? this.toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf || contains(bbox, childBBox)) return true; + nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); } - return this; + + return false; } - - // recursively build the tree with the given data from scratch using OMT algorithm - var node = this._build(data.slice(), 0, data.length - 1, 0); - - if (!this.data.children.length) { - // save as is if tree is empty - this.data = node; - - } else if (this.data.height === node.height) { - // split root if trees have the same height - this._splitRoot(this.data, node); - - } else { - if (this.data.height < node.height) { - // swap trees if inserted one is bigger - var tmpNode = this.data; + + load(data) { + if (!(data && data.length)) return this; + + if (data.length < this._minEntries) { + for (let i = 0; i < data.length; i++) { + this.insert(data[i]); + } + return this; + } + + // recursively build the tree with the given data from scratch using OMT algorithm + let node = this._build(data.slice(), 0, data.length - 1, 0); + + if (!this.data.children.length) { + // save as is if tree is empty this.data = node; - node = tmpNode; + + } else if (this.data.height === node.height) { + // split root if trees have the same height + this._splitRoot(this.data, node); + + } else { + if (this.data.height < node.height) { + // swap trees if inserted one is bigger + const tmpNode = this.data; + this.data = node; + node = tmpNode; + } + + // insert the small tree into the large tree at appropriate level + this._insert(node, this.data.height - node.height - 1, true); } - - // insert the small tree into the large tree at appropriate level - this._insert(node, this.data.height - node.height - 1, true); + + return this; } - - return this; - }, - - insert: function (item) { - if (item) this._insert(item, this.data.height - 1); - return this; - }, - - clear: function () { - this.data = createNode([]); - return this; - }, - - remove: function (item, equalsFn) { - if (!item) return this; - - var node = this.data, - bbox = this.toBBox(item), - path = [], - indexes = [], - i, parent, index, goingUp; - - // depth-first iterative tree traversal - while (node || path.length) { - - if (!node) { // go up - node = path.pop(); - parent = path[path.length - 1]; - i = indexes.pop(); - goingUp = true; - } - - if (node.leaf) { // check current node - index = findItem(item, node.children, equalsFn); - - if (index !== -1) { - // item found, remove the item and condense tree upwards - node.children.splice(index, 1); - path.push(node); - this._condense(path); - return this; + + insert(item) { + if (item) this._insert(item, this.data.height - 1); + return this; + } + + clear() { + this.data = createNode([]); + return this; + } + + remove(item, equalsFn) { + if (!item) return this; + + let node = this.data; + const bbox = this.toBBox(item); + const path = []; + const indexes = []; + let i, parent, goingUp; + + // depth-first iterative tree traversal + while (node || path.length) { + + if (!node) { // go up + node = path.pop(); + parent = path[path.length - 1]; + i = indexes.pop(); + goingUp = true; } + + if (node.leaf) { // check current node + const index = findItem(item, node.children, equalsFn); + + if (index !== -1) { + // item found, remove the item and condense tree upwards + node.children.splice(index, 1); + path.push(node); + this._condense(path); + return this; + } + } + + if (!goingUp && !node.leaf && contains(node, bbox)) { // go down + path.push(node); + indexes.push(i); + i = 0; + parent = node; + node = node.children[0]; + + } else if (parent) { // go right + i++; + node = parent.children[i]; + goingUp = false; + + } else node = null; // nothing found } - - if (!goingUp && !node.leaf && contains(node, bbox)) { // go down - path.push(node); - indexes.push(i); - i = 0; - parent = node; - node = node.children[0]; - - } else if (parent) { // go right - i++; - node = parent.children[i]; - goingUp = false; - - } else node = null; // nothing found + + return this; + } + + toBBox(item) { return item; } + + compareMinX(a, b) { return a.minX - b.minX; } + compareMinY(a, b) { return a.minY - b.minY; } + + toJSON() { return this.data; } + + fromJSON(data) { + this.data = data; + return this; } - - return this; - }, - - toBBox: function (item) { return item; }, - - compareMinX: compareNodeMinX, - compareMinY: compareNodeMinY, - - toJSON: function () { return this.data; }, - - fromJSON: function (data) { - this.data = data; - return this; - }, - - _all: function (node, result) { - var nodesToSearch = []; - while (node) { - if (node.leaf) result.push.apply(result, node.children); - else nodesToSearch.push.apply(nodesToSearch, node.children); - - node = nodesToSearch.pop(); + + _all(node, result) { + const nodesToSearch = []; + while (node) { + if (node.leaf) node.children.forEach(child => result.push(child.source || child)); + else nodesToSearch.push(...node.children); + + node = nodesToSearch.pop(); + } + return result; } - return result; - }, - - _build: function (items, left, right, height) { - - var N = right - left + 1, - M = this._maxEntries, - node; - - if (N <= M) { - // reached leaf level; return leaf - node = createNode(items.slice(left, right + 1)); + + _build(items, left, right, height) { + + const N = right - left + 1; + let M = this._maxEntries; + let node; + + if (N <= M) { + // reached leaf level; return leaf + node = createNode(items.slice(left, right + 1)); + calcBBox(node, this.toBBox); + return node; + } + + if (!height) { + // target height of the bulk-loaded tree + height = Math.ceil(Math.log(N) / Math.log(M)); + + // target number of root entries to maximize storage utilization + M = Math.ceil(N / Math.pow(M, height - 1)); + } + + node = createNode([]); + node.leaf = false; + node.height = height; + + // split the items into M mostly square tiles + + const N2 = Math.ceil(N / M); + const N1 = N2 * Math.ceil(Math.sqrt(M)); + + multiSelect(items, left, right, N1, this.compareMinX); + + for (let i = left; i <= right; i += N1) { + + const right2 = Math.min(i + N1 - 1, right); + + multiSelect(items, i, right2, N2, this.compareMinY); + + for (let j = i; j <= right2; j += N2) { + + const right3 = Math.min(j + N2 - 1, right2); + + // pack each entry recursively + node.children.push(this._build(items, j, right3, height - 1)); + } + } + calcBBox(node, this.toBBox); + return node; } - - if (!height) { - // target height of the bulk-loaded tree - height = Math.ceil(Math.log(N) / Math.log(M)); - - // target number of root entries to maximize storage utilization - M = Math.ceil(N / Math.pow(M, height - 1)); + + _chooseSubtree(bbox, node, level, path) { + while (true) { + path.push(node); + + if (node.leaf || path.length - 1 === level) break; + + let minArea = Infinity; + let minEnlargement = Infinity; + let targetNode; + + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const area = bboxArea(child); + const enlargement = enlargedArea(bbox, child) - area; + + // choose entry with the least area enlargement + if (enlargement < minEnlargement) { + minEnlargement = enlargement; + minArea = area < minArea ? area : minArea; + targetNode = child; + + } else if (enlargement === minEnlargement) { + // otherwise choose one with the smallest area + if (area < minArea) { + minArea = area; + targetNode = child; + } + } + } + + node = targetNode || node.children[0]; + } + + return node; } - - node = createNode([]); - node.leaf = false; - node.height = height; - - // split the items into M mostly square tiles - - var N2 = Math.ceil(N / M), - N1 = N2 * Math.ceil(Math.sqrt(M)), - i, j, right2, right3; - - multiSelect(items, left, right, N1, this.compareMinX); - - for (i = left; i <= right; i += N1) { - - right2 = Math.min(i + N1 - 1, right); - - multiSelect(items, i, right2, N2, this.compareMinY); - - for (j = i; j <= right2; j += N2) { - - right3 = Math.min(j + N2 - 1, right2); - - // pack each entry recursively - node.children.push(this._build(items, j, right3, height - 1)); + + _insert(item, level, isNode) { + const bbox = isNode ? item : this.toBBox(item); + const insertPath = []; + + // find the best node for accommodating the item, saving all nodes along the path too + const node = this._chooseSubtree(bbox, this.data, level, insertPath); + + // put the item into the node + node.children.push(item); + extend(node, bbox); + + // split on node overflow; propagate upwards if necessary + while (level >= 0) { + if (insertPath[level].children.length > this._maxEntries) { + this._split(insertPath, level); + level--; + } else break; } + + // adjust bboxes along the insertion path + this._adjustParentBBoxes(bbox, insertPath, level); + } + + // split overflowed node into two + _split(insertPath, level) { + const node = insertPath[level]; + const M = node.children.length; + const m = this._minEntries; + + this._chooseSplitAxis(node, m, M); + + const splitIndex = this._chooseSplitIndex(node, m, M); + + const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); + newNode.height = node.height; + newNode.leaf = node.leaf; + + calcBBox(node, this.toBBox); + calcBBox(newNode, this.toBBox); + + if (level) insertPath[level - 1].children.push(newNode); + else this._splitRoot(node, newNode); + } + + _splitRoot(node, newNode) { + // split root node + this.data = createNode([node, newNode]); + this.data.height = node.height + 1; + this.data.leaf = false; + calcBBox(this.data, this.toBBox); } - - calcBBox(node, this.toBBox); - - return node; - }, - - _chooseSubtree: function (bbox, node, level, path) { - - var i, len, child, targetNode, area, enlargement, minArea, minEnlargement; - - while (true) { - path.push(node); - - if (node.leaf || path.length - 1 === level) break; - - minArea = minEnlargement = Infinity; - - for (i = 0, len = node.children.length; i < len; i++) { - child = node.children[i]; - area = bboxArea(child); - enlargement = enlargedArea(bbox, child) - area; - - // choose entry with the least area enlargement - if (enlargement < minEnlargement) { - minEnlargement = enlargement; + + _chooseSplitIndex(node, m, M) { + let index; + let minOverlap = Infinity; + let minArea = Infinity; + + for (let i = m; i <= M - m; i++) { + const bbox1 = distBBox(node, 0, i, this.toBBox); + const bbox2 = distBBox(node, i, M, this.toBBox); + + const overlap = intersectionArea(bbox1, bbox2); + const area = bboxArea(bbox1) + bboxArea(bbox2); + + // choose distribution with minimum overlap + if (overlap < minOverlap) { + minOverlap = overlap; + index = i; + minArea = area < minArea ? area : minArea; - targetNode = child; - - } else if (enlargement === minEnlargement) { - // otherwise choose one with the smallest area + + } else if (overlap === minOverlap) { + // otherwise choose distribution with minimum area if (area < minArea) { minArea = area; - targetNode = child; + index = i; } } } - - node = targetNode || node.children[0]; + + return index || M - m; } - - return node; - }, - - _insert: function (item, level, isNode) { - - var toBBox = this.toBBox, - bbox = isNode ? item : toBBox(item), - insertPath = []; - - // find the best node for accommodating the item, saving all nodes along the path too - var node = this._chooseSubtree(bbox, this.data, level, insertPath); - - // put the item into the node - node.children.push(item); - extend(node, bbox); - - // split on node overflow; propagate upwards if necessary - while (level >= 0) { - if (insertPath[level].children.length > this._maxEntries) { - this._split(insertPath, level); - level--; - } else break; + + // sorts node children by the best axis for split + _chooseSplitAxis(node, m, M) { + const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX; + const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY; + const xMargin = this._allDistMargin(node, m, M, compareMinX); + const yMargin = this._allDistMargin(node, m, M, compareMinY); + + // if total distributions margin value is minimal for x, sort by minX, + // otherwise it's already sorted by minY + if (xMargin < yMargin) node.children.sort(compareMinX); } - - // adjust bboxes along the insertion path - this._adjustParentBBoxes(bbox, insertPath, level); - }, - - // split overflowed node into two - _split: function (insertPath, level) { - - var node = insertPath[level], - M = node.children.length, - m = this._minEntries; - - this._chooseSplitAxis(node, m, M); - - var splitIndex = this._chooseSplitIndex(node, m, M); - - var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); - newNode.height = node.height; - newNode.leaf = node.leaf; - - calcBBox(node, this.toBBox); - calcBBox(newNode, this.toBBox); - - if (level) insertPath[level - 1].children.push(newNode); - else this._splitRoot(node, newNode); - }, - - _splitRoot: function (node, newNode) { - // split root node - this.data = createNode([node, newNode]); - this.data.height = node.height + 1; - this.data.leaf = false; - calcBBox(this.data, this.toBBox); - }, - - _chooseSplitIndex: function (node, m, M) { - - var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; - - minOverlap = minArea = Infinity; - - for (i = m; i <= M - m; i++) { - bbox1 = distBBox(node, 0, i, this.toBBox); - bbox2 = distBBox(node, i, M, this.toBBox); - - overlap = intersectionArea(bbox1, bbox2); - area = bboxArea(bbox1) + bboxArea(bbox2); - - // choose distribution with minimum overlap - if (overlap < minOverlap) { - minOverlap = overlap; - index = i; - - minArea = area < minArea ? area : minArea; - - } else if (overlap === minOverlap) { - // otherwise choose distribution with minimum area - if (area < minArea) { - minArea = area; - index = i; - } + + // total margin of all possible split distributions where each node is at least m full + _allDistMargin(node, m, M, compare) { + node.children.sort(compare); + + const toBBox = this.toBBox; + const leftBBox = distBBox(node, 0, m, toBBox); + const rightBBox = distBBox(node, M - m, M, toBBox); + let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox); + + for (let i = m; i < M - m; i++) { + const child = node.children[i]; + extend(leftBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(leftBBox); } + + for (let i = M - m - 1; i >= m; i--) { + const child = node.children[i]; + extend(rightBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(rightBBox); + } + + return margin; } - - return index; - }, - - // sorts node children by the best axis for split - _chooseSplitAxis: function (node, m, M) { - - var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, - compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, - xMargin = this._allDistMargin(node, m, M, compareMinX), - yMargin = this._allDistMargin(node, m, M, compareMinY); - - // if total distributions margin value is minimal for x, sort by minX, - // otherwise it's already sorted by minY - if (xMargin < yMargin) node.children.sort(compareMinX); - }, - - // total margin of all possible split distributions where each node is at least m full - _allDistMargin: function (node, m, M, compare) { - - node.children.sort(compare); - - var toBBox = this.toBBox, - leftBBox = distBBox(node, 0, m, toBBox), - rightBBox = distBBox(node, M - m, M, toBBox), - margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), - i, child; - - for (i = m; i < M - m; i++) { - child = node.children[i]; - extend(leftBBox, node.leaf ? toBBox(child) : child); - margin += bboxMargin(leftBBox); + + _adjustParentBBoxes(bbox, path, level) { + // adjust bboxes along the given tree path + for (let i = level; i >= 0; i--) { + extend(path[i], bbox); + } } - - for (i = M - m - 1; i >= m; i--) { - child = node.children[i]; - extend(rightBBox, node.leaf ? toBBox(child) : child); - margin += bboxMargin(rightBBox); + + _condense(path) { + // go through the path, removing empty nodes and updating bboxes + for (let i = path.length - 1, siblings; i >= 0; i--) { + if (path[i].children.length === 0) { + if (i > 0) { + siblings = path[i - 1].children; + siblings.splice(siblings.indexOf(path[i]), 1); + + } else this.clear(); + + } else calcBBox(path[i], this.toBBox); + } } - - return margin; - }, - - _adjustParentBBoxes: function (bbox, path, level) { - // adjust bboxes along the given tree path - for (var i = level; i >= 0; i--) { - extend(path[i], bbox); + } + + function findItem(item, items, equalsFn) { + if (!equalsFn) return items.indexOf(item); + + for (let i = 0; i < items.length; i++) { + if (equalsFn(item, items[i])) return i; } - }, - - _condense: function (path) { - // go through the path, removing empty nodes and updating bboxes - for (var i = path.length - 1, siblings; i >= 0; i--) { - if (path[i].children.length === 0) { - if (i > 0) { - siblings = path[i - 1].children; - siblings.splice(siblings.indexOf(path[i]), 1); - - } else this.clear(); - - } else calcBBox(path[i], this.toBBox); + return -1; + } + + // calculate node's bbox from bboxes of its children + function calcBBox(node, toBBox) { + distBBox(node, 0, node.children.length, toBBox, node); + } + + // min bounding rectangle of node children from k to p-1 + function distBBox(node, k, p, toBBox, destNode) { + if (!destNode) destNode = createNode(null); + destNode.minX = Infinity; + destNode.minY = Infinity; + destNode.maxX = -Infinity; + destNode.maxY = -Infinity; + + for (let i = k; i < p; i++) { + const child = node.children[i]; + extend(destNode, node.leaf ? toBBox(child) : child); } - }, - - _initFormat: function (format) { - // data format (minX, minY, maxX, maxY accessors) - - // uses eval-type function compilation instead of just accepting a toBBox function - // because the algorithms are very sensitive to sorting functions performance, - // so they should be dead simple and without inner calls - - var compareArr = ['return a', ' - b', ';']; - - this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); - this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); - - this.toBBox = new Function('a', - 'return {minX: a' + format[0] + - ', minY: a' + format[1] + - ', maxX: a' + format[2] + - ', maxY: a' + format[3] + '};'); + + return destNode; } -}; - -function findItem(item, items, equalsFn) { - if (!equalsFn) return items.indexOf(item); - - for (var i = 0; i < items.length; i++) { - if (equalsFn(item, items[i])) return i; + + function extend(a, b) { + a.minX = Math.min(a.minX, b.minX); + a.minY = Math.min(a.minY, b.minY); + a.maxX = Math.max(a.maxX, b.maxX); + a.maxY = Math.max(a.maxY, b.maxY); + return a; } - return -1; -} - -// calculate node's bbox from bboxes of its children -function calcBBox(node, toBBox) { - distBBox(node, 0, node.children.length, toBBox, node); -} - -// min bounding rectangle of node children from k to p-1 -function distBBox(node, k, p, toBBox, destNode) { - if (!destNode) destNode = createNode(null); - destNode.minX = Infinity; - destNode.minY = Infinity; - destNode.maxX = -Infinity; - destNode.maxY = -Infinity; - - for (var i = k, child; i < p; i++) { - child = node.children[i]; - extend(destNode, node.leaf ? toBBox(child) : child); + + function compareNodeMinX(a, b) { return a.minX - b.minX; } + function compareNodeMinY(a, b) { return a.minY - b.minY; } + + function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } + function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } + + function enlargedArea(a, b) { + return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * + (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); } - - return destNode; -} - -function extend(a, b) { - a.minX = Math.min(a.minX, b.minX); - a.minY = Math.min(a.minY, b.minY); - a.maxX = Math.max(a.maxX, b.maxX); - a.maxY = Math.max(a.maxY, b.maxY); - return a; -} - -function compareNodeMinX(a, b) { return a.minX - b.minX; } -function compareNodeMinY(a, b) { return a.minY - b.minY; } - -function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } -function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } - -function enlargedArea(a, b) { - return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * - (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); -} - -function intersectionArea(a, b) { - var minX = Math.max(a.minX, b.minX), - minY = Math.max(a.minY, b.minY), - maxX = Math.min(a.maxX, b.maxX), - maxY = Math.min(a.maxY, b.maxY); - - return Math.max(0, maxX - minX) * - Math.max(0, maxY - minY); -} - -function contains(a, b) { - return a.minX <= b.minX && - a.minY <= b.minY && - b.maxX <= a.maxX && - b.maxY <= a.maxY; -} - -function intersects(a, b) { - return b.minX <= a.maxX && - b.minY <= a.maxY && - b.maxX >= a.minX && - b.maxY >= a.minY; -} - -function createNode(children) { - return { - children: children, - height: 1, - leaf: true, - minX: Infinity, - minY: Infinity, - maxX: -Infinity, - maxY: -Infinity - }; -} - -// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; -// combines selection algorithm with binary divide & conquer approach - -function multiSelect(arr, left, right, n, compare) { - var stack = [left, right], - mid; - - while (stack.length) { - right = stack.pop(); - left = stack.pop(); - - if (right - left <= n) continue; - - mid = left + Math.ceil((right - left) / n / 2) * n; - quickselect(arr, mid, left, right, compare); - - stack.push(left, mid, mid, right); + + function intersectionArea(a, b) { + const minX = Math.max(a.minX, b.minX); + const minY = Math.max(a.minY, b.minY); + const maxX = Math.min(a.maxX, b.maxX); + const maxY = Math.min(a.maxY, b.maxY); + + return Math.max(0, maxX - minX) * + Math.max(0, maxY - minY); } -} - -},{"quickselect":2}],2:[function(require,module,exports){ -'use strict'; - -module.exports = partialSort; - -// Floyd-Rivest selection algorithm: -// Rearrange items so that all items in the [left, k] range are smaller than all items in (k, right]; -// The k-th element will have the (k - left + 1)th smallest value in [left, right] - -function partialSort(arr, k, left, right, compare) { - - while (right > left) { - if (right - left > 600) { - var n = right - left + 1; - var m = k - left + 1; - var z = Math.log(n); - var s = 0.5 * Math.exp(2 * z / 3); - var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); - var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); - var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); - partialSort(arr, k, newLeft, newRight, compare); - } - - var t = arr[k]; - var i = left; - var j = right; - - swap(arr, left, k); - if (compare(arr[right], t) > 0) swap(arr, left, right); - - while (i < j) { - swap(arr, i, j); - i++; - j--; - while (compare(arr[i], t) < 0) i++; - while (compare(arr[j], t) > 0) j--; - } - - if (compare(arr[left], t) === 0) swap(arr, left, j); - else { - j++; - swap(arr, j, right); + + function contains(a, b) { + return a.minX <= b.minX && + a.minY <= b.minY && + b.maxX <= a.maxX && + b.maxY <= a.maxY; + } + + function intersects(a, b) { + return b.minX <= a.maxX && + b.minY <= a.maxY && + b.maxX >= a.minX && + b.maxY >= a.minY; + } + + function createNode(children) { + return { + children, + height: 1, + leaf: true, + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity + }; + } + + // sort an array so that items come in groups of n unsorted items, with groups sorted between each other; + // combines selection algorithm with binary divide & conquer approach + + function multiSelect(arr, left, right, n, compare) { + const stack = [left, right]; + + while (stack.length) { + right = stack.pop(); + left = stack.pop(); + + if (right - left <= n) continue; + + const mid = left + Math.ceil((right - left) / n / 2) * n; + quickselect(arr, mid, left, right, compare); + + stack.push(left, mid, mid, right); } - - if (j <= k) left = j + 1; - if (k <= j) right = j - 1; } -} - -function swap(arr, i, j) { - var tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; -} - -function defaultCompare(a, b) { - return a < b ? -1 : a > b ? 1 : 0; -} - -},{}]},{},[1])(1) -}); + + return RBush; + + })); \ No newline at end of file diff --git a/GDJS/Runtime/runtimeobject.ts b/GDJS/Runtime/runtimeobject.ts index 7d84b6064a14..4a103719942b 100644 --- a/GDJS/Runtime/runtimeobject.ts +++ b/GDJS/Runtime/runtimeobject.ts @@ -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 = []; _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; + 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); } /** diff --git a/GDJS/Runtime/runtimescene.ts b/GDJS/Runtime/runtimescene.ts index 4c4422675e0c..8112739fbf71 100644 --- a/GDJS/Runtime/runtimescene.ts +++ b/GDJS/Runtime/runtimescene.ts @@ -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 = {}; + private _layerObjectManagers = new Map(); + /** + * Objects that were rendered for the last frame. + * + * They keep to be hide back without iterating every objects from the scene. + */ + private _objectsInsideCamera: Record> = {}; + /** * @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: diff --git a/GDJS/Runtime/types/rbush.d.ts b/GDJS/Runtime/types/rbush.d.ts new file mode 100644 index 000000000000..7dd67cedb130 --- /dev/null +++ b/GDJS/Runtime/types/rbush.d.ts @@ -0,0 +1,19 @@ +type SearchArea = { minX: float; minY: float; maxX: float; maxY: float }; +type SearchedItem = { + source: T; + minX: float; + minY: float; + maxX: float; + maxY: float; +}; + +declare class RBush { + constructor(maxEntries?: number); + search(bbox: SearchArea, result?: Array): Array; + insert(item: SearchedItem): RBush; + clear(): RBush; + remove( + item: SearchedItem, + equalsFn?: (item: SearchedItem, otherItem: SearchedItem) => boolean + ): RBush; +} diff --git a/GDJS/tests/karma.conf.js b/GDJS/tests/karma.conf.js index c14839fef2e6..be14ce7660d0 100644 --- a/GDJS/tests/karma.conf.js +++ b/GDJS/tests/karma.conf.js @@ -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',