Back to JetBrains

Death by a Thousand Ticks: Why Gameplay Systems Become Slow | Konrad Słoń | GameDev Day 2026

JetBrainsOctober 19, 202657m
Topics49
Performance Problems in Game Development0:31Speaker Background1:30Understanding the Frame Concept2:00Frame Production Requirements3:30Frame Rate vs Frame Time4:00Time Budgets by Frame Rate5:02CPU vs GPU Division6:01Gameplay as Partial Budget Consumer7:00The Scale Problem8:00The Title's Meaning9:00The Accumulation Problem10:01Three Categories for Improvement10:31Interconnected Improvements11:30The Danger of Cost Optimization12:01Focus on Frequency and Range12:30Structural Control13:30The Tick Function Problem14:00The Default Every-Frame Assumption15:00Enemy Behavior Requirements16:00Every-Frame Implementation17:30Single Enemy Analysis19:00The Range Factor20:31Cumulative Impact21:30Questioning Every-Frame Assumptions22:30Distance Update Optimization24:00Periodic Execution Implementation25:01The Distribution Problem26:01Frame Cadence Importance27:01Frame Rhythm Definition28:01Two Questions for Frequency Improvement29:01Distributing Work Across Frames29:30Benefits of Distribution30:30Measuring Impact of Frequency Changes31:10Eliminating Unnecessary Work32:30Status Update Function Analysis33:01Event-Driven Approach35:01Event System Trade-offs38:01Performance Results After Two Optimizations39:01Ownership and Responsibility40:31Path System Architecture43:02Selective Processing Based on Group Context45:01Summary of Three Optimization Strategies46:01Average Frame Time vs. Spikes47:30Core Principles48:01When These Approaches Don't Apply49:30Priority of Optimization Strategies50:33Three Key Questions for Performance Issues51:00Q&A: Frame Budget Allocation52:30Q&A: Batching and Frame Rate Considerations54:01
In a Nutshell

Performance problems in games rarely come from single expensive functions but from many small operations accumulating across frames—especially when work runs every frame for many objects. Instead of optimizing cost, first reduce frequency by asking whether work needs to happen every frame, then eliminate unnecessary polling by responding to state changes rather than constantly checking, and finally shift ownership to systems with broader context to avoid redundant processing. The cheapest work is the work you don't have to do.

AI-Generated Notes

These notes were generated by AI and may contain inaccuracies.

Game performance problems rarely result from a single, obviously expensive piece of code. Most of the time when working with game code, developers find code snippets that seem to make perfect sense and may be very cheap, but when they accumulate, expensive features accumulate and problems become difficult to address.

The speaker is Conrad, a senior game programmer who works for a team supporting CD Projekt Red on their upcoming game. Game systems constitute a large part of daily work, and architectural problems are dealt with regularly.

Games don't have an abstract performance budget. There is a "framework" in game development where everything that needs to happen competes for time. At the simplest level, a frame is a snapshot of the game at a particular moment in time. Frames tell us about the locations of objects, type of motion, gameplay events, user interface appearance, camera location, visual effects, and all other elements contributing to the state seen on screen.

Games create frames over and over to create the illusion of continuous motion. The key performance question is how much time is available to produce the next frame. Games must produce frames quickly to maintain the illusion of continuous motion.

When discussing game performance, the term frames per second (FPS) is used, which is what players see. From the developer perspective, frame time is more useful because it tells how long the production process of that frame actually takes. They describe the same thing but from different angles.

At 30 frames per second, approximately 33 milliseconds are available per frame. At 60 frames per second, 16 milliseconds are available. Depending on the required frequency, code must execute faster accordingly. For this presentation, 60 frames per second is the goal, requiring approximately 16 milliseconds to produce each frame.

The 16 milliseconds is not entirely available for CPU work because the frame is the product of the entire game. Work occurs on both CPU and GPU, each with their own limitations and dependencies. Even on the CPU side, physics, animation, engine systems, sound, networking, and other resources all compete for the available time.

The 16 milliseconds is a constraint on the game as a whole, not a budget specific to gameplay only. Gameplay is only one contributor to frame time. Programmers do not own the entire framework and only receive a small, non-constant portion of the total budget.

Four milliseconds of gameplay code might seem insignificant against 16 milliseconds total, but this could be very close to what is actually available for gameplay code. Adding new gameplay elements requires thinking about function speed, as adding more calls can accumulate even when individual numbers seem small.

Performance issues are easy to address when they resemble a single expensive function taking 4 milliseconds, as this is easy to spot and fix. However, gameplay performance rarely deteriorates this way. Instead, many reasonable individual pieces of work accumulate, each appearing cheap when examined separately.

When work is performed every frame with dozens or hundreds of objects doing the same thing, small individual costs accumulate to consume a large portion of the performance budget allocated to gameplay. Big functions are easy to discover and fix, but small decisions made along the way are hard to discover and fix in later stages.

