Lab — Virtual reality engineering and debugging

Develop a structured, instrumented VR application that is easy to debug, applying Unity/XR engineering best practices.

Objective

This lab (~7 hours) aims to teach you how to develop and maintain a VR application with:

  • a clear architecture (separation of responsibilities, explicit dependencies),
  • an efficient iteration workflow (test as much as possible without a headset, debug builds),
  • basic observability (VR overlay, structured logs),
  • XR debugging practices,
  • an introduction to VR profiling (optional, depending on the time remaining).

The application is a VR solar system visualization workbench: a manipulable tabletop model, a world-space UI, viewing modes and time controls.


Prerequisites

Skills

  • C# / Unity basics (scenes, prefabs, scripts)
  • Git basics (commit, tag, push)
  • VR basics (comfort, world-space UI) are helpful but not essential

Tools

  • Unity LTS (2022.3 or 2023 LTS)
  • Unity packages:
    • OpenXR
    • XR Interaction Toolkit (XRI)
    • Input System
  • An OpenXR-compatible VR headset

Resources


Engineering rules (required)

In this lab, you must explicitly apply the following rules:

  1. No implicit dependencies

    • No FindObjectOfType, no GameObject.Find, no “magic” dependencies.
    • Required references are injected through the Inspector or a bootstrapper.
  2. No business logic in Update() by default

    • Reserve Update() for continuous operations that are actually necessary.
    • Prefer events, actions and controlled timers.
  3. Separation of responsibilities

    • Data models / services (calculations, time, conversion) ≠ views (GameObjects) ≠ controllers.
  4. Debuggability

    • Every critical behavior must produce useful logs.
    • Implement a DebugOverlay visible in the headset.
  5. Performance (optional)

    • No avoidable per-frame allocations (strings/LINQ).
    • Profiling is required (before/after evidence for one improvement).

Scenario: Solar System VR Workbench

You will develop a VR application containing:

  • a solar system tabletop model (with adjustable scale),
  • planets positioned at a given time t (calculation functions are already provided by PlanetData),
  • time controls (date + speed),
  • orbits that can be shown or hidden,
  • a “planet focus” mode (centering/inspection),
  • a world-space VR UI to control the application,
  • a debug overlay (FPS, state, recent events, warnings).

Lab structure

The lab consists of 5 main steps and 2 optional extensions.

Required:

1 — XR boot
2 — Architecture
3 — Events / simulation
4 — VR interactions
5 — UI + debug overlay

Optional (if time allows):

6 — Guided XR debugging
7 — Performance optimization


Submission organization (Git)

You must submit:

  • a Git repository (link),
  • Git tags for each step (v1.0, v1.1, … v2.0),
  • a short report (README or PDF):
    • screenshots (headset view or mirror),
    • implemented features,
    • architecture diagram (simple and readable),
    • a “debug story” (if you completed step 6),
    • a “perf story” (if you completed step 7).

Tag reminder

At each step:

  • git add .
  • git commit -m "…"
  • git tag -a v1.x -m "…"
  • git push origin main
  • git push origin --tags

Useful concepts for this lab

Before starting, here are some important concepts we will use throughout the session. They are common in Unity projects and even more important in XR.

1 — Business logic

Business logic means the rules that define how your application works, independently of its visual presentation.

Examples in this lab:

Business logic:

  • calculating a planet’s position at a given date
  • changing the speed of time
  • showing or hiding orbits
  • changing the solar system’s scale

Display (not business logic):

  • moving a GameObject
  • changing UI text
  • showing a panel
Bad example
void Update()
{
    planet.transform.position = PlanetData.ComputePosition(DateTime.Now);
}

Calculation and display are mixed together.

Good example
DateTime currentTime;

void OnTimeChanged(DateTime t)
{
    currentTime = t;
    UpdateView();
}

The logic decides when to update; the view decides how to display the result.

2 — Separation of responsibilities

In a Unity project, it is very easy to put a lot of functionality in a single script. This works for small projects but quickly becomes difficult to maintain.

We therefore separate the roles.

Simple structure used in this lab

