Search Unity

How to Reorder Lists

Discussion in 'Scripting' started by Mashimaro7, Oct 20, 2020.

  1. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    I have a list that collects all TextMesh objects in the scene. I would like to sort it by hierarchy, but for some reason it seems to be sorting it by when it was created, ascending. I tried organizing them in the hierarchy but it doesn't seem to make any difference. I am using
    Code (CSharp):
    1. texts = FindObjectsOfType<TextMeshProUGUI>().ToList();
    to populate my list. I tried using the .Orderby() Linq function, but I can't really figure out how it works. Even sorting by position y would work, as my project is pretty well top to bottom.

    Thanks in advance.
     
    DAEXDO_3240 likes this.
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    You can use Array.Sort with a comparison delegate. Untested, but this should work:
    Code (csharp):
    1.  
    2.             var texts = FindObjectsOfType<TextMeshProUGUI>();
    3.             Array.Sort<TextMeshProUGUI>(texts, (a, b) => {
    4.                 return a.transform.position.y.CompareTo(b.transform.position.y);
    5.             });
     
    Bunny83 and Mashimaro7 like this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    ignore Linq whenever you can. It wasn't made for games. don't run away from it either, sometimes it's useful.
    you need to supply a lambda to your sorting function.

    StarManta was quicker with a working example of a lambda.
     
    Mashimaro7 likes this.
  4. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    I can't get it to work, it says "Array does not exist in this context." It could be because it's a list? There seems to be a sort function if I type "texts.Sort" but it doesn't work with your code there.

    Also, how does this work exactly? what are "a" and "b"? Does this cycle through the whole list?
     
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Make sure you have
    using System;
    in your file.

    And yes, it goes through the list and sorts it. "a" and "b" are two hypothetical elements of the list that need to be compared. The sort function will use that comparison function over and over again so it knows what order things are supposed to go in. The actual algorithm used to sort is not important, just know that sorting will happen.
     
    Mashimaro7 likes this.
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    @Mashimaro7
    StarManta just forgot to end the first line with .ToList();
    look at your code and do the same thing.

    Lists =/= arrays
    And the array is what you get normally.

    edit:
    he didn't forget, he used a slightly different approach that works with arrays instead, so go with his PraetorBlue's answer.

    also try learning more about lambdas in general. you need them only rarely, but it helps if you can understand them.
     
    Last edited: Oct 20, 2020
    Mashimaro7 likes this.
  7. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Hmm, I'm still running into an error.
    Code (CSharp):
    1.     private void Start()
    2.     {
    3.         print(FindObjectsOfType<TextMeshProUGUI>());
    4.         texts = FindObjectsOfType<TextMeshProUGUI>().ToList();
    5.         texts.Sort<TextMeshProUGUI>(texts, (a, b) => {
    6.             return a.transform.position.y.CompareTo(b.transform.position.y);
    7.         });
    8.     }
    "The nongeneric method "List<TextMeshProUGUI>().Sort() cannot be used with type arguments"
     
  8. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Try
    Code (CSharp):
    1. texts.Sort((a, b) => a.transform.position.y.CompareTo(b.transform.position.y));
     
  9. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    No errors now, but it is still sorting by most recent it appears... I tried reordering in hierarchy, and renaming, but it's sorting by the most recently created ones. Which is pretty inconvenient because I have a script that changes the text based on the number in the List.
     
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    What are you basing this on? If you're basing it on your print() statement in your code, note that that is logging the unmodified result. Do you have another debug log somewhere that has the order after you sort it?
     
  11. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    I have it public, I'm viewing it in the inspector. Here's a screenshot.
    upload_2020-10-21_3-42-55.png
    "Titles" was "title (1)" before, I just renamed it to see if it would reposition it, but no luck.
     
  12. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Strange. I think my next step in your shoes would be to put in some logs to ensure that the code is being called when I expect it to be. So like:
    Code (csharp):
    1.  
    2.         texts.Sort<TextMeshProUGUI>(texts, (a, b) => {
    3.             Debug.Log($"Comparing {a.gameObject.name} at {a.transform.position} to {b.gameObject.name} at {b.transform.position}");
    4.             return a.transform.position.y.CompareTo(b.transform.position.y);
    5.         });
    And you should see that log statement being outputted a bunch, covering a bunch of combinations of your text objects. This is just to make sure your sort code is being called when you expect it to.

    Another possibility to look into would be the possibility that your "texts" field is getting overwritten at some point. In VS you can right click on texts and "Find All References", and put a Debug.Log next to each line you see there that may be setting your array's values.

    One thing that's a bit suspicious to me: there are text objects there that are NOT included in your array, which means they must have been created after FindObjectsOfType was run. If that's the case, I wonder if there is more initialization that must be run before your sorting order is valid. Maybe try adding a one-frame delay to this searching and sorting thing to make sure that all the initialization is done running?
     
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    reposition it? You mean inside the array? Or inside the hierarchy? Have you checked the elements one by one as they appear in your array and compared the y coordinates of them?

    I'm still not sure what exact criteria you want to use to sort your elements. Are you actually interested to sort them based on their phyiscal y position in the scene?
     
  14. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    I think (s)he means as ordered in the Transform itself, not by world coordinates

    @Mashimaro7
    take a look at Transform.GetSiblingIndex()
     
  15. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    And then
    Code (csharp):
    1. texts.Sort((a, b) => a.transform.GetSiblingIndex().CompareTo(b.transform.GetSiblingIndex()));
     
  16. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    What are you trying to do that requires the List of TMP objects to be sorted by the order they appear in the hierarchy anyway? More often than not, sorting the collection isn't the only solution.
     
    PraetorBlue likes this.
  17. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    Well it's not entirely nonsensical. Maybe (s)he just wants to be able to author the order through hierarchy itself, which is a valid technique imo. Though your question is also valid, maybe there is a better solution, and the whole sorting ordeal is a conceptual mislead.
     
  18. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    I'm trying to make a serializable list of editable text objects, that can be increased or decreased with the click of a button. The issue is, say I have 3 texts, it's sorting them in order of ,"3,2,1." so, if I make another one and add it to that list, it will be "3,2,1,4" then when I load the game again, it will load up the new one as 4,3,2,1, and the text will be all outta whack because i t will be loading the index "3" which will have been saved to number 4, into index 0 because number 4 is the top of the list now.

    Edit: Each text object has an attached script which serializes it to PlayerPrefs based on the index of the manager object
     
  19. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    your problem is weird. I'm not convinced it's the right approach to solve serialization issues. you kind of messed something else up if your indices get shuffled every time you make a change.
     
  20. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    The issue is for some reason it's sorting by time of creation. I'm not sure why? I really didn't do anything, the above script is the only one that populates the list.

    But the issue is, if I create and add to the list during playtime, then after saving and quitting and loading again, it will repopulate the list.
     
  21. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    wait a minute, you mean FindObjectsOfType?
    you have a problem with FindObjectsOfType basically reversing your intended order?
    is that it?

    can you come up with a code that clearly reproduces an issue, so we can fix it, I'm sure a simpler and more logical solution is at your grasp.
     
  22. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    there is frankly an issue with documentation on this one as well. the order isn't exactly specified, and well, maybe it's not specified by design, but knowing unity, maybe it's just not documented. but let's investigate this first.
     
  23. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    are you by any chance building this list from a single transform (excluding children*)?
    because if you do, I think the best way would be to, in fact, implement your own FindObjectsOfType.
    not only you get more control, but it's also very easy, and it'll likely run faster, because I doubt Unity is using the C# 8 pattern matching to match the types.

    (* though it's certainly doable to do full tree traversal, I'm not sure if we really want to reinvent the wheels)
     
  24. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Here's my script,
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.UI;
    6. using TMPro;
    7. using UnityEngine.UIElements;
    8.  
    9. public class GUIControl : MonoBehaviour
    10. {
    11.     public GameObject panel;
    12.     public List<TextMeshProUGUI> texts;
    13.     public TextMeshProUGUI fieldo;
    14.  
    15.     string thisText;
    16.  
    17.     private int number;
    18.  
    19.     private void Start()
    20.     {
    21.         texts = FindObjectsOfType<TextMeshProUGUI>().ToList();
    22.         texts.Sort((a, b) => a.transform.position.y.CompareTo(b.transform.position.y));
    23.     }
    24.     public void OpenPanel(int num)
    25.     {
    26.         number = num;
    27.         panel.SetActive(true);
    28.     }
    29.  
    30.     public void ClosePanel()
    31.     {
    32.         panel.SetActive(false);
    33.     }
    34.  
    35.     private void Update()
    36.     {
    37.  
    38.             if (Input.GetKeyDown(KeyCode.Return))
    39.             {
    40.                 texts[number].text = fieldo.text;
    41.                 thisText = texts[number].text;
    42.                 PlayerPrefs.SetString(number.ToString(), thisText);
    43.             print(number);
    44.             }
    45.      
    46.     }
    47. }
    48.  
    I can't really understand how it finds the objects, to be honest. It just seems to be by order of creation.
     
  25. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    The only fact is that the docs do not specify the order. So it might be anything really.
    You haven't answered any of my questions so that I can help you. Nvm, let's try this anyway

    Code (csharp):
    1. List<T> FindInTransform<T>(Transform xf) {
    2.   var list = new List<T>();
    3.   for(int i = 0; i < xf.childCount; i++) {
    4.     var found = xf.GetChild(i).GetComponent<T>();
    5.     if(found != null) {
    6.       Debug.Log($"{found.gameObject.name} was found");
    7.       list.Add(found);
    8.     }
    9.   }
    10.   return list;
    11. }
    Try this and show us the logged results (disable sorting for the time being)
    Code (csharp):
    1. texts = FindInTransform<TextMeshProUGUI>(this.transform);
    (Hopefully all your texts are in the same transform, so specify that transform as an argument here)
     
    Last edited: Oct 21, 2020
  26. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry, I figured my script answered most your questions haha.

    I got an error with this script, "T does not contain a definition for 'gameObject' the intellisense doesn't let me put anything there except "found" and it just seems to print nothing to the console.
     
  27. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    You've written a generic method with an unconstrained type parameter T. That means T can be literally any type possible. In your Debug.Log call you used
    found.gameObject
    . which is not possible since T can be anything. There is no gameObject in a float, Vector3 or any other type that is not a Component derived type.

    If you want to keep the type parameter unconstraint (so it would also work with interface types) you just have to remove your Debug.Log statement which also seems to just clutter the log.

    If you want to log the gameobjects name, use the actual Transform reference you have:

    Code (CSharp):
    1.  
    2. for(int i = 0; i < xf.childCount; i++)
    3. {
    4.     var child = xf.GetChild(i);
    5.     var found = child.GetComponent<T>();
    6.     if(found != null)
    7.     {
    8.         Debug.Log($"{child.gameObject.name} was found");
    9.         list.Add(found);
    10.     }
    11. }
    Another solution is to constrain the Type parameter T to Component. So you can only use types derived from Component as parameter. That way it's guaranteed that the type T is a Component and therefore has a gameObject property

    Code (CSharp):
    1.  
    2. List<T> FindInTransform<T>(Transform xf) where T : Component
    3. {
    4.  
     
    Mashimaro7 likes this.
  28. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    True, I wanted him/her to test the premise, and it was 3 AM where I live.
    Yes it should be constrained to Component, obviously.

    I mean, wasn't this a dead giveaway that it's a typo?
    But thanks for fixing my mistake and explaining why it matters.

    Yes it's supposed to clutter the log, this is not a final solution, I want OP to log the results.
    No, it doesn't have to work in an unconstrained manner.

    This is only a half-baked quasi-replacement for FindObjectsOfType, where we try to establish some control over the order of the output, because this is what OP wanted.


    @Mashimaro7
    And? Does it work?
    Sorry about the typo btw.
     
    Last edited: Oct 21, 2020
    Mashimaro7 likes this.
  29. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    As Bunny83 pointed out, this is because found is of type T, which ends up being TextMeshProUGUI at the calling site.

    So I've mistakenly made it assume there was gameObject property available for this object, however the compiler cannot see this to be true in the general case. Because it's not.

    If we constrain the type T only to Component type (and TextMeshProUGUI derives from one), then it's fine, because gameObject does belong to this and any derived type.

    This is why Bunny83 offered an unconstrained solution, as a workaround for the general case, where we query the actual Transform instead of the component we get with GetComponent, which in an unconstrained case could've potentially be an interface as well.

    But no, I intended this to be constrained so just add
    where T : Component
    in the method declaration.

    Hopefully this serves as a simple generic method tutorial and exercise for you. Now you know why we sometimes use <> thingies to specify types (and how it can fail).

    Sorry for not replying sooner, I really had to sleep.

    We've done this because I'm curious to see the actual order you'll get from this.
     
    Mashimaro7 likes this.
  30. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry for the late response, I've been busy with other projects.

    But I just picked it up again and was able to solve the issue. I just simply removed the text objects I had on scene(except for the title), and now made it texts.Add(intantiate) for every object, added the button that adds texts and had it essentially save the number of texts to player prefs, then instantiate the number of texts. I don't know why, but the weird ordering issue no longer occurs, even if I add new ones during gameplay.

    Thanks for the help guys. Sorry I wasn't able to test the code you provided haha.
     
  31. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    it's okay, maybe you can use it to debug stuff or traverse through transforms in the future.
     
    Mashimaro7 likes this.