Write the Cutscene Spec Before You Write the Cutscene Code

Cutscenes in a MonoGame project tend to start the same way: someone opens a script file, starts hardcoding camera positions and Task.Delay calls, and three scenes later nobody can explain why shot four is half a second longer on one machine than another. The root problem usually isn’t the rendering code — it’s that there was never a spec. The cutscene existed only as code, so timing, intent, and content were tangled together from the first line.

A more maintainable approach is to treat a cutscene as data first, and let both the runtime code and any visual reference material be generated from that same data. Here’s a minimal shape for that.

```csharp
public record AudioCue(string Id, float StartSeconds, string Description);

public record ActorMotion(string ActorId, string FromMarker, string ToMarker, string Easing);

public record CameraIntent(string Type, string FocusTarget, float FovDegrees, string Movement);

public record Shot(
string Id,
float DurationSeconds,
CameraIntent Camera,
IReadOnlyList ActorMotions,
IReadOnlyList VisualConstraints,
IReadOnlyList AudioCues);

public record CutsceneSpec(string Name, IReadOnlyList Shots);
```

Nothing here touches SpriteBatch, a camera matrix, or an audio engine directly. VisualConstraints is deliberately just strings — things like “low camera angle,” “rain, no direct sunlight,” or “keep protagonist silhouette dark.” They’re intent, not implementation, and they’re meant to be read by a human or a tool, not compiled.

Once the spec exists as data, validation becomes straightforward and testable without ever opening the game window:

```csharp
public static class CutsceneValidator
{
public static IEnumerable Validate(CutsceneSpec spec)
{
if (spec.Shots.Count == 0)
yield return “Cutscene has no shots.”;

    foreach (var shot in spec.Shots)
    {
        if (shot.DurationSeconds <= 0f)
            yield return $"Shot {shot.Id} has non-positive duration.";

        foreach (var cue in shot.AudioCues)
            if (cue.StartSeconds > shot.DurationSeconds)
                yield return $"Audio cue {cue.Id} starts after shot {shot.Id} ends.";

        if (shot.Camera is null)
            yield return $"Shot {shot.Id} is missing camera intent.";

        if (shot.ActorMotions.Count == 0)
            yield return $"Shot {shot.Id} has no actor motion defined.";
    }
}

}
```

This is ordinary unit-test territory. A [Fact] that loads a JSON-serialized spec and asserts Validate returns nothing catches the class of bug that normally only shows up during a late playtest: a missing audio cue, a shot with zero duration because someone forgot to fill in a field, an actor motion referencing a marker that doesn’t exist in the level.

From here, the spec becomes the single source of truth for two very different things, and it’s worth keeping them separate on purpose. First, runtime playback: a CutscenePlayer in MonoGame reads Shot.DurationSeconds and CameraIntent to drive an actual Matrix.CreateLookAt sequence and trigger SoundEffectInstance playback at the right offsets. Second, pre-production reference: the same shot descriptions — duration, camera movement, actor motion, visual constraints, audio direction — can be handed to a separate visual reference workflow so a director or artist can see roughly what a shot might look like before anyone models an environment or blocks actors in the actual game scene.

That second use is worth being deliberate about, because it’s easy to let reference material sneak into the runtime asset pipeline. If your team wants to try generating this kind of reference footage, Muse Video offers a prompt-led, browser-based workflow where you describe subject, motion, camera treatment, visual detail, and audio direction together — which maps reasonably well onto the shot record above. It’s worth being clear that the underlying model is presented as preview-stage on its own site, and any output from it should be labeled and stored as a distinct “reference” folder, never imported as a game asset or treated as an authoritative substitute for actual in-engine testing.

The limitations are real. A generated reference clip won’t know your actual level geometry, your actor rig limits, or your target frame budget, and it shouldn’t be asked to. The spec, not the reference video, is what your CutscenePlayer and your tests consume. Keeping that boundary explicit — spec as the contract, code as the implementation, reference video as an optional, clearly labeled preview artifact — is what keeps a growing cutscene list debuggable instead of becoming a pile of one-off Task.Delay calls nobody wants to touch again.