Unity
TextMeshPro

GCAlloc無しで数値をTextMeshProで表示する

確認version

  • Unity 2017.2.1
  • TextMeshPro 1.0.560b2

モチベーション

  • ゲーム開発において、スコアをテキスト表示することはよくある
  • スコアは頻繁(ときには毎フレーム)更新されることもよくある
  • int型float型に対してラベルにせっとするためにToString()を呼び出すとGCAllocが走ってしまう
  • 数値をテキスト表示するときにGCAlloc無しで表示したい!
  • 1文字ずつSprite作って変換して並べて...とやればできそうだけど有り者でできないか

概要

  • 数値を桁数ごとに区切ってchar[]型に変換する
  • TextMeshProのSetCharArrayを利用して表示する
  • 更新のたびにGCAllocせずにラベル表示できる!✌(╹◡╹)✌

TextMeshProのSetCharArrayを知る

数値をchar[]に変換する

  • 桁ごとにchar配列に変換してくれるExtentionを用意
  • new char[] をしてしまうと結局allocが走ってしまうので、外から char[] を渡すようにしました
public static class NumberToCharArrayExtentions
{
    public static int ToCharsNonAlloc(this int self, char[] output, int start = 0)
    {
        int digitsNum = (int)System.Math.Log10(self) + 1;
        int zero = '0';

        for (int i = digitsNum - 1; i >= 0; i--)
        {
            int digit = self % 10;
            output[start + i] = (char)(digit + zero);
            self /= 10;
        }

        return digitsNum;
    }
}

使用例

using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;
using TMPro;

public class Sample : MonoBehaviour
{
    [SerializeField] TextMeshProUGUI _text1;
    [SerializeField] TextMeshProUGUI _text2;

    [SerializeField] TextMeshProUGUI _text3;
    [SerializeField] TextMeshProUGUI _text4;

    private char[] _chars = new char[10]; // 仕様上十分な桁数桁数を用意

    void Update()
    {
        Profiler.BeginSample("Sample SetText");
        {
            var score = Time.frameCount;
            _text1.SetText("{0}", score);

            var score2 = Time.frameCount * 2;
            _text2.SetText("{0}", score2);
        }
        Profiler.EndSample();

        Profiler.BeginSample("Sample SetCharArray");
        {
            var score = Time.frameCount;
            var length = score.ToCharsNonAlloc(_chars);
            _text3.SetCharArray(_chars, 0, length);

            // char[] は使いまわせる
            var score2 = Time.frameCount * 2;
            var length2 = score2.ToCharsNonAlloc(_chars);
            _text4.SetCharArray(_chars, 0, length2);

            // 単位文字を追加したければcharsに追加すればOK
            var score3 = Time.frameCount * 3;
            var length3 = score3.ToCharsNonAlloc(_chars);
            _chars[length3] = 'm';
            _text5.SetCharArray(_chars, 0, length3 + 1);
        }
        Profiler.EndSample();
    }
}

Dec-16-2017 18-05-10.gif

毎フレーム数値を更新表示していますが、GCAllocが 0B となっていることがわかります。
ちなみに _text4.text と取り出そうとしても元の値(New Text)のままで更新されていません。

float は?

いい感じに計算誤差を取り除くアルゴリズムを思いつけませんでした。
ぜひともコメントで良い方法を教えていただきたいです。

一応やってみたものを載せておきます。
表示桁数にもよりますが、123.456fを渡すと 123.456005 とかになっちゃいます。

    public static int ToCharsNonAlloc(this float self, char[] output, int digits = 7)
    {
        var big = (int)self;
        var small = (int)System.Math.Round((self % 1) * System.Math.Pow(10, digits));

        int length = big.ToCharsNonAlloc(output, 0);
        if (digits == 0 || small <= 0)
        {
            return length;
        }

        output[length] = '.';
        length++;

        var smallLength = small.ToCharsNonAlloc(output, length);

        return length + smallLength;
    }

まとめ

  • TextMeshPro.SetCharArray を使えば char[] をそのまま表示してくれる
  • 数値から char[] を生成できれば GC Alloc 無しにテキスト表示できる
  • スコア表示はだいたいintなので、とりあえず需要は満たした

追記 その他試していたパターンでのalloc計測

using System.Collections;
 using System.Collections.Generic;
 using System.Text;
 using UnityEngine;
 using UnityEngine.Profiling;

 public class Test : MonoBehaviour
 {
     private StringBuilder _builder = new StringBuilder();

     void Update()
     {
        // GCAlloc 180B
        // string.Formatを使ったパターン
         Profiler.BeginSample("Pattern 1");
         {
             var frame = Time.frameCount + 100000;
             var frameStr = frame.ToString();
             var text = string.Format("text {0}", frameStr);
         }
         Profiler.EndSample();

            // GCAlloc 146B
        // StringBuilderを使い、数値はAppendFormatに直渡し
         Profiler.BeginSample("Pattern 2");
         {
             var frame = Time.frameCount + 200000;
             _builder.Length = 0;
             _builder.AppendFormat("text {0}", frame);
             var text = _builder.ToString();
         }
         Profiler.EndSample();

        // GCAlloc 126B
        // StringBuilderを使い、数値はToStringしてからAppendFormatに渡す
         Profiler.BeginSample("Pattern 3");
         {
             var frame = Time.frameCount + 300000;
             var frameStr = frame.ToString();
             _builder.Length = 0;
             _builder.AppendFormat("text {0}", frameStr);
             var text = _builder.ToString();
         }
         Profiler.EndSample();

        // GCAlloc 86B
        // StringBuilderを使い、AppendFormatは使わずにAppendで文字列結合
         Profiler.BeginSample("Pattern 4");
         {
             var frame = Time.frameCount + 400000;
             _builder.Length = 0;
             _builder.Append("text ");
             _builder.Append(frame.ToString());
             var text = _builder.ToString();
         }
         Profiler.EndSample();

         // GCAlloc 76B
         // 数値だけをStringBuilderを使って生成
         Profiler.BeginSample("Pattern B 1");
         {
             var frame = Time.frameCount + 500000;
             _builder.Length = 0;
             _builder.Append(frame.ToString());
             var text = _builder.ToString();
         }
         Profiler.EndSample();

        // GCAlloc 38B
        // 数値だけを単にToString()
         Profiler.BeginSample("Pattern B 2");
         {
             var frame = Time.frameCount + 600000;
             var text = frame.ToString();
         }
         Profiler.EndSample();
     }
 }

_ddffe2565f827ee64ddf37b50a52c58b-png.png