---
title: "View a physics replay in C#"
url: https://recursiveai.org/onescope/cookbook/view-physics-replay/
description: "Open a OnePhysics replay with OneScope, scrub its timeline and inspect body properties. Includes a recorded sample and a runnable .NET 10 example."
version: "1.0.0"
---

# View a physics replay in C#

**OneScope** is a free, open-source viewer for deterministic libraries from Recursive AI. MIT or Apache-2.0.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneScope.Tool) · [GitHub](https://github.com/recursive-ai-oss/onescope-csharp) · [Docs](https://recursiveai.org/onescope/docs/) · [Download example (.zip)](https://recursiveai.org/onescope/cookbook/view-physics-replay/view-physics-replay.zip)

View OnePhysics and OnePath replays with bundled modules; other formats need a module.

```shell
dotnet tool install --global RecursiveAI.OneScope.Tool
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```shell
onescope open run.oppr
```

Opens `run.oppr` from the download with a timeline and body inspector.

Unzip [the example](https://recursiveai.org/onescope/cookbook/view-physics-replay/view-physics-replay.zip), then `dotnet run`.

## The full program

`run.oppr` is in the download: a 2D recording of one ball falling onto a static ground box, with 120 simulation steps. The ball’s entity ID is `72057598332895233`. The ground is 20 × 1 meters at (0, −1); the ball starts at (0, 4), with radius 0.5 meters. It was recorded with OnePhysics 1.0.0 at 1/60 second per step.

The C# launcher installs OneScope.Tool 1.0.0 into its build folder, runs the operations headless, checks the exit code and prints selected JSON fields. Headless still needs a working graphics renderer.

```csharp
using System.Diagnostics;
using System.Text.Json;

// Install the published command-line tool into the build folder.
string toolDir = Path.GetFullPath("obj/onescope-tool");
string tool = Path.Combine(toolDir,
    OperatingSystem.IsWindows() ? "onescope.exe" : "onescope");
if (!File.Exists(tool))
    Run("dotnet", "tool", "install", "RecursiveAI.OneScope.Tool",
        "--version", "1.0.0", "--tool-path", toolDir);

// Results: [0] opens run.oppr, [1] seeks, [2] lists bodies, [3] inspects the ball.
var results = Open(
    "seek {\"step\":60}",
    "entities {\"kind\":\"body\"}",
    "inspect {\"entity\":72057598332895233}");
var time = results[1];
var body = results[2].GetProperty("entities")[1];
Console.WriteLine($"step {time.GetProperty("step")}: " +
    time.GetProperty("sources")[0].GetProperty("hash"));
Console.WriteLine($"body {body.GetProperty("id")}: {body.GetProperty("name")}");
var position = results[3].GetProperty("properties")
    .EnumerateArray().First(p => p.GetProperty("name").GetString() == "Position");
Console.WriteLine($"Position: {position.GetProperty("value")}");

JsonElement[] Open(params string[] operations)
{
    var arguments = new List<string>
    {
        "open", "run.oppr", "--headless", "--size", "640x480"
    };
    foreach (string operation in operations)
        arguments.AddRange(["--run", operation]);
    string output = Run(tool, arguments.ToArray());
    return output.Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Select(line => JsonDocument.Parse(line).RootElement.Clone())
        .ToArray();
}

string Run(string command, params string[] arguments)
{
    var start = new ProcessStartInfo(command)
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true
    };
    foreach (string argument in arguments) start.ArgumentList.Add(argument);
    using var process = Process.Start(start)!;
    var output = process.StandardOutput.ReadToEndAsync();
    var errors = process.StandardError.ReadToEndAsync();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException(output.Result + errors.Result);
    return output.Result;
}
```

`dotnet run` prints:

```text
step 60: cb0162b65afd19f4
body 72057598332895233: Body 1 (Awake)
Position: [0,-4.162779E-05,0]
```

If an absolute seek is outside the recording, the operation fails with `bad_args`; use the start and end reported when opening the source.

## Variations

### Seek and inspect without a window

```shell
onescope open run.oppr --headless \
  --run 'seek {"step":60}' \
  --run 'inspect {"entity":72057598332895233}'
