Skip to content

Commit

Permalink
Allow components to define before/after constraints (#5478)
Browse files Browse the repository at this point in the history
* Allow components to define before/after constraints

* Fix bug preventing runtime registered components from being sorted into componentOrder

* Move order logic into a-scene.js and introduce callComponentBehaviors method

---------

Co-authored-by: Noeri Huisman <mrxz@users.noreply.github.com>
  • Loading branch information
mrxz and mrxz committed Mar 28, 2024
1 parent 81f9626 commit c7736c4
Show file tree
Hide file tree
Showing 17 changed files with 486 additions and 192 deletions.
3 changes: 3 additions & 0 deletions examples/showcase/tracked-controls/components/grab.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Updates its position to move along the controller.
*/
AFRAME.registerComponent('grab', {
after: ['tracked-controls-webxr'],
before: ['aabb-collider'],
init: function () {
this.GRABBED_STATE = 'grabbed';
// Bind event handlers
Expand Down Expand Up @@ -67,6 +69,7 @@ AFRAME.registerComponent('grab', {
y: position.y + this.deltaPosition.y,
z: position.z + this.deltaPosition.z
});
hitEl.object3D.updateMatrixWorld();
},

updateDelta: function () {
Expand Down
2 changes: 2 additions & 0 deletions src/components/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ module.exports.Component = registerComponent('cursor', {
rayOrigin: {default: 'entity', oneOf: ['mouse', 'entity', 'xrselect']}
},

after: ['tracked-controls'],

multiple: true,

init: function () {
Expand Down
2 changes: 2 additions & 0 deletions src/components/generic-tracked-controller-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ module.exports.Component = registerComponent('generic-tracked-controller-control
disabled: {default: false}
},

after: ['tracked-controls'],

/**
* Button IDs:
* 0 - trackpad
Expand Down
2 changes: 2 additions & 0 deletions src/components/hand-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ module.exports.Component = registerComponent('hand-controls', {
handModelStyle: {default: 'lowPoly', oneOf: ['lowPoly', 'highPoly', 'toon']}
},

after: ['tracked-controls'],

init: function () {
var self = this;
var el = this.el;
Expand Down
2 changes: 2 additions & 0 deletions src/components/hand-tracking-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ module.exports.Component = registerComponent('hand-tracking-controls', {
modelOpacity: {default: 1.0}
},

after: ['tracked-controls'],

bindMethods: function () {
this.onControllersUpdate = this.onControllersUpdate.bind(this);
this.checkIfControllerPresent = this.checkIfControllerPresent.bind(this);
Expand Down
2 changes: 2 additions & 0 deletions src/components/oculus-touch-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ module.exports.Component = registerComponent('oculus-touch-controls', {
orientationOffset: {type: 'vec3', default: {x: 43, y: 0, z: 0}}
},

after: ['tracked-controls'],

mapping: INPUT_MAPPING,

bindMethods: function () {
Expand Down
4 changes: 4 additions & 0 deletions src/components/tracked-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ module.exports.Component = registerComponent('tracked-controls', {
space: {type: 'string', oneOf: ['targetRaySpace', 'gripSpace'], default: 'targetRaySpace'}
},

// Run after both tracked-controls-webvr and tracked-controls-webxr to allow other components
// to be after either without having to list them both.
after: ['tracked-controls-webvr', 'tracked-controls-webxr'],

update: function () {
var data = this.data;
var el = this.el;
Expand Down
2 changes: 2 additions & 0 deletions src/components/valve-index-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ module.exports.Component = registerComponent('valve-index-controls', {
orientationOffset: {type: 'vec3'}
},

after: ['tracked-controls'],

mapping: {
axes: {
trackpad: [0, 1],
Expand Down
2 changes: 2 additions & 0 deletions src/components/vive-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ module.exports.Component = registerComponent('vive-controls', {
orientationOffset: {type: 'vec3'}
},

after: ['tracked-controls'],

mapping: INPUT_MAPPING,

init: function () {
Expand Down
2 changes: 2 additions & 0 deletions src/components/vive-focus-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ module.exports.Component = registerComponent('vive-focus-controls', {
armModel: {default: true}
},

after: ['tracked-controls'],

mapping: INPUT_MAPPING,

bindMethods: function () {
Expand Down
1 change: 1 addition & 0 deletions src/components/wasd-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports.Component = registerComponent('wasd-controls', {
wsEnabled: {default: true},
wsInverted: {default: false}
},
after: ['look-controls'],

init: function () {
// To keep track of the pressed keys.
Expand Down
2 changes: 2 additions & 0 deletions src/components/windows-motion-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ module.exports.Component = registerComponent('windows-motion-controls', {
hideDisconnected: {default: true}
},

after: ['tracked-controls'],

mapping: INPUT_MAPPING,

bindMethods: function () {
Expand Down
8 changes: 8 additions & 0 deletions src/core/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,8 @@ module.exports.registerComponent = function (name, definition) {
components[name] = {
Component: NewComponent,
dependencies: NewComponent.prototype.dependencies,
before: NewComponent.prototype.before,
after: NewComponent.prototype.after,
isSingleProperty: NewComponent.prototype.isSingleProperty,
isObjectBased: NewComponent.prototype.isObjectBased,
multiple: NewComponent.prototype.multiple,
Expand All @@ -702,6 +704,12 @@ module.exports.registerComponent = function (name, definition) {
schema: schema,
stringify: NewComponent.prototype.stringify
};

// Notify all scenes
for (var i = 0; i < scenes.length; i++) {
scenes[i].emit('componentregistered', {name: name}, false);
}

return NewComponent;
};

Expand Down
167 changes: 148 additions & 19 deletions src/core/scene/a-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ var initWakelock = require('./wakelock');
var loadingScreen = require('./loadingScreen');
var scenes = require('./scenes');
var systems = require('../system').systems;
var components = require('../component').components;
var THREE = require('../../lib/three');
var utils = require('../../utils/');
var warn = utils.debug('core:a-scene:warn');
// Require after.
var AEntity = require('../a-entity').AEntity;
var ANode = require('../a-node').ANode;
Expand Down Expand Up @@ -56,7 +58,8 @@ class AScene extends AEntity {
self.time = self.delta = 0;
self.usedOfferSession = false;

self.behaviors = {tick: [], tock: []};
self.componentOrder = [];
self.behaviors = {};
self.hasLoaded = false;
self.isPlaying = false;
self.originalHTML = self.innerHTML;
Expand Down Expand Up @@ -129,6 +132,12 @@ class AScene extends AEntity {
});

this.initSystems();
// Compute component order
this.componentOrder = determineComponentBehaviorOrder(components, this.componentOrder);
this.addEventListener('componentregistered', function () {
// Recompute order
self.componentOrder = determineComponentBehaviorOrder(components, self.componentOrder);
});

// WebXR Immersive navigation handler.
if (this.hasWebXR && navigator.xr && navigator.xr.addEventListener) {
Expand Down Expand Up @@ -210,16 +219,32 @@ class AScene extends AEntity {
* @param {object} behavior - A component.
*/
addBehavior (behavior) {
var behaviorArr;
var behaviors = this.behaviors;
var behaviorSet;
var behaviors = this.behaviors[behavior.name];
var behaviorType;

if (!behaviors) {
behaviors = this.behaviors[behavior.name] = {
tick: { inUse: false, array: [], markedForRemoval: [] },
tock: { inUse: false, array: [], markedForRemoval: [] }
};
}

// Check if behavior has tick and/or tock and add the behavior to the appropriate list.
for (behaviorType in behaviors) {
if (!behavior[behaviorType]) { continue; }
behaviorArr = this.behaviors[behaviorType];
if (behaviorArr.indexOf(behavior) === -1) {
behaviorArr.push(behavior);
behaviorSet = behaviors[behaviorType];

// In case the behaviorSet is in use, make sure this behavior isn't on the removal list.
if (behaviorSet.inUse) {
var index = behaviorSet.markedForRemoval.indexOf(behavior);
if (index !== -1) {
behaviorSet.markedForRemoval.splice(index, 1);
}
}
// Add behavior to the set
if (behaviorSet.array.indexOf(behavior) === -1) {
behaviorSet.array.push(behavior);
}
}
}
Expand Down Expand Up @@ -522,18 +547,30 @@ class AScene extends AEntity {
* @param {object} behavior - A component.
*/
removeBehavior (behavior) {
var behaviorArr;
var behaviorSet;
var behaviorType;
var behaviors = this.behaviors;
var behaviors = this.behaviors[behavior.name];
var index;

// Check if behavior has tick and/or tock and remove the behavior from the appropriate
// array.
for (behaviorType in behaviors) {
if (!behavior[behaviorType]) { continue; }
behaviorArr = this.behaviors[behaviorType];
index = behaviorArr.indexOf(behavior);
if (index !== -1) { behaviorArr.splice(index, 1); }
behaviorSet = behaviors[behaviorType];
index = behaviorSet.array.indexOf(behavior);
if (index !== -1) {
// Check if the behavior can safely be removed.
if (behaviorSet.inUse) {
// Set is in use, so only mark for removal.
if (behaviorSet.markedForRemoval.indexOf(behavior) === -1) {
behaviorSet.markedForRemoval.push(behavior);
}
} else {
// Swap and remove from the end
behaviorSet.array[index] = behaviorSet.array[behaviorSet.array.length - 1];
behaviorSet.array.pop();
}
}
}
}

Expand Down Expand Up @@ -689,10 +726,7 @@ class AScene extends AEntity {
var systems = this.systems;

// Components.
for (i = 0; i < this.behaviors.tick.length; i++) {
if (!this.behaviors.tick[i].el.isPlaying) { continue; }
this.behaviors.tick[i].tick(time, timeDelta);
}
this.callComponentBehaviors('tick', time, timeDelta);

// Systems.
for (i = 0; i < this.systemNames.length; i++) {
Expand All @@ -711,10 +745,7 @@ class AScene extends AEntity {
var systems = this.systems;

// Components.
for (i = 0; i < this.behaviors.tock.length; i++) {
if (!this.behaviors.tock[i].el.isPlaying) { continue; }
this.behaviors.tock[i].tock(time, timeDelta, camera);
}
this.callComponentBehaviors('tock', time, timeDelta);

// Systems.
for (i = 0; i < this.systemNames.length; i++) {
Expand Down Expand Up @@ -750,8 +781,106 @@ class AScene extends AEntity {
this.object3D.background = savedBackground;
}
}

callComponentBehaviors (behavior, time, timeDelta) {
var i;

for (var c = 0; c < this.componentOrder.length; c++) {
var behaviors = this.behaviors[this.componentOrder[c]];
if (!behaviors) { continue; }
var behaviorSet = behaviors[behavior];

behaviorSet.inUse = true;
for (i = 0; i < behaviorSet.array.length; i++) {
if (!behaviorSet.array[i].el.isPlaying) { continue; }
behaviorSet.array[i][behavior](time, timeDelta);
}
behaviorSet.inUse = false;

// Clean up any behaviors marked for removal
for (i = 0; i < behaviorSet.markedForRemoval.length; i++) {
this.removeBehavior(behaviorSet.markedForRemoval[i]);
}
behaviorSet.markedForRemoval.length = 0;
}
}
}

/**
* Derives an ordering from the components, taking any before and after
* constraints into account.
*
* @param {object} components - The components to order
* @param {array} array - Optional array to use as output
*/
function determineComponentBehaviorOrder (components, array) {
var graph = {};
var i;
var key;
var result = array || [];
result.length = 0;

// Construct graph nodes for each element
for (key in components) {
var element = components[key];
if (element === undefined) { continue; }
var before = element.before ? element.before.slice(0) : [];
var after = element.after ? element.after.slice(0) : [];
graph[key] = { before: before, after: after, visited: false, done: false };
}

// Normalize to after constraints, warn about missing nodes
for (key in graph) {
for (i = 0; i < graph[key].before.length; i++) {
var beforeName = graph[key].before[i];
if (!(beforeName in graph)) {
warn('Invalid ordering constraint, no component named `' + beforeName + '` referenced by `' + key + '`');
continue;
}

graph[beforeName].after.push(key);
}
}

// Perform topological depth-first search
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
function visit (name) {
if (!(name in graph) || graph[name].done) {
return;
}

if (graph[name].visited) {
warn('Cycle detected, ignoring one or more before/after constraints. ' +
'The resulting order might be incorrect');
return;
}

graph[name].visited = true;

for (var i = 0; i < graph[name].after.length; i++) {
var afterName = graph[name].after[i];
if (!(afterName in graph)) {
warn('Invalid before/after constraint, no component named `' +
afterName + '` referenced in `' + name + '`');
}
visit(afterName);
}

graph[name].done = true;
result.push(name);
}

for (key in graph) {
if (graph[key].done) {
continue;
}
visit(key);
}
return result;
}

module.exports.determineComponentBehaviorOrder = determineComponentBehaviorOrder;

/**
* Return size constrained to maxSize - maintaining aspect ratio.
*
Expand Down

0 comments on commit c7736c4

Please sign in to comment.