Search code examples
c#unity-game-engineframe-rate

Is this a correct way to implement FPS counter in Unity?


My code calculates average FPS. Every 0.4s it updates the FPS text. Every 2s it resets the number of average FPS. I need to calculate accurate and smooth FPS (so any temporary spikes will be eliminated). So is this FPS counter code will follow my requirements?

[RequireComponent(typeof(TMP_Text))]
public class FpsCounter : MonoBehaviour
{
    private struct AverageFloat
    {
        public float Average => _value / _count;

        private float _value;
        private int _count;

        public void Add(float number)
        {
            _value += number;
            _count++;
        }

        public void Reset()
        {
            _value = 0f;
            _count = 0;
        }
    }

    [SerializeField] private float _averageValueCollectionTime = 2f;
    [SerializeField] private float _refreshFrequency = 0.4f;

    private float _timeSinceFpsReset = 0f;
    private float _timeSinceUpdate = 0f;
    private AverageFloat _fpsValue;

    private TMP_Text _text;

    private void Start()
    {
        _text = GetComponent<TMP_Text>();
    }

    private void Update()
    {
        if (_timeSinceFpsReset > _averageValueCollectionTime)
        {
            _timeSinceFpsReset = 0;
            _fpsValue.Reset();
        }

        _fpsValue.Add(1f / Time.unscaledDeltaTime);
        _timeSinceFpsReset += Time.deltaTime;

        if (_timeSinceUpdate < _refreshFrequency)
        {
            _timeSinceUpdate += Time.deltaTime;
            return;
        }

        int fps = Mathf.RoundToInt(_fpsValue.Average);
        _text.text = fps.ToString();

        _timeSinceUpdate = 0f;
    }
}

Solution

  • So... It's better to calculate FPS with exponentially weighted moving average. This solution gives more accurate and smooth results.

    [RequireComponent(typeof(TMP_Text))]
    public class FpsCounter : MonoBehaviour
    {
        [SerializeField] [Range(0f, 1f)] private float _expSmoothingFactor = 0.9f;
        [SerializeField] private float _refreshFrequency = 0.4f;
    
        private float _timeSinceUpdate = 0f;
        private float _averageFps = 1f;
    
        private TMP_Text _text;
    
        private void Start()
        {
            _text = GetComponent<TMP_Text>();
        }
    
        private void Update()
        {
            // Exponentially weighted moving average (EWMA)
            _averageFps = _expSmoothingFactor * _averageFps + (1f - _expSmoothingFactor) * 1f / Time.unscaledDeltaTime;
    
            if (_timeSinceUpdate < _refreshFrequency)
            {
                _timeSinceUpdate += Time.deltaTime;
                return;
            }
    
            int fps = Mathf.RoundToInt(_averageFps);
            _text.text = fps.ToString();
    
            _timeSinceUpdate = 0f;
        }
    }
    

    _expSmoothingFactor must be set from 0 to 1. It determines how much smoothing will be applied. 0.9 value is most optimal (higher value = more smoothing).