Performance problems can be addressed through three categories: cost, frequency, and range. Cost refers to making a process less expensive. Frequency refers to doing something less frequently. Range refers to reducing the number of objects that call something. When multiplied together, these three factors determine frame time for code execution.

The three improvement categories are not contradictory. In real systems, improvements often affect multiple categories simultaneously. Cost improvement is the most obvious and common approach, involving better algorithms, avoiding allocations, caching data, and speeding up iterative loops.

When improving cost, the assumption is made that the work itself must be done. Before improving how something is done, the assumption that the process should exist in the first place should be questioned.

The presentation focuses primarily on frequency and range rather than cost. Frequency tells how often work needs to be done. Range tells how many things need to do the work. Neither requires making the process itself faster. If something costs 1 microsecond, doing it 10 times instead of 60 times, or for 10 actors instead of 100 actors, significantly changes its contribution to frame time.

Frequency and range are often controlled by gameplay structure. The location of work, ownership of work, and motivation for work need to be determined. Rather than starting with functional improvements, changing how game systems schedule work can save time.

For Unreal Engine programmers, the Tick function is where frequency can quietly get out of control. For Unity, this would be the Update function. The Tick function itself is not the problem as it is a useful mechanism. The trap is that putting something in Tick implicitly answers the question of how many times this code needs to run, with the answer being every frame.

The question is whether something really needs to be called every frame. A simple shooting game example demonstrates the issue, where the player moves along a line shooting enemies that move toward them in queues.

For each enemy, the game needs to move it toward the player, track distance from the player, update gameplay status (alive or dead), and track progress along the path to determine when the enemy reaches the end. Enemies also need to determine if the player is in line of sight before shooting.

Every enemy is an Actor with its own Tick. Each Tick moves the enemy forward, calculates distance to the player, checks if the enemy can attack or target the player, updates status including health, updates UI elements, possibly destroys the enemy if health is low, and updates path progress for UI display.

When captured in Unreal Insights, a single enemy Tick takes approximately 84 microseconds. This appears cheap and not immediately concerning when analyzing individual cases.

While single executions are cheap, the range factor becomes significant when scaling up. Ten enemies means 10 executions. 100 enemies means 100 operations. Something irrelevant for a single enemy becomes important at the frame level when combining all processes for large numbers of actors.

With 50 enemies, the small tasks accumulate to approximately 4.2 milliseconds per frame. This becomes critically important because gameplay only receives a small fraction of the total 16 milliseconds available, and 4+ milliseconds spent on a simple enemy system doesn't scale well.

Instead of making updates faster, the four parts of game logic (location, distance to player, status, path progress) should be examined to determine if they really need to execute every frame. Location updates must remain every frame due to movement requirements.

Distance updates to the player don't require per-frame accuracy. Scanning every few frames or every 50-100 milliseconds would be imperceptible to players due to attack delays and cooldown periods. These functions don't need to match the movement update frequency.

Instead of executing distance calculations every frame, periodic execution can be implemented. For example, distance checks occur only on every fourth frame while movement continues every frame. The process itself is not improved, but the cost is not paid every frame.

Periodic execution creates a new problem: all enemies follow the same schedule. With 50 enemies, they all skip distance calculations for three frames, then all execute together on the fourth frame. This reduces overall work but concentrates it within single frames.

The constraint is whether individual frames fit within their time budget, not average processor consumption. Three lightweight frames and one expensive frame still exceeds budget and causes visible stuttering. Players experience individual frames, not average frame rates.

Frame rhythm refers to consistent distribution of work across frames. When work is concentrated in the same frames, frame budgets can be exceeded and visual freezes occur even if average frame rates appear perfect. Reducing average cost does not automatically provide good frame rhythm.

When improving frequency, two questions must be answered: how often does this action need to happen, and how is this work distributed across frames. Frame time, frame rate, and frame cadence all need consideration.

Instead of all enemies checking distance on the same fourth frame, work can be distributed. With 100 enemies divided into four groups, different groups check distance on different frames while maintaining the every-fourth-frame frequency from each enemy's perspective.

From the individual enemy perspective, nothing changes as they still perform procedures every fourth frame. However, specific frames perform fewer callbacks for the same task, achieving savings while maintaining consistent frame loading.

After redistributing distance calculations across frames, the frame time improved from 4.2 milliseconds to 3.8 milliseconds, recovering 0.4 milliseconds. This improvement came without rewriting algorithms, using advanced data structures, or speeding up individual calculations—simply by stopping unnecessary work and performing tasks only as needed rather than as frequently as possible.

The process emphasized the importance of measurement: forming a hypothesis, making changes, then analyzing performance again to confirm actual results. Working blindly without this iterative analysis approach is not recommended.

Reducing frequency still means performing the same work periodically. The next consideration is whether certain work needs to happen at all. Instead of asking questions repeatedly, the approach shifts to determining if the question itself can be eliminated.

