Skip to content

Instantly share code, notes, and snippets.

@keijiro
Last active September 24, 2025 21:29
Show Gist options
  • Save keijiro/df2c866ccbdb2ac02fded6e70aaae04a to your computer and use it in GitHub Desktop.
Save keijiro/df2c866ccbdb2ac02fded6e70aaae04a to your computer and use it in GitHub Desktop.

Revisions

  1. keijiro revised this gist Sep 16, 2024. No changes.
  2. keijiro created this gist Sep 16, 2024.
    125 changes: 125 additions & 0 deletions FpsCapper.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,125 @@
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.LowLevel;
    using System.Linq;
    using System.Threading;

    namespace EditorUtils {

    //
    // Serializable settings
    //
    [FilePath("UserSettings/FpsCapperSettings.asset",
    FilePathAttribute.Location.ProjectFolder)]
    public sealed class FpsCapperSettings : ScriptableSingleton<FpsCapperSettings>
    {
    public bool enable = false;
    public int targetFrameRate = 60;
    public void Save() => Save(true);
    void OnDisable() => Save();
    }

    //
    // Settings GUI
    //
    sealed class FpsCapperSettingsProvider : SettingsProvider
    {
    public FpsCapperSettingsProvider()
    : base("Project/FPS Capper", SettingsScope.Project) {}

    public override void OnGUI(string search)
    {
    var settings = FpsCapperSettings.instance;
    var enable = settings.enable;
    var fps = settings.targetFrameRate;

    EditorGUI.BeginChangeCheck();

    enable = EditorGUILayout.Toggle("Enable", enable);
    fps = EditorGUILayout.IntField("Target Frame Rate", fps);

    if (EditorGUI.EndChangeCheck())
    {
    settings.enable = enable;
    settings.targetFrameRate = fps;
    settings.Save();
    }
    }

    [SettingsProvider]
    public static SettingsProvider CreateCustomSettingsProvider()
    => new FpsCapperSettingsProvider();
    }

    //
    // Player loop system
    //
    [UnityEditor.InitializeOnLoad]
    sealed class FpsCapperSystem
    {
    // Synchronization object
    static AutoResetEvent _sync;

    // Interval in milliseconds
    static int IntervalMsec;

    // Interval thread function
    static void IntervalThread()
    {
    _sync = new AutoResetEvent(true);

    while (true)
    {
    Thread.Sleep(Mathf.Max(1, IntervalMsec));
    _sync.Set();
    }
    }

    // Custom system update function
    static void UpdateSystem()
    {
    var cfg = FpsCapperSettings.instance;

    // Property update
    IntervalMsec = 1000 / Mathf.Max(5, cfg.targetFrameRate);

    // Rejection cases
    if (_sync == null) return; // Not ready
    if (!cfg.enable) return; // Not enabled
    if (cfg.targetFrameRate < 1) return; // Wrong FPS value
    if (Time.captureDeltaTime != 0) return; // Recording

    // Synchronization with the interval thread
    _sync.WaitOne();
    }

    // Static constructor (custom system installation)
    static FpsCapperSystem()
    {
    // Interval thread launch
    new Thread(IntervalThread).Start();

    // Custom system definition
    var system = new PlayerLoopSystem()
    { type = typeof(FpsCapperSystem),
    updateDelegate = UpdateSystem };

    // Custom system insertion
    var playerLoop = PlayerLoop.GetCurrentPlayerLoop();

    for (var i = 0; i < playerLoop.subSystemList.Length; i++)
    {
    ref var phase = ref playerLoop.subSystemList[i];
    if (phase.type == typeof(UnityEngine.PlayerLoop.EarlyUpdate))
    {
    phase.subSystemList
    = phase.subSystemList.Concat(new[]{system}).ToArray();
    break;
    }
    }

    PlayerLoop.SetPlayerLoop(playerLoop);
    }
    }

    } // namespace EditorUtils