Sorting SimSets (A Code Snippet)
by Charlie Patterson · in Torque Game Builder · 02/16/2012 (2:19 pm) · 11 replies
I'm on my third situation where I want to sort a simset. Each time, though, the criterion for sorting is different. Maybe I want to sort by position.y or maybe by some "natural order" field I keep around or maybe by the "cost" of buying the items in the SimSet. I was uncomfortable writing and debugging a new sort for each situation.
The solution for me is a simple "bubble sort" that takes the sort criterion as an input function. If you have used C's qsort(), you get the interface already. Here is the code:
That's it! Now for the harder part. Each call must provide a "comparator" function of the form "compare(%a, %b)". The comparator must return a negative number if a should be sorted first, positive if b should go first, and 0 if they are equal. With this function, easySort can sort any SimSet.
Here's an example to sort by the y position of your objects:
Get it? If we return a's value minus b's value, then if a's is smaller, I'll get a negative number. If they are the same, I'll get 0, and if a's is bigger, I'll get a positive. If I wanted to sort in reverse order, I would change the last line to "return %b.position.y - %a.position.y;"
One more example, this time with some spit on it to show it can get complicated (though nowhere near as bad as making little sorts all over your code that have to do the same thing anyway). This time, I have a field, in my ButtonBehavior, named naturalOrder. No matter what order things are created I want to sort by this order. However, if the natural order field is -1, I'm basically saying, I don't care what order I'm end. Just put me at the end somewhere. With that, here's the comparator:
Caveats:
Well you could probably write a faster sort, but this one is full TorqueScript and does the best it can (or at least the best I can think of) with the SimSet interface. For instance, you can't swap two elements in the SimSet interface; you have to reorder(). To that end, this will be fast enough for small SimSets or for a little larger SimSets that you run once a level. To beat the performance, though, you'd probably need to find a way to just have your SimObjects pre-sorted somehow.
The solution for me is a simple "bubble sort" that takes the sort criterion as an input function. If you have used C's qsort(), you get the interface already. Here is the code:
function easySort(%set, %comparator)
{
for (%i = 0; %i < %set.getCount() - 1; %i++) {
%minIndex = %i;
for (%j = %i + 1; %j < %set.getCount(); %j++) {
%which = call(%comparator, %set.getObject(%minIndex), %set.getObject(%j));
if (%which > 0)
%minIndex = %j;
}
if (%minIndex != %i)
%set.reorderChild(%set.getObject(%minIndex).getId(), %set.getObject(%i).getId());
}
}That's it! Now for the harder part. Each call must provide a "comparator" function of the form "compare(%a, %b)". The comparator must return a negative number if a should be sorted first, positive if b should go first, and 0 if they are equal. With this function, easySort can sort any SimSet.
Here's an example to sort by the y position of your objects:
easySort(%mySimSet, yComparator);
function yComparator(%a, %b)
{
return %a.position.y - %b.position.y;
}Get it? If we return a's value minus b's value, then if a's is smaller, I'll get a negative number. If they are the same, I'll get 0, and if a's is bigger, I'll get a positive. If I wanted to sort in reverse order, I would change the last line to "return %b.position.y - %a.position.y;"
One more example, this time with some spit on it to show it can get complicated (though nowhere near as bad as making little sorts all over your code that have to do the same thing anyway). This time, I have a field, in my ButtonBehavior, named naturalOrder. No matter what order things are created I want to sort by this order. However, if the natural order field is -1, I'm basically saying, I don't care what order I'm end. Just put me at the end somewhere. With that, here's the comparator:
function buttonComparator(%a, %b)
{
%aOrder = %a.getBehavior(ButtonBehavior).naturalOrder;
%bOrder = %b.getBehavior(ButtonBehavior).naturalOrder;
if (%aOrder == -1 && %bOrder != -1) return 1;
if (%aOrder != -1 && %bOrder == -1) return -1;
return %aOrder - %bOrder;
}Caveats:
Well you could probably write a faster sort, but this one is full TorqueScript and does the best it can (or at least the best I can think of) with the SimSet interface. For instance, you can't swap two elements in the SimSet interface; you have to reorder(). To that end, this will be fast enough for small SimSets or for a little larger SimSets that you run once a level. To beat the performance, though, you'd probably need to find a way to just have your SimObjects pre-sorted somehow.
#2
02/17/2012 (9:25 am)
Thanks @Patrick!
#3
02/18/2012 (11:47 pm)
Very useful, I must do a speed test on this to see if it's viable at runtime on iOS devices. I've often wanted to do 'perform an action on the 10 nearest objects from an unsorted list of around 100' but always assumed it would be too slow to do often, and designed around it (using expanding colliders to find close objects).
#4
I should point out that this code isn't *speedy* just as fast as I think I can make it with the standard SimSet interface. It's more about being usable on anything as long as you provide a "comparator". I'm using it in three places in my code already.
In your case, you'd probably want to enhance this code to stop after the first 10. This would be easy because the code literally finds the minimum and puts it first, find the next minimum and puts it second.... dead simple. You could stop after 10.
02/19/2012 (12:00 am)
Hmm. interesting case @Conor. I've never heard of expanding colliders. That sounds much cooler. :PI should point out that this code isn't *speedy* just as fast as I think I can make it with the standard SimSet interface. It's more about being usable on anything as long as you provide a "comparator". I'm using it in three places in my code already.
In your case, you'd probably want to enhance this code to stop after the first 10. This would be easy because the code literally finds the minimum and puts it first, find the next minimum and puts it second.... dead simple. You could stop after 10.
#5
Make an invisible scene object with circular collision, set it to size 0,0 and put it at the location you want to search from. Over the next 10 frames size it up in steps, so that it expands to its full size which is the upper limit you want to search to. Then you just add anything it gets an onCollision callback from (filtered by appropriate collision group masks) to a simset and they'll be in roughly the right order.
02/21/2012 (10:27 pm)
Charlie - my 'expanding collider' sorting hack works like this:Make an invisible scene object with circular collision, set it to size 0,0 and put it at the location you want to search from. Over the next 10 frames size it up in steps, so that it expands to its full size which is the upper limit you want to search to. Then you just add anything it gets an onCollision callback from (filtered by appropriate collision group masks) to a simset and they'll be in roughly the right order.
#6
But... I can't help writing a little (untested) code. :) Something like this:
So you'd probably want to modify easy_sort to add the total number you need sorted, a way to pass in needed data, like $testPosition, and some way for your comparator to remember Vectordist($testPosition, %a) and not rebuild it over and over, because that's expensive. :)
02/22/2012 (6:50 pm)
I see @Conor. That sounds pretty clever! I assume you know about pickRadius if you decided to go that route. Unfortunately, pickRadius doesn't return a SimSet. So by the time you pick stuff, put it in a SimSet and sort it all out, you may be better off as you are. This is especially true because the comparator would be naive and would recalculate a lot of distance math, unless you found a way to cache results as you go.But... I can't help writing a little (untested) code. :) Something like this:
%enemies = sceneWindow2D.getSceneGraph().pickRadius(%this.owner.position, %radius, %groupMask, %layerMask);
%set = new SimSet();
for (%i = 0; %i < getWordCount(%enemies); %i++)
%set.add(getWord(%enemies, %i));
$testPosition = %this.owner.position; // ugh! a global, but it's simple!
easy_sort(%set, distance_comparator);
// in order now
for (%i = 0; %i < %set.getCount(); %i++) {
// use it here!
}
%set.delete();
function distance_comparator(%a, %b) {
return VectorDist($testPosition, %a) - VectorDist($testPosition, %b);
}So you'd probably want to modify easy_sort to add the total number you need sorted, a way to pass in needed data, like $testPosition, and some way for your comparator to remember Vectordist($testPosition, %a) and not rebuild it over and over, because that's expensive. :)
#7
I've used pickRadius before as the basis for my iPhone onTouchDown system, it's particularly useful when you know the user might be touching multiple overlapping objects and you don't want them all getting a callback.
02/29/2012 (5:13 am)
That's a nice way to go from a big unsorted picklist to a useful sorted simset, I'll give that a try in my next game.I've used pickRadius before as the basis for my iPhone onTouchDown system, it's particularly useful when you know the user might be touching multiple overlapping objects and you don't want them all getting a callback.
#8
Let's say you have a SimSet filled with t2dSceneObjects. To sort ascending on "Lifetime" just call
...or if you have your own defined members, for example MyParameter.
...and because SimGroup is derived from SimSet, the function works on a SimGroup object as well. In fact all t2dSceneGraph and GuiControl, to mention a few more are derived from SimSet too. Enjoy...
01/29/2013 (4:41 pm)
I have modified the sort function a bit. It felt more natural to actually add the whole sortfunction to the SimSet class and pass in what field you want to sort in ascend or descend order.$Sortorder::Ascend = false;
$Sortorder::Descend = true;
function SimSet::Sort(%this,%field,%descend)
{
for (%i = 0; %i < %this.getCount() - 1; %i++) {
%minIndex = %i;
for (%j = %i + 1; %j < %this.getCount(); %j++) {
%which = %this.StdComparator(%this.getObject(%minIndex), %this.getObject(%j),%field,%descend);
if (%which > 0)
%minIndex = %j;
}
if (%minIndex != %i)
%this.reorderChild(%this.getObject(%minIndex).getId(), %this.getObject(%i).getId());
}
}
function SimSet::StdComparator(%this,%a,%b,%field,%descend) {
if(%descend == true)
return %b.getFieldValue(%field) - %a.getFieldValue(%field);
else
return %a.getFieldValue(%field) - %b.getFieldValue(%field);
}Let's say you have a SimSet filled with t2dSceneObjects. To sort ascending on "Lifetime" just call
mySimSet.Sort("Lifetime"); // Sort ascending (default sort order)...or if you have your own defined members, for example MyParameter.
mySimSet.Sort("MyParameter",$Sortorder::Descend); // Sort descending...and because SimGroup is derived from SimSet, the function works on a SimGroup object as well. In fact all t2dSceneGraph and GuiControl, to mention a few more are derived from SimSet too. Enjoy...
#9
01/29/2013 (5:58 pm)
Oh hey this will probably come in handy! Thanks for the update.
#10
I use it to sort objects displayed in the GUI when pressing different sort options. Each sort option is a guiBitmapButtonCtrl set to a radiobutton type with the text field set to the field I want to sort on.
Example:
01/29/2013 (6:33 pm)
I guess the function is a little bit slower than the original because of the call to getFieldValue().I use it to sort objects displayed in the GUI when pressing different sort options. Each sort option is a guiBitmapButtonCtrl set to a radiobutton type with the text field set to the field I want to sort on.
Example:
new guiBitmapButtonCtrl()
{
class = "TSortButton";
buttonType = "RadioButton";
groupNum = 1;
text = "Health";
bitmap = "MyBitmap1";
}
new guiBitmapButtonCtrl()
{
class = "TSortButton";
buttonType = "RadioButton";
groupNum = 1;
text = "Cost";
bitmap = "MyBitmap2";
}
TSortButton::onAction(%this) {
MyList.Sort(%this.getText(),$GUISortOrder);
UpdateListGUI(MyList);
}
#11
For 90% of cases, your StdComparator works great and is probably the same speed.
If you want to sort non-numbers, or if you want to sort numbers in a special way, like my "naturalOrder" example, you'll have to provide a special comparator.
01/29/2013 (7:30 pm)
Sweet! I like how you put it in the SimSet class. Back then, I felt odd about it, but now it feels very natural.For 90% of cases, your StdComparator works great and is probably the same speed.
If you want to sort non-numbers, or if you want to sort numbers in a special way, like my "naturalOrder" example, you'll have to provide a special comparator.
Torque Owner RollerJesus
Dream. Build. Repeat.