Adaptive quality for Roblox. It watches the client's frame rate and quietly turns your own systems up or down to hold a target, so the game runs smooth on a cheap phone without the player ever opening a settings menu.
The reason this needs to exist is that Roblox's graphics quality is a single 1-to-10 slider, and it only knows about engine stuff, shadows, textures, render distance. It has no idea your game spawns 200 NPCs or throws a particle storm on every kill, so a Chromebook eats the exact same effect load as a desktop and just chokes on it. You can't even read back what "Automatic" landed on, which means you can't react to it either. Governor comes at it from the other direction: you describe your systems, and it picks the level each one runs at based on what the device can actually keep up with.
Every frame it drops the delta into a ring buffer, and once a second it pulls the 95th percentile frame time out of it. I use a percentile instead of an average on purpose, because an average smooths right over the stutters, and the stutters are exactly the thing that makes a game feel bad.
From there it's simple. Under target, it drops one tier on the highest-priority system that still has room to fall. Over target with headroom to spare, it raises one tier on the lowest-priority system that still has room to climb. After any change it wipes the samples and sits on its hands for a cooldown, since the old samples were measuring the tier it just replaced and acting on them again is how you get it oscillating forever.
Two things sit on top of that. A single frame slower than PanicFrameTime skips the whole once-a-second cadence and drops immediately, which is what you want when someone detonates half the map and the frame rate falls off a cliff. And when two neighbouring tiers straddle the target, one a hair too slow, one a hair too fast, it would happily ping-pong between them, so after a system gets dropped it's blocked from climbing back for a bit, and if a raise gets immediately undone that block doubles each time. It settles on the lower tier and shuts up. That last part took a couple tries to get right.
local Governor = require(game.ReplicatedStorage.Governor)
Governor.Configure({ TargetFrameRate = 60 })
Governor.Register({
Name = "Particles",
Priority = 3,
Tiers = {0, 0.5, 1},
Apply = function(Value)
ParticleScale = Value
end
})
Governor.Register({
Name = "NpcUpdateRate",
Priority = 2,
Tiers = {10, 30, 60},
Apply = function(Value)
NpcHertz = Value
end
})
Governor.Register({
Name = "Tracers",
Priority = 1,
Tiers = {false, true},
Apply = function(Value)
TracersEnabled = Value
end
})
Governor.Prime()
Governor.Start()Tiers go cheapest first, so index 1 is the floor and the last index is the richest. Apply runs with the tier value and its index every time the tier changes, including once when you register.
Priority decides who moves first. Higher priority gets dropped before lower and restored after it, so put your particles high and your gameplay-critical stuff low.
Prime sets an opening tier off the device, mobile at the floor, console in the middle, desktop up top. It's worth calling because otherwise a phone crawls all the way down from max one interval at a time while the player watches it stutter.
Snapshot hands you a table of system name to tier index, and Restore takes one back. Save a settled snapshot to a datastore and restore it next join and the client picks up where it left off instead of reconverging from scratch every session.
local Saved = Governor.Snapshot()
Governor.Restore(Saved)Pause stops the sampling and the decisions, Resume clears the buffer and starts again after a cooldown. Frame time during a loading screen or a cutscene has nothing to do with gameplay, so you don't want it poisoning the measurements.
Governor.Pause()
Governor.Resume()Once nothing has changed for StableAfter seconds it calls itself stable. OnStable fires when it gets there, OnUnstable when something knocks it back out.
Governor.OnStable(function()
print("settled at", Governor.GetFrameRate())
end)SetTier forces a system to a tier, and Lock takes it out of the automatic pool so your choice sticks. Handy if you're building a real settings menu on top and want the player's picks to win.
Governor.SetTier("Particles", 3)
Governor.Lock("Particles", true)| call | does |
|---|---|
| Configure(Options) | override config |
| Register(Definition) | add a system, returns it |
| Unregister(Name) | remove a system |
| Clear() | remove every system |
| Start() | begin sampling and deciding |
| Stop() | disconnect |
| Prime() | set opening tiers from device, returns the device kind |
| Step() | run one decision by hand |
| SetTier(Name, Tier) | force a tier |
| GetTier(Name) | returns tier index and value |
| Lock(Name, Locked) | exclude from automatic changes |
| Snapshot() | table of system name to tier index |
| Restore(Snapshot) | apply a snapshot |
| GetSystems() | array of registered names |
| Each(Callback) | iterate name, tier, value, locked |
| Pause() | stop sampling and deciding |
| Resume() | continue after a cooldown |
| IsPaused() | boolean |
| IsStable() | boolean |
| OnStable(Callback) | fires on entering the stable state |
| OnUnstable(Callback) | fires on leaving it |
| GetFrameRate() | current percentile frame rate, nil until enough samples |
| Stats() | table of Rate, MinRate, MaxRate, Changes, Uptime, Idle |
| OnChanged(Callback) | fires on every tier change |
| Report() | printable state summary |
Config keys: TargetFrameRate, Interval, Cooldown, Percentile, Headroom, Window, PanicFrameTime, StableAfter, RaiseBackoff, RaiseBackoffMax.
- It's client only, since it's measuring render frames. Nothing to measure on the server.
- It can only move what you register. It can't touch the built-in graphics slider, that property isn't writable from a script, which is half the reason this thing had to exist.
- It doesn't actually know which system caused a drop, only that the budget got missed. It leans entirely on the
Prioritynumbers you give it to decide what to sacrifice, so if those are wrong it'll drop the wrong thing. - One change per interval, except the panic path. So a scene that's way over budget still takes a few seconds to fully climb down.
- A tier only helps if that system's cost actually changes between tiers. Register something whose tiers all cost the same and it'll get dropped for nothing when the budget's tight.
MIT