-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
New hierarchy find functions #4603
Conversation
Personal take as an end user. I think Entity.findScript and Entity.findScripts could be very useful but the * in parent don't seem as useful given that you can get the parent and then call find * functions on that? |
@yaustar What if you don't know if the script or component you want is the direct parent or higher in the hierarchy? What if the parent doesn't have the script or component you want but a child do? For instance, as I love having generic scripts, I have some to tint, scale, etc. elements based on a parent button state (the button component can only tint OR change sprite of a single element) thus my scripts can simply find the closest button in its ancestors (which can be itself) and I don't need to write specific scripts each time a button doesn't have the same hierarchy. |
Does |
All the parents, same as Unity though "*InParents" would be more accurate indeed. |
Suggestion for /**
* Search the graph node and all of its ascendants for the nodes that satisfy some search
* criteria.
*
* @param {FindNodeCallback|string} attr - This can either be a function or a string. If it's a
* function, it is executed for each ascendant node to test if node satisfies the search
* logic. Returning true from the function will include the node into the results. If it's a
* string then it represents the name of a field or a method of the node. If this is the name
* of a field then the value passed as the second argument will be checked for equality. If
* this is the name of a function then the return value of the function will be checked for
* equality against the valued passed as the second argument to this function.
* @param {object} [value] - If the first argument (attr) is a property name then this value
* will be checked against the value of the property.
* @param {this[]} [results] - Array where the results are appended to.
* @returns {this[]} The array of graph nodes that match the search criteria.
* @example
* // Finds all nodes that have a group element component
* var groups = element.findInParent(function (node) {
* return node.element && node.element.type === pc.ELEMENTTYPE_GROUP;
* });
* @example
* // Finds all nodes that have the name property set to 'Test'
* var entities = entity.findInParent('name', 'Test');
*/
findInParent(attr, value, results = []) {
if (attr instanceof Function) {
if (attr(this))
results.push(this);
} else if (this[attr]) {
let testValue;
if (this[attr] instanceof Function) {
testValue = this[attr]();
} else {
testValue = this[attr];
}
if (testValue === value)
results.push(this);
}
if (this._parent) {
this._parent.findInParent(attr, value, results);
}
return results;
} Motivation is that right now empty arrays are created and merged all the time, which is basically waste. You need exactly one That turns: The class TestNode extends GraphNode {
// Nothing yet.
};
const testNode = new TestNode("hai");
const ret = testNode.findInParent("name", "hai"); IMO |
I included the suggestion to avoid empty arrays, and also applied it to However, using a class TestNode extends GraphNode {
// Nothing yet.
};
const rootNode = new GraphNode("hai");
const testNode = new TestNode("hai");
rootNode.addChild(testNode);
const ret = testNode.findInParent("name", "hai"); Moreover, that would be way beyond the initial objective of this PR because every properties and functions to get, add and remove nodes are documented as returning or expecting |
ed73b6e
to
dc35892
Compare
Function names are now pluralized as By the way, about whether current node can be returned or not, I noticed that |
Your You can just reuse find(attr, value) {
const results = [];
if (attr instanceof Function) {
this.forEach((node) => {
if (attr(node))
results.push(node);
});
} else {
this.forEach((node) => {
let testValue;
if (node[attr] instanceof Function) {
testValue = node[attr]();
} else {
testValue = node[attr];
}
if (testValue === value)
results.push(node);
});
}
return results;
} I am also contemplating more helper functions for code like this: while (current) {
if (current.name === name)
return current;
current = current._parent;
}
Then there is also more code duplication regarding: // ...
if (this[attr] instanceof Function) {
testValue = this[attr]();
} else {
testValue = this[attr];
} Usually its called |
I added I already did I'm not sure I understood correctly what you meant with the duplicated code to get the test value, but I added a const function at the beginning of the file to directly check if an attribute matches a value and all four functions are now using it. Is this what you had in mind? |
Thank you for the quick implementation, yes, that is what I had in mind and you can even go a step further: /**
*
* @param {Function | string} attr
* @param {*} value
* @returns {Function}
*/
function createTest(attr, value) {
if (attr instanceof Function) {
return attr;
}
return (node) => {
let x = node[attr];
if (x instanceof Function) {
x = x();
}
return x === value;
};
} If you do not "hardcode" For example, find(attr, value) {
const results = [];
const test = createTest(attr, value);
this.forEach((node) => {
if (test(node))
results.push(node);
});
return results;
} That boils this method from 37 lines down to these 9 lines and makes it more reusable aswell. You can simplify/shorten the other methods with the same technique. |
GraphNode != Array Again, GraphNode - is not an array. Its an individual node. Also, breaking projects - is not an option. Either changes are backwards compatible or they are additions in parallel. That is the rule been followed since first days of an engine open sourcing. |
Problem is However, as people never had the choice of ignoring current node, I'm now wondering about the actual usage of the different find functions and in how many cases people are relying on the fact these functions can return self node (or self component). Because maybe the behaviour can be changed without actually breaking many projects but just a few ones? For instance, most people using Edit: All new functions to find among ascendants were renamed from |
Removing current node from But removing self from You also need to test your changes against non-trivial projects to find the special cases. SeeMore for example is doing this: You can do so by e.g.: findByName(name) {
if (this.name === name) {
console.log("findByName", this, name);
debugger;
}
return this.findOne('name', name);
}
@Maksims Can you expand on that? Do you mean testing To bring in even more keywords: I would also like |
So let's try another solution by adding a parameter to let the user choose to include self node in the search. It's true by default for However, I'm faking overloading so the second parameter of some functions can be skipped when irrelevant (or not used) and I'm not sure if it respects PlayCanvas standards. |
I can't speak for others obviously so my personal objection is increased code complexity. I liked to simplify the methods, they are shorter and (at least for me) easier/clearer to read.
The initially reduced complexity is back and it is hard to understand/track and just harder than necessary. You are basically trapping into flag argument code smell, when you just want e.g.
And then the question is: how many people need that method or in which context do you require it? |
I'm puzzled, how is the following (which was modified just 8 months ago) findByTag() {
const query = arguments;
const results = [];
const queryNode = (node, checkNode) => {
if (checkNode && node.tags.has(...query)) {
results.push(node);
}
for (let i = 0; i < node._children.length; i++) {
queryNode(node._children[i], true);
}
};
queryNode(this, false);
return results;
} less complex and easier to read than findByTag(...query) {
let includeSelf = false;
if (typeof query[query.length - 1] === 'boolean')
includeSelf = query.pop();
return this.find(node => node.tags.has(...query), includeSelf);
} You can also notice how the current version of Anyway, I don't see how adding tons of functions for every single variant would help reduce complexity for the user. And that's worse if adding as you're suggesting functions like So now I'm thinking of simply closing this PR and just keep the new functions for myself. The real usefulness relies in the new |
Speaking from an end user: I think the Entity.findScript and Entity.findScripts are the most useful out of the list and I'm thinking about pulling that out in a separate PR, keep them as consistent as possible with the out find functions (which from this thread seem to be inconsistent as it is). The find in parents/ancestors would need an engine team call about whether they should be core engine functionality. It's not something I've seen asked about on the forums but wonder how many teams/developers have written their own variant 🤔 |
Our team made a similar one to |
Good point, a helper function could be: findExcludeSelf(attr, value) {
const results = [];
const test = _createTest(attr, value);
this._children.forEach(child => {
child.forEach((node) => {
if (test(node))
results.push(node);
});
});
return results;
} And then it just becomes: findByTag(...query) {
return this.findExcludeSelf(node => node.tags.has(...query));
}
I agree, so why create even more overloading?
Technically you already used This code: isDescendantOf(node) {
return !!this.findAncestor(parent => parent === node);
} Is semantically this: isDescendantOf(node) {
return this.someAncestor(ancestor => ancestor === node);
}
someAncestor(attr, value) {
const test = _createTest(attr, value);
let parent = this._parent;
while (parent) {
if (test(parent))
return true;
parent = parent._parent;
}
return false;
}
Right, I definitely see the point for your
@LeXXik Just regexing a bit (via engine/src/framework/components/collision/system.js Lines 102 to 113 in b17180c
This looks like this could be expressed way shorter via const isTypeCompound = entity => entity.collision && entity.collision.type === 'compound';
component._compoundParent = entity.findAncestor(isTypeCompound); |
@kungfooman ah, I meant in our team's custom code, not the engine 😅 |
So, last try to make everyone happy. I considered There are now I kept |
Thank you for the changes @NewboO ! I had no time to test it yet, just looking it over. Two notes:
JSDoc that should work:
I would also remove the underscore, it is private by default if you don't export it in in ES6 file scope, but I've seen it with and without underscore, so it doesn't really matter I guess.
|
Co-authored-by: Hermann Rolfes <lama12345@gmail.com>
We had a quick discussion, and agreed it's a great PR from many points of view, but it's so large, including the follow up discussion, that it's becoming hard to continue the work on this. The suggestion is to break it down into small issues, addressing individual improvements / new APIs. The discussion should take place on those separate issues, and a relevant PR should be created when an agreement is reached there. If there are some optimizations to existing done here for example, it'd be awesome to move those to a separate PR, as that would be valuable to have reviewed and merged. |
This PR add various functions to find entities, components and script instances either through parents or children. It completes the already existing functions to find through children and can be used the same way.
List of new functions:
It also simplifies some already existing functions in
GraphNode
.I hope I documentated them correctly. I also added unit tests inspired by the existing ones.
I confirm I have read the contributing guidelines and signed the Contributor License Agreement.