// Recipe

Level QA: instrument once, iterate on what players actually do.

Level designers spend most of their time looking at the level. The problem with that is the level looks the same whether players are using it the way you intended or not — the geometry doesn't change when the playtest goes off the rails. Traced turns a playtest into a layered visualization on top of your scene, so you can iterate against evidence rather than feel. This recipe is the lightweight instrumentation pass to run before any internal playtest, designed so you can wire it up once per project and forget about it.

Event vocabulary

Keep the surface small. Level QA wants signal, not telemetry exhaust. Five events get you most of the way:

  • death — Point + rotation. Standard preset. Metadata: cause (combat / hazard / fall / scripted).
  • checkpoint_reached — Point. Fire at each named checkpoint. Metadata: checkpoint_id, elapsed_s since spawn.
  • pickup — Point. Standard preset. Metadata: item. Critical: pickups that never get picked up are where the navigation is failing.
  • area_entered — Point. Fire on trigger volumes around the zones you want to measure traffic in. Metadata: zone_id.
  • player_position — Position stream (Pro+). Continuous path data. The aggregated path-flow visualization is the headline deliverable of any level playtest.

Set build_version and map in session context on every match start. Without those two metadata keys you cannot compare iterations, and the whole point is comparison.

Sample wiring

Drop this on a manager object in your level scene. The class is a single bootstrap plus per-event hooks you can wire into existing gameplay code without restructuring anything.

using UnityEngine;
using Traced;
using System.Collections.Generic;

public class LevelQaTelemetry : MonoBehaviour {
  public string MapId = "arena_01";
  public string BuildVersion;
  public Transform Player;
  public Rigidbody PlayerRb;

  void Start() {
    if (string.IsNullOrEmpty(BuildVersion)) BuildVersion = Application.version;
    Traced.SetSessionContext(new Dictionary<string, string> {
      { "map", MapId },
      { "build_version", BuildVersion },
      { "playtest_kind", "internal" },
    });
  }

  void Update() {
    if (Player != null && PlayerRb != null) {
      Traced.TrackPosition("player_1", Player.position, Player.rotation, PlayerRb.velocity);
    }
  }

  public void OnDeath(Vector3 pos, Quaternion rot, string cause) {
    Traced.TracedPresets.Death(pos, rot,
      new Dictionary<string, string> { { "cause", cause } });
  }

  public void OnCheckpoint(string id, Vector3 pos, float elapsedSeconds) {
    Traced.TrackEvent("checkpoint_reached", pos, Quaternion.identity,
      new Dictionary<string, string> {
        { "checkpoint_id", id },
        { "elapsed_s", elapsedSeconds.ToString("F1") },
      });
  }
}

What to look for in the dashboard

After the playtest ends, open the project Overview, set the session filter to today's playtest window, and run two passes.

First pass: Spatial Density. Toggle death on. Walk the level mentally. Each cluster is a question — "is this where I wanted them to die?". If yes, it's working. If no, it's the next iteration. Add the pickup layer. Pickups with no density next to them are invisible to players; either move them or signpost them. Add area_entered filtered to the zones you care about; low density on a story zone is your missed-content list.

Second pass: Path Flow. Aggregate player_position across all sessions in the window. The major flow lines are the routes players actually used. If those don't match the routes you designed, you have a level-readability problem, not a difficulty problem. The recent-events table sorted by checkpoint_reached.elapsed_s gives you the fastest-clears outlier paths, which often reveal exploits.

After the iteration, repeat with the new build_version and compare side by side. The whole loop is fast enough that you can instrument in the morning, playtest at lunch, and ship a recut by end of day.

See also: FPS recipe for combat- specific instrumentation, scene export to get your real Unity geometry into the viewer.