Published on

[Dev Log] The Truth Behind Vampire Survivors Crushing i9 Processors: The Betrayal of Update and the Global Bus

Authors
  • Name
    Logan Kim
    Twitter

1. The Real Reason Vampire Survivors Brought High-End PCs to Their Knees

Remember the early days of 'Vampire Survivors,' the myth of indie gaming? Amidst the tens of thousands of monsters and gems covering the screen, a bizarre phenomenon occurred: even high-end PCs equipped with the latest i9 processors and RTX xx80 series GPUs saw their framerates plummet into the 10s.

Gamers were baffled, asking, "It's just pixel art, why is it lagging?" But to a system architect, the cause was glaringly obvious. The initial version of Vampire Survivors was built on a JavaScript framework (Electron + Phaser). Tens of thousands of objects were trying to update their lifecycles on a single thread, eventually suffocating under the bottleneck. (After a healthy dose of "financial therapy" from their massive success, they solved this by porting the engine to Unity.)

Of course, it is not a direct comparison, but a similar pattern exists in Unity as well. That is none other than Update(), the kindest and most dangerous tool provided by Unity.

2. The Betrayal of Update(): The "Is Anybody Home?" Anti-Pattern

The heart of Unity (the engine core) runs on C++, while the scripts we write run on C#. If you attach an Enemy.cs script to 10,000 monster prefabs and leave even an empty Update() function open inside them, what exactly happens at the bottom of the system?

Every single frame, the Unity engine checks via reflection, "Does this script have an Update function?", and then hurls 10,000 function calls (Context Switches) across the border from C++ to C#. Let me borrow some greybeard jargon to name this absolute nonsense: it's the "Is anybody home?" anti-pattern. Knocking on a neighbor's door isn't hard. But here, the engine is standing on the C++/C# border, violently knocking on 10,000 doors every single frame just to ask, "Is anybody home?" And depending on your CPU, this happens hundreds of times per second.

3. The Junior's Way: Every Man for Himself (The Naive Approach)

Let's assume thousands of projectiles or monsters each have their own script and move like this:

// A typical junior's projectile script
void Update() {
    transform.Translate(Vector3.forward * speed * Time.deltaTime);
}

Sure, it's comfortable to write. But the moment you hit 10,000 objects, Unity triggers 10,000 Update() callbacks, mercilessly trashing your CPU cache memory. This is the price you pay for failing to control your architecture and letting the framework's convenience drag you around.

4. The Architect's Solution: The Global Update Bus

If a junior wants to skip the senior phase and evolve straight into an architect, they need to approach the very essence of the system. The solution is to nuke the Update() from every individual object and restructure the architecture so that only a single central Manager possesses an Update() method.

// 1. When spawned, the object simply registers itself to the central bus.
public class Enemy : MonoBehaviour {
    void Start() { GlobalUpdateBus.Register(this); }
    void OnDestroy() { GlobalUpdateBus.Unregister(this); }

    // A standard public method, NOT the magic Update
    public void Tick(float deltaTime) {
        transform.Translate(Vector3.forward * speed * deltaTime);
    }
}

// 2. The Global Bus, holding the one and only Update
public class GlobalUpdateBus : MonoBehaviour {
    private List<Enemy> activeEnemies = new List<Enemy>(10000);

    void Update() {
        // The C++ -> C# context switch happens EXACTLY once.
        // After that, it simply iterates through an array purely within C#,
        // annihilating the overhead.
        for(int i = 0; i < activeEnemies.Count; i++) {
            activeEnemies[i].Tick(Time.deltaTime);
        }
    }
}

Those 10,000 "Knocking on Johnny's door" checks have been reduced to exactly 1. Because the data is packed into a sequential List and processed continuously, your CPU Cache Hit Rate skyrockets. This is the most fundamental and powerful design to prevent memory pointer waste and defend your framerate.

It’s a simple change, but this is exactly how a struggling 50 FPS climbs to a rock-solid 60 FPS, and how a Steam Deck burning 12 Watts drops to 11.5 Watts. True optimization is achieved by improving these seemingly small, foundational elements.

If your mindset is, "Computers are so fast these days, why can't I just wing it?", and you casually dump your unoptimized game on Steam, your game will end up casually swimming in the deep abyss of ignored releases, weighed down by "Not Recommended" reviews.