The Status Update function checks enemy health, updates health bars, and destroys enemies when health reaches zero. This creates repeated callbacks in every frame asking "Are you alive?" The same frequency reduction technique could apply, but a better solution might eliminate the callbacks entirely.

Analysis of health changes reveals the inefficiency: with health dropping from 100 to 75 in frame four, five callbacks occur but only one actual change happens. The question becomes whether checking is necessary when nothing changes.

Rather than polling health status every frame, health changes themselves trigger the necessary actions. When health changes, the system broadcasts this information. The final gameplay result remains identical—enemies react immediately to health changes—but the timing of actions shifts from constant questioning to event-triggered responses.

This represents a move to an event-driven approach where specific actions trigger responses rather than repeated polling. However, this pattern isn't universally applicable. Repeated questioning has valid use cases when monitoring over time is genuinely required. The key question is whether polling happens because monitoring is needed or simply because it's convenient.

Events aren't free. Delegates carry invocation costs, subscriptions have lifetime concerns, and complex architectures can become difficult to track. Multiple events firing simultaneously can create sequencing challenges and potential bottlenecks. The total number of invocations may drop, but this doesn't mean everything should convert to delegate-based patterns.

The lesson: if work exists only because something changed, execute it when the change occurs rather than every time it's possible.

Starting from 4.2 milliseconds for 50 enemies, removing another class of unnecessary work brought the time to approximately 3.2 milliseconds, saving another 0.4 milliseconds. The cumulative savings reached approximately one millisecond without improving algorithms, memory usage, or individual function performance—only by changing when work happens or whether it should happen at all.

The third optimization concerns ownership—which part of the game architecture is responsible for knowing information and determining when to process it. The Progress Update function tracks enemy movement along paths and displays progress percentages on UI.

Placing this on individual enemies seems logical since enemies move every frame, but enemies don't exist in isolation—they belong to paths. Multiple enemies share the same path but don't know about each other.

Instead of each enemy independently calculating progress, responsibility moves to a path system that knows which enemies belong to it. Initially, this doesn't improve performance—100 enemies performing an operation before becomes the path system performing the same operation for 100 enemies.

However, ownership provides control over the group. The path system understands the entire collection: enemy count, locations, and relative importance. This enables decisions that individual enemies couldn't make.

Not every enemy is equally important. For determining which enemy is nearing the end of a path, the path system already knows which enemy is ahead. Instead of 100 enemies checking progress, only the relevant enemies (seven, eight, and six, for example) need processing.

After this change, time dropped from 3.2 to 2.9 milliseconds, saving another 0.3 milliseconds. The total improvement from 4.2 to 2.9 milliseconds came from changing when and where work happens, not from improving the underlying calculations.

The first optimization addressed iteration frequency—performing work only as often as necessary rather than as frequently as possible. The second addressed state changes—responding to changes rather than constantly asking about current state. The third addressed ownership—transferring responsibility to systems with broader context.

The starting assumption was that anything that could update every frame should update every frame, but this reflects gameplay requirements rather than technical necessities. Location needs frequent updates; distance calculations don't require the same precision.

Average frame time isn't everything. Concentrating all work in the same frame can cause performance spikes. Scheduling requires considering both when work happens and how to distribute it across frames.

The principle for state updates: don't initiate when you can respond. If work exists simply because you're waiting for a change, that change can become the trigger for action.

Ownership changes provide more context for decision-making. Individual enemies have limited world views; path systems have knowledge of all enemies on a path, enabling more accurate decisions about what needs updating.

These optimizations assume individual operations are reasonably inexpensive. When work itself is expensive—due to poor algorithm scaling, memory access patterns, or large batch processing needs—different approaches become necessary. This enters the domain of algorithm optimization, data schema design, batch processing, and potentially data-oriented design or systems like Massive in Unreal Engine.

Before attempting to significantly speed up a process, verify that the process needs to exist in the first place. Doubling the speed of something is beneficial, but avoiding 90% of it represents a larger win.

When facing performance problems, return to three questions:

  • How necessary is the work?
  • How often do we perform this work?
  • How many things require this work?

Cost isn't the only dimension to improve. The best improvement occurs before a function even starts executing.

The cheapest work is the work you don't have to do.

No ideal distribution exists between gameplay and engine work that applies universally. The allocation depends on the specific situation and where bottlenecks occur. A profiler is necessary to identify where time is being spent and what improvements are achievable in the current context.

All optimization topics lack single universal answers because engines, situations, and circumstances vary. Work can sometimes be delayed by a frame or two without player noticing—for example, visual effects from a grenade. There's no magic solution that works across all hardware configurations.

Experimentation across different machines and components is required. When targeting weaker machines, overall frame rate reduction may be necessary. Conditional scheduling based on average frame rate data from specific devices can help determine appropriate timing adjustments.

Keep JetBrains in your library

Save the videos and channels worth coming back to, and find them again in one place.