Data / Model ←→ Services ←→ Controllers ←→ Views

Model (data)

Contains only data.

Example:

TimeModel

  • currentTime
  • timeSpeed

A model normally does not depend on Unity.

Services

Contain calculations or rules.

Example:

PlanetEphemerisService

  • computePosition()
Controllers

Coordinate the application.

They receive events and update views.

Example:

SolarSystemController

Views

Visible Unity objects.

Examples:

  • PlanetView
  • OrbitRenderer
  • UI panels
Summary
  • Model → store state
  • Service → calculate
  • Controller → decide what to do
  • View → display

3 — Bootstrapper

The problem

Many Unity projects contain:

  • FindObjectOfType
  • GameObject.Find
  • Singletons used everywhere

These approaches work but make the project fragile.

A simple solution

Create an object that initializes the application.

This is called a Bootstrapper.

Example:

AppBootstrapper

Its role:

  • create services
  • connect objects
  • initialize data
Simple example
public class AppBootstrapper : MonoBehaviour
{
    public SolarSystemController controller;

    void Start()
    {
        var timeModel = new TimeModel();
        controller.Init(timeModel);
    }
}

Dependencies become visible and controlled.

4 — Events

Events allow different objects to communicate without being tightly coupled.

Without events:

UI → Manager → Controller → Camera

Everything depends on everything else.

With events:

UI → Event → Interested objects

Each system remains independent.

Example
public event Action<DateTime> OnTimeChanged;

When the date changes:

OnTimeChanged?.Invoke(newTime);

Another script can listen for this event:

void Start()
{
    planetManager.OnTimeChanged += UpdatePosition;
}

5 — Actions

An Action is simply a function stored in a variable.

Example:

Action<DateTime> callback;

In Unity, actions are used for:

  • events
  • callbacks
  • UI interactions

6 — Managing simulation state

In this application, the central variable is not really a technical “timer”. What matters is simply the simulation date.

In other words, we manipulate a global state:

Simulated date → planet positions → display

When this date changes:

  • planet positions must be recalculated
  • some visualizations must be updated
  • the interface can display the new value

Instead of constantly recalculating in Update(), trigger an event only when the state changes.

Bad approach

void Update()
{
    UpdatePlanets(currentDate);
}

Each object constantly checks whether something has changed.

Use an event to signal a state change.

public event Action<DateTime> OnTimeChanged;

When the date changes:

UI → PlanetManager → event → interested objects

Example:

void Start()
{
    PlanetManager.current.OnTimeChanged += UpdatePosition;
}

This provides:

  • fewer unnecessary calculations
  • more readable code
  • easier debugging
  • better VR performance

7 — Why this matters even more in XR

In VR and XR:

  • builds take longer
  • bugs are harder to diagnose
  • performance is critical

A clear architecture makes it possible to:

  • test some parts without a headset
  • understand a bug quickly
  • modify an interaction without breaking the whole project

8 — ScriptableObject

A ScriptableObject is a Unity asset used to store editable parameters.

Unlike a MonoBehaviour, it is not attached to a GameObject.

Example in this lab:

[CreateAssetMenu(menuName = "XR/Solar System Config")]
public class SolarSystemConfig : ScriptableObject
{
    public float distanceScale;
    public float planetSizeScale;
    public bool showOrbits;
}

Then create the asset in Unity:

Assets → Create → XR → Solar System Config

Assign it in the Inspector.

Benefits:

  • centralized configuration
  • editable without changing code
  • reusable across scenes

What to remember

Throughout the lab, keep asking yourself:

  • Does this script do too many things?
  • Does this code depend on another object unnecessarily?
  • Can I understand this system quickly?
  • Could I easily debug this on a VR headset?

If the answer to either of the last two questions is “no”, you probably need to simplify or separate responsibilities more clearly.


Step 1 — XR boot + minimal scene (Tag: v1.0) — ~45 min

Objectives

  • Have a working VR scene and a debuggable build pipeline.

