Search Unity

Condensing the amount of Code used for CompareTo Functions

Discussion in 'Scripting' started by TheFunkMonk, Jul 22, 2016.

  1. TheFunkMonk

    TheFunkMonk

    Joined:
    May 10, 2015
    Posts:
    4
    Hi all,

    I have been trying to get my head around List.Sort () function and I've run into an issue with the amount of code required to do all the sorting. I'm not a total beginner to unity but i don't do much posting on here so i hope you'll bear with me while i explain. I have created a class called character that looks like this:

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. [System.Serializable]
    6. public class Character
    7. {
    8.      public enum Race {Human = 0, Elf, Dwarf, Gnome};
    9.      public enum Role {Warrior = 0, Wizard, Priest, Ranger, Rouge, Bard};
    10.  
    11.      public string name;
    12.      public int level;
    13.      public Race race;
    14.      public Role role;
    15.  
    16.      public Character ()
    17.      {
    18.          name = "DEFAULT NAME";
    19.          level = 1;
    20.          race = Race.Human;
    21.          role = Role.Warrior;
    22.      }
    23.      public Character (string newName, int startLevel, Race newRace, Role newRole)
    24.      {
    25.          name = newName;
    26.          level = startLevel;
    27.          race = newRace;
    28.          role = newRole;
    29.      }
    30. }
    I have a another function in a separate 'Manage' type script that uses the second constructor (above) to generate several random characters and add them to a list. I then wanted to be able to sort that list using any of a series of the variable in the class 'Character'. For example, if i sort by 'level' i want all Characters of the same level to be sorted again by 'Race' and then again by 'Class' if needed. I have done this in the Manager script by using the following function:

    Code (csharp):
    1. public int SortByLevel (Character subject, Character other)
    2. {
    3.     if (other == null)
    4.     {
    5.         return 1;
    6.     } else {
    7.         int firstComp = -subject.level.CompareTo (other.level);
    8.         int secondComp = subject.race.CompareTo (other.race);
    9.         int thirdComp = subject.role.CompareTo (other.role);
    10.  
    11.         if (firstComp != 0) {
    12.             return firstComp;
    13.         } else if (secondComp != 0) {
    14.             return secondComp;
    15.         } else {
    16.             return thirdComp;
    17.         }
    18.     }
    19. }
    I then sort the list by calling the function:

    Code (csharp):
    1.  listOfCharacterst.Sort (SortByLevel);
    I have written 'SortByWhatever' functions for each of the other 3 variables in the class and a further function that uses a 'Switch' statement to select between them. All this works fine, However, the amount of code needed to write a 'SortByWhatever' type function for each of the variables in the class seems like a lot, especially when i consider that there may be many more variables that i would want to sort and several separate lists.

    I'm a little concerned about things getting out of hand and i'd really like to try and pair down on the amount of coding involved. So, is it possible to create a kind of generic 'SortBy' function and pass it the variables i want to sort by? ultimately i want to be able to call a fiction that looks something like this:

    Code (csharp):
    1. listOfCharacters.Sort (SortBy (level, race, role));
    Is this kind of thing possible? it seems to me like it should be doable using a minimal amount code but i can't seem to work it out, can anyone help me make sense of this?

    Thanks in advance for your help :)
    TheFunkMonk
     
  2. MathiasDG

    MathiasDG

    Joined:
    Jul 1, 2014
    Posts:
    114
    Maybe you're looking for something like this:

    Code (CSharp):
    1. public class Character
    2.     {
    3.         public static CharacterComparer levelComparer = new CharacterComparer(new int[3] { (int)Variable.level, (int)Variable.race, (int)Variable.role });
    4.         public static CharacterComparer raceComparer = new CharacterComparer(new int[3] { (int)Variable.race, (int)Variable.role, (int)Variable.level });
    5.         public static CharacterComparer roleComparer = new CharacterComparer(new int[3] { (int)Variable.role, (int)Variable.race, (int)Variable.level });
    6.  
    7.         public int[] variables;
    8.  
    9.         public enum Variable
    10.         {
    11.             level = 0,
    12.             race = 1,
    13.             role = 2,
    14.         }
    15.  
    16.         public void SortByLevel(List<Character> list)
    17.         {
    18.             list.Sort(levelComparer);
    19.         }
    20.  
    21.         public class CharacterComparer : Comparer<Character>
    22.         {
    23.             int[] rule;
    24.  
    25.             public CharacterComparer(int[] rule)
    26.             {
    27.                 this.rule = rule;
    28.             }
    29.  
    30.             public override int Compare(Character subject, Character other)
    31.             {
    32.                 for (int i = 0; i < rule.Length; i++)
    33.                 {
    34.                     int j = rule[i];
    35.                     int compare = subject.variables[j].CompareTo(other.variables[j]);
    36.                     if (compare != 0) return compare;
    37.                 }
    38.                 return 0;
    39.             }
    40.         }
    41.     }
    I haven't tested it. It could work. Or not.
    Anyway, it should give you a head start.
    Cons are, you don't get your variables named beautifully as "level", "race" and whatever you want to name them. You have to either use the enum to acess the array variable you want everytime, or you can use intergers directly if you remember them.
    Pros are you get a slim code.

    I think you can make variables array accept objects instead of ints, and then compare objects using their comparer if you wanted to store multiple types of variables.

    I would only go this path if i had really a lot of variables to compare, and if i wanted to be able to easily add more in the future.

    If you wanted to to this, you could use some helper properties, such as
    Code (CSharp):
    1.  
    2. public int Level { get { return variables[(int)Variable.Level]; } }
    3. public int Race { get { return variables[1]; } }
    4.  
    But that will give you a slight overhead over direct accessing a field, if performance is an issue for this.

    Let me know if it works.
     
    Last edited: Jul 22, 2016
  3. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    Generics to the rescue!

    Code (csharp):
    1. public static int Compare<T>(T first, T second, params Func<T, IComparable>[] compareFuncs) {
    2.     for (int i = 0; i < compareFuncs.Length; i++) {
    3.         IComparable firstVal = compareFuncs[i](first);
    4.         IComparable secondVal = compareFuncs[i](second);
    5.  
    6.         int compareResult = firstVal.CompareTo(secondVal);
    7.         if(compareResult != 0)
    8.             return compareResult;
    9.     }
    10.     return 0;
    11. }
    You could replace T with Character here and make the method non-generic, but this is a reusable concept. The method takes two objects, and a variable amount of functions that takes objects of that kind and returns a comparable value.

    Your SortByLevel will look like this:

    Code (csharp):
    1. public int SortByLevel(Character subject, Character other) {
    2.     if (other == null) {
    3.         return 1;
    4.     }
    5.     else {
    6.         return Compare(subject, other,
    7.                         c => -c.level,
    8.                         c => c.race,
    9.                         c => c.role);
    10.     }
    11. }
    Now ain't that pretty?

    The downside here is that the array sent in as the compareFuncs will get re-allocated each time you call SortByLevel, which has a slight garbage cost. If that's a problem, you can ditch the params implementation for a normal array and cache the array (left as an exercise to the reader!).
     
    Kiwasi, KelsoMRK, Mycroft and 2 others like this.
  4. TheFunkMonk

    TheFunkMonk

    Joined:
    May 10, 2015
    Posts:
    4
    thanks for the quick response guys!

    @Baste, Yes indeed, that does look beautiful :)

    Haven't had a chance to try it out yet but, at a glance, it does look like what I've been after.

    I'm afraid my 'Generics' kung fu is not strong but i suspected things were moving in that direction and i've been warming up. I have had success with them in the past and I'm sure ill get my head around it once i get a chance take a proper look.

    I'll post again once I've had a play around.

    Thanks!
     
  5. TheFunkMonk

    TheFunkMonk

    Joined:
    May 10, 2015
    Posts:
    4
    @Baste

    Ok, so I've finally had a chance to sit down and go through your code and its great. I dropped it straight into my manager class and it does its thing perfectly. Outstanding, thank you. However, I don't think it is quite what i was looking for.

    Your code slims down my SortBy___ methods very nicely but unfortunately, it doesn't actually solve my fundamental problem which is that (as far a i can see) i still need to write a 'SortBy___' function for each variable in the class 'Character'.

    At this point I think i should elaborate on the intended purpose of this code as i may be approaching this problem in the wrong way or maybe overthinking it. I will try to be concise. :)

    I have a list containing elements of the class 'Character' which is displayed in my UI as a grid. Each 'Character' in the list has its own row in the grid and 'character stats' (by this i mean of course "public variables of the class 'character' that I may wish to display in the UI", I'm gonna call them 'Stats') are separated by the gird columns. At the top each column is a 'UI text object' showing the name of the 'Stat' that is displayed bellow.

    What i want to do is add a UI button Component to each header and use the built-in functionality to call the sort function in my 'Manager' script and what i don't want to do for many, many reasons is write a separate sort function for each 'Stat' or write a custom button script.

    It looks to me like your code allows me to call a Sort function which then sets up and executes the 3-fold sorting using a generic method (is that correct?). Whereas what I think I'm after is a method that takes in my Sort parameters as arguments and then sorts the list accordingly.

    Any thoughts?
    Does any of that make sense?

    Cheers
    FM
     
  6. TheFunkMonk

    TheFunkMonk

    Joined:
    May 10, 2015
    Posts:
    4
    @MathiasDG

    thanks for the ideas, i did have a little play around with your code. Its not something i think i would have come up with myself and I think it might work so if this other idea docent work i may fall back on it. In the meantime, I've tried to outline my goal in a little more detail above, if you fancy putting your mind to it. Thanks.FM
     
    MathiasDG likes this.
  7. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    You could do this via reflection and some editor scripting. Be warned it will make your code longer. But it does allow you to set a variable to access to sort on via the inspector.

    I used a similar concept in the remote field in Trend Me

    https://bitbucket.org/BoredMormon/t...wer.cs?at=master&fileviewer=file-view-default

    Be warned, reflection is not for the faint hearted. You will end up with a lot more code. But once it's established you can simply use the inspector to pick a component and variable to sort by.