```

The seek result is:

```json
{"step":60,"start":0,"steps":120,"sources":[{"source":0,"step":60,"hash":"cb0162b65afd19f4"}]}
```

The following inspector result contains the ball’s transform, motion, body and solver properties:

```json
{
  "source": 0,
  "step": 60,
  "entity": 72057598332895233,
  "kind": "body",
  "name": "Body 1 (Awake)",
  "properties": [
    {
      "group": "Transform",
      "name": "Position",
      "kind": "vector",
      "value": [
        0,
        -4.162779e-05,
        0
      ]
    },
    {
      "group": "Transform",
      "name": "Angle",
      "kind": "number",
      "value": 0
    },
    {
      "group": "Motion",
      "name": "Linear velocity",
      "kind": "vector",
      "value": [
        0,
        0,
        0
      ]
    },
    {
      "group": "Motion",
      "name": "Angular velocity",
      "kind": "number",
      "value": 0
    },
    {
      "group": "Body",
      "name": "Type",
      "kind": "text",
      "value": "Dynamic"
    },
    {
      "group": "Body",
      "name": "Mass",
      "kind": "number",
      "value": 0.7853981852531433
    },
    {
      "group": "Body",
      "name": "Awake",
      "kind": "bool",
      "value": true
    },
    {
      "group": "Solver",
      "name": "Island",
      "kind": "integer",
      "value": 0
    },
    {
      "group": "Solver",
      "name": "Sleep time",
      "kind": "number",
      "value": 0.10000000149011612
    },
    {
      "group": "Solver",
      "name": "Constraints",
      "kind": "integer",
      "value": 1
    },
    {
      "group": "Solver",
      "name": "Constraint colours",
      "kind": "integer",
      "value": 1048576
    },
    {
      "group": "Solver",
      "name": "External impulse",
      "kind": "vector",
      "value": [
        0,
        0,
        0
      ]
    }
  ]
}
```

To record your own `.oppr`, wrap your simulation steps in OnePhysics’s `world.StartRecording(stream)`. [Record a replay](https://recursiveai.org/onescope/docs/getting-started/#record-a-replay) shows the complete recording program.

## Related recipes

- [Render replay frames to PNG headless](https://recursiveai.org/onescope/cookbook/render-replay-frames-headless/)
- [Debug a determinism desync in C#](https://recursiveai.org/onescope/cookbook/debug-determinism-desync/)
- [Fork and change a simulation replay](https://recursiveai.org/onescope/cookbook/fork-and-poke-a-replay/)

---

## The short answer

Install the `onescope` .NET tool and open a OnePhysics `.oppr` file. OneScope loads its bundled physics module, draws the world and provides a timeline, entity tree and inspector. No extra module assembly is needed for OnePhysics recordings. The command is a separate viewer; your simulation does not need to reference the viewer package.

To explore the supplied replay interactively, run the snippet at the top from the unzipped folder.

## How do I view a replay?

Open the recording in the window, then move the timeline cursor to the step you want. Time in OneScope is a simulation step index, not a video timestamp. Selecting a body in the entity tree shows its properties in the inspector. Selecting it in a scene view outlines the same entity in every view of that source. The window can hold both 2D and 3D views; the bundled module supports both kinds of OnePhysics world.

For the supplied file, select Body 1 rather than Body 0: Body 0 is the static ground. At step 60, Body 1 is near the ground. The inspector reports position and velocity, along with body and solver properties. This gives you the numbers behind the picture rather than requiring you to estimate a position from pixels.

The source declares overlays for shapes, bounds, contacts, joints, centers of mass and joint limits. Turn on the ones relevant to the question you are investigating. For a collision question, contacts and bounds help connect the geometry in the view to the properties in the inspector. Opening a replay is read-only; changing the camera or the cursor does not change the recorded simulation.

## How do I inspect a physics replay without a window?

Use `--headless` and pass operations with `--run`, as in the variation. Each operation prints a JSON result, and the process exits after the requested operations. `entities` lists IDs; `inspect` takes an entity ID from that list. IDs belong to the recording, so the ball ID shown here is not a general way to select the first dynamic body in another file.

`seek` moves to an absolute step. `step` moves by a delta and clamps to the recording’s range. A seek outside the time axis fails instead of silently choosing a different absolute step. Read the source’s start and end from the opening result before choosing a step in your own recording.

Seeking backward and then forward reconstructs the same state as playing straight to that step. The reported hash is the physics library’s world hash, not a checksum of the screenshot. Playback speed affects which step is presented, not the simulation’s recorded time step. Those distinctions matter when you are trying to inspect one reproducible collision rather than simply watch an animation. The [time model](https://recursiveai.org/onescope/docs/history/) explains how keyframes and replay support that behavior.