Tasks

  1. Create the Unity project.
  2. Install and enable:
    • OpenXR
    • XR Interaction Toolkit
      • You can add the Starter Assets (for a complete XR rig) and the XR Simulator (to test without a headset) from the package samples.
    • Input System
  3. Configure the project:
    • Enable the plug-in providers required for your headset (OpenXR, Oculus, etc.) in Player Settings / XR Plug-in Management.
  4. Add a VR rig (XRI):
    • XR Origin
    • XR Interaction Manager
  5. Create a Boot scene:
    • ground + reference frame (for example, X/Y/Z axes to show the world origin in the scene)
    • an interactable cube (grab) to validate XRI
  6. Configure a debug build:
    • Development Build
    • Script Debugging
    • (if possible) Autoconnect Profiler

Expected deliverables

  • Screenshot (headset mirror view or XR simulator) showing the cube being grabbed in VR
  • Repository tagged v1.0-boot (remember the .gitignore)

Step 2 — Architecture: models, services, controllers

Tag: v1.1-architecture — ~1h15

Objective

Before adding VR, interactions and UI, we will structure the application properly.

In XR projects, problems often come from:

  • scripts referencing each other in every direction
  • logic mixed with display code
  • invisible dependencies
  • code that cannot be tested without a headset

We will build a simple architecture:

Bootstrapper
   ↓
Models / Config
   ↓
Services
   ↓
Controllers
   ↓
Views (Unity)

1 — Folder organization

Create the following folders:

Scripts
 ├── Bootstrap
 ├── Models
 ├── Services
 ├── Controllers
 ├── Views
 └── Config

Purpose:

FolderRole
Bootstrapentry point
Modelssimulation state
Servicescalculations
Controllerscoordination
ViewsUnity GameObjects
Configeditable parameters

2 — The time model

In our simulation, the central variable is simulated time.

Create:

Scripts/Models/TimeModel.cs
using System;

public class TimeModel
{
    public DateTime CurrentTime { get; private set; }

    public float TimeScale { get; private set; } = 1f;

    public bool IsPlaying { get; private set; } = true;

    public event Action<DateTime> OnTimeChanged;

    public void SetTime(DateTime t)
    {
        CurrentTime = t;
        OnTimeChanged?.Invoke(CurrentTime);
    }

    public void SetScale(float scale)
    {
        TimeScale = scale;
    }

    public void Play()
    {
        IsPlaying = true;
    }

    public void Pause()
    {
        IsPlaying = false;
    }
}

Important:

  • this script does not depend on Unity
  • it contains only state and events.

3 — Solar system configuration

Some parameters must be editable in Unity.

Create:

Scripts/Config/SolarSystemConfig.cs
using UnityEngine;

[CreateAssetMenu(menuName = "XR/Solar System Config")]
public class SolarSystemConfig : ScriptableObject
{
    public float distanceScale = 0.000001f;
    public float planetSizeScale = 0.01f;
    public bool showOrbits = true;
}

Then create the asset:

Assets → Create → XR → Solar System Config

4 — Using the provided astronomical data

The lab already provides a class:

PlanetData.cs

It contains:

  • orbital parameters
  • orbit calculations
  • the following function:
PlanetData.GetPlanetPosition(PlanetData.Planet planet, DateTime time)

⚠️ This class is the official source for planet positions. Students must not modify this file.

5 — Planet position service

Even though the function already exists, we access it through a service.

Why?

  • keep the architecture clean
  • allow the implementation to be replaced later
  • make testing easier

Create:

Scripts/Services/IPlanetEphemerisService.cs
using System;
using UnityEngine;

public interface IPlanetEphemerisService
{
    Vector3 GetPlanetPosition(PlanetData.Planet planet, DateTime date);
}

Implementation:

Scripts/Services/PlanetEphemerisService.cs
using System;
using UnityEngine;

public class PlanetEphemerisService : IPlanetEphemerisService
{
    public Vector3 GetPlanetPosition(PlanetData.Planet planet, DateTime date)
    {
        return PlanetData.GetPlanetPosition(planet, date);
    }
}

This service acts as an abstraction layer.

6 — Planet view

Create:

Scripts/Views/PlanetView.cs
using UnityEngine;

