Inspiration
Making a game run well on lower-end PCs usually means hours of manual work: change a graphics setting, run the scene, write down the frame rate, repeat. It's slow, it's easy to get wrong, and it's tempting to trust a hunch over real numbers. I wanted an AI agent that does the trial and error for you, but never gets to decide what "better" means. That's the job of real measurements.
The AI suggests. Measurements decide. The developer approves.
What it does
FrameOptimizer is an agent loop for Unreal Engine 5:
- Measure: launches your Unreal scene and records every frame with Unreal's CSV profiler.
- Find the ceiling: a "potato test" runs every setting at its lowest allowed value, to show the most FPS that settings can buy.
- Diagnose: works out whether the game thread, render thread or GPU is the bottleneck, and which GPU passes cost the most.
- Propose: Claude reads the measurements and the developer's direction (for example "hit 150 FPS, keep the colors") and picks 1–3 settings from an allowlist of 27.
- Validate and test: anything not on the allowlist is rejected. Approved changes run in Unreal with the same scene, resolution and test length.
- Decide: plain Python, not the AI, compares median and p95 frame time. Only real improvements are kept, and the AI learns from every result.
- Approve: the developer reviews the reports and visual trade-offs, then clicks Apply to project. A backup is made first, and Undo restores it.
The agent reasons in frame time, not just FPS, because frame time is what the profiler measures:
$$\text{FPS} = \frac{1000}{\text{frame time in ms}}$$
So a 150 FPS target means getting the median frame under \( 1000 / 150 \approx 6.67 \) ms.
In one real run (9 AI rounds plus the potato test), FrameOptimizer took the test scene from 96.4 to 187.7 FPS. Median frame time went from 10.38 to 5.33 ms, and p95 from 12.27 to 6.89 ms. Five changes were kept, two gave no clear change, and two made things worse and were discarded. That run was told shadows didn't matter, so it's an aggressive result. The biggest gain came from turning off Lumen global illumination, and the report flags that as a severe visual cost.
How I built it
- Python for the loop, the validator, the CSV analysis and the Markdown reports. It uses only the standard library plus the Anthropic SDK.
- Claude (Anthropic API) as the agent. It gets the measurements, the allowlist with each setting's visual cost, the developer's direction, and the result of every earlier experiment. It returns one experiment at a time: a name, the settings, and its reason.
- Unreal Engine 5 runs each test in
-gamemode with the settings passed on the command line, so the project itself never changes during testing. VSync and the frame cap are off, and per-pass GPU timing is on. - A local window: a small Python
http.serverserving one React page with hand-drawn SVG charts, opened as a Microsoft Edge app window. - pytest: 67 tests that use a fake AI client and a fake benchmark, so the whole loop can be tested without Unreal or an API key.
- Claude Code as my coding partner and teacher throughout.
Challenges I ran into
- Unreal doesn't quit after profiling. FrameOptimizer watches for the finished CSV (its last line is a metadata line) and closes Unreal as soon as the capture is complete.
- Noisy measurements. Every run uses a fixed resolution, has no frame cap, and skips 60 warm-up frames. Any change under 3% counts as noise. A test-length slider allows longer, steadier runs.
- CPU-bound scenes. Many graphics settings only reduce GPU work, so they can't help when the CPU is the bottleneck. I added bottleneck detection and "CPU lever" settings (view distance, foliage, LOD and shadow distance), and the AI targets whichever part is slowest.
- Keeping the AI honest. An AI can suggest anything, so every proposal goes through an allowlist validator, and Python, not the AI, decides what's kept.
- Unreal's .ini files allow duplicate keys and lines that start with + or -, which Python's
configparserwould break. Apply edits the file line by line and leaves everything else untouched. - Keeping it simple. I rewrote the codebase partway through so each file has one job and starts with a header that says where to begin reading.
Accomplishments that I'm proud of
- The whole loop runs end to end on a real Unreal project, and every number on screen comes from a real profiler capture.
- The AI is useful but never in charge. It can't use unapproved settings, it can't decide what's kept, and it can't touch the project.
- The potato test tells you when settings can't fix your bottleneck, instead of promising gains that aren't there.
What I learned
I learned Python, Git, Windows development and Unreal profiling while building this. Some Unreal lessons surprised me:
- Average FPS hides stutter. Median and p95 frame time tell you much more.
- Vscode integration with git can be extremely buggy at times. THis was my first time using VsCode.
- Shadow resolution and cascade settings do nothing while Virtual Shadow Maps are on.
- Measure before you guess. Some changes that looked promising made no measurable difference.
What's next
- Screenshot comparisons, so visual cost is measured instead of only described.
- Repeating each test several times, to confirm smaller gains with confidence.
- Running on several PCs to build low, medium and high presets.
- possibly ultithreading tests to sae time
Built With
- anthropic
- claude
- claude-code
- css
- git
- html
- javascript
- pytest
- python
- react
- unreal-engine
- windows