bevy_director 0.4.0

Unreal-Sequencer-inspired cinematic camera and sequence system for Bevy
Documentation
# bevy_director

Camera director, shots, sequences: an Unreal-Sequencer-inspired
cinematic layer for Bevy 0.19. Sequences are RON assets made of shots,
shots are camera rigs (eased keys, constant-speed dolly rails, orbits)
with a film-literate lens (focal length against a filmback, focus pulls,
aperture, exposure), and playback hands off from your gameplay camera and
glides back onto it when the take ends.

## Quickstart

```rust,ignore
use bevy::prelude::*;
use bevy_director::prelude::*;

app.add_plugins(DirectorPlugin);

// Let the director borrow the frame: gate your own camera-driving
// system so it yields during a take and runs during the glide home.
app.add_systems(Update, my_camera_rig.run_if(gameplay_camera_free));

// Anywhere, later:
commands.play_sequence(asset_server.load("sequences/intro.dir.ron"));
```

The director finds your active `Camera3d`, snapshots it, swaps
`is_active` to its own `CineCamera` (spawning a plain one if you did not
provide one; spawn your own to keep your post stack), plays the shots,
and at the end blends toward wherever your live camera is by then,
sampling it every frame. Control can return to the player while the
camera is still gliding.

Listen for `SequenceStarted`, `MarkerReached { name, .. }` (cues,
stingers; on-screen text has its own track now), and
`SequenceFinished { reason }` (story flags belong here, never on
markers). `commands.skip_sequence()` jumps to the end and takes
the normal blend home; `stop_sequence()` cuts hard.

## Director's Cut (feature `editor`, opt-in)

Director's Cut is the docked, mouse-driven authoring surface. It uses the
same `ViewfinderSession`, evaluator, and `.dir.ron` assets as the runtime,
so there is no conversion or editor-only project format.

```rust,ignore
use bevy::prelude::*;
use bevy_director::prelude::*;

App::new()
    .add_plugins((DefaultPlugins, DirectorsCutPlugin))
    .run();
```

Run the included stage with:

```text
cargo run --example directors_cut --features editor
```

Press Backquote to possess the camera; Director's Cut opens automatically.
Press Tab to switch between the editor cursor and camera piloting.

| Action | Control |
| --- | --- |
| Move a shot | Drag its body |
| Resize a shot | Drag its bright right edge |
| Retime a transform or lens key | Drag the diamond |
| Scrub | Click or drag the timeline |
| Edit camera values | Select a key; use the inspector for position, rotation, FOV/focal value, and time |
| Capture the current camera | Key button |
| Add a camera cut | Shot button |
| Add a text block at the playhead | Text button |
| Move / resize a text block | Drag its body / its bright right edge |
| Edit a text block | Select it; the inspector has start, duration, fades, size, and anchor; click the content to type, Enter confirms |
| Preview | Play button or Space |
| Freeze or resume the game world | Freeze button (pauses `Time<Virtual>`) |
| Undo / redo | Ctrl+Z / Ctrl+Y |
| Open a sequence | Open button or Ctrl+O lists the folder's `.dir.ron` files; click one to load it; Escape closes |
| Save | Ctrl+S |
| Rename the sequence file | Click the pencil filename; Enter confirms and Escape cancels |
| Zoom timeline | zoom-out / zoom-in buttons in the toolbar |

New/Open/Save use `assets/sequences/<sequence name>.dir.ron` by default.
The Open panel lists that folder; loading a file also renames the
working take after it, so a following Ctrl+S writes back to the same
file. The filename field accepts either a plain name or the full
`.dir.ron` filename; the extension is added exactly once.
Set `DirectorsCutConfig::sequence_path` when a fixed path is preferable;
the Open panel then lists that file's folder instead.
The editor feature is not enabled by default, keeping UI and picking out
of shipping builds unless requested.

### Integrating with a live game

The editor runs inside your game, over the live scene. That is the
point: you frame shots against real lighting, real actors, real physics.
It also means the game still sees the same keyboard and mouse while you
direct. Anything you do not gate will fight the editor: the character
walks away while you fly the camera on WASD, or a click-to-grab-cursor
handler locks the pointer mid-drag and the timeline scrub sticks.

The crate tells you when to stand down. `DirectorState.phase` says who
owns the frame, and three run conditions wrap the common cases:

| Condition | True when | Gate with it |
| --- | --- | --- |
| `gameplay_camera_free` | `Idle` or `Handback` | your camera-driving system |
| `director_idle` | `Idle` only | gameplay input, cursor grabbing, click-to-act handlers |
| `director_active` | anything but `Idle` | editor-only helpers |

The prerequisites, in order of pain avoided:

1. **Yield the camera.** Run your camera rig under
   `gameplay_camera_free`, as in the quickstart.