public class PlanetView : MonoBehaviour
{
    public PlanetData.Planet planet;

    public void SetPosition(Vector3 pos)
    {
        transform.localPosition = pos;
    }
}

Role:

  • represent a planet in the scene
  • apply the calculated positions

7 — Solar system controller

The controller connects:

  • the model
  • the service
  • the views

Create:

Scripts/Controllers/PlanetSystemController.cs
using System;
using UnityEngine;

public class PlanetSystemController
{
    TimeModel timeModel;
    IPlanetEphemerisService ephemeris;

    PlanetView[] planets;

    public PlanetSystemController(
        TimeModel timeModel,
        IPlanetEphemerisService ephemeris,
        PlanetView[] planets)
    {
        this.timeModel = timeModel;
        this.ephemeris = ephemeris;
        this.planets = planets;

        timeModel.OnTimeChanged += UpdatePlanets;
    }

    void UpdatePlanets(DateTime time)
    {
        Debug.Log("[TIME] Updating planets " + time);

        foreach (var planet in planets)
        {
            Vector3 pos =
                ephemeris.GetPlanetPosition(planet.planet, time);

            planet.SetPosition(pos);
        }
    }
}

Responsibilities:

ElementRole
Modelsimulation state
Serviceposition calculation
Controllercoordination
Viewdisplay

8 — Bootstrapper

Create:

Scripts/Bootstrap/AppBootstrapper.cs
using UnityEngine;
using System;

public class AppBootstrapper : MonoBehaviour
{
    public SolarSystemConfig config;

    public PlanetView[] planets;

    TimeModel timeModel;
    PlanetSystemController controller;

    void Start()
    {
        Debug.Log("[BOOT] Initializing application");

        timeModel = new TimeModel();

        var ephemeris = new PlanetEphemerisService();

        controller = new PlanetSystemController(
            timeModel,
            ephemeris,
            planets
        );

        timeModel.SetTime(DateTime.Now);
    }
}

Role:

  • create the main objects
  • connect dependencies
  • start the simulation

This is called the composition root.

9 — Scene setup

Create a GameObject:

App

Add:

AppBootstrapper

In the Inspector:

  • assign SolarSystemConfig
  • assign the PlanetView references

Possible structure:

App
SolarSystem
 ├ Sun
 ├ Mercury
 ├ Venus
 ├ Earth
 ├ Mars
 ├ Jupiter
 ├ Saturn
 ├ Uranus
 └ Neptune

Each planet has a:

PlanetView

10 — Quick test

Run the scene.

Expected result:

  • the planets appear
  • they are positioned correctly
  • a [BOOT] log appears

If nothing happens, check that:

  • the PlanetView references are assigned
  • the bootstrapper is active
  • the scene is saved

If you follow the steps carefully, you should notice a small bug. Modify the service to fix it.

11 — Architecture check

Your code must meet these requirements:

  • no FindObjectOfType
  • no GameObject.Find
  • visible dependencies
  • a model independent of Unity
  • logic separated from display

12 — Architecture diagram

