13min

Building a Professional Game Loop in TypeScript: From Basic to Advanced Implementation

Building a Professional Game Loop in TypeScript: From Basic to Advanced Implementation (placeholder)Building a Professional Game Loop in TypeScript: From Basic to Advanced Implementation

Understanding Game Loops in Game Development

A game loop is the heartbeat of every game engine, orchestrating the continuous cycle of processing input, updating game state, and rendering frames. It's the fundamental mechanism that determines how your game runs, responds to player input, and maintains smooth gameplay.

In this article, we'll explore the intricacies of implementing game loops in TypeScript, focusing on advanced patterns like fixed timestep updates, efficient frame timing, and crucial performance optimizations that will help you build professional-grade games.

Core Game Loop Concepts

A typical game loop consists of three primary phases, which repeat over and over:

  1. Process input
  2. Update game state
  3. Render the current game state then repeat.

Typical Game Loop

Let's dive into what happens in each phase:

Process Input: Input processing involves checking the current state of:

  • Keyboard keys (pressed/released)
  • Mouse position and buttons
  • Gamepad inputs
  • Touch events
  • Any other input devices

Update Game Physics: The physics update step:

  • Updates object positions based on velocity
  • Applies forces or acceleration
  • Checks for collisions
  • Resolves physics interactions
  • Updates game state based on input

Render Frame: The render phase:

  • Clears the previous frame
  • Draws the game world
  • Renders game objects
  • Applies visual effects
  • Updates the display

The Naive Approach: While Loop

Let's start with the simplest possible implementation - a while loop:

This approach has several critical problems:

  1. The main thread gets blocked, causing the browser to freeze: Because the javascript engine is single-threaded, the while loop will not allow the main thread to perform other tasks.
  2. No consistent timing between updates: The loop runs as fast as possible, without any control over the game speed.
  3. Different devices will run at different speeds: Different devices may run at different speeds, leading to inconsistent timing between updates.

The Recursive Approach

Another attempt might be to use recursion for the game loop:

This approach has the same problems as the while loop approach, with the following additional issues:

  1. Each recursive call adds a new frame to the call stack: Each recursive call adds a new frame to the call stack, which can lead to a stack overflow after few frames.

  2. The game will crash after few seconds with the following error: "Maximum call stack size exceeded"

Enter requestAnimationFrame

Neither the while loop nor recursive approach provides the control we need for a proper game loop. We need a way to:

  • Control frame timing precisely
  • Avoid stack overflow issues
  • Keep the browser responsive

Is there any built-in way to create an optimized recursive loop in the browser?

Yes, there is and it's called requestAnimationFrame. This browser API is specifically designed for smooth animations and game loops.

Using requestAnimationFrame gives us several immediate benefits:

  • The browser optimizes the timing of our animations
  • The loop pauses automatically when switching tabs
  • No more stack overflow or browser freezing
  • Smoother animations by syncing with the screen refresh rate

Performance Optimization

While requestAnimationFrame solves our initial problems, this implementation still has important issues:

  1. No control over game speed across different devices
  2. Physics calculations are tied to frame rate
  3. Inconsistent time steps between updates
  4. No way to handle performance drops gracefully

To address these challenges, we'll implement a robust game loop with fixed timestep updates. Let's break down the implementation piece by piece.

The GameLoop Class Implementation

Our GameLoop class uses the Singleton pattern and implements a fixed timestep game loop. This ensures consistent physics updates across different devices while maintaining smooth rendering. Let's examine each component:

Core Properties and State Management

At the heart of our game loop, we need to track various timing-related states:

Each variable serves a specific purpose:

  • lastRequestId: Stores the animation frame ID for cleanup when stopping the loop
  • isRunning: Controls the game loop state (running/stopped)
  • lastTimestamp: Records the previous frame's time for delta calculations
  • deltaTime: Time elapsed since last frame, used for time-based updates
  • accumulator: Tracks leftover time for fixed timestep updates
  • gameStartTime: Records when the game started for total time tracking

Together, these variables ensure smooth gameplay timing and proper game loop control.

Frame Rate Control

To ensure consistent performance across different devices, we implement FPS boundaries:

These settings provide a balance between smooth gameplay and performance constraints, with 60 FPS being the sweet spot for most games.

Singleton Pattern Implementation

To ensure that only one instance of the game loop exists, we use the Singleton pattern.

The getInstance method ensures that there is only one instance of the game loop, making it a singleton.

The constructor is made private to prevent direct instantiation, ensuring that only one instance exists.

