Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

[Solved] Checking equality between ScriptableObject instances

Discussion in 'Scripting' started by Sayon, Feb 26, 2018.

  1. Sayon

    Sayon

    Joined:
    May 23, 2014
    Posts:
    13
    So, I'm using ScriptableObjects to create an Item system. A simplified version of my Item class looks like this:

    Code (CSharp):
    1. public class Item : ScriptableObject {
    2.         public string name;
    3.         public Sprite icon;
    4.         public int value;
    5. }
    I'm wanting to create unique instances of my items, so I can have values in game that are different between items (such as durability, uses left, etc.) I found out I could easily do this by doing this when creating a new Item.

    Code (CSharp):
    1. public Item itemAsset; // The item asset from the Editor
    2.  
    3. // The new, unique item instance being created from the asset.
    4. Item uniqueInstance = Object.Instantiate(itemAsset);
    This works nicely, but causes a bit of a problem. I want to be able to check if two items are equal (equal meaning they're made from the same asset/ScriptableObject). Prior to doing this the equality between items could be checked very easily. It was just a matter of asking if item1 == item2. This of course no longer works because the two item objects will no longer be referencing the same object. So, to check equality I thought to override the Object's Equals method by doing something like this in the Item class:

    Code (CSharp):
    1. public override bool Equals(object other) {
    2.         if (obj == null || GetType() != obj.GetType())
    3.                     return false;
    4.        
    5.         Item item = (Item)other;
    6.         return name == item.name && icon == item.icon && value == other.value;
    7. }
    But I can see this being a real pain when my Items begin having a lot more variables (like item rarity or meshes) and even more so when I inherit from my Item class and start creating other classes like Armor or Weapons.

    I've been searching for a more elegant solution in the documentation, but haven't found anything. Does Unity have some sort of way to check if two ScriptableObject instances were created from the same asset? Or is there maybe a simpler way that I can implement myself?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,599
    Interesting question! I got too googling and came across this page.

    https://stackoverflow.com/questions/375996/compare-the-content-of-two-objects-for-equality

    Look for about the fifth entry, a blurb of code for a private void Compare(object o1, object o2); method.

    I brought it into Unity 5.5.1 and had to modify the PropertyInfo.GetValue() call to have a second (null) argument, probably because it's .NET 2.0.

    That said, I tried it out on some ScriptableObjects. The code picks through all Public and NonPublic fields and properties. This has the side effect of finding other fields that Unity uses that it decides are non-identical, such as 'name,' and possibly others.

    Therefore I modified the original code from that site to ignore Non-Publics, and to disregard the "name"-named property. That superficially makes instance-created ScriptableObjects compare as true.

    Good luck with it. Hopefully it does what you need. Do some more testing though, because I didn't really exercise it that much.

    My modified code here:

    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System;
    5. using System.Reflection;
    6.  
    7. public class ComparingArbitraryObjects
    8. {
    9.     // Code originally found here:
    10.     // https://stackoverflow.com/questions/375996/compare-the-content-of-two-objects-for-equality
    11.     //
    12.     // See modification notes below.
    13.  
    14.     public static bool Compare(object obj1, object obj2)
    15.     {
    16.         if (obj1 == null || obj2 == null)
    17.         {
    18.             return false;
    19.         }
    20.         if (!obj1.GetType().Equals(obj2.GetType()))
    21.         {
    22.             return false;
    23.         }
    24.  
    25.         Type type = obj1.GetType();
    26.         if (type.IsPrimitive || typeof(string).Equals(type))
    27.         {
    28.             return obj1.Equals(obj2);
    29.         }
    30.         if (type.IsArray)
    31.         {
    32.             Array first = obj1 as Array;
    33.             Array second = obj2 as Array;
    34.  
    35. // hat tip to @aka3eka below... adding the suggested fix line here:
    36.             if (first.Length != second.Length) return false; // THIS IS THE FIX..
    37.  
    38.             var en = first.GetEnumerator();
    39.             int i = 0;
    40.             while (en.MoveNext())
    41.             {
    42.                 if (!Compare(en.Current, second.GetValue(i)))
    43.                     return false;
    44.                 i++;
    45.             }
    46.         }
    47.         else
    48.         {
    49.             // following mods to try and make it
    50.             // useful for Unity ScriptableObject
    51.             // equality comparisons:
    52.             //
    53.             // - ignore non-public fields
    54.             // - disregard differences in name
    55.             // - there may be other instance-ish fields but this at least
    56.             //        gets two Instance-copied ScriptableObjects to compare 'true'
    57.             //
    58.             // If you get unexplained inequalities, put a breakpoint on the
    59.             // 'return false' and check what the pi.Name field is and add
    60.             // your own disregarding code below that matches the "name" one.
    61.             //
    62.             foreach (PropertyInfo pi in type.GetProperties(
    63.                 /* BindingFlags.NonPublic | */ BindingFlags.Instance | BindingFlags.Public))
    64.             {
    65.                 if (pi.Name == "name")
    66.                     continue;
    67.                 var val = pi.GetValue(obj1,null);
    68.                 var tval = pi.GetValue(obj2,null);
    69.                 if (!Compare(val, tval))
    70.                     return false;
    71.             }
    72.             foreach (FieldInfo fi in type.GetFields(
    73.                 /* BindingFlags.NonPublic | */ BindingFlags.Instance | BindingFlags.Public))
    74.             {
    75.                 var val = fi.GetValue(obj1);
    76.                 var tval = fi.GetValue(obj2);
    77.                 if (!Compare(val, tval))
    78.                     return false;
    79.             }
    80.         }
    81.         return true;
    82.     }
    83. }
    84.  
    85.  
     
    Last edited: Jun 20, 2020
  3. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    897
    just store what the item was instantiated from as its uniquely identifying trait. then control and template out how all Items are fabricated (via the Static factory Item.CreateInstance method below). now only create Item instances via that method.

    Then compare if the two items are the "same" via the IsSameItemType() method. Personally, I'd avoid overriding the Equals method as I think it would obfuscate the meaning of the == operator between equaling item types or equaling item instances. but to each their own.

    Code (CSharp):
    1.  
    2. public abstract class Item: ScriptableObject
    3. {
    4.  
    5.     public static Item CreateInstance(Item template)
    6.     {
    7.         Item instance = (Item) ScriptableObject.Instantiate(template);
    8.  
    9.         //Unity will automatically call the instance's OnEnable during Instantiate
    10.  
    11.         instance.itemTemplate = template;
    12.  
    13.         return instance;
    14.     }
    15.  
    16.     [HideInInspector, SerializeField]
    17.     private Item itemTemplate = null; //Assets will always have this as a null value, but instantiated copies will reference the asset they are templating from
    18.    
    19.     protected virtual void OnEnable(){}
    20.  
    21.     public virtual bool IsSameItemType(Item otherItem)
    22.     {
    23.         if(!otherItem) return false;
    24.  
    25.         // test in-case either item being compared was an asset
    26.         if(this.itemTemplate == null)
    27.         {
    28.            return ReferenceEquals(this,otherItem) || ReferenceEquals(this,otherItem.itemTemplate);
    29.         }
    30.         else if (otherItem.itemTemplate == null)
    31.         {
    32.            return ReferenceEquals(this,otherItem) || ReferenceEquals(this.itemTemplate,otherItem);
    33.         }
    34.        
    35.         return ReferenceEquals(this.itemTemplate, otherItem.itemTemplate);
    36.     }
    37. }
    38.  
    the fact that derived classes can have more properties shouldn't factor into this (and if they did, IsSameItemType() is virtual so it can be overidden).
     
    adamb70 and michaelday008 like this.
  4. Sayon

    Sayon

    Joined:
    May 23, 2014
    Posts:
    13
    Hey guys, thank you for your answers! After posting my question and playing around with my code, I ended up doing something similar to your answer, Joshua. I created a version of the CreateInstance method for the Item class that initializes an Item variable called asset. Which let's me simply check between Items if they're from the same asset/template. However, I still used the Equals method, because elsewhere in my project I store Lists of these items for inventories and loot tables and use the List Contains method (as I understand it uses an object's Equals method). Your IsSameItemType() method looks to be more in depth than my Equals method though so I think I'll have another look at my implementation.

    Thanks again!
     
  5. aka3eka

    aka3eka

    Joined:
    May 10, 2019
    Posts:
    32
    Don't want to be the necroing one here in this thread, but have to mention that the code provided will fail to detect difference in two Arrays if the second one has more items than the first one and first items in the second one are the same as all items in the first one. This can be fixed by adding array length comparison:

    Code (csharp):
    1.  
    2. ...
    3. if (type.IsArray)
    4.         {
    5.             Array first = obj1 as Array;
    6.             Array second = obj2 as Array;
    7.            
    8.             if (first.Length != second.Length) return false; // THIS IS THE FIX..
    9.  
    10.             var en = first.GetEnumerator();
    11.             int i = 0;
    12. ...
    13.  
     
    Kurt-Dekker likes this.
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,599
    Hey, awesome... didn't notice that. Thanks! I added your line to my code above with an edit comment. THANKS!!!!
     
    aka3eka likes this.