AR/VR Application Development Patterns
Modern patterns for building immersive experiences, why they matter now, and practical strategies from the field.

Developers often ask me whether AR/VR development is just traditional mobile or game dev with a headset attached. It’s a fair question. The honest answer is that it’s adjacent to both, but with a distinct set of constraints and opportunities that change how you structure code, think about performance, and design interactions. In the last few years, devices like Meta Quest 3 and Apple Vision Pro have moved from novelty to real productivity and entertainment platforms. WebXR has matured enough to make browser-based immersive experiences viable for broad audiences. At the same time, edge AI on phones and headsets now enables robust hand tracking and spatial mapping, opening doors to natural interactions that feel closer to science fiction than tech demos.
If you’ve felt a mix of excitement and uncertainty about AR/VR development patterns, that’s normal. The ecosystem is fragmented, the hardware is demanding, and the tooling changes quickly. This article distills patterns that work in real projects, not just demos. We’ll cover architectural approaches, interaction paradigms, performance strategies, and cross-platform realities. You’ll see practical code in Unity/C# (the most common professional stack) and a small WebXR example using Three.js. You’ll also find honest tradeoffs, lessons from personal experience, and vetted resources to go deeper.
Where AR/VR development sits today
AR/VR development is not a single path. It spans native platform SDKs, engines, and the web, each with different audiences and constraints.
Unity remains the dominant engine for professional AR/VR on mobile and headsets. It’s cross-platform, supports fast iteration, and has mature XR tooling via XR Interaction Toolkit and OpenXR. Native development with Swift (ARKit) and Kotlin (ARCore) is common for high-fidelity AR on phones, especially when you need deep integration with platform features or optimal performance. WebXR (via Three.js or Babylon.js) is increasingly used for lightweight, shareable experiences that run in browsers without app store friction. It’s especially strong for marketing demos, education, and prototypes that need reach over raw performance.
Who uses these tools? Indie studios, enterprise teams building training or remote assistance, product companies embedding AR in shopping experiences, and web developers exploring immersive UX. Compared to alternatives like console game engines (Unreal) or native mobile AR, Unity offers the best balance of cross-platform coverage and developer resources. Unreal excels for cinematic visuals but can be heavier for mobile and headset apps. Native SDKs give tight control and platform-native UX, but you pay for it in duplicated effort.
Real-world usage shows a split. Mobile AR (phones) dominates in consumer apps for try-on, visualization, and education. Standalone headsets (Quest, Vision Pro) are taking over gaming, collaboration, and spatial productivity. The web bridges the gap for accessibility. Regardless of the platform, the patterns below help you ship maintainable, scalable apps.
Core architectural patterns
At the heart of AR/VR apps is the loop: perceive, update, render. Perceive means reading sensors and input. Update is your simulation and logic. Render is drawing the world. Patterns help you tame this loop across devices and scenarios.
Component-driven scene composition
Traditional game objects and components still rule. Instead of monolithic scripts, decompose behavior into small, composable components. In Unity, think XRGrabInteractable for grabbable objects, DistanceGrabber for far selection, and custom components for domain logic like hologram annotation or physics-based training tools.
A component-driven layout avoids tight coupling. For example, a medical visualization app might have:
- A
SpatialAnchorComponentthat manages persistence and updates. - A
MeasurementToolthat computes distances between points. - A
HandRayInteractionthat renders a ray from the palm and handles selection.
This is easier to test and iterate on. You can enable or disable components for different devices (e.g., disable hand tracking on a headset without it).
Layered app structure
Think in layers: Platform, Engine, Domain, UI.
- Platform layer: XR subsystems, permissions, input backends.
- Engine layer: Rendering, physics, audio, XR frameworks.
- Domain layer: Business logic, data models, services.
- UI layer: Diegetic and non-diegetic interfaces, canvases.
This layering helps when porting. Keep the domain layer engine-agnostic; push platform specifics down. For example, the domain layer might define a “SpatialAnchor” interface; the platform layer implements it with ARKit’s ARAnchor or WebXR’s anchors.
Data flow and state management
Immersive apps often have asynchronous operations (spatial mapping, anchor creation, asset streaming). Avoid global state. Use an event bus or reactive streams. In Unity, ScriptableObjects make great event channels. In WebXR, simple Observable patterns or signals work well.
A typical flow:
- User taps to place an object.
- App requests a spatial anchor (async).
- On success, the object is attached to the anchor and serialized.
- UI updates via events (no direct references).
Interaction patterns
AR/VR interactions differ from 2D. Input spans gaze, hand tracking, controllers, voice, and touch. A good app handles multiple input paths and gracefully degrades.
Ray and direct selection
Ray selection (a laser from controller or hand) is the most robust. Direct selection (touch) feels natural but requires precise collision and haptics. A common pattern is a hybrid: ray for far, direct for near. Use Unity’s XR Interaction Toolkit ray interactor with distance limits.
Spatial UI
Place UI in world space, not screen space. Anchor panels to surfaces or the user’s wrist. Keep UI at comfortable distances (0.5–1.5 meters) and angles (20–40 degrees). For readability, use large fonts, high contrast, and avoid small interactive targets.
Hand tracking and gestures
Hand tracking is now reliable on Quest and Vision Pro. Patterns:
- Pinch to select, hold to drag, release to drop.
- Palm-facing-user as a “home” state.
- Avoid gestures that strain the wrist.
Capture raw joint data for custom gestures, but keep a thresholded approach to avoid false positives.
Voice and multimodal
Voice commands complement gestures. Define a small, memorable vocabulary. Combine gaze + pinch + voice for accessibility and robustness. For example, “Place red cube” while looking at a surface.
Haptics and feedback
Haptics increase realism and clarity. Short pulses on selection, stronger pulses on collisions, and “confirmation” patterns for success. Don’t overuse haptics; they drain battery and can cause fatigue.
Performance patterns
AR/VR demands stable frame rates. On Quest, 72–90 Hz is typical; on Vision Pro, 90–120 Hz. Drops are immediately noticeable and can cause discomfort.
Frame budgeting
Aim for ~13 ms per frame on 90 Hz devices. Profile early. Typical Unity targets:
- Draw calls < 100 for mobile, < 200 for headsets.
- CPU main thread < 8 ms.
- GPU < 10 ms.
Use GPU and CPU profilers (Unity Profiler, RenderDoc) and platform tools (Quest Metrics, Xcode Instruments).
Asset optimization
- Use mobile-optimized PBR materials with 1–2 lights.
- Prefer texture atlases; compress textures (ASTC on mobile, BC7 on PC).
- Use LODs for complex meshes; enable occlusion culling.
- Bake static lighting; avoid real-time shadows where possible.
Spatial mapping and anchors
Spatial mapping (meshing) is CPU/GPU heavy. Throttle mesh updates and limit resolution. Persist anchors rather than recomputing. On WebXR, rely on platform anchors if available; otherwise, approximate with plane detection and save positions relative to a reference.
Multithreading and jobs
Unity’s Job System and Burst compiler help offload heavy work. For example, processing mesh data or raycasts. Avoid main-thread bottlenecks.
Asset streaming
For large assets, stream asynchronously. Show low-LOD placeholders first. Use progressive loading to avoid frame hitches.
Code examples: patterns in practice
Below are realistic patterns for Unity (C#) and WebXR (Three.js). They illustrate component-driven architecture, event flow, and performance-conscious design.
Unity: Component-driven interaction with ScriptableObject events
This example shows a simple setup: a grabbable hologram with an event channel that updates a UI panel when placed. The component decouples placement logic from UI.
// Event channel for placement events
[CreateAssetMenu(menuName = "XR/Events/HologramEvent")]
public class HologramEventChannel : ScriptableObject
{
public System.Action<GameObject> OnPlaced;
public void Raise(GameObject obj) => OnPlaced?.Invoke(obj);
}
// Placement component using XR Interaction Toolkit
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
[RequireComponent(typeof(XRGrabInteractable))]
public class HologramPlacer : MonoBehaviour
{
[SerializeField] private HologramEventChannel placementChannel;
[SerializeField] private float placementRayDistance = 5f;
[SerializeField] private LayerMask placementLayer;
private XRGrabInteractable grabInteractable;
private bool isPlaced;
private void Awake()
{
grabInteractable = GetComponent<XRGrabInteractable>();
grabInteractable.selectExited.AddListener(OnRelease);
}
private void OnRelease(SelectExitEventArgs args)
{
if (isPlaced) return;
// Cast from controller position forward to find a placement surface
var interactor = args.interactorObject as XRBaseInteractor;
if (interactor == null) return;
Vector3 origin = interactor.transform.position;
Vector3 direction = interactor.transform.forward;
if (Physics.Raycast(origin, direction, out var hit, placementRayDistance, placementLayer))
{
// Snap to surface
transform.position = hit.point;
transform.rotation = Quaternion.LookRotation(hit.normal, Vector3.up);
// Notify systems
placementChannel.Raise(gameObject);
isPlaced = true;
// Optional: disable further grabbing after placement
grabInteractable.enabled = false;
}
}
}
// UI updater listening to the event
using UnityEngine;
using UnityEngine.UI;
public class HologramStatusUI : MonoBehaviour
{
[SerializeField] private HologramEventChannel placementChannel;
[SerializeField] private Text statusText;
private void OnEnable() => placementChannel.OnPlaced += OnPlaced;
private void OnDisable() => placementChannel.OnPlaced -= OnPlaced;
private void OnPlaced(GameObject obj)
{
statusText.text = $"Hologram placed: {obj.name}";
}
}
Notes:
- This pattern keeps UI independent of interaction details.
- It uses Unity’s XR Interaction Toolkit for robust controller/hand interactions.
- You can extend it with anchors for persistence across sessions.
Unity: Spatial anchoring with ARFoundation
Anchors are essential for persistent AR. This snippet shows a simple anchor placement flow. It’s designed for mobile AR via ARFoundation but the pattern applies to headsets too.
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public class AnchorManager : MonoBehaviour
{
[SerializeField] private ARRaycastManager raycastManager;
[SerializeField] private GameObject prefabToPlace;
[SerializeField] private HologramEventChannel placementChannel;
private void Update()
{
// Use screen center for placement
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
var hits = new List<ARRaycastHit>();
if (raycastManager.Raycast(screenCenter, hits, TrackableType.PlaneWithinPolygon))
{
// Place on the first hit
var pose = hits[0].pose;
var instance = Instantiate(prefabToPlace, pose.position, pose.rotation);
// Create an anchor so the object stays locked to the world
var anchor = new ARAnchor("PlacedHologram", pose);
// Note: In ARFoundation 5.x you can add an ARAnchor component or use ARAnchorManager to add anchors.
// This is a simplified placeholder for the concept.
placementChannel.Raise(instance);
}
}
}
Important details:
- The code uses ARRaycastManager to find planes. For headsets, replace with XR Raycast and plane subsystems.
- Anchors persist across frames. For cross-session persistence, serialize anchor IDs and reattach on reload (platform-dependent).
WebXR: Basic immersive scene with Three.js and WebXR Manager
WebXR enables quick, shareable experiences. Below is a minimal setup that enters VR, places a cube on a click, and handles selection via controller ray.
// main.js (ES module)
import * as THREE from 'https://unpkg.com/three@0.158.0/build/three.module.js';
import { VRButton } from 'https://unpkg.com/three@0.158.0/examples/jsm/webxr/VRButton.js';
import { XRControllerModelFactory } from 'https://unpkg.com/three@0.158.0/examples/jsm/webxr/XRControllerModelFactory.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x202020);
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 20);
camera.position.set(0, 1.6, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
document.body.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// Lighting
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444);
scene.add(hemi);
// Floor for reference
const floorGeo = new THREE.PlaneGeometry(10, 10);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 1 });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// Controllers (ray controllers)
const controllerModelFactory = new XRControllerModelFactory();
function buildController(idx) {
const ctrl = renderer.xr.getController(idx);
scene.add(ctrl);
const grip = renderer.xr.getControllerGrip(idx);
grip.add(controllerModelFactory.createControllerModel(grip));
scene.add(grip);
// Ray geometry
const geo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1) ]);
const line = new THREE.Line(geo, new THREE.LineBasicMaterial({ color: 0x00ffff }));
line.name = 'ray';
line.scale.z = 5;
ctrl.add(line);
ctrl.addEventListener('selectstart', onSelectStart);
return ctrl;
}
const controller1 = buildController(0);
const controller2 = buildController(1);
const objects = []; // Track placed objects
function onSelectStart(event) {
const controller = event.target;
const intersections = getIntersections(controller);
if (intersections.length > 0) {
// If we hit an existing object, attach it
const intersect = intersections[0];
const object = intersect.object;
object.userData.isGrabbed = true;
controller.attach(object);
} else {
// Place a cube in front of controller
const cubeGeo = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const cubeMat = new THREE.MeshStandardMaterial({ color: 0xff8800 });
const cube = new THREE.Mesh(cubeGeo, cubeMat);
const pos = new THREE.Vector3(0, 0, -0.2).applyMatrix4(controller.matrixWorld);
cube.position.copy(pos);
cube.userData.isGrabbed = false;
scene.add(cube);
objects.push(cube);
}
}
function getIntersections(controller) {
const tempMatrix = new THREE.Matrix4();
tempMatrix.identity().extractRotation(controller.matrixWorld);
const raycaster = new THREE.Raycaster();
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
return raycaster.intersectObjects(objects, true);
}
// Simple animation loop
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebXR Basic</title>
<style>body { margin: 0; overflow: hidden; } </style>
</head>
<body>
<script type="module" src="./main.js"></script>
</body>
</html>
Notes:
- This pattern uses controller rays for selection and placement. It’s a foundation for more complex tools.
- For production, add hand tracking (where supported) and fallback for 3DoF devices.
Async patterns with error handling
AR/VR operations are often async: anchor creation, meshing, asset loading. Use cancellable async tasks with timeouts. In Unity:
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class AnchorService : MonoBehaviour
{
public async Task<bool> TryCreateAnchorAsync(Pose pose, CancellationToken ct)
{
try
{
// Simulate async anchor creation
await Task.Delay(500, ct); // Replace with real platform call
ct.ThrowIfCancellationRequested();
// Example: ARAnchorManager.CreateAnchor
return true;
}
catch (OperationCanceledException)
{
Debug.Log("Anchor creation canceled.");
return false;
}
catch (System.Exception ex)
{
Debug.LogError($"Anchor creation failed: {ex.Message}");
return false;
}
}
}
Cancel tokens are crucial when the user exits a mode or the app loses focus.
Real-world tradeoffs and honest evaluation
Strengths
- Unity’s XR ecosystem is mature. OpenXR support simplifies cross-device targeting.
- Component-driven design makes features modular and testable.
- ScriptableObjects and event channels decouple systems, easing refactoring.
- WebXR offers frictionless distribution and broad reach; great for marketing and education.
Weaknesses
- Fragmentation is real. Input differences between Quest, Vision Pro, Pico, and mobile require careful abstraction.
- Performance constraints are stricter than traditional apps. You must budget assets and draw calls aggressively.
- Tooling is evolving. Some platform-specific features (e.g., Vision Pro hand occlusion, Quest passthrough) require platform SDKs and may not be available in cross-platform engines.
- Debugging immersive apps is hard. Console logs are less accessible; you need in-app telemetry and remote logging.
When to choose what
- Use Unity for professional cross-platform AR/VR on mobile and headsets, especially if you have a team familiar with C#.
- Use native (Swift/Kotlin) for high-fidelity mobile AR with tight platform integration or when you target a single OS and need the best performance.
- Use WebXR when reach and speed matter more than raw performance, and you want to avoid app store friction.
Personal experience: learning curves and common mistakes
I learned the hard way that “good on desktop” doesn’t mean “comfortable in VR.” My first pass at a physics-heavy tool jittered badly on Quest. The culprit: heavy colliders and real-time shadows. Switching to simple colliders, baked lighting, and LODs stabilized the frame rate. Another early mistake: designing menus floating at arm’s length. Users found them tiring. Anchoring UI to the wrist or 1–1.5 meters away at a 30-degree tilt improved usability.
Hand tracking surprised me with its reliability and limitations. Pinch detection works well in good lighting, but low-light or busy backgrounds cause jitter. Adding a fallback to controller rays saved the experience. Haptics were another learning moment: subtle pulses dramatically improved feedback without feeling intrusive. Overuse, however, led to fatigue and battery drain.
One of the most valuable moments was when we integrated an event channel (ScriptableObject) for all UI updates. Before that, UI components referenced interactable objects directly, making testing a maze of dependencies. After the refactor, we could test placement logic in isolation and unit-test UI updates by simulating events. That single change reduced bugs and sped up iteration.
Getting started: setup, tooling, and workflow
A good workflow matters more than any specific tool. Below is a mental model and a minimal project structure.
Unity workflow (recommended)
- Install Unity LTS (2022 or newer).
- Add packages:
- XR Interaction Toolkit (Unity Registry)
- AR Foundation (for mobile AR; optional for headsets)
- OpenXR Plugin (Unity Registry)
- Device Simulator (for testing mobile layouts)
- Enable XR Plug-in Management for target platforms (Android/iQuest, iOS/ARKit, OpenXR for PC headsets).
- Set Player Settings:
- Minimum API Level: Android 10 (API 29) or higher
- Target iOS 15+
- IL2CPP backend, ARM64 only
Example folder structure (line-based):
Assets/
Scenes/
Main.unity
Scripts/
Core/
Events/
HologramEventChannel.asset (generated)
Services/
AnchorService.cs
Features/
Holograms/
HologramPlacer.cs
UI/
HologramStatusUI.cs
Prefabs/
HologramCube.prefab
XR/
XR Interaction Toolkit/
... (package files)
Settings/
XR Plug-in Management/
OpenXR/
Android/
iOS/
Project settings tips:
- Quality: set medium for mobile, high for PC headsets.
- Frame rate: lock to 72/90/120 depending on device.
- Shaders: use URP for mobile; keep post-processing light.
- Profiling: keep the Profiler window open; build with “Development Build” for device testing.
WebXR workflow (Three.js)
- Local dev server: use
python3 -m http.serverorvitefor ES modules. - Test in Chrome with WebXR support; enable flags if necessary.
- For production: minify, compress assets, and add service workers for caching.
- Consider a fallback for non-WebXR browsers: show a 360 viewer or static 3D.
Example package.json snippet:
{
"name": "webxr-basic",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.0"
},
"dependencies": {
"three": "^0.158.0"
}
}
What makes AR/VR patterns stand out
- Input abstraction: Build a unified input layer that maps gaze, hands, controllers, and voice to actions. It pays off when adding new devices.
- Event-driven UI: ScriptableObjects or Observable patterns keep UI reactive and testable.
- Performance-first asset pipeline: Start with low-poly, simple materials; add LODs early; profile on target hardware weekly.
- Anchor persistence: Treat anchors as first-class data; build a persistence service that can rehydrate anchors across sessions.
- User comfort: Avoid locomotion that conflicts with vestibular expectations; prefer teleport, snap-turn, and anchored UI. Provide comfort toggles.
Developer experience is improved by:
- Fast iteration: use the XR simulator for quick testing without wearing the headset every time.
- Telemetry: log frame time, input latency, and errors to a remote endpoint for field debugging.
- Automated QA: capture input traces and replay them for regression tests.
Free learning resources
- Unity XR Interaction Toolkit Official Samples: Practical examples of interactions and locomotion. Unity Manual: XR Interaction Toolkit
- WebXR Fundamentals (MDN): Concepts and API details for WebXR. MDN WebXR Device API
- OpenXR Overview: Understand the cross-platform standard. Khronos OpenXR Registry
- Apple Vision Pro Developer Docs: Platform-specific patterns and best practices. Apple Developer: Vision Pro
- Meta Quest Developer Center: Guidance for Quest-specific optimization and interaction. Meta Quest Developer Documentation
These resources are practical and current. They complement hands-on experimentation, which remains the best teacher in AR/VR.
Summary: who should use these patterns, and who might skip
Use these patterns if:
- You’re building AR/VR apps for mobile (ARKit/ARCore) or headsets (Quest/Vision Pro) in Unity.
- You need cross-platform reliability without reinventing interaction systems.
- You’re exploring WebXR and want a production-ready structure for input and events.
- You value maintainability and testability over quick hacks.
You might skip if:
- Your project is purely native mobile AR targeting iOS only and demands deep ARKit features that Unity doesn’t expose well (though Unity covers most).
- You need cinematic, high-end graphics and have a dedicated Unreal team; Unreal’s rendering pipeline may suit you better.
- Your app is extremely lightweight and benefits more from a single-file WebXR demo than a structured engine setup.
AR/VR development is a craft. The right patterns reduce complexity and help you focus on what matters: clear interactions, stable performance, and an experience that feels natural. Start with a small, modular prototype, profile early, and iterate with real users in the headset. The medium is young, but the tools are mature enough to ship real value now.




