From c7cb6d06371eb2ef77a34daef5050cd799048b4f Mon Sep 17 00:00:00 2001 From: diarmidmackenzie Date: Sat, 24 Dec 2022 16:50:23 +0000 Subject: [PATCH] Reduced-scope version of detaching unused pool elements from scene graph As per feedback in PR #5186 --- docs/components/pool.md | 2 ++ src/components/raycaster.js | 16 ++++++++--- src/components/scene/pool.js | 22 +++++++++++++++ tests/components/raycaster.test.js | 38 ++++++++++++++++++++++++++ tests/components/scene/pool.test.js | 42 +++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/components/pool.md b/docs/components/pool.md index f9405fb16a3..0bd7bef5693 100644 --- a/docs/components/pool.md +++ b/docs/components/pool.md @@ -15,6 +15,8 @@ entities in dynamic scenes. Object pooling helps reduce garbage collection pause Note that entities requested from the pool are paused by default and you need to call `.play()` in order to activate their components' tick functions. +For performance reasons, unused entities in the pool are detached from the THREE.js scene graph, which means that they are not rendered, their matrices are not updated, and they are excluded from raycasting. + ## Example For example, we may have a game with enemy entities that we want to reuse. diff --git a/src/components/raycaster.js b/src/components/raycaster.js index b23652f0b3a..2fcde614d30 100644 --- a/src/components/raycaster.js +++ b/src/components/raycaster.js @@ -409,13 +409,23 @@ module.exports.Component = registerComponent('raycaster', { var key; var i; var objects = this.objects; + var scene = this.el.sceneEl.object3D; + + function isAttachedToScene (object) { + if (object.parent) { + return isAttachedToScene(object.parent); + } else { + return (object === scene); + } + } // Push meshes and other attachments onto list of objects to intersect. objects.length = 0; for (i = 0; i < els.length; i++) { - if (els[i].isEntity && els[i].object3D) { - for (key in els[i].object3DMap) { - objects.push(els[i].getObject3D(key)); + var el = els[i]; + if (el.isEntity && el.object3D && isAttachedToScene(el.object3D)) { + for (key in el.object3DMap) { + objects.push(el.getObject3D(key)); } } } diff --git a/src/components/scene/pool.js b/src/components/scene/pool.js index 618b2c76b1a..0190a11e3ae 100644 --- a/src/components/scene/pool.js +++ b/src/components/scene/pool.js @@ -63,6 +63,13 @@ module.exports.Component = registerComponent('pool', { el.pause(); this.container.appendChild(el); this.availableEls.push(el); + + var usedEls = this.usedEls; + el.addEventListener('loaded', function () { + if (usedEls.indexOf(el) !== -1) { return; } + el.object3DParent = el.object3D.parent; + el.object3D.parent.remove(el.object3D); + }); }, /** @@ -94,6 +101,10 @@ module.exports.Component = registerComponent('pool', { } el = this.availableEls.shift(); this.usedEls.push(el); + if (el.object3DParent) { + el.object3DParent.add(el.object3D); + this.updateRaycasters(); + } el.object3D.visible = true; return el; }, @@ -109,8 +120,19 @@ module.exports.Component = registerComponent('pool', { } this.usedEls.splice(index, 1); this.availableEls.push(el); + el.object3DParent = el.object3D.parent; + el.object3D.parent.remove(el.object3D); + this.updateRaycasters(); el.object3D.visible = false; el.pause(); return el; + }, + + updateRaycasters () { + var raycasterEls = document.querySelectorAll('[raycaster]'); + + raycasterEls.forEach(function (el) { + el.components['raycaster'].setDirty(); + }); } }); diff --git a/tests/components/raycaster.test.js b/tests/components/raycaster.test.js index f2a1324c8a3..8744187b28d 100644 --- a/tests/components/raycaster.test.js +++ b/tests/components/raycaster.test.js @@ -120,6 +120,44 @@ suite('raycaster', function () { }); }); + test('Objects not attached to scene are not whitelisted', function (done) { + var el2 = document.createElement('a-entity'); + var el3 = document.createElement('a-entity'); + el2.setAttribute('class', 'clickable'); + el2.setAttribute('geometry', 'primitive: box'); + el3.setAttribute('class', 'clickable'); + el3.setAttribute('geometry', 'primitive: box'); + el3.addEventListener('loaded', function () { + el3.object3D.parent = null; + el.setAttribute('raycaster', 'objects', '.clickable'); + component.tock(); + assert.equal(component.objects.length, 1); + assert.equal(component.objects[0], el2.object3D.children[0]); + assert.equal(el2, el2.object3D.children[0].el); + done(); + }); + sceneEl.appendChild(el2); + sceneEl.appendChild(el3); + }); + + test('Objects with parent not attached to scene are not whitelisted', function (done) { + var el2 = document.createElement('a-entity'); + var el3 = document.createElement('a-entity'); + el2.setAttribute('class', 'clickable'); + el2.setAttribute('geometry', 'primitive: box'); + el3.setAttribute('class', 'clickable'); + el3.setAttribute('geometry', 'primitive: box'); + el3.addEventListener('loaded', function () { + el2.object3D.parent = null; + el.setAttribute('raycaster', 'objects', '.clickable'); + component.tock(); + assert.equal(component.objects.length, 0); + done(); + }); + sceneEl.appendChild(el2); + el2.appendChild(el3); + }); + suite('tock', function () { test('is throttled by interval', function () { var intersectSpy = this.sinon.spy(raycaster, 'intersectObjects'); diff --git a/tests/components/scene/pool.test.js b/tests/components/scene/pool.test.js index d24d64a7b70..e590c1ff5e9 100644 --- a/tests/components/scene/pool.test.js +++ b/tests/components/scene/pool.test.js @@ -99,6 +99,48 @@ suite('pool', function () { }); }); + suite('attachmentToThreeScene', function () { + test('Pool entity is not initially attached to scene', function () { + var sceneEl = this.sceneEl; + var poolComponent = sceneEl.components.pool; + assert.equal(poolComponent.availableEls[0].object3D.parent, null); + }); + + test('Pool entity is attached to scene when requested, and detached when released', function () { + var sceneEl = this.sceneEl; + var poolComponent = sceneEl.components.pool; + var el = poolComponent.requestEntity(); + assert.equal(el.object3D.parent, sceneEl.object3D); + poolComponent.returnEntity(el); + assert.equal(el.object3D.parent, null); + }); + + test('Raycaster is updated when entities are attached to / detached from scene', function (done) { + var sceneEl = this.sceneEl; + var rayEl = document.createElement('a-entity'); + rayEl.setAttribute('raycaster', ''); + rayEl.addEventListener('loaded', function () { + var rayComponent = rayEl.components.raycaster; + assert.equal(rayComponent.dirty, true); + rayComponent.tock(); + assert.equal(rayComponent.dirty, false); + var poolComponent = sceneEl.components.pool; + var el = poolComponent.requestEntity(); + assert.equal(el.object3D.parent, sceneEl.object3D); + assert.equal(rayComponent.dirty, true); + rayComponent.tock(); + assert.equal(rayComponent.dirty, false); + poolComponent.returnEntity(el); + assert.equal(el.object3D.parent, null); + assert.equal(rayComponent.dirty, true); + rayComponent.tock(); + assert.equal(rayComponent.dirty, false); + done(); + }); + sceneEl.appendChild(rayEl); + }); + }); + suite('wrapPlay', function () { test('cannot play an entity that is not in use', function () { var sceneEl = this.sceneEl;