Forager can run 5,000 instances at once. That was the problem Gabe 'lazyeye' Weiner was hired to solve — and the reason the obvious solution did not work is the reason this story is worth stealing.
At a glance
Forager, a crafting game by HopFrog, built in GameMaker.
The problem: up to 5,000 instances active at once, if not more, on an enormous player-built map.
Who fixed it: Gabe 'lazyeye' Weiner, first hired only for optimization, later the game's lead programmer.
The core fix: instance culling — deactivate what the player cannot see, reactivate it just before it pops into view.
The catch: 500-plus calls to instance_exists, instance_number and instance_find only see active instances, so culling broke them.
Source: lazyeye's post on the official GameMaker blog.
Last verified: October 26, 2026.
Handed 50,000 lines and one job: make it fast
Sometimes you get hired onto a project from the start, with full control over the codebase. Other times you are handed a mildly troublesome project of 50,000 lines and told to please fix it. That was lazyeye's start on Forager: he was contracted for one task, optimization, on a massive crafting game where the player collects resources and builds structures on an enormous map. On a map like that, there could easily be 5,000 instances active at a time, if not more — and the genre makes it worse, because the player can create seemingly infinite instances, and the game still has to run smoothly on every platform.
In nearly every project lazyeye had worked on, excessive instance counts were the worst performance drag, and the usual trap was using an object where another tool would do: a particle system, a data structure, an asset layer. His rule of thumb is simple — if something is not moving around, is not being depth sorted, and is not colliding with stuff, it probably should not be an object. Forager, though, was different. Almost every single instance seemed justified, and simulating them without instances would be a nightmare. So 'use fewer objects' was off the table before the real work even started.
Three costs, then one big illusion
The first cost is drawing. A batch is a set of instructions GameMaker sends to the GPU, and GPUs are fast when instructions come grouped in a single batch — but every time code changes a draw setting, like color, alpha, font or blend mode, or touches a surface, a shader or a primitive, the batch breaks and the GPU has to switch instruction sets. The fix is grouping: instead of every outlined instance setting and resetting a shader inside its own Draw event, do all the outlines at once inside a controller's Draw event. Depth sorting sometimes blocks this trick, but layer scripts can help by running code exactly before and after a layer is drawn.
The second cost is the Step event, and the question to ask is always the same: does this need to be running every step? Is an instance fetching information from a global data structure every frame? Do it once in the Create event instead. Is it iterating through the whole player inventory each step? Only do it when an item is added or removed. Nothing else? Maybe nobody will notice if the code runs every other step. And use short-circuiting: GML stops reading conditionals once one value is false, so put lighter checks first, and put the check most likely to be false early — five checks that are almost always true, followed by one that is almost always false, is wasted work.
The third cost is the big one, and the answer is an illusion. Take a tree: it is depth sorted, it has collisions, visual effects, and interactions when the player approaches. But the player can only interact with it when nearby, so perhaps it does not need processing 95 percent of the time. If a tree vanishes in a forest and the player is not there to see it, do they notice? The culling system says no: if the player cannot see an instance, deactivate it; when the player moves somewhere it would become visible, reactivate it before it pops into view. The boundary box is based on the actual sprite drawn, with its scaling — not the collision box — plus a little padding, because Forager sometimes applies minor scaling to draws.
The fix broke everything, then got a second fix
Moments after pushing the culling commit, lazyeye had a thought: the game uses instance_exists, instance_number and other instance functions a lot. A quick search returned over 500 results. Those functions can only check active instances, so culling was about to become a major issue — and there was a second sting. Deactivated instances can be read from, but they cannot be written to, and code that retrieves an instance often wants to write values to it.
Rather than give up, he added a second layer. Every cullable instance is added to a global cache data structure when it is created: its ID goes into its object's list, and into every parent list it descends from, because GameMaker's instance functions with a parent argument include all of that parent's children. When an instance is destroyed, the Clean Up event removes it from the cache. Replacement functions — TrueInstanceExists, TrueInstanceFind and friends — check the cache, and temporarily activate an instance before returning it, without flipping its real active flag, so the system can tell a temporary activation from a real one. A controller script then deactivates the temporarily activated instances again, in both the Step and Draw events, because GameMaker rebuilds its event queues between Step and Draw.
Spread the work, or the fix becomes the bottleneck
The same 'every step?' question applies to the system itself. A macro called SYSTEM_CHECK_INTERVAL controls how often the system scripts run: if checks run every 20 frames, values like plant growth are multiplied by 20, and a Switch event spreads the work evenly across those 20 frames. And lazyeye's closing advice is worth keeping: do not prematurely optimize. GameMaker caters well to the projects it is built for, and most developers will never need any of this — but if you do build a large game with performance problems, be clever and curious, experiment, and do research.
What to do Monday morning
Count first: confirm instance count is actually your worst drag before rewriting anything.
Replace unjustified objects: if it does not move, is not depth sorted and does not collide, use a particle system, a data structure or an asset layer instead.
Order conditionals cheap-first: put lighter checks and most-likely-false checks before expensive ones, so short-circuiting skips the heavy work.
Ask 'every step?' of each Step event: run code once in Create, on change, or every other step where nobody will notice.
Before you cull, search for instance_exists, instance_number and instance_find: they only see active instances, and deactivated instances cannot be written to — build the cache and replacement functions first.
Cull off-screen instances with padding, and reactivate them before they pop into view — using the drawn sprite's box, not the collision box.

