# Understanding the Manim Scene Class and Animation Update Cycle in 3b1b/manim

> Master the Manim Scene class and its animation update cycle. Learn how virtual time, Mobject updates, and frame rendering drive your animations for impressive visuals in 3b1b/manim.

- Repository: [Grant Sanderson/manim](https://github.com/3b1b/manim)
- Tags: deep-dive
- Published: 2026-02-28

---

**The Manim `Scene` class orchestrates every animation through a deterministic per-frame update cycle that advances virtual time, updates Mobjects via `update_frame`, and renders frames, while the `play` method drives animation progression through `progress_through_animations`.**

The `Scene` class in 3b1b/manim serves as the central orchestrator for all mathematical animations. Located in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py), it manages the complete lifecycle from camera initialization to final video output, coordinating the complex interplay between time progression, Mobject state updates, and frame rendering. Understanding this architecture is essential for creating efficient animations and advanced interactive visualizations.

## Scene Initialization and Core Components

The `Scene.__init__` method establishes the foundational infrastructure required for every animation. It merges configuration dictionaries, initializes the `Camera` instance, and sets up the `SceneFileWriter` for video output.

```python
class Scene(object):
    def __init__(self, window=None, camera_config=dict(), …):
        # Merge global, subclass-level and user-provided configs

        self.camera_config = merge_dicts_recursively(…)
        self.file_writer_config = merge_dicts_recursively(…)

        # Initialise camera (samples, FPS, etc.)

        self.camera = Camera(window=self.window, samples=self.samples, **self.camera_config)

        # The camera frame is a special Mobject that always lives in the scene

        self.frame = self.camera.frame
        self.frame.reorient(*self.default_frame_orientation)
        self.frame.make_orientation_default()

        # File writer, Mobject containers and various bookkeeping fields

        self.file_writer = SceneFileWriter(self, **self.file_writer_config)
        self.mobjects = [self.camera.frame]

```

In [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 64-78), this initialization creates the **camera frame** as a special Mobject that always remains in the scene, establishes the **mobjects** list containing all renderable objects, and prepares the file writer configuration. The camera frame's orientation is set via `reorient` and locked with `make_orientation_default`.

## Scene Execution Lifecycle

The `Scene.run` method controls the high-level execution flow, managing timing anchors and coordinating the main animation phases. This method bridges user-defined construction code with the internal update machinery.

```python
def run(self):
    self.virtual_animation_start_time = 0
    self.real_animation_start_time = time.time()
    self.file_writer.begin()
    self.setup()
    try:
        self.construct()
        self.interact()
    except EndScene:
        pass
    …
    self.tear_down()

```

As implemented in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 49-66), the `run` method establishes **virtual animation time** and **real wall-clock time** anchors, begins the file writer, and invokes the user-defined `setup` and `construct` methods. If a window exists, it enters the `interact` loop for real-time user input before finalizing with `tear_down`.

## The Per-Frame Update Cycle

The heart of Manim's animation engine is the `update_frame` method, which executes the core update cycle for every frame. This method synchronizes time progression, Mobject updates, and rendering.

```python
def update_frame(self, dt: float = 0, force_draw: bool = False):
    self.increment_time(dt)                # advance the scene clock

    self.update_mobjects(dt)               # call each Mobject.update(dt)

    if self.skip_animations and not force_draw:
        return

    if self.is_window_closing():
        raise EndScene()

    # Skip rendering when there are no new pyglet events and dt == 0

    if self.window and dt == 0 and not self.window.has_undrawn_event() and not force_draw:
        self.window._window.dispatch_events()
        return

    self.camera.capture(*self.render_groups)   # rasterise the scene

    # Real-time pacing when not skipping

    if self.window and not self.skip_animations:
        vt = self.time - self.virtual_animation_start_time
        rt = time.time() - self.real_animation_start_time
        time.sleep(max(vt - rt, 0))

```

In [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 36-46), this method performs three crucial steps: incrementing the scene clock via `increment_time`, updating all Mobjects through `update_mobjects`, and conditionally capturing the frame via `camera.capture`. The **real-time pacing** logic ensures virtual time stays synchronized with wall-clock time using `time.sleep`.

The `update_mobjects` method iterates over the current `self.mobjects` list and invokes each object's update logic:

```python
def update_mobjects(self, dt: float):
    for mobject in self.mobjects:
        mobject.update(dt)

```

This simple loop in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 64-66) handles all **updaters** attached to Mobjects, allowing per-frame modifications based on the elapsed time delta.

## Animation Playback Pipeline

The `Scene.play` method serves as the primary user-facing interface for executing animations. It transforms animation prototypes into executable objects and drives them through the frame loop.

```python
def play(self, *proto_animations, run_time=None, rate_func=None, lag_ratio=None):
    if len(proto_animations) == 0:
        log.warning("Called Scene.play with no animations")
        return
    animations = list(map(prepare_animation, proto_animations))
    for anim in animations:
        anim.update_rate_info(run_time, rate_func, lag_ratio)
    self.pre_play()
    self.begin_animations(animations)
    self.progress_through_animations(animations)
    self.finish_animations(animations)
    self.post_play()

```

Located in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 76-94), this method orchestrates the full animation lifecycle: preparation via `prepare_animation`, pre-play bookkeeping via `pre_play`, animation insertion via `begin_animations`, frame-by-frame progression via `progress_through_animations`, and final cleanup via `finish_animations` and `post_play`.

The `progress_through_animations` method implements the actual frame loop for active animations:

```python
def progress_through_animations(self, animations):
    last_t = 0
    for t in self.get_animation_time_progression(animations):
        dt = t - last_t
        last_t = t
        for animation in animations:
            animation.update_mobjects(dt)
            alpha = t / animation.run_time
            animation.interpolate(alpha)
        self.update_frame(dt)     # ← the core update cycle

        self.emit_frame()

```

As shown in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) (lines 55-65), this method ties the animation timeline to the core update cycle. For each time step `t`, it calculates the delta time `dt`, updates Mobject states via `animation.update_mobjects`, interpolates animation progress via `animation.interpolate`, and triggers `update_frame` to render the result.

## Skipping Animations and Real-Time Pacing

Manim provides sophisticated controls for **animation skipping** and **temporal synchronization**. The `skip_animations` flag enables rapid scene development by bypassing frame rendering while maintaining state updates.

When `self.skip_animations` is `True`, the `update_frame` method returns early without calling `camera.capture`, significantly accelerating execution. However, the animation logic continues updating Mobject states, ensuring that subsequent animations see correct final positions.

The real-time pacing mechanism ensures smooth playback during interactive sessions:

```python
vt = self.time - self.virtual_animation_start_time
rt = time.time() - self.real_animation_start_time
time.sleep(max(vt - rt, 0))

```

This code block within `update_frame` calculates the difference between **virtual time** (`vt`) and **real time** (`rt`), pausing execution when the renderer advances faster than the display can present frames.

## Practical Examples

### Basic Scene Construction

```python
from manimlib import *

class HelloWorld(Scene):
    def construct(self):
        text = TextMobject("Hello, Manim!")
        self.play(FadeIn(text))
        self.wait(2)
        self.play(FadeOut(text))

```

When `self.play` executes, it triggers the full pipeline: `pre_play` → `begin_animations` → `progress_through_animations` (which calls `update_frame` each tick) → `post_play`.

### Custom Updater Implementation

```python
class RotatingSquare(Scene):
    def construct(self):
        square = Square()
        # Attach a per-frame updater (called from Mobject.update)

        square.add_updater(lambda m, dt: m.rotate(dt * TAU / 2))
        self.add(square)  # Triggers render-group recompute via @affects_mobject_list

        self.wait(4)

```

The `add_updater` method registers a callable receiving `(mobject, dt)`. During each `update_frame` cycle, `Scene.update_mobjects` invokes `square.update(dt)`, executing the rotation logic at approximately 60 frames per second. The `self.add` call triggers `assemble_render_groups` to rebuild the rendering pipeline.

### Skipping Long Animations

```python
class LongAnimation(Scene):
    def construct(self):
        dot = Dot()
        self.add(dot)
        self.skip_animations = True
        self.play(dot.animate.shift(RIGHT * 5), run_time=30)
        self.skip_animations = False
        self.play(dot.animate.shift(LEFT * 5), run_time=2)

```

Setting `skip_animations = True` before the 30-second shift causes `update_frame` to return immediately after state updates without rendering intermediate frames. The final state is preserved, and normal rendering resumes for the subsequent 2-second animation.

## Summary

- The **Manim Scene class** in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) serves as the central orchestrator for all animations, managing camera initialization, Mobject containers, and file writing.
- **Scene.run** establishes timing anchors and coordinates the lifecycle through `setup`, `construct`, `interact`, and `tear_down` phases.
- **update_frame** implements the core per-tick engine: advancing time via `increment_time`, updating Mobjects via `update_mobjects`, and rendering via `camera.capture` with optional real-time pacing.
- **Scene.play** drives animations through `progress_through_animations`, which links interpolation logic to the frame update cycle.
- **Animation skipping** via `skip_animations` accelerates development by bypassing frame capture while preserving state updates, ensuring temporal consistency across the scene.

## Frequently Asked Questions

### How does the Manim Scene class handle timing between frames?

The Scene class maintains two timing references: **virtual animation time** tracking the progression of the mathematical animation, and **real wall-clock time** tracking actual execution. In `update_frame`, the system compares these values via `vt = self.time - self.virtual_animation_start_time` and `rt = time.time() - self.real_animation_start_time`, then sleeps for `max(vt - rt, 0)` seconds to synchronize animation speed with real-time playback.

### What is the difference between Scene.update_mobjects and Animation.update_mobjects?

`Scene.update_mobjects(dt)` in [`manimlib/scene/scene.py`](https://github.com/3b1b/manim/blob/main/manimlib/scene/scene.py) iterates over all Mobjects in the scene and invokes their base `update(dt)` methods, typically executing custom updaters attached via `add_updater`. In contrast, `Animation.update_mobjects(dt)` (called within `progress_through_animations`) handles animation-specific state changes, such as calculating intermediate positions during a `FadeIn` or `Shift` operation before interpolation occurs.

### Can I skip specific animations without affecting the final scene state?

Yes. Setting `self.skip_animations = True` before calling `self.play()` causes `update_frame` to return early without invoking `camera.capture`, meaning no frames are written for that animation segment. However, the animation still executes its logic via `progress_through_animations`, updating all Mobjects to their final states. This ensures subsequent animations begin from correct positions while significantly reducing render time during development.

### How does the Scene class manage interactive events during animation playback?

When running with a window (`self.window` is not None), the `interact` method runs a continuous loop calling `update_frame(1 / self.camera.fps)`. The Scene processes user input through callbacks like `on_mouse_press` and `on_key_press`, which forward events via the `EVENT_DISPATCHER`. The `update_frame` method checks `self.window.has_undrawn_event()` to determine whether rendering is necessary, optimizing performance during idle periods.