flowchart TD subgraph BOOT[Bootstrap] AB[AppBootstrapper - MonoBehaviour] end subgraph MC[Models and Config] TM[TimeModel - model] SSC[SolarSystemConfig - ScriptableObject] end subgraph SVC[Services] IES[IPlanetEphemerisService interface] PES[PlanetEphemerisService implementation] PD[PlanetData static data] end subgraph CTRL[Controllers] PSC[PlanetSystemController controller] end subgraph VIEW[Views] PV[PlanetView array MonoBehaviour] GO[Planet GameObjects] end AB -->|creates| TM AB -->|reads| SSC AB -->|creates| PES AB -->|injects| PSC AB -->|assigns refs| PV PES -.->|implements| IES PSC -->|uses| IES PSC -->|uses| TM PSC -->|updates| PV PES -->|calls GetPlanetPosition| PD PV -->|attached to| GO TM -->|OnTimeChanged| PSC PSC -->|SetPosition| PV
classDiagram class AppBootstrapper { +SolarSystemConfig config +PlanetView[] planets -TimeModel timeModel -PlanetSystemController controller +Start() } class TimeModel { +DateTime CurrentTime +float TimeScale +bool IsPlaying +OnTimeChanged : ActionDateTime +SetTime(DateTime) +SetScale(float) +Play() +Pause() } class SolarSystemConfig { +float distanceScale +float planetSizeScale +bool showOrbits } class IPlanetEphemerisService { +GetPlanetPosition(Planet, DateTime) Vector3 } class PlanetEphemerisService { +GetPlanetPosition(Planet, DateTime) Vector3 } class PlanetData { +GetPlanetPosition(Planet, DateTime) Vector3 } class PlanetSystemController { -TimeModel timeModel -IPlanetEphemerisService ephemeris -PlanetView[] planets +PlanetSystemController(TimeModel, IPlanetEphemerisService, PlanetView[]) } class PlanetView { +Planet planet +SetPosition(Vector3) } AppBootstrapper --> TimeModel : creates AppBootstrapper --> PlanetEphemerisService : creates AppBootstrapper --> PlanetSystemController : creates AppBootstrapper --> PlanetView : references inspector AppBootstrapper --> SolarSystemConfig : references asset PlanetSystemController --> TimeModel : subscribes PlanetSystemController --> IPlanetEphemerisService : uses PlanetEphemerisService ..|> IPlanetEphemerisService : implements PlanetEphemerisService --> PlanetData : calls PlanetSystemController --> PlanetView : updates

Expected result

At the end of this step:

  • the application is structured
  • the planets use PlanetData
  • dependencies are explicit
  • the simulation is driven by a model

Git tag:

v1.1-architecture

Step 3 — Planet positioning + events

Tag: v1.2-events — ~1h15

Objective

Implement the main simulation flow:

  • time changes,
  • an event is triggered,
  • the controller updates the planets.

We deliberately avoid running all the logic in Update().

Expected flow:

TimeController
     ↓
TimeModel
     ↓
OnTimeChanged
     ↓
PlanetSystemController
     ↓
PlanetView

We already created most of these elements in the previous step. Now we need to connect them properly using events.

Responsibilities:

ElementRole
Modeltime state
Serviceposition calculation
Controllerapplies positions
Viewmoves objects

1 — Trigger updates through an event

This was already implemented in the previous step, but it is important to understand how it works.

In TimeModel:

public void SetTime(DateTime t)
{
    CurrentTime = t;
    OnTimeChanged?.Invoke(CurrentTime);
}

In the controller:

timeModel.OnTimeChanged += UpdatePlanets;

The planets therefore update only when time changes.

2 — Advancing time

Create a small Unity component:

TimeController
public class TimeController : MonoBehaviour
{
    public float secondsPerDay = 1f;

    TimeModel model;
    DateTime current;

    public void Init(TimeModel m)
    {
        model = m;
        current = DateTime.Now;
        model.SetTime(current);
    }

    void Update()
    {
        if (!model.IsPlaying) return;

        current = current.AddDays(Time.deltaTime * secondsPerDay);
        model.SetTime(current);
    }
}

In AppBootstrapper:

timeController.Init(timeModel);

3 — Verification

When the scene starts, the Console should show:

[BOOT] Initializing application
[TIME] Updating planets ...

The planets should move.

Checklist:

  • no FindObjectOfType
  • no GameObject.Find
  • updates go through OnTimeChanged
  • the PlanetView references are assigned

(Optional) Orbits

To display an orbit:

  • add a LineRenderer
  • calculate several positions
  • do not recalculate every frame

Principle:

for (int i = 0; i < samples; i++)
{
    var t = start.AddDays(i * 10);
    points[i] = ephemeris.GetPlanetPosition(planet, t);
}

Expected result

At the end:

  • the simulation is driven by TimeModel
  • the planets correctly follow the simulated time
  • updates go through the OnTimeChanged event
  • the architecture remains clear and testable

Your report must also include:

  • a simple diagram of the TimeModel → Controller → PlanetView flow
  • a short explanation of how it works
  • a screenshot of the application

Tag:

v1.2-events

