Search Unity

BigInteger in ScriptableObject

Discussion in 'Scripting' started by patryksz14, Aug 4, 2020.

  1. patryksz14

    patryksz14

    Joined:
    May 24, 2017
    Posts:
    4
    Hi, I am trying to create an idle game. Currently project contain only 3 scripts, one for currency based on BigInteger, one for ScriptableObjects as some kind of generators (where cost is BigInteger too), and the last one for custom inspector to these generators. Everything was fine, but I realised that cost of generator is resetting every time I run the project. When I "disabled" custom inspector script it doesn't helped, so problem is somwhere else. Script for generators:
    upload_2020-8-4_11-44-52.png
    I know that this code is really far from perfection, but this is work for later. Firstly I want to fix this problem.
    Also I read whole script with currency, and there is nothing that can change cost of generators, there is only one line for creating a list for now:
    Code (CSharp):
    1. public List<Generator> generators;
    I don't know if ScriptableObject is capable of storing BigInteger. Or maybe I am doing it wrong? Any help will be very appreciated.
     
  2. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    Whenever you need to serialize a built-in .NET type that doesn't have built-in support from Unity, you can create a union-like wrapper type. As an example, this is how you can create a serializable version of System.Guid:

    Code (CSharp):
    1. [Serializable]
    2. [StructLayout(LayoutKind.Explicit)]
    3. public struct SerializableGuid
    4. {
    5.     [FieldOffset(0)]
    6.     public Guid Guid;
    7.  
    8.     [FieldOffset(0), SerializeField]
    9.     RawGuid raw;
    10.  
    11.     [Serializable]
    12.     struct RawGuid
    13.     {
    14.         public int A;
    15.         public short B, C;
    16.         public byte D, E, F, G, H, I, J, K;
    17.     }
    18. }
    Here's an example for BigInteger (I've also included a basic PropertyDrawer for it):

    Code (CSharp):
    1. using System;
    2. using System.Numerics;
    3. using System.Runtime.InteropServices;
    4. using UnityEngine;
    5. #if UNITY_EDITOR
    6. using UnityEditor;
    7. #endif
    8.  
    9. [Serializable]
    10. [StructLayout(LayoutKind.Explicit)]
    11. public struct SerializableBigInteger
    12. {
    13.     [FieldOffset(0)]
    14.     public BigInteger BigInteger;
    15.  
    16.     [FieldOffset(0), SerializeField]
    17.     RawBigInteger raw;
    18.  
    19.     [Serializable]
    20.     struct RawBigInteger
    21.     {
    22.         public int Sign;
    23.         public uint[] Bits;
    24.     }
    25.  
    26. #if UNITY_EDITOR
    27.     [CustomPropertyDrawer(typeof(SerializableBigInteger))]
    28.     class SerializableBigIntegerPropertyDrawer : PropertyDrawer
    29.     {
    30.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    31.         {
    32.             string bstr;
    33.             property = property.FindPropertyRelative(nameof(raw));
    34.             var sign = property.FindPropertyRelative(nameof(RawBigInteger.Sign));
    35.             var bits = property.FindPropertyRelative(nameof(RawBigInteger.Bits));
    36.             var bint = new SerializableBigInteger
    37.             {
    38.                 raw = new RawBigInteger
    39.                 {
    40.                     Sign = sign.intValue,
    41.                     Bits = GetBitsArray(bits)
    42.                 }
    43.             };
    44.  
    45.             try
    46.             {
    47.                 bstr = bint.BigInteger.ToString();
    48.             }
    49.             catch
    50.             {
    51.                 bint.BigInteger = BigInteger.Zero;
    52.                 bstr            = bint.BigInteger.ToString();
    53.             }
    54.  
    55.             bstr = EditorGUI.TextField(position, label, bstr);
    56.  
    57.             if (!BigInteger.TryParse(bstr, out bint.BigInteger))
    58.                 bint.BigInteger = BigInteger.Zero;
    59.  
    60.             sign.intValue = bint.raw.Sign;
    61.             SetBitsArray(bits, bint.raw.Bits);
    62.         }
    63.  
    64.         static void SetBitsArray(SerializedProperty property, uint[] array)
    65.         {
    66.             property.arraySize = array != null ? array.Length : 0;
    67.  
    68.             for (int i = 0; i < property.arraySize; i++)
    69.                 property.GetArrayElementAtIndex(i).intValue = unchecked((int)array[i]);
    70.         }
    71.  
    72.         static uint[] GetBitsArray(SerializedProperty property)
    73.         {
    74.             if (property.arraySize == 0)
    75.                 return null;
    76.  
    77.             var array = new uint[property.arraySize];
    78.  
    79.             for (int i = 0; i < array.Length; i++)
    80.                 array[i] = unchecked((uint)property.GetArrayElementAtIndex(i).intValue);
    81.  
    82.             return array;
    83.         }
    84.     }
    85. #endif
    86. }
    In order to find out what a .NET type looks like internally, you can use source.dot.net. Here's BigInteger.
     
    Digzol, NMJ_GD, DragonCoder and 2 others like this.
  3. codestage

    codestage

    Joined:
    Jul 27, 2012
    Posts:
    1,931
    Nice one, thanks for the smart solution!

    For those who might find this thread, it could be a good idea to switch from
    SerializedProperty.intValue
    for uint[] bits array serialization to
    SerializedProperty.longValue
    starting from Unity 5 or
    SerializedProperty.uintValue
    starting from Unity 2022.1 in order to avoid unit bits capping to shorter int range causing value corruption:

    Code (CSharp):
    1. // at SetBitsArray
    2. property.GetArrayElementAtIndex(i).longValue = array[i];
    3. // at GetBitsArray
    4. array[i] = (uint)property.GetArrayElementAtIndex(i).longValue;
     
    Last edited: Jun 26, 2022
    Digzol likes this.
  4. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    That won't happen. Casting between int/uint won't affect the underlying bits, only how they're interpreted. If such a conversion would result in underflow or overflow, then you can get an exception in a checked context, but I already handled that by explicitly specifying an unchecked conversion.

    EDIT: Or so I thought, but it seems Unity does a rather silly thing: all of the properties on SerializedProperty that deal with integers end up casting to long anyway, rather than having logic specific to the type being used. This seemingly defeats much of the purpose of having the separate properties to begin with (in my opinion). So you are correct: using longValue is more appropriate for uint here, because Unity isn't going to treat intValue as "assign this 32-bit value" but rather "cast this 32-bit value to a 64-bit one, truncate it to the target type, and assign it".

    All that said, using uintValue is definitely more correct when you're on a Unity version that has it available.
     
    Last edited: Jun 26, 2022
  5. codestage

    codestage

    Joined:
    Jul 27, 2012
    Posts:
    1,931
    That's actually happening since BigInteger stores data in uint[] array and do use uint numbers which are bigger than maximum allowed int positive number.
    Storing it as int[] array will just cap values without exceptions leading to silent data corruption (unchecked is not necessary here).

    Please try storing
    123456789012345678901234567891234567890
    number in your version and see it gets corrupted to the
    123456789012345678888921828588752207872
    after serialization and deserialization cycle.

    Then change
    intValue
    to
    longValue / uintValue
    (and remove casting to int) and see it doesn't corrupt anymore.
     
  6. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    266
    Yup, you're correct about the truncation, you replied just as I finished editing my message :D

    unchecked would have been necessary to avoid exceptions when checked arithmetic is enabled (which it isn't by default, but it's a good idea to specify unchecked to harden code that needs it).

    Funnily enough, the ulongValue setter actually does the "cast away the unsignedness" trick that I used, and it works because the target type is 64-bit to begin with. It just doesn't work for smaller types due to everything jumping through long first.
     
    codestage likes this.
  7. indicia

    indicia

    Joined:
    Apr 1, 2019
    Posts:
    1


    So I've been playing around with this and I found this thread,

    I took the BigInteger example code and while it works great for numbers in the millions, the index is outside the bounds of the array when we are using it for smaller numers (e.g. 1).

    Something is not right in the PropertyDrawer - I thought I should let you (and people finding this thread) know that - ill give an update when I fix it.

    For the OP I would actually suggest not to use bigints for idle games but instead take a look at innogames [dealing-with-huge-numbers-in-idle-games] - this seems to fit the usecase for an idle game a bit better imo.
     
  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,006
    As a side note: I would highly recommend to replace the TextField with the DelayedTextField and only parsing the string when the value has changed. Parsing such numbers can be quite expensive. The DelayedTextField allows you to edit the string and it only returns the new string when you press enter or when the text field looses focus. This is better for the editor performance and also a better user experience.
     
    codestage and TheZombieKiller like this.
  9. codestage

    codestage

    Joined:
    Jul 27, 2012
    Posts:
    1,931
    Make sure to force RawBigInteger.Bits to be null instead of empty array. Since it can be null for the small numbers, but Unity serializes null arrays to the empty arrays by default, for example:

    Code (CSharp):
    1. public static implicit operator BigInteger(SerializableBigInteger value)
    2. {
    3.     if (value.raw.bits != null && value.raw.bits.Length == 0)
    4.         value.raw.bits = null;
    5.  
    6.     return value.value;
    7. }
    Good point, thanks!
     
    TheZombieKiller likes this.
  10. datagreed

    datagreed

    Joined:
    Sep 17, 2018
    Posts:
    44
    Thank you so much for this thread and thanks to everyone who contributed, you saved my day.

    There is a problem with serialization though. Some values cannot be saved, seems like some kind of precision problem I guess?

    E.g. try saving 75*10^23 - it will actually save 73*10^23

    Do I understand correctly that the value is actually serialized by Unity to double? Could it be the reason? Won't it be more reliable to save it as a String?

    I've also noticed that when I save the value in editor it sometimes gets progressively smaller several times, like if the method being called several times for some reason and the loss of precision accumulates or something.
     
  11. codestage

    codestage

    Joined:
    Jul 27, 2012
    Posts:
    1,931
    If you take a look at the initial post by @TheZombieKiller, it actually should serialize into this struct:

    Code (CSharp):
    1. struct RawBigInteger
    2. {
    3.     public int Sign;
    4.     public uint[] Bits;
    5. }
    So there is no any kind of precision loss possible due to only integer numbers are involved.
    Maybe something went wrong at the Property Drawer part?
     
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,006
    Yes, my guess would be that the issue is that the array is of type "uint" but he does an int to uint conversion as well as the other way round. This would probably mess with the most significant bit of each int value. So it may work as long as the most significant bit / sign bit is 0 but may cause issues when you actually deal with a "negative numbers" in the array. Maybe using the longValue property of the SerializedProperty could fix it.
     
  13. datagreed

    datagreed

    Joined:
    Sep 17, 2018
    Posts:
    44
    So I reworked this and I just save everything as string.

    Here's the version stripped down from all additional the bells & whistles, hopefully I didn't accidentally left anything essential out:

    Code (CSharp):
    1. [Serializable]
    2. public class SerializedBigInteger: ISerializationCallbackReceiver
    3. {
    4.    
    5.     [SerializeField]
    6.     //this is the actual text value that Unity will serialize and save to disk
    7.     //since there is no inbuilt support for BigInteger serialization
    8.     private string _textValue;
    9.    
    10.     public BigInteger ActualValue;
    11.  
    12.     public SerializedBigInteger(BigInteger rawValue)
    13.     {
    14.         ActualValue = rawValue;
    15.     }
    16.  
    17.     public void OnBeforeSerialize()
    18.     {      
    19.         _textValue = ActualValue.ToString();
    20.     }
    21.  
    22.     public void OnAfterDeserialize()
    23.     {
    24.         try
    25.         {
    26.             ActualValue = BigInteger.Parse(_textValue);
    27.         }catch{
    28.  
    29.             Debug.LogError($"Could not parse value '{_textValue}' into big integer. Set to zero");
    30.             ActualValue = BigInteger.Zero;
    31.         }
    32.  
    33.     }
    34.  
    35.  
    36.     #if UNITY_EDITOR
    37.        
    38.         [CustomPropertyDrawer(typeof(SerializedBigInteger))]
    39.         class SerializableBigIntegerPropertyDrawer : PropertyDrawer
    40.         {
    41.             public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    42.             {
    43.                 string bstr;
    44.                 property = property.FindPropertyRelative(nameof(_textValue));
    45.                            
    46.  
    47.                 var sbint = new SerializedBigInteger(BigInteger.Parse(property.stringValue));
    48.                
    49.                 bstr = EditorGUI.DelayedTextField(position, label, sbint._value.ToString());
    50.  
    51.                 //set the value we get from text field              
    52.                 property.stringValue = BigInteger.Parse(bstr).ToString();
    53.                            
    54.             }
    55.                                
    56.         }
    57.     #endif
    58.  
    59. }  
     
  14. codestage

    codestage

    Joined:
    Jul 27, 2012
    Posts:
    1,931
    Yes, just use longValue or uintValue as I suggest at one of the previous posts and it should be fine. It stores data the same way it's stored in original BigInteger.
     
  15. datagreed

    datagreed

    Joined:
    Sep 17, 2018
    Posts:
    44
    That didn't fix the issue for me, so I serializing it as a string