0

I have a strict similar to this :

struct Chunk
 {
   public float d; // distance from player
   public bool active; // is it active
 }

I have an array of this struct.

What I need:

I need to sort it so the first element is an inactive chunk that is also the furthest from the player, than the next element is active and is also the closest to the player and that is the pattern,

  1. Inactive, furthest
  2. Active, closest
  3. Inactive, furthest
  4. Active, closest
  5. Inactive, furthest And so on...

Currently I'm using LINQ, And I'm doing this :

chunk = chunk.AsParallel().OrderBy(x => x.active ? x.d : -x.d).ToArray();

But I don't know how to make it alternate one after another.

3
  • Try using OrderBy(x => !x.active ? -x.d : x.d).ThenBy(x.active ? x.d : -x.d) to sort on multiple fields. Commented Feb 9, 2022 at 1:31
  • 1
    What would you expect to happen if you have 10 active elements and only 2 inactive? Commented Feb 9, 2022 at 1:48
  • The unity3d tag isn't really necessary but thanks anyway. :) Commented Feb 9, 2022 at 2:55

3 Answers 3

5

It looks like you want to split this into two lists, sort them, then interleave them.

var inactive = chunk.Where(x => !x.active).OrderByDescending(x => x.d);
var active = chunk.Where(x => x.active).OrderBy(x => x.d);
var interleaved = inactive.Zip(active, (a, b) => new [] { a, b }).SelectMany(x => x);
Sign up to request clarification or add additional context in comments.

1 Comment

It seems to have broken my rendering system, because the chunks stopped unloading. LOL
2

But I don't know how to make it alternate one after another.

If you sort the array so that all the inactives are at the start, descending and all the actives are at the end, descending..

OrderBy(x => x.active?1:0).ThenBy(x=>-x.d)

then you can take an item from the start, then an item from the end, then from the start + 1, then the end - 1 working your way inwards

public static IEnumerable<Chunk> Interleave(this Chunk[] chunks){
  
    for(int s = 0, e = chunks.Length - 1; s<e;){

        if(!chunks[s].active)
            yield return chunks[s++]; 

        if(chunks[e].active)
            yield return chunks[e--];

    }

}

There's a bit in this so let's unpack it. This is an extension method that acts on an array of chunk. It's a custom enumerator method, so you'd call foreach on it to use it

foreach(var c in chunk.Interleave())

It contains a for loop that tracks two variables, one for the start index and one for the end. The start increments and the end decrements. At some point they'll meet and s will no longer be less than e, which is when we stop:

for(int s = 0, e = chunks.Length - 1; s<e;){

We need to look at the chunk before we return it, if it's an inactive near the start, yield return it and bump the start on by one. s++ increments s, but resolves to the value s was before it incremented. It's thus conceptually like doing chunks[s]; s += 1; but in a one liner

if(!chunks[s].active)
    yield return chunks[s++]; 

Then we look at the chunk near the end, if it's active then return the ending one and bump the end index down

The inactive chunks are tracked by s, and if s reaches an active chunk it stops returning (every pass of the loop it is skipped), which means e will work its way down towards s returning only the actives

Similarly if there are more inactives than actives, e will stop decrementing first and s will work its way up towards e


If you never came across yield return before think of it as a way to allow you to resume from where you left off rather than starting the method over again. It's used with enumerations to provide a way for the enumeration to return an item, then be moved on one and return the next item. It works a bit like saving your game and going doing something else, then coming back, realising your save game and carrying on from where you left off. Asking an enumerator for Next makes it load the game, play a bit, then save and stop.. Then you Next again and the latest save id loaded, play some more, save and stop. This way you gradually get through the game a bit at a time. If you started a new enumeration by calling Interleave again, that's like starting a new game over from the beginning

MSDN will get more detailed on yield return if you want to dig in more


Edit:

You can perform an in-place sort of your Chunk[] by having a custom comparer:

public class InactiveThenDistancedDescending : IComparer
{
   public int Compare(object x, object y)
   {
       var a = (Chunk)x;
       var b = (Chunk)y;
       if(a.Active == b.Active)
         return -a.Distance.CompareTo(b.Distance);
       else
         return a.Active.CompareTo(b.Active);
   }
}

And:

Array.Sort(chunkArray, _someInstanceOfThatComparerAbove);

13 Comments

I like this, and it also seems pretty fast, THANKS
Also how can I learn more about sorting stuff in this way? it sounds very interesting.
And one more thing.. Does adding AsParallel() do anything? and if it does, where should I add it?
Welcome! I don't know whether there is anything out there that teaches this; I expect so - I didn't set out to demo the use of anything in particular - I was just musing on a way to do as much as possible with an array and doing a custom interleaving iterator with yield return seemed like a reasonable teaching point! You might actually get another performance boost by switching to using Array.Sort and sorting the original chunks array in place rather than making a new array with the OrderBy.ToArray. As for Parallel, it might make things worse - you'd have to perf test it and see
Great answer Caius. I would suggest if the O.P. is unfamiliar with yield return maybe link to an existing Q&A
|
1

Not sure if you can do it with only one line of code.

I wrote a method that would only require the Array to be sorted once. Then, it enters either the next closest or furthest chunk based on the current index of the for loop (odd = closest, even = furthest). I remove the item from the sorted list to ensure that it will not be reentered in the results list. Finally, I return the results as an Array.

public Chunk[] SortArray(List<Chunk> list_to_sort)
{
    //Setup Variables
    var results = new List<Chunk>();
    int count = list_to_sort.Count;

    //Tracking the sorting list so that we only need to sort the list once
    list_to_sort = list_to_sort.OrderBy(x => x.active).ThenBy(x => x.d).ToList();

    //Loop through the list
    for (int i = 0; i < count; i++)
    {
        results.Add(list_to_sort[i % 2 == 0 ? list_to_sort.Count - 1 : 0]);
        list_to_sort.RemoveAt(i % 2 == 0 ? list_to_sort.Count - 1 : 0);
    }

    // Return Results
    return results.ToArray();
}

There is probably a better way of doing this but hopefully it helps. Please note that I did not test this method.

3 Comments

I suspect you meant to sort by x.active first, then by x.d instead of the other way around. Also, what happens to this method if the number of active entries is not equal to the number of inactive entries?
@AndrewWilliamson You are right, I've updated my answer. You also bring up a good point and it depends how they want to handle it. As of right now, let say there are 3 extra Active chunks in the list, it will insert it as (closest, furthest, closest).
My frame rate just goes to 0 for a few secs...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.