Step 4 — VR interactions: grab / focus / scale

Tag: v1.3-vr-interactions — ~1h30

Objective

In this step, you will turn your simulation into an object that can be manipulated in VR.

The user must be able to:

  • move the solar system,
  • change its scale,
  • select a planet to obtain information.

The goal is to make these interactions work and integrate them cleanly into the project architecture.

XR interactions should trigger intentions, which are then applied by a clearly defined system (controller / manager), rather than directly manipulating all the objects in the scene.

Recommended flow:

XR Interaction
      ↓
Interaction script
      ↓
Controller
      ↓
Views

1 — Prepare a manipulable tabletop

To make interactions easier, organize the scene around a manipulable root object.

Create a structure similar to:

SolarSystemRoot
 ├ SolarSystem
 │   ├ Sun
 │   ├ Mercury
 │   ├ Venus
 │   └ ...
 └ Handle

SolarSystemRoot becomes the object you will:

  • move
  • rotate
  • scale

All planets must be children of this object.

Why?

  • it makes interactions much simpler,
  • it avoids modifying each planet individually,
  • it provides a central point for controllers.

2 — Grab interaction

Objective

Allow the user to grab the solar system and move it through space.

Tasks

Create an object to serve as a manipulation handle.

Add an XR interactable that allows the user to:

  • grab the object with the controller,
  • move the entire system.

Some necessary components:

  • collider
  • rigidbody (often kinematic)
  • XR interaction component

Expected test:

  • the user can grab the object,
  • move it,
  • release it.
Best practices
  • do not attach this interaction directly to the planets,
  • keep a dedicated handle object,
  • add useful logs:
[XR] Table grabbed
[XR] Table released

3 — Scale control

Objective

Allow the user to zoom in / out on the solar system.

The scale must:

  • remain within reasonable limits,
  • be centralized in a single script,
  • produce useful logs.
Implementation

Create a system responsible for:

  • receiving a scale value,
  • clamping it if necessary,
  • applying the transformation to SolarSystemRoot.

For example, you could create a dedicated component:

ScaleController

This component could expose a method such as:

SetScale(float value)

This component is responsible for actually applying the scale.

User interaction

You are free to choose how to change the scale:

  • a VR UI slider
  • a + button
  • a rotating knob
  • a two-handed gesture (bonus)

In all cases:

  • the interaction must not manipulate the transform directly
  • it calls a controller
Expected logs

Examples:

[INPUT] Scale requested
[XR] Scale applied
[WARN] Scale clamped

4 — Selecting a planet

Objective

Allow the user to point at a planet with an XR ray and select it.

When a planet is selected:

  • an event must be triggered,
  • the system must know which planet is active.
Setup

Each planet must be able to:

  • be detected by a ray interactor,
  • signal its selection.

For example, you could create a script such as:

PlanetSelectable

This script can:

  • reference the PlanetView,
  • trigger an event or call a central system.

Important: the selection script must not contain all the focus or UI logic.

It only signals that a planet has been selected.

5 — Focus mode

Objective

When a planet is selected:

  • the planet must be highlighted (for example, centered, enlarged, etc.),
  • an information panel appears.
Implementation

Create a system responsible for focus.

For example:

FocusController

Possible responsibilities:

  • move the XR origin or camera,
  • activate an information panel,
  • record the active planet.

The panel could display:

  • planet name
  • distance from the Sun
  • orbital period
  • current simulated time

You can choose the exact content.

One possible division of responsibilities:

ElementRole
XR Interactablecapture the interaction
Interaction scripttranslate the user’s action
Controllerapply the logic
Viewdisplay the result

Example flow:

Ray Interactor
      ↓
PlanetSelectable
      ↓
PlanetSelectionSystem
      ↓
FocusController
      ↓
UI

7 — Checks

Before moving on to the next step, check:

Grab
  • the solar system can be moved
  • movement is stable
  • logs appear
Scale
  • the scale can change
  • limits are respected
  • transformations remain consistent
Focus
  • ray selection works
  • the correct planet is detected
  • visual feedback appears

Expected result

