BigInteger in ScriptableObject

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:


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:

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.

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:

[Serializable]
[StructLayout(LayoutKind.Explicit)]
public struct SerializableGuid
{
    [FieldOffset(0)]
    public Guid Guid;

    [FieldOffset(0), SerializeField]
    RawGuid raw;

    [Serializable]
    struct RawGuid
    {
        public int A;
        public short B, C;
        public byte D, E, F, G, H, I, J, K;
    }
}

Here’s an example for BigInteger (I’ve also included a basic PropertyDrawer for it):

using System;
using System.Numerics;
using System.Runtime.InteropServices;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
[StructLayout(LayoutKind.Explicit)]
public struct SerializableBigInteger
{
    [FieldOffset(0)]
    public BigInteger BigInteger;

    [FieldOffset(0), SerializeField]
    RawBigInteger raw;

    [Serializable]
    struct RawBigInteger
    {
        public int Sign;
        public uint[] Bits;
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(SerializableBigInteger))]
    class SerializableBigIntegerPropertyDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            string bstr;
            property = property.FindPropertyRelative(nameof(raw));
            var sign = property.FindPropertyRelative(nameof(RawBigInteger.Sign));
            var bits = property.FindPropertyRelative(nameof(RawBigInteger.Bits));
            var bint = new SerializableBigInteger
            {
                raw = new RawBigInteger
                {
                    Sign = sign.intValue,
                    Bits = GetBitsArray(bits)
                }
            };

            try
            {
                bstr = bint.BigInteger.ToString();
            }
            catch
            {
                bint.BigInteger = BigInteger.Zero;
                bstr            = bint.BigInteger.ToString();
            }

            bstr = EditorGUI.TextField(position, label, bstr);

            if (!BigInteger.TryParse(bstr, out bint.BigInteger))
                bint.BigInteger = BigInteger.Zero;

            sign.intValue = bint.raw.Sign;
            SetBitsArray(bits, bint.raw.Bits);
        }

        static void SetBitsArray(SerializedProperty property, uint[] array)
        {
            property.arraySize = array != null ? array.Length : 0;

            for (int i = 0; i < property.arraySize; i++)
                property.GetArrayElementAtIndex(i).intValue = unchecked((int)array[i]);
        }

        static uint[] GetBitsArray(SerializedProperty property)
        {
            if (property.arraySize == 0)
                return null;

            var array = new uint[property.arraySize];

            for (int i = 0; i < array.Length; i++)
                array[i] = unchecked((uint)property.GetArrayElementAtIndex(i).intValue);

            return array;
        }
    }
#endif
}

In order to find out what a .NET type looks like internally, you can use source.dot.net. Here’s BigInteger.

5 Likes

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:

// at SetBitsArray
property.GetArrayElementAtIndex(i).longValue = array[i];
// at GetBitsArray
array[i] = (uint)property.GetArrayElementAtIndex(i).longValue;
1 Like

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.

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.

Yup, you’re correct about the truncation, you replied just as I finished editing my message :smile:

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.

1 Like

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.

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.

2 Likes

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:

public static implicit operator BigInteger(SerializableBigInteger value)
{
    if (value.raw.bits != null && value.raw.bits.Length == 0)
        value.raw.bits = null;
  
    return value.value;
}

Good point, thanks!

1 Like

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 7510^23 - it will actually save 7310^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.

If you take a look at the initial post by @TheZombieKiller , it actually should serialize into this struct:

struct RawBigInteger
{
    public int Sign;
    public uint[] Bits;
}

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?

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.

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:

[Serializable]
public class SerializedBigInteger: ISerializationCallbackReceiver
{
   
    [SerializeField]
    //this is the actual text value that Unity will serialize and save to disk
    //since there is no inbuilt support for BigInteger serialization
    private string _textValue;
   
    public BigInteger ActualValue;

    public SerializedBigInteger(BigInteger rawValue)
    {
        ActualValue = rawValue;
    }

    public void OnBeforeSerialize()
    {       
        _textValue = ActualValue.ToString();
    }

    public void OnAfterDeserialize()
    {
        try
        {
            ActualValue = BigInteger.Parse(_textValue);
        }catch{

            Debug.LogError($"Could not parse value '{_textValue}' into big integer. Set to zero");
            ActualValue = BigInteger.Zero;
        }

    }


    #if UNITY_EDITOR
       
        [CustomPropertyDrawer(typeof(SerializedBigInteger))]
        class SerializableBigIntegerPropertyDrawer : PropertyDrawer
        {
            public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
            {
                string bstr;
                property = property.FindPropertyRelative(nameof(_textValue));
                           

                var sbint = new SerializedBigInteger(BigInteger.Parse(property.stringValue));
               
                bstr = EditorGUI.DelayedTextField(position, label, sbint._value.ToString());

                //set the value we get from text field               
                property.stringValue = BigInteger.Parse(bstr).ToString();
                           
            }
                                
        }
    #endif

}

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.

That didn’t fix the issue for me, so I serializing it as a string