Time Management System

The timing system provides crucial calculations for frame timing and game duration:

Let's look at what each getter does:

  • time: Returns the total elapsed time since game start in milliseconds.
  • targetFrameTime: Calculates the ideal time per frame (e.g., 16.67ms for 60 FPS).
  • maxDeltaTime: The maximum allowed delta time per frame (50ms at 20 FPS).

Frame Time Calculation

Delta time represents how long it took to render the previous frame. This is crucial for two reasons:

  1. On a fast device running at 120 FPS, each frame takes about 8ms
  2. On a slower device running at 30 FPS, each frame takes about 33ms

Without delta time, game objects would move 4 times faster on the 120 FPS device! By multiplying movement by delta time, we ensure consistent speed across all devices.

However, we need to cap the delta time to handle extreme cases(Spiral of Death). Consider this scenario:

  1. Player is moving their character forward at 100 pixels per second
  2. Player switches to another browser tab for 5 seconds
  3. When they return to the game tab:
    • Uncapped delta time: 5000ms × 100 pixels/second = character teleports 500 pixels forward!
    • Capped delta time (50ms): Maximum movement is 5 pixels per frame, preventing the teleport

This is why we use Math.min(deltaTime, this.maxDeltaTime) - it ensures smooth gameplay even after interruptions.

Why "Spiral of Death"? Imagine a game starting to lag. Without a cap, the lag makes objects move too far, which causes more lag, which makes them move even further... Like a spiral that keeps getting bigger until the game crashes. That's why we cap it!

Fixed Timestep Update System

This method uses an accumulator pattern to handle time. Here's how it works:

  1. First, we add the frame's deltaTime to our accumulator:

  2. We cap the accumulator to prevent the spiral of death:

  3. We calculate how many updates we need to perform:

    For example: If accumulator is 32ms and targetFrameTime is 16.67ms (60 FPS), we'll perform 1 update (floor(32/16.67) = 1) and save the leftover time.

  4. Finally, we perform the updates in fixed steps:

    Each update uses the same fixed timestep, ensuring consistent physics simulation.

The accumulator ensures we never lose time: any remainder is carried over to the next frame, maintaining perfect timing.

Main Loop Implementation

The start method takes two callbacks: update for game logic and physics, and render for drawing the game. The update callback receives delta time in seconds, while render is called after each update.

First, we initialize our timing system:

Then we define our core game loop. It first checks if the game is still running and schedules the next frame immediately:

Next, we calculate how much time passed since the last frame using our delta time system:

Finally, we update game logic at a fixed timestep and render the frame:

By requesting the next animation frame early, we give the browser more time to prepare, improving performance.

Loop Control Interface

The stop() method safely shuts down the game loop:

  • Checks if the loop is actually running
  • Cancels the next animation frame
  • Resets game timing variables

The setTargetFPS() method controls game speed:

  • Clamps FPS between 20 (MIN_FPS) and 144 (MAX_FPS)
  • Example: setTargetFPS(30) for slower devices
  • Example: setTargetFPS(144) for high-end displays

Putting It All Together

Our GameLoop class creates a robust game engine through several key components:

  1. Time Management:

    • Uses deltaTime for frame-independent movement
    • Caps maximum delta at maxDeltaTime to prevent spiral of death
    • Tracks total game time with gameStartTime
  2. Fixed Physics Updates:

    • accumulator collects passed time
    • Updates run at fixed targetFrameTime intervals (e.g., every 16.67ms at 60 FPS)
    • Leftover time carries over to next frame
  3. Render vs Update Calls:

    • Rendering happens every frame (could be 30, 60, or 144+ FPS)
    • Physics updates run at fixed intervals (typically 60 times per second)
    • Example: At 144 FPS, you might see:
      • 144 render calls per second
      • But only 60 physics updates per second
      • This ensures smooth visuals without breaking physics!

The result is a game loop that maintains perfect timing while adapting to any device's capabilities.

Conclusion

Understanding game loops is crucial for building performant games that provide consistent experiences across different devices. We've covered everything from basic implementations to advanced concepts like fixed timestep updates and delta time handling. These patterns will help you create smooth, professional-grade games in TypeScript.

I learned about and implemented these concepts while building a Flappy Bird clone during a live coding session. If you'd like to see these concepts in action and learn more about game development, you can watch the implementation here:

The live stream demonstrates how to apply these game loop concepts in a real project, making it easier to understand their practical applications.

See all postsSee all posts