Building an immersive experience with RealityKit
At a glance
| Item | Summary |
|---|---|
| Purpose | Use systems and postprocessing effects to create a realistic underwater scene. |
| App architecture | The UIKit GameViewController owns the AR experience, GameManager coordinates gameplay, and custom components, InputSystem, and MetalRenderer create the underwater simulation and postprocessing path. |
| Main patterns | UIKit lifecycle composition, UI–RealityKit bridge, Session owner boundary, Protocol capability boundary, Delegate callback adapter, Underwater postprocess pipeline |
| Project style | Code-rich sample with 59 scanned C/Objective-C header, Metal, Swift file(s) and 7988 implementation line(s). |
Project structure
Source bundle/
└── BugBreakAR/
├── GameViewController.swift # GameViewController
├── Rendering/
│ └── MetalRenderer.swift # MetalRenderer
├── Components/
│ ├── EntitySwitcherComponent.swift # EntitySwitcherAnimationType, EntitySwitcherComponent, ChildEntityInfo
│ ├── AudioComponent.swift # AudioComponent, HasAudioComponent
│ └── InteractionComponent.swift # InteractionComponent, InteractionComponentDelegate, HasInteraction
├── AppDelegate.swift # AppDelegate
└── GameManager.swift # GameManager, GameState
Structure observations
- The runtime boundary is UIApplicationDelegate + UIViewController; the tree is pruned to lifecycle, state, framework, rendering, and ECS files.
- The source is C/Objective-C header, Metal, Swift; role-named types and feature folders separate the major responsibilities.
- This archive extracts to the same source-tree SHA-256 as
Creating a game with scene understanding; this document focuses on underwater systems and postprocessing.
Overall architecture
flowchart LR
AppDelegate_1["AppDelegate"]
UIApplicationDelegate___UIViewController_2["UIApplicationDelegate + UIViewController"]
GameViewController_3["GameViewController"]
GameManager_4["GameManager"]
MetalRenderer_5["MetalRenderer"]
RealityKit_entity_graph_6["RealityKit entity graph"]
RealityKit_gameplay___Metal_postprocess_7["RealityKit gameplay + Metal postprocess"]
AppDelegate_1 --> UIApplicationDelegate___UIViewController_2
UIApplicationDelegate___UIViewController_2 --> GameViewController_3
GameViewController_3 --> GameManager_4
GameManager_4 --> MetalRenderer_5
MetalRenderer_5 --> RealityKit_entity_graph_6
RealityKit_entity_graph_6 --> RealityKit_gameplay___Metal_postprocess_7
Reference code
BugBreakAR/AppDelegate.swift:13 — executable or app-lifecycle anchor.
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It separates lifecycle, presentation, stable state, framework/session work, and the RealityKit entity graph.
Ownership and state
classDiagram
GameManager o-- GameViewController : viewController
GameManager *-- AnchorEntity : creaturesAnchor
GameManager --> AnchorEntity : audioAnchor
GameManager --> InputSystem : inputSystemInstance
Ownership evidence
BugBreakAR/GameManager.swift:20 — representative stored state or nearest verified lifecycle anchor.
weak var viewController: GameViewController?
var inputSystemInstance: InputSystem?
var voxels: Voxels?
weak var assets: GameAssets?
// Game state related
var currentState = GameState.applicationLaunched
var assetsLoaded = false| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
GameManager |
GameViewController as viewController |
receives a shared or non-owning reference | The upstream owner controls lifetime; this scope may invoke the exposed mutable API |
GameManager |
AnchorEntity as creaturesAnchor |
creates and retains | The owning type coordinates writes |
GameManager |
AnchorEntity as audioAnchor |
stores and coordinates | The owning type coordinates writes |
GameManager |
InputSystem as inputSystemInstance |
stores and coordinates | The owning type coordinates writes |
Ownership is intentionally narrow: environment, bindings, observed objects, weak links, and delegate callbacks do not prove lifetime ownership. Initialized stored services and wrapper-managed state do; entities added inside a RealityKit content closure belong to that entity graph.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
AppDelegate (BugBreakAR/AppDelegate.swift:13) |
Declares app lifecycle and top-level dependency lifetime. | UIResponder, UIApplicationDelegate |
GameViewController (BugBreakAR/GameViewController.swift:14) |
Coordinates long-lived framework or cross-view work. | UIViewController, UIGestureRecognizerDelegate |
GameManager (BugBreakAR/GameManager.swift:11) |
Coordinates long-lived framework or cross-view work. | Concrete framework collaborators |
MetalRenderer (BugBreakAR/Rendering/MetalRenderer.swift:12) |
Wraps a framework, input, rendering, or session capability. | Concrete framework collaborators |
HasAudioComponent (BugBreakAR/Components/AudioComponent.swift:16) |
Declares a replaceable capability or collaboration contract. | Concrete framework collaborators |
HasEntitySwitcher (BugBreakAR/Components/EntitySwitcherComponent.swift:66) |
Declares a replaceable capability or collaboration contract. | Concrete framework collaborators |
EntitySwitcherComponentDelegate (BugBreakAR/Components/EntitySwitcherComponent.swift:68) |
Declares a replaceable capability or collaboration contract. | AnyObject |
InteractionComponentDelegate (BugBreakAR/Components/InteractionComponent.swift:25) |
Declares a replaceable capability or collaboration contract. | AnyObject |
Verified protocol-oriented seams: DebugSettingBool → DebugSettingProtocol, DebugSettingInt → DebugSettingProtocol, DebugSettingFloat → DebugSettingProtocol, DebugSettingEnum → DebugSettingProtocol, CreatureEntity → HasEntitySwitcher, CreatureEntity → EntitySwitcherComponentDelegate. Framework conformances remain adapters, not proof of an app-wide protocol architecture.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
private let log = OSLog(subsystem: appSubsystem, category: "EntitySwitc... (BugBreakAR/Components/EntitySwitcherComponent.swift:15) |
private |
Use stays inside the declaration and same-file extensions permitted by Swift. | Inference: Hide lifecycle-sensitive state or an implementation step. |
fileprivate var childEntityInfoList = [ChildEntityInfo]() (BugBreakAR/Components/EntitySwitcherComponent.swift:48) |
fileprivate |
Use is limited to declarations in this source file. | Inference: Share with same-file helpers without making the symbol module-wide. |
public var audioComponent: AudioComponent { (BugBreakAR/Components/AudioComponent.swift:19) |
public |
The symbol is available to importing modules, subject to its containing type’s visibility. | Inference: Expose an intentional package or cross-target surface. |
internal func configurePathfinding() { (BugBreakAR/CreatureEntity+Pathfinding.swift:12) |
internal |
With no narrower modifier, Swift keeps the declaration internal to the module. | Inference: Allow app-target collaboration without exporting a library API. |
No reviewed Swift access example uses private(set), open; declarations without a modifier are internal.
Objective-C/C header and implementation visibility is documented separately from Swift lexical access control.
Reference code
BugBreakAR/Components/EntitySwitcherComponent.swift:15 — representative visibility boundary.
private let log = OSLog(subsystem: appSubsystem, category: "EntitySwitchComponent")Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Application/command lifecycle | AppDelegate |
The executable boundary determines app, controller, scene, or command lifetime. |
| Presentation and user intent | GameViewController |
The view/controller forwards input and owns only presentation-local work. |
| Shared feature state and commands | GameManager |
A stable owner prevents view recomputation or callbacks from duplicating transitions. |
| Framework/session/render coordination | MetalRenderer |
The role-named collaborator isolates long-lived or per-frame work from presentation code. |
| GPU and low-level resource work | BugBreakAR/Rendering/MetalRenderer.swift:12 |
Buffer, texture, shader, or frame-resource mutation stays in a rendering/compute boundary. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| UIKit lifecycle composition | BugBreakAR/AppDelegate.swift:13 |
Uses the application delegate and view controller as the explicit lifecycle and scene-ownership boundary. |
| UI–RealityKit bridge | BugBreakAR/MainMenuViewController.swift:24 |
Builds or hosts the RealityKit entity graph from SwiftUI or a platform view/controller lifecycle. |
| Session owner boundary | BugBreakAR/GameViewController+ARSessionDelegate.swift:12 |
Keeps authorization, session lifetime, and framework callbacks in a stable model, manager, or controller. |
| Protocol capability boundary | BugBreakAR/DebugSettings/DebugSettings.swift:27 |
Defines a local capability with concrete conformers; this is the verified protocol-oriented seam. |
| Delegate callback adapter | BugBreakAR/AppDelegate.swift:13 |
Inverts imperative framework callbacks into the sample’s owning controller, coordinator, or model. |
| Underwater postprocess pipeline | BugBreakAR/Rendering/MetalRenderer.swift:12 |
Keeps the underwater frame effect in a dedicated renderer while the game manager owns gameplay state. |
Naming conventions
- Role suffixes are evidence, not decoration: Model:
MetalModel; Controller:DebugSettingsViewController,GameViewController,MainMenuViewController; Manager:GameManager; Renderer:MetalRenderer; System:InputSystem. - ECS names pair data and behavior: components
AudioComponent,CustomCameraComponent,EntitySwitcherComponent,InteractionComponent,PathfindingComponent; systems none. - Protocols:
HasAudioComponent,HasEntitySwitcher,EntitySwitcherComponentDelegate,InteractionComponentDelegate,HasInteraction,PathfindingComponentDelegate. - Commands use verb-led methods:
viewWillAppear,viewDidAppear,prepare,configureView,resetTracking,setupIBL,gestureRecognizer,nearbyFaceWithClassification. - Files and folders use primary-type or responsibility names such as
Views,Models,Rendering,Components,Systems,Packages, or feature names where those boundaries exist.
Architecture takeaways
- Treat
AppDelegateas the owner of executable or scene lifecycle, not automatically as the owner of every entity created later. - Keep view/controller input near presentation, but move sessions, reconstruction, capture, rendering, shared game state, or documents into stable owners when their lifetime exceeds one callback or render pass.
- A shared source tree can support different documentation questions; keep each page focused on its specific architecture question rather than duplicating generic analysis.
Source map
| Source file | Relevant symbols |
|---|---|
BugBreakAR/GameViewController.swift:14 |
GameViewController |
BugBreakAR/Rendering/MetalRenderer.swift:12 |
MetalRenderer |
BugBreakAR/Components/EntitySwitcherComponent.swift:18 |
EntitySwitcherAnimationType, EntitySwitcherComponent, ChildEntityInfo, HasEntitySwitcher, EntitySwitcherComponentDelegate |
BugBreakAR/AppDelegate.swift:13 |
AppDelegate |
BugBreakAR/GameManager.swift:11 |
GameManager, GameState |
BugBreakAR/Components/AudioComponent.swift:12 |
AudioComponent, HasAudioComponent |
BugBreakAR/Components/InteractionComponent.swift:12 |
InteractionComponent, InteractionComponentDelegate, HasInteraction |