At the end of this step, the user can:

  • manipulate the solar system,
  • change its scale,
  • select a planet,
  • see associated information.

The architecture must remain:

  • readable,
  • decoupled,
  • debuggable.

Git tag:

v1.3-vr-interactions

Step 5 — World-space UI + instrumentation (Tag: v1.4) — ~1h00

Objectives

Make the application controllable and debuggable inside the headset.

Tasks

A) World-space UI

Create a VR panel with at least:

  • Date (or scrubbing)
  • Play/pause + speed (x1, x10, x100…)
  • Orbit visibility toggle
  • Reset viewpoint / reset scale

The UI panel can be an interactive world-space Canvas using the XR Interaction Toolkit.

B) DebugOverlay (required)

Create a discreet overlay displaying:

  • FPS / frame time (approximate)
  • Current date + speed
  • Last user action
  • Key warnings (scale out of bounds, suspected performance issues, event spam)

The DebugOverlay can be a simple TextMeshPro panel updated periodically.

Unity allows you to capture all logs (Debug.Log, warnings, errors) through:

Application.logMessageReceived

You can use this callback to:

  • capture important messages
  • keep recent logs
  • display them in the overlay
Example
using UnityEngine;
using TMPro;
using System.Collections.Generic;

public class DebugOverlay : MonoBehaviour
{
    public TextMeshProUGUI text;

    Queue<string> lines = new Queue<string>();

    public int maxLines = 15;

    void OnEnable()
    {
        Application.logMessageReceived += HandleLog;
    }

    void OnDisable()
    {
        Application.logMessageReceived -= HandleLog;
    }

    void HandleLog(string logString, string stackTrace, LogType type)
    {
        lines.Enqueue(logString);

        while (lines.Count > maxLines)
            lines.Dequeue();

        text.text = string.Join("\n", lines);
    }
}

C) Structured logs

All critical events must produce logs in the following format:

  • [TIME] …
  • [INPUT] …
  • [XR] …
  • [PERF] …

Expected deliverables

  • Screenshot showing the UI panel + debug overlay visible in VR
  • Tag v1.4-ui-debug

Step 6 — Guided XR debugging (Tag: v1.5) — (Optional)

Objectives

Learn a reproducible XR debugging method.

Required work

You must address at least 2 tickets from the following 3 (depending on the starter project provided by the instructor):

  • Ticket A — Input works in the Editor but not on the headset
  • Ticket B — Events fire twice after reloading the scene
  • Ticket C — Inconsistent scale/reference frames: objects appear “far away” or “huge”

For each ticket:

  • provide a minimal reproduction
  • describe the method (instrumentation → hypothesis → test → fix)
  • add a simple regression test (for example, log + guard + assert)

Expected deliverables

  • “Debug story” section in the README
  • Tag v1.5-debug-tickets

Step 7 — XR performance (Tag: v2.0) — (Optional)

Objectives

Identify and fix a typical VR performance problem (CPU/GPU/GC).

Tasks

  1. Measure (Profiler):
    • CPU main thread
    • GC allocations
    • scripts running in Update
  2. Choose one bottleneck and improve it:
    • orbit caching
    • pooling
    • reducing update frequency
    • eliminating allocations
  3. Demonstrate the improvement:
    • before/after profiler screenshots
    • describe the change (2–5 lines)

Expected deliverables

  • “Perf story” section in the README
  • Tag v2.0-final

Bonus tasks (optional)

  • Proper planet rotation (axial + orbital)
  • Skybox / celestial sphere
  • “Planet surface” mode (viewpoint, constellations)
  • Teleportation + comfort (snap turn, vignette)
  • Small in-VR “debug console” with category filters

Assessment criteria (indicative)

  • Architecture & code quality — 40%
  • VR interactions — 30%
  • Observability — 20%

Bonus:

  • Debugging (tickets + method)
  • Performance (measurement + improvement)

Quick checklist (before submission)

  • Public/private repository accessible, tags present
  • Application works on the headset
  • World-space UI works
  • DebugOverlay works
  • README includes screenshots + diagrams
  • No FindObjectOfType / GameObject.Find in the final code