【Unity】放置ゲームやクリッカーゲームに出てくる単位を表現する

タイトルの通り、徐々に扱う数値が大きくなってくゲームを「インクリメンタルゲーム」と言ったりします。このジャンルは色々と呼び方があるようで「放置ゲー/放置ゲーム」や「クリッカーゲーム」(クッキークリッカーが特に有名です)などの呼び方もあります。扱う数値が指数関数的に大きくなっていくものはインフレゲー等と言ったりするようです。

このようなゲーム中で使用される非常に大な数字の扱い方と単位を表現する方法を実装例を交えて紹介したいと思います。

仕様

まずタップタイタンなどで見られる (k, m, aa, ab...) などの単位表示を扱ういたいと思います。具体的には

  • 極大の数値を扱うため多倍長整数演算ができる「BigInteger」型を使用する
    • double (有効桁数が17桁) / decimal (有効桁数28桁) と有限長のため使用しない
  • 頻繁に使用するので単位変換時にメモリアロケーションは可能な限り少なくする

単位の仕様は以下の通りです。

// 7桁未満はそのまま表示
1
10
100
1000
10000
100000
// 7桁以降から単位を付与、小数点3桁まで表示する
10^61.000M
10^710.000M
10^8100.000M
10^91.000G
10^910.000G
10^10100.000G
10^111.000T
10^1210.000T
10^13100.000T
// 10^14から固有の単位表記になる
10^141.000a
10^151.000b
10^161.000c
...
10^1000100.456ls

単位は、「a → b → c → d → .... → z → aa → ab」のようなアルファベットの小文字の24進数で表します。

確認環境

  • Unity 2021.3.21f1
  • Windows11
  • エディター上で確認

実装コード

BigIntegerExtensionsクラス

まず、数値を保持する BigInteger 型に対し、単位変換用の ToReadableString メソッドを拡張メソッド形式で追加します。

以下の通り BigIntegerExtensions クラスを用意しそこにメソッド定義を行います。

// BigIntegerExtensions.cs

using System;
using System.Numerics;

// BigIntegerの拡張メソッドを定義する
public static class BigIntegerExtensions
{
    // サポートする最大の桁数
    const int MaxDigit = 1024; // 10^512まで
    const char Dot = '.';
    const string Error = "ERROR";

    // 単位のリスト
    private static readonly UnitMgr conv = new UnitMgr(MaxDigit);

    // 1234564789 → 1.234b のような形式に変換する
    // ** Spanを用いて可能な限りメモリアロケーションを抑えるように実装している
    public static string ToReadableString(this BigInteger src)
    {
        Span<char> buffer = stackalloc char[MaxDigit];
        if (!src.TryFormat(buffer, out int length))
        {
            return Error; // サポートしてる桁数を超えた
        }
        if (length < 7) // 7桁未満はそのまま数字を返す
        {
            return buffer.Slice(0, length).ToString();
        }

        // 7桁以上は桁数に応じて加工する
        int _len = length - 1;
        ReadOnlySpan<char> unitSpan = conv.GetUnitSpan(_len / 3);
        int d = _len % 3;

        // 結果を入れるいれもの
        Span<char> result = stackalloc char[5 + d + unitSpan.Length];

        d++;
        var a = buffer.Slice(0, d);
        var c = buffer.Slice(d, 3);

        a.CopyTo(result);
        result[a.Length] = Dot;
        c.CopyTo(result.Slice(a.Length + 1));
        unitSpan.CopyTo(result.Slice(a.Length + 1 + c.Length));
        return result.ToString();
    }
}

UnitMgrクラス

次に、単位を生成、管理するクラスです。

a, b, c, aa, ab... などの扱う単位を生成・保持し、要求に応じて適切な単位を返す機能を持っています。

// UnitMgr.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// 単位を管理するクラス
public class UnitMgr
{
    // 最大の桁数
    readonly int _maxDigit = 1000;
    // 単位のリスト
    static string[] _units;

    public UnitMgr(int maxDigit)
    {
        _maxDigit = maxDigit;
    }

    // 指定した位置の単位を取得します
    public string GetUnit(int index)
    {
        Init();
        if (index >= _units.Length)
        {
            return "ERROR";
        }
        return _units[index];
    }

    // 指定した位置の単位をSpanとして取得します
    public ReadOnlySpan<char> GetUnitSpan(int index)
    {
        Init();
        if (index >= _units.Length)
        {
            //Trace.WriteLine("Overflow");
            return "ERROR";
        }
        return _units[index].AsSpan();
    }

    // 単位を初期化する
    private void Init()
    {
        if (_units == null) // 一回目に初期化
        {
            _units = CreateUnits(_maxDigit).ToArray();
        }
    }

    // 指定した数の分だけ単位を生成する
    private IEnumerable<string> CreateUnits(int count)
    {
        var sb = new StringBuilder();
        string f(int i)
        {
            sb.Clear();
            int d = i;
            while (d > 0)
            {
                int mod = (d - 1) % 26;
                sb.Insert(0, Convert.ToChar(97 + mod));
                d = (d - mod) / 26;
            }
            return sb.ToString();
        }
        yield return "";
        yield return "K";
        yield return "M";
        yield return "B";
        yield return "T";
        for (int i = 1; i < count - 4; i++)
        {
            yield return f(i);
        }
    }
}

使い方

上記コードの使い方です。

public static void TestRun()
{
    foreach (BigInteger value in GetTestData())
    {
        string a = value.ToReadableString(); // ToReadableStringで単位変換できる
        Console.WriteLine(a);
        //      0
        //      1
        //      2
        //      3
        //     12
        //     123
        //    1234
        //   12345
        //  123456
        //   1.234M
        //  12.345M
        // 123.456M
        //   1.234B
        //  12.345B
        // 123.456B
        //   1.234T
        //  12.345T
        // 123.456T
        //   1.234a
        //  12.345a
        // 123.456a
        //   1.234b
        //  12.345b
        // 123.456b
        //   1.234c
        //  12.345c
        // 123.456c
        //   1.234ls
        //  12.345ls
        // 123.456ls // 1000桁
    }
}

// 確認用のテストデータを作成する
public static IEnumerable<BigInteger> GetTestData()
{
    yield return new BigInteger(0);
    yield return new BigInteger(1);
    yield return new BigInteger(2);
    yield return new BigInteger(3);
    yield return new BigInteger(12);
    yield return new BigInteger(123);
    yield return new BigInteger(1234);
    yield return new BigInteger(12345);
    yield return new BigInteger(123456);
    yield return new BigInteger(1234567);
    yield return new BigInteger(12345678);
    yield return new BigInteger(123456789);

    var b = new BigInteger(123456789);
    for (int i = 1; i < 1000; i++)
    {
        yield return b * BigInteger.Pow(10, i); // 1.23456789E+1000 まで生成する
    }
}

小数点3桁固定、1000桁までしか単位をサポートしないため、変更が必要な場合各は各自で修正してください。

実数表示・指数表示のやり方

余談ですが、実数(省略しないで全部の数値を表示)、と指数表記の方法です。

BigInteger
var value = BigInteger.Parse("12345678900000000000000000000000000");

Console.WriteLine(value.ToReadableString()); // 独自単位
// > 12.345g

Console.WriteLine($"{value:F0}"); // 実数表示はF + 小数点の桁数を指定
// > 12345678900000000000000000000000000

Console.WriteLine($"{value:e3}"); // 指数はe + 小数点の桁数を指定
// > 1.235e+034

こっちは ToString の書式指定がもともと存在するためそれらを使用します。

関連記事

takap-tech.com