Creating a navigation mesh with Reality Composer Pro
At a glance
| Item | Summary |
|---|---|
| Purpose | Move a character around a game level by defining its walkable surface and driving motion with a behavior tree. |
| App architecture | A two-file SwiftUI shell. RCPNavigationMeshApp starts the RealityKitScripting runtime and opens an immersive space (visionOS) or a window (other platforms). ContentView loads one exported scene into a RealityView and forwards input to the scene’s script graphs. Gameplay is not written in Swift. |
| Main patterns | No named design pattern. The Swift code only wires the app to the asset; the navigation mesh, behavior tree, and script graphs are Reality Composer Pro data. |
| Project style | One application target backed by a file-system-synchronized RCP-NavigationMesh/ folder, plus one remote Swift package product (RealityKitScripting). The Reality Composer Pro project sits beside the Xcode project, and the app folder holds the exported .reality file. |
| Execution model | The target defaults to MainActor isolation. The only suspension point is try await Entity(named:) inside the RealityView make closure. The source contains no Task, dispatch queue, or Swift System. |
| State/event model | The Swift code has no app state. Taps and drags reach the script graphs through .enableRealityKitScriptingInput(). |
| Key frameworks/packages | SwiftUI, RealityKit, RealityKitScripting imports. The project manifest declares RealityKitScripting as a remote package product. |
Project structure
RCP-NavigationMesh.xcodeproj/ one app target, one remote package product
RCP-NavigationMesh/ synchronized root group for the app target
├── RCPNavigationMeshApp.swift @main App: runtime setup + scene selection
├── ContentView.swift RealityView: load "world", add camera, input
├── rcp-navigationmesh.reality exported scene the app loads at run time
└── Assets.xcassets
rcp-navigationmesh.realitycomposerpro/ authoring project (not in the target)
├── Entities/ world, Player, NavigationTargetMarker
├── Behaviors/ Player_Behavior behavior tree
└── Script Graphs/ TapTerrain, PlayerLogic, PlayerModelLogic, ...
Info.plist visionOS-only scene manifest
Configuration/SampleCode.xcconfig
Structure observations
- The target has a single app product. Its only package product dependency is
RealityKitScripting(RCP-NavigationMesh.xcodeproj/project.pbxproj:90), resolved from a remote repository with an up-to-next-minor requirement starting at 1.4.0 (RCP-NavigationMesh.xcodeproj/project.pbxproj:387). There is no local Swift package and noPackage.swiftin the tree. RCP-NavigationMesh/is aPBXFileSystemSynchronizedRootGroup(RCP-NavigationMesh.xcodeproj/project.pbxproj:23). The exportedrcp-navigationmesh.realityfile sits in that folder next to the Swift files. Inference: that placement is what letsEntity(named: "world")find the scene in the app bundle at run time.- The
rcp-navigationmesh.realitycomposerproauthoring project is not referenced by the Xcode project. Its asset files hold the navigation behavior.Entities/world.tm_entitycontains atm_navigation_mesh_component.Entities/Player.tm_entitycontainstm_navigation_componentandtm_re_behavior_tree_component.Behaviors/Player_Behavior.tm_behavior_treeuses aBTMoveOnNavMeshNodeDefinitionnode. This is a deliberate split: code handles hosting, assets handle gameplay. - The root
Info.plistapplies only to visionOS SDKs (INFOPLIST_FILE[sdk=xros*]). It makes an immersive space the preferred default scene role, with mixed immersion (Info.plist:8,Info.plist:16).
Platform boundary
Apple’s documentation lists iOS, iPadOS, Mac Catalyst, macOS, and visionOS. The project file lists iphoneos, macosx, and xros as supported platforms and sets SUPPORTS_MACCATALYST = NO (RCP-NavigationMesh.xcodeproj/project.pbxproj:185). The downloaded project therefore builds native macOS rather than Mac Catalyst. The front matter keeps Apple’s documented platform list.
Overall architecture
flowchart LR
App["RCPNavigationMeshApp"] -->|init| RT["ScriptingRuntime.setup"]
App -->|visionOS| IS["ImmersiveSpace"]
App -->|other platforms| WG["WindowGroup"]
IS --> CV["ContentView"]
WG --> CV
CV -->|await| Load["Entity(named: world)"]
Load --> Asset["rcp-navigationmesh.reality"]
CV -->|input| Graphs["Script graphs + behavior tree"]
Reference code
RCP-NavigationMesh/RCPNavigationMeshApp.swift:19 — RCPNavigationMeshApp.init and body
@main
struct RCPNavigationMeshApp: App {
// ...
init() {
// The scene doesn't use hand tracking or world sensing, so it skips
// the ARKit input provider.
ScriptingRuntime.setup(inputOptions: .all.subtracting(.ar))
}
var body: some Scene {
#if os(visionOS)
// An immersive space is unbounded, so the level isn't clipped to a box
// and the person wearing the device can walk around it.
ImmersiveSpace {
ContentView()
}
#else
WindowGroup {
ContentView()
}
.defaultSize(width: Self.windowSize, height: Self.windowSize)
#endif
}
}Interpretation
Observed: the app entry point does two things. It configures the scripting runtime synchronously in init, and it picks a scene type at compile time. Both branches host the same ContentView, so platform differences stay in the scene declaration and a single #if !os(visionOS) block in the view.
Inference: running ScriptingRuntime.setup in App.init ensures the runtime is configured before any RealityView loads entities that carry script graphs. The source comment explains only why AR input is excluded, not the ordering, so treat the ordering rationale as inference.
Ownership and state
classDiagram
RCPNavigationMeshApp *-- ContentView : creates in scene body
ContentView --> RealityViewContent : adds entities
RealityViewContent o-- Entity : world (loaded asset)
RealityViewContent o-- PerspectiveCamera : non-visionOS only
Ownership evidence
RCP-NavigationMesh/ContentView.swift:15 — RealityView make closure
struct ContentView: View {
var body: some View {
RealityView { content in
// The Reality Composer Pro project exports a scene named "world".
let world: Entity
do {
world = try await Entity(named: "world")
} catch {
print("Failed to load the scene named \"world\": \(error)")
return
}
content.add(world)
// ...
}
// Sends taps and drags to the script graphs on the scene's entities.
.enableRealityKitScriptingInput()
}
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
RCPNavigationMeshApp |
ContentView |
Constructs the view value inside ImmersiveSpace or WindowGroup |
SwiftUI scene system |
ContentView (make closure) |
world: Entity |
A local constant. Handed to content.add(_:) and not stored by the view |
RealityKit scene graph and the script graphs inside the asset |
ContentView (make closure, non-visionOS) |
PerspectiveCamera |
Created locally, positioned from world.visualBounds, and added to the content (RCP-NavigationMesh/ContentView.swift:32) |
RealityKit scene graph |
RCPNavigationMeshApp |
windowSize |
A static let constant, compiled only for non-visionOS platforms (RCP-NavigationMesh/RCPNavigationMeshApp.swift:16) |
Immutable |
Observed: ContentView has no stored properties, no @State, and no references to entities after the make closure returns. After content.add, the Swift code never mutates the scene. Inference: the entities’ runtime state belongs to the scripting runtime and RealityKit. The source does not show which framework object retains them.
Concurrency, scheduling, and thread safety
Evidence limit: actor isolation, async/await, or Task creation does not by itself prove background-thread execution; Sendable conformance alone does not prove thread-safe mutation.
| Concern | Source mechanism | Verified placement or handoff | Evidence |
|---|---|---|---|
| Main-actor isolation | Build setting SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, with SWIFT_APPROACHABLE_CONCURRENCY = YES and Swift 5 language mode |
Declarations in the app target that carry no annotation default to main-actor isolation. No source file writes @MainActor or MainActor.run explicitly |
RCP-NavigationMesh.xcodeproj/project.pbxproj:187 |
| Main-queue scheduling | None | No DispatchQueue appears in the source |
RCP-NavigationMesh/ContentView.swift:15 |
| Task or worker execution | RealityView async make closure |
The closure suspends at try await Entity(named: "world"). The task that runs the closure is created by RealityView, not by sample code. The source does not establish its executor or any cancellation |
RCP-NavigationMesh/ContentView.swift:19 |
| Synchronous setup | ScriptingRuntime.setup(inputOptions:) in App.init |
A synchronous, non-throwing call made before the scene body is evaluated | RCP-NavigationMesh/RCPNavigationMeshApp.swift:22 |
| Shared-state safety | Not applicable | The Swift code has no shared mutable state to protect | RCP-NavigationMesh/ContentView.swift:12 |
| Per-frame scheduling | Not visible in Swift | The source declares no RealityKit System and no update(context:) method. Per-frame navigation runs inside the RealityKitScripting runtime and the asset’s behavior tree, and the sample source does not show how they are scheduled |
RCP-NavigationMesh/RCPNavigationMeshApp.swift:22 |
Task retention and cancellation: the sample creates no task, so it has nothing to retain or cancel. If loading fails, the closure logs with print and returns before adding anything (RCP-NavigationMesh/ContentView.swift:21).
Reference code
RCP-NavigationMesh.xcodeproj/project.pbxproj:186 — target concurrency settings
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
State propagation, frameworks, and dependencies
Evidence limit: an import proves a source-level compilation dependency at the cited line; it does not prove runtime use, architectural adoption, or whether a Swift package is a direct application dependency.
| Category | Mechanism or module | Verified role | Evidence |
|---|---|---|---|
| Input propagation | .enableRealityKitScriptingInput() view modifier |
Forwards taps and drags on the RealityView to script graphs on the scene’s entities. The source comment states this purpose |
RCP-NavigationMesh/ContentView.swift:40 |
| Swift state propagation | None | No property wrappers, Observation, Combine, or notifications are used | RCP-NavigationMesh/ContentView.swift:12 |
| Framework import | SwiftUI |
Imported in both Swift files | RCP-NavigationMesh/RCPNavigationMeshApp.swift:8 |
| Framework import | RealityKit |
Imported in ContentView.swift, which uses RealityView, Entity, and PerspectiveCamera |
RCP-NavigationMesh/ContentView.swift:9 |
| Framework import | RealityKitScripting |
Imported in both files, which use ScriptingRuntime and .enableRealityKitScriptingInput() |
RCP-NavigationMesh/ContentView.swift:10 |
| Package declaration | XCRemoteSwiftPackageReference for realitykitscripting |
A direct package product dependency of the app target. It is listed in the target’s packageProductDependencies and linked in its Frameworks phase |
RCP-NavigationMesh.xcodeproj/project.pbxproj:399 |
Reference code
RCP-NavigationMesh.xcodeproj/project.pbxproj:387 — remote package requirement
repositoryURL = "https://github.com/apple/realitykitscripting";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 1.4.0;
};
Class and protocol design
The sample declares only two types, both value types that conform to SwiftUI protocols. It declares no local protocols, classes, RealityKit Component types, or System types. A class diagram would not add information.
RCP-NavigationMesh/ContentView.swift:12 — ContentView
struct ContentView: View {
var body: some View {
RealityView { content in
// ...
}
// Sends taps and drags to the script graphs on the scene's entities.
.enableRealityKitScriptingInput()
}
}| Type | Responsibility | Depends on |
|---|---|---|
RCPNavigationMeshApp |
Configures the scripting runtime and chooses the scene type for each platform | App, ScriptingRuntime, ImmersiveSpace/WindowGroup |
ContentView |
Loads the exported scene, adds a camera when there is no device viewpoint, and enables scripting input | View, RealityView, Entity, PerspectiveCamera |
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
RCPNavigationMeshApp |
implicit internal |
Visible across the app module. No public or private modifier appears anywhere in the source |
Inference: a single app target exports no API |
ContentView |
implicit internal |
Visible across the app module, so the App body can construct it |
Inference: same as above |
RCPNavigationMeshApp.windowSize |
implicit internal static let, conditionally compiled |
Exists only when !os(visionOS). The visionOS build has no such symbol |
Inference: an immersive space is unbounded, so a window size would be meaningless there |
Reference code
RCP-NavigationMesh/RCPNavigationMeshApp.swift:14 — conditional constant
@main
struct RCPNavigationMeshApp: App {
#if !os(visionOS)
/// The width and height of the window that holds the scene, in points.
static let windowSize = 800.0
#endif
// ...
}The sample uses conditional compilation rather than access modifiers to separate platform concerns. It has no private or fileprivate members because neither type holds mutable state.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Scripting runtime configuration | RCPNavigationMeshApp.init |
Runs once at app startup, before any scene is built |
| Scene type per platform | RCPNavigationMeshApp.body |
Compile-time #if os(visionOS) selects ImmersiveSpace or WindowGroup |
| Asset loading and error handling | ContentView make closure |
Loads asynchronously, logs on failure, and adds the root entity |
| Framing the camera on non-visionOS platforms | ContentView make closure |
Derives the camera position from world.visualBounds(relativeTo: nil), so the asset’s placement in the Reality Composer Pro project doesn’t matter (RCP-NavigationMesh/ContentView.swift:30) |
| Navigation mesh, pathfinding, tap-to-move behavior | Reality Composer Pro assets (Entities/, Behaviors/, Script Graphs/) |
Authored as data and executed by the scripting runtime. No Swift code is involved |
Design patterns
No named design pattern is supported. The Swift code composes framework types directly. The project does show one structural decision worth studying: gameplay is data-driven and lives outside the code. The Swift layer is a thin host that installs the runtime, loads the asset, and routes input. The navigation component, navigation mesh, and behavior tree are defined in the Reality Composer Pro project. This is described here as a project-structure observation, not as a named pattern (RCP-NavigationMesh/ContentView.swift:40).
Main application flow
The sequence-diagram checks are not all satisfied. The app has one async load, and the order of tap handling inside the scripting runtime is not visible in Swift. The flow is therefore described in prose:
App.initcallsScriptingRuntime.setup(RCP-NavigationMesh/RCPNavigationMeshApp.swift:22).- The scene body builds
ContentViewinside the scene type chosen for the platform. - The
RealityViewmake closure awaitsEntity(named: "world")and adds the result (RCP-NavigationMesh/ContentView.swift:24). - Taps reach the script graphs through
.enableRealityKitScriptingInput(). From there, the asset’sTapTerrainandPlayerLogicscript graphs and thePlayer_Behaviortree move the player. That last step is visible only in asset names, not in Swift.
Naming conventions
- Types:
RCPNavigationMeshAppfollows the<ProductName>Appconvention, with theRCPprefix taken from the target nameRCP-NavigationMesh.ContentViewis the Xcode-template root view name. - Asset identifiers: the Swift code refers to the exported scene by the string
"world", which must match the root entity name in the Reality Composer Pro project (Entities/world.tm_entity). The coupling is a single literal, not a named constant. - Asset folders: Reality Composer Pro groups its assets by role (
Entities,Behaviors,Script Graphs). Script graphs are named<Entity>Logic(PlayerLogic,NavigationTargetMarkerLogic) or after an action (TapTerrain). - Protocols: none declared.
- Files: one primary type per file, and each file is named after its type.
Architecture takeaways
- A thin Swift host can run a full gameplay loop. The navigation mesh, navigation component, and behavior tree are assets, and Swift only installs the runtime and loads the scene.
- Global framework runtimes are configured in
App.init, before anyRealityViewloads content that depends on them. - Platform divergence is pushed to two seams: the scene type in
App.body, and a#if !os(visionOS)camera block in the view. The rest of the code is shared. - Concurrency is minimal and framework-owned.
RealityViewcreates the only async context, and the target’s defaultMainActorisolation covers everything else. - The coupling between code and asset is a single string,
"world". Renaming the root entity in Reality Composer Pro breaks loading at run time, not at compile time.
Source map
| Source file | Relevant symbols |
|---|---|
RCP-NavigationMesh/RCPNavigationMeshApp.swift |
RCPNavigationMeshApp, init, ScriptingRuntime.setup, ImmersiveSpace, WindowGroup, windowSize |
RCP-NavigationMesh/ContentView.swift |
ContentView, RealityView, Entity(named:), PerspectiveCamera, enableRealityKitScriptingInput() |
RCP-NavigationMesh.xcodeproj/project.pbxproj |
App target, synchronized root group, RealityKitScripting package product, concurrency build settings, supported platforms |
Info.plist |
Immersive-space default scene role, mixed immersion style |