So there I was, looking at the good profiler, and noticed an irritating regular spiking in the GC lane. The spike was very small, but really obvious because it was the only thing doing regular allocations. Diving in, it was seemingly the most innocent thing.
You know that FPS Counter that comes with Unity Standard Assets? Well, I've put it in my project because I prefer to rely on something like that in builds, as opposed to attaching a profiler, it is more accurate in the player(arguably) and it just looks neat.
Here is the Unity's code for it:
using System;
using UnityEngine;
using UnityEngine.UI;
namespace UnityStandardAssets.Utility {
[RequireComponent(typeof(Text))]
public class FPSCounter : MonoBehaviour {
const float fpsMeasurePeriod = 0.5f;
private int m_FpsAccumulator = 0;
private float m_FpsNextPeriod = 0;
private int m_CurrentFps;
const string display = "{0} FPS";
private Text m_Text;
private void Start() {
m_FpsNextPeriod = Time.realtimeSinceStartup + fpsMeasurePeriod;
m_Text = GetComponent<Text>();
}
private void Update() {
// measure average frames per second
m_FpsAccumulator++;
if(Time.realtimeSinceStartup > m_FpsNextPeriod) {
m_CurrentFps = (int)(m_FpsAccumulator / fpsMeasurePeriod);
m_FpsAccumulator = 0;
m_FpsNextPeriod += fpsMeasurePeriod;
m_Text.text = string.Format(display, m_CurrentFps);
}
}
}
}
So, what's wrong with this (except the ugly coding convention)? Nothing, it works, but it could be a bit better when it comes to GC.
See, strings in C# are immutable reference types, which means whenever you need to change a string it creates a new one on the heap and the previous pointer now points to the new one. The old one will be cleaned at the next Garbage Collection time because it's not used any more. So, whenever they do string.Format()
a new string is created and the old one generates a bit of garbage, ultimately creating a small spike.
You may think this is not terribly useful since it's a small amount of garbage, but think again. You're using the same system in numeric health bars, damage displayed on screen etc. Imagine something like Diablo with damage numbers splashing all around.
This is a classic problem and has a pretty well known solution. If you have a pre-defined range of values you can display in strings, you pre-allocate all the strings and just get them by index in array. Here is my solution:
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Text))]
public class FPSCounter : MonoBehaviour {
public float fpsMeasurePeriod = 0.5f;
public int maxFPS = 500;
private int _accumulatedFPS;
private float _nextFlushTime;
private int _currentFPS;
private string[] _fpsStrings;
private Text _textComponent;
private void Start() {
_nextFlushTime = Time.realtimeSinceStartup + fpsMeasurePeriod;
_textComponent = GetComponent<Text>();
_fpsStrings = new string[maxFPS + 1];
for(int i = 0; i < maxFPS; i++) {
_fpsStrings[i] = i.ToString() + " FPS";
}
_fpsStrings[maxFPS] = maxFPS.ToString() + "+ FPS";
}
private void Update() {
_accumulatedFPS++;
if(Time.realtimeSinceStartup >= _nextFlushTime) {
_currentFPS = (int)(_accumulatedFPS / fpsMeasurePeriod);
_accumulatedFPS = 0;
_nextFlushTime += fpsMeasurePeriod;
if(_currentFPS <= maxFPS) {
_textComponent.text = _fpsStrings[_currentFPS];
} else {
_textComponent.text = _fpsStrings[maxFPS];
}
}
}
}
The code is pretty much self-explanatory. One caveat is there is a limit to the maximum number of FPS you can display. If we get at or over the limit (here 500), it will simply display "500+ FPS". Of course, with a bit of cleverness this can be worked around (2 text objects, the other displaying hundreds), but for our purposes it will do.
Another problem is keeping all the strings in memory. You probably want to know how much memory that is. For 1000 strings, it is 17802B, less than 18KB. Not bad. If you opt for 500 strings it will be 8800B, less then 9KB, which is nothing really.
If you're finding this article helpful, consider our asset Dialogical on the Unity Asset store for your game dialogues.
Here is the code to check this size of a string array for yourself, add this at the end of Update()
:
if(Input.GetButtonDown("Fire2")) {
int totalSize = 0;
for(int i = 0; i < _fpsStrings.Length; i++) {
totalSize += _fpsStrings[i].Length * sizeof(System.Char) + sizeof(int);
}
Debug.Log(totalSize);
}