2. **Yield the keyboard and mouse.** While the director is not `Idle`,
   stop reading gameplay input. Per-system `run_if(director_idle)`
   works; if your game has a state machine, one bridge system is
   cleaner because every state-gated system and input context follows
   automatically:

   ```rust,ignore
   fn yield_to_director(
       director: Res<DirectorState>,
       state: Res<State<GameState>>,
       mut next: ResMut<NextState<GameState>>,
   ) {
       match (director.phase, state.get()) {
           (DirectorPhase::Viewfinder, GameState::Playing) => {
               next.set(GameState::Directing)
           }
           (DirectorPhase::Idle, GameState::Directing) => {
               next.set(GameState::Playing)
           }
           _ => {}
       }
   }
   ```

3. **Leave the cursor alone.** The viewfinder manages the cursor
   itself: locked while piloting, free while the editor cursor is up,
   and your previous grab state is restored on eject. A gameplay system
   that re-locks the cursor on click breaks timeline dragging; gate it
   like the rest of the input.
4. **Drive gameplay off the default clock.** The Freeze button pauses
   `Time<Virtual>`, so actors hold still while you frame a shot.
   Systems that read `Time<Real>` ignore the freeze. The editor itself
   runs on real time, so flying, scrubbing, and previewing keep working
   in a frozen world; ejecting resumes the clock if the editor paused
   it. A physics engine with its own clock (avian's `Time<Physics>`,
   for example) needs a small mirror system that pauses it whenever
   `Time<Virtual>` is paused.

Key bindings that overlap are harmless once input is gated: the
editor's pilot/editor toggle defaults to Tab, and if your game also
binds Tab, the two never see it at the same time. Rebind via
`DirectorsCutConfig::toggle` if you want a different key.

`DirectorsCutState` is a public resource; `open` tells you whether the
dock is up if you want to gate anything the run conditions do not
cover.

### Development vs shipping

Enabling a Cargo feature only makes its code available. A tool becomes
active—and starts reading its shortcuts—only when its plugin is added to
the app.

| Setup | What runs |
| --- | --- |
| `DirectorPlugin` | Sequence playback only. No authoring shortcuts or UI. |
| `(DirectorPlugin, ViewfinderPlugin)` | Playback plus the keyboard/mouse capture mode. Backquote opens the viewfinder. |
| `DirectorsCutPlugin` | The complete authoring setup: runtime, viewfinder, timeline UI, Bevy 0.19's infinite grid, and editor shortcuts. It installs the other required plugins automatically. |

For a game that authors cinematics during development but ships only the
RON player, expose the editor through a game-specific feature:

```toml
[features]
director_editor = ["bevy_director/editor"]

[dependencies]
bevy_director = { version = "0.3", default-features = false }
```

Select the plugin at compile time:

```rust,ignore
#[cfg(feature = "director_editor")]
app.add_plugins(DirectorsCutPlugin);

#[cfg(not(feature = "director_editor"))]
app.add_plugins(DirectorPlugin);
```

Use the authoring build to create and save the asset:

```text
cargo run --features director_editor
```

Then run or release the normal game without that feature:

```text
cargo run
cargo build --release
```

The shipping build loads and plays the saved asset normally:

```rust,ignore
commands.play_sequence(asset_server.load("sequences/intro.dir.ron"));
```

It contains no Director's Cut UI or editor shortcuts because
`DirectorsCutPlugin` is not installed. This also makes Director's Cut
usable as a temporary authoring tool whose only shipped output is the
`.dir.ron` sequence.

The editor's infinite grid appears only while the viewfinder is active.
Disable it when the game already supplies an authoring grid:

```rust,ignore
app.insert_resource(DirectorsCutConfig {
    show_grid: false,
    ..default()
});
```

The spawned entity is marked with `DirectorsCutGrid`; you can despawn it
and add your own `InfiniteGrid` and `InfiniteGridSettings` when you need
custom colors, spacing, or fade distance.

## Viewfinder (feature `viewfinder`, on by default)

The director's chair, no widgets: press Backquote in your running game.

| Key | Action |
| --- | --- |
| Backquote | Toggle the viewfinder (possesses the camera at the current view) |
| WASD + mouse | Fly and look |
| Q / E | Sink / rise |
| Wheel | Fly speed (Ctrl+Wheel: fov) |
| K | Capture the framing as a key at the playhead |
| [ / ] | Scrub (Shift: single frame) |
| P | Preview the working take |
| Delete | Remove the nearest key |
| Ctrl+S | Write `assets/sequences/<name>.dir.ron` (native only) |

Gizmos draw every shot's path, keys, aim rays, and the playhead ghost
while the viewfinder is up.

## `.dir.ron` format

```ron
SequenceAsset(
    name: "intro",
    shots: [
        Shot(
            start: 0.0, duration: 8.0,
            rig: Rail(
                points: [(-2.0, 2.0, -6.0), (0.0, 2.2, 0.0), (2.0, 2.5, 6.0)],
                constant_speed: true,
                progress: ScalarTrack(keys: [
                    ScalarKey(time: 0.0, value: 0.0, ease: SmoothStep),
                    ScalarKey(time: 8.0, value: 1.0),
                ]),
            ),
            look: At(target: Entity("gate"), damping: Some(8.0)),
            lens: Lens(
                fov: FocalLengthMm(track: ScalarTrack(keys: [ScalarKey(time: 0.0, value: 35.0)])),
                focus: Some(Target(target: Entity("gate"))),
                aperture_f_stops: Some(ScalarTrack(keys: [ScalarKey(time: 0.0, value: 2.0)])),
            ),
        ),
    ],
    markers: [Marker(time: 1.0, name: "stinger_one")],
    texts: [
        TextBlock(
            start: 1.0, duration: 3.0,
            text: "THE GATE",
            anchor: Center,
            fade_in: 0.5, fade_out: 0.5,
        ),
    ],
    blend_out: Some(Blend(secs: 1.5, ease: SmoothStep)),
)
```

Rules worth knowing: shot starts are absolute and sorted; a shot with no
`blend_in` cuts hard; during a blend the previous shot is sampled
clamped (extend it into the overlap for a moving crossfade); markers
fire only while playing across them (skips and seeks never retro-fire);
text blocks are the opposite — level-triggered, so whatever the playhead
sits inside is visible at the fade-resolved alpha, and they may overlap
each other and span camera cuts (their window is half-open, and anything
past the last shot's end never shows because playback stops there);
`constant_speed` rails cover equal distance in equal time via an
arc-length table, the piece Bevy itself does not ship.

## Text overlays

Text blocks carry a duration, fades, an anchor (Center for title cards,
LowerThird for subtitles, TopCenter for location stamps), and a style
(size, color, shadow, optional background box). The core crate always
parses and evaluates them: every frame, the `ActiveTexts` resource holds
the blocks visible right now with their fade alphas, which is the whole
contract for rendering captions with your own UI (localization included:
treat `text` as a key and look it up when you draw).

The `titles` feature adds the built-in renderer, `TitlesPlugin`:
bevy_ui text on the cine camera with the fades applied for you. Because
the overlay targets the cine camera, layout happens inside its viewport,
so anchors respect the letterbox crop with no extra work. It enables
Bevy's `default_font` so text renders out of the box; set
`TitlesConfig::font` to ship your own face (if you assemble Bevy
features by hand and text is invisible, the missing default font is
why). `DirectorsCutPlugin` installs it automatically so the TEXT lane
previews exactly what ships. Markers remain the right tool for cues and
stingers; captions belong to text blocks now.

## Glossary for Unreal users

| Unreal Sequencer | bevy_director |
| --- | --- |
| Level Sequence asset | `SequenceAsset` (`.dir.ron`) |
| Sequencer (the editor) | Director's Cut (`editor` feature) |
| Camera Cuts track | the shot list itself |
| CineCameraActor | a `CineCamera` entity |
| Ease In/Out on a section | `Blend` (`blend_in` / `blend_out`) |
| Camera Rail rig | `Rig::Rail` |
| Crane / orbit rig | `Rig::Orbit` |
| Transform keyframes | `Rig::Keys` |
| Look-at Tracking | `Look::At { target, damping }` |
| Filmback | `Filmback` presets (Super35 default) |
| Focal Length | `FovSpec::FocalLengthMm` |
| Focus Settings | `FocusTrack` -> bevy `DepthOfField` |
| Event track | `Marker` -> `MarkerReached` |
| Text / subtitle track | `TextBlock` (`texts:`), rendered by the `titles` feature |
| Play Rate | `SequencePlayer::rate` (negative plays backward) |
| Possess / Eject | the handoff and `DirectorPhase::Handback` |
| Pilot camera | viewfinder fly mode |
| Camera Shake | `Shake` (seeded, deterministic) |

## Features

- `viewfinder` (default): the in-game capture mode. Implies `gizmos`.
- `gizmos` (default): path and key overlay drawing.
- `titles` (opt-in): the built-in text overlay renderer (`TitlesPlugin`)
  for the sequence's text blocks. Pulls bevy_ui/bevy_text and the
  default font. The text data model and `ActiveTexts` evaluation are in
  the core crate regardless.
- `editor` (opt-in): Director's Cut dock, camera inspector, timeline,
  undo/redo, and the Open panel. Implies `viewfinder` and `titles`.

With everything off, the crate is the data model, the evaluator, the
RON loader, and the playback state machine: enough to ship cutscenes.

## Compatibility

| bevy | bevy_director |
| --- | --- |
| 0.19 | 0.1–0.4 |

## License

Dual-licensed under MIT or Apache-2.0, at your option.