確認version
- Unity 2017.2.1
- TextMeshPro 1.0.560b2
モチベーション
- ゲーム開発において、スコアをテキスト表示することはよくある
- スコアは頻繁(ときには毎フレーム)更新されることもよくある
-
int型
やfloat型
に対してラベルにせっとするためにToString()
を呼び出すとGCAlloc
が走ってしまう - 数値をテキスト表示するときにGCAlloc無しで表示したい!
- 1文字ずつSprite作って変換して並べて...とやればできそうだけど有り者でできないか
概要
- 数値を桁数ごとに区切って**char[]**型に変換する
- TextMeshProの
SetCharArray
を利用して表示する - 更新のたびにGCAllocせずにラベル表示できる!✌(╹◡╹)✌
TextMeshProのSetCharArrayを知る
- もともと取り組んでいたプロジェクトで毎フレームスコア表示をしてGCAllocしていたのをなんとかしたくて調べてて見つけた
- https://forum.unity.com/threads/ugui-text-and-char-arrays.274198/#post-1811083
- リファレンス
- 数値をchar[]に変換できたらGCAlloc0にできるのでは...?
数値を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();
}
}
毎フレーム数値を更新表示していますが、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();
}
}