Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduced-scope version of detaching unused pool elements from scene graph #5188

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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