Skip to content

Commit

Permalink
Reduced-scope version of detaching unused pool elements from scene graph
Browse files Browse the repository at this point in the history
As per feedback in PR aframevr#5186
  • Loading branch information
diarmidmackenzie committed Dec 24, 2022
1 parent e3b0153 commit c7cb6d0
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/components/pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 13 additions & 3 deletions src/components/raycaster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/components/scene/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
},

/**
Expand Down Expand Up @@ -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;
},
Expand All @@ -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();
});
}
});
38 changes: 38 additions & 0 deletions tests/components/raycaster.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
42 changes: 42 additions & 0 deletions tests/components/scene/pool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c7cb6d0

Please sign in to comment.