17.
ECS & Collaborative Experiences
Written by Chris Language
In the early days of ARKit, it quickly became apparent that something important was missing: the ability to share augmented reality experiences among multiple users.
Later versions of ARKit addressed the issue by introducing ARWorldMap. The map contains a space-mapping state along with a set of anchors from a world-tracking AR session. This map can be shared, allowing multiple users to experience persistent AR anchors within the same space.
With the assistance of a peer-to-peer network, multiple users can share an ARWorldMap in real time, creating a collaborative experience. Using ARKit, the process is somewhat painful, requiring vast amounts of manual labor from a coding perspective.
Apple created a fantastic ARKit example project that you can explore. Find the project here: Creating a Multiuser AR Experience
However, since iOS 13, you’ve been able to pair RealityKit with ARKit to automate most of the manual effort that ARKit-based apps typically require.
In this chapter, you’ll create a modern take on the classic Tic-Tac-Toe game and deliver a RealityKit-based collaborative experience. The new project will borrow from Apple’s ARKit example project, but will mainly focus on the RealityKit side of things.
It’s time to get going!
Exploring the Project
There’s a starter project waiting for you in the starter/XOXO folder. The project is a basic Swift-based app that uses a classic style storyboard UI.
Load the project in Xcode so you can take a quick tour of the important components within.
ViewController.swift
Open ViewController.swift. By now, you’re very familiar with the inner workings of the ViewController.
This is where you’ll place most of your code. You’ll also find pre-allocated stubs within the code which not only keep things organized, but also make placing code easier.
MultipeerController.swift
Open MultipeerController.swift.
This is an exact copy of the file from Apple’s ARKit-based example project. It’s a nice little class that handles all the network management for you. Why reinvent the wheel, right?
Main.storyboard
Open Main.storyboard and flip the orientation to landscape.
It contains an ARView with an area at the top that you’ll use to send messages to the user. You’ll also see three buttons at the bottom of the view. These will let the user choose to be Player1 or Player2 or to clear the game board with the Clear button.
Also, note that these components are already connected to their @IBActions and @IBOutlets, which are located within the ViewController.
Info.plist
Open Info.plist.
This is where you’ll ask for network permissions. Note that Camera Usage Description is already set, so your app will ask for permission to use the camera when it starts.
Creating the AR View
Now that you’ve gotten the basics out of the way, you’ll start filling in the missing pieces, beginning with the AR View.
Setting the Player’s Color
Add the following variable to the Properties section:
var playerColor = UIColor.blue
This property will indicate the player’s color. Player1 will be blue and Player2 will be red.
Next, add the following to player1ButtonPressed(_:):
playerColor = UIColor.blue
This sets the player’s color to blue when the player chooses Player1.
Similarly, add this to player2ButtonPressed(_:):
playerColor = UIColor.red
This sets the player’s color to red when they choose Player2.
Sending Messages
Add the following helper function to Helper Functions:
func sendMessage(_ message: String) {
DispatchQueue.main.async {
self.message.text = message
}
}
With this function in place, you’ll be able to send messages to keep the user informed of all crucial events and states.
Creating the AR Configuration
You start all AR experiences by creating an ARConfiguration and running the AR session. Well, this AR experience is no different.
Add the following call to viewDidAppear() under AR View Functions:
initARView()
This generates an error because the function doesn’t exist yet. Fear not, you’ll fix that next.
Add the following function to AR View Functions:
func initARView() {
arView.session.delegate = self
arView.automaticallyConfigureSession = false
let arConfiguration = ARWorldTrackingConfiguration()
arConfiguration.planeDetection = [.horizontal]
arConfiguration.environmentTexturing = .automatic
arView.session.run(arConfiguration)
}
This ensures that ViewController is the session delegate. It then creates and runs an AR session with a standard ARWorldTrackingConfiguration that detects horizontal planes.
For good measure, do a build and run test, just to make sure everything’s in working order.
The AR session is active and scanning for horizontal surfaces, but nothing much else is happening. There are a few buttons to press, but no feedback yet. For your next step, you’ll actually put something in the scene.
What is ECS?
When using the RealityKit framework to create content for your AR experiences, it’s important to note that the framework runs a CPU-based entity-component system (ECS) to manage physics, animations, audio processing and network synchronization. The framework then relies on Metal for GPU-based multithreaded rendering.
For your first step, take a look at a typical RealityKit-based experience.
There are four main elements you need when dealing with an AR experience based on RealityKit:
-
ARView: This is your window into the world of AR, serving as RealityKit’s entry point. It’s essentially just a view that goes into your app’s view hierarchy.
-
Scene: The scene, which is owned by
ARView, holds all the virtual content of your AR experience. -
ARAnchor: Anchors describe how your AR content relates to the real world. You assign a target to an anchor, and when the app finds an appropriate target, it creates the anchor and attaches it to the real world.
-
Entity: Entities represent the virtual content of an AR experience — its building blocks. Entities consist of Components, which define their behavior. It’s also important to point out that entities can contain other entities, forming a parent-child-like hierarchy.
Predefined Entities
With RealityKit, you can easily create your own custom entities with custom behaviors based on the various components you add to them.
You can also choose from a list of predefined entities:
-
AnchorEntity: An entity with an anchor component. It attaches itself to the real world and automatically tracks its target based on the anchoring type you’ve defined.
-
ModelEntity: Contains geometry, materials, animation and physics components. It’s commonly used to represent the visual parts of your AR experience.
-
PointLight, SpotLight & DirectionalLight: Produce various lighting conditions for virtual content.
-
PerspectiveCamera: Provides a virtual camera that establishes the rendering perspective.
-
TriggerVolume: Defines a 3D shape that detects when other objects enter or exit the defined space.
-
BodyTrackedEntity: Animates a virtual character within an AR scene based on real-time tracking data from a real person.
Creating the Game Board
Now that you have some background, it’s time to create some of your very own entities, starting with the game board.
You’ll construct the game board from scratch using primitive shapes. It will contain:
- A vertical grid bar
- A horizontal grid bar
- Nine tiles
The tiles allow the players to interact with the game board. When the player touches a tile, that tile changes into the player’s chosen color.
Add the following variables to the Properties section:
var gridModelEntityX:ModelEntity?
var gridModelEntityY:ModelEntity?
var tileModelEntity:ModelEntity?
These are the core entities that will construct the entire game board.
Creating the Model Entities
Look carefully at the game board and you can see that the entire board is constructed out of just three distinct shapes: the two grid bars and the square tiles. You’ll create those shapes next.
Add a call to the following function at the bottom of viewDidAppear(_:):
initModelEntities()
This generates an error, which you’ll fix by adding the following function to Model Entity Functions:
func initModelEntities() {
// 1
gridModelEntityX = ModelEntity(
mesh: .generateBox(size: SIMD3(x: 0.3, y: 0.01, z: 0.01)),
materials: [SimpleMaterial(color: .white, isMetallic: false)]
)
// 2
gridModelEntityY = ModelEntity(
mesh: .generateBox(size: SIMD3(x: 0.01, y: 0.01, z: 0.3)),
materials: [SimpleMaterial(color: .white, isMetallic: false)]
)
// 3
tileModelEntity = ModelEntity(
mesh: .generateBox(size: SIMD3(x: 0.07, y: 0.01, z: 0.07)),
materials: [SimpleMaterial(color: .gray, isMetallic: true)]
)
// 4
tileModelEntity!.generateCollisionShapes(recursive: false)
}
Now, take a closer look at the three model entities you’re constructing here:
-
The Tic-Tac-Toe grid consists of two types of grid bars. Here, you define the vertical grid bar with a mesh component generated from a box that measures
(X:30cm, Y:1cm, Z:1cm). It assigns a single white plastic material to the bar. -
This defines the horizontal grid bar with a mesh component generated from a box that measures
(X:1cm, Y:1cm, Z:30cm). It also assigns a single white plastic material to the bar. -
This defines the tile with a mesh component generated from a box that measures
(X:7cm, Y:1cm, Z:7cm). It assigns a single gray metallic material to the tile. -
To interact with elements in the scene, those elements require a collision component. Here, you generate a collision shaped component for the tile model entity by using the mesh component. Now, you’ll be able to hit test against the tiles.
Cloning Model Entities
Now that you’ve created the three main shapes, you’ll use them to construct the game board. Instead of re-creating each element from scratch, you’ll clone the original entities.
Add the following helper function to Model Entity Functions:
func cloneModelEntity(_ modelEntity: ModelEntity,
position: SIMD3<Float>) -> ModelEntity {
let newModelEntity = modelEntity.clone(recursive: false)
newModelEntity.position = position
return newModelEntity
}
This nifty helper function lets you clone an existing ModelEntity with the option to give it a new position.
Adding the Grid
Now, you’re going to use the helper function above to create the grid. Add the following function to Model Entity Functions:
func addGameBoardAnchor(transform: simd_float4x4) {
// 1
let arAnchor = ARAnchor(name: "XOXO Grid", transform: transform)
let anchorEntity = AnchorEntity(anchor: arAnchor)
// 2
anchorEntity.addChild(cloneModelEntity(gridModelEntityY!,
position: SIMD3(x: 0.05, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(gridModelEntityY!,
position: SIMD3(x: -0.05, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(gridModelEntityX!,
position: SIMD3(x: 0.0, y: 0, z: 0.05)))
anchorEntity.addChild(cloneModelEntity(gridModelEntityX!,
position: SIMD3(x: 0.0, y: 0, z: -0.05)))
}
Now, take a closer look:
-
The entire game board is connected to an
AnchorEntitythat forms the root entity of the game board. Here, you create anAnchorEntitywith anARAnchorusing the provided transform value for the anchor’s position. -
Here, the nifty new cloning function creates two vertical bars and two horizontal bars to form the grid for the Tic-Tac-Toe experience. All the entities become children of the root
anchorEntity.
Adding the Tiles
With the grid out of the way, you need to add the tiles to the game board. There are nine slots to fill.
Add the following to the bottom of addGameBoardAnchor(_:):
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: -0.1, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0.1, y: 0, z: -0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: -0.1, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0.1, y: 0, z: 0)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: -0.1, y: 0, z: 0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0, y: 0, z: 0.1)))
anchorEntity.addChild(cloneModelEntity(tileModelEntity!,
position: SIMD3(x: 0.1, y: 0, z: 0.1)))
This follows the same process as before, making cloned copies of the original tile. It places each clone at a different position to fill the 9×9 grid. Also, note that each tile becomes a child of the root anchorEntity.
Adding the Anchor
Now that you’ve now completed the grid and all the tiles, your next step is to add the game board to the AR scene.
Add the following to the bottom of addGameBoardAnchor(_:):
// 1
anchorEntity.anchoring = AnchoringComponent(arAnchor)
// 2
arView.scene.addAnchor(anchorEntity)
// 3
arView.session.add(anchor: arAnchor)
This creates a new game board and places it in the scene. It also anchors the game board to the surface at the provided position.
Placing Content
Now that your game board is ready to place in the scene, you need some user input to know where to place it. All the user needs to do is tap the horizontal surface and the game board should appear in that position. Your next step is to ensure the app recognizes the user’s tap.
Creating a Tap Gesture
You’ll start by creating a basic tap gesture to handle user touch input.
Add the following call to the bottom of viewDidAppear(_:):
initGestures()
This generates an error, but you can easily fix it by adding the following function to Gesture Functions:
func initGestures() {
// 1
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap))
// 2
self.arView.addGestureRecognizer(tap)
}
Now, take a closer look:
- This creates a new tap gesture recognizer, nominating the
ViewControlleras the target and saying thathandleTap()should be called when the user taps the screen. - This adds the newly created gesture to the AR view.
Handling Tap Gestures
But hang, on there’s an error. You still need to define handleTap().
Add the following functions to Gesture Functions:
@objc func handleTap(recognizer: UITapGestureRecognizer?) {
}
Excellent, now you just need to add the code to handle the actual tap.
Getting the Touch Location
After the user taps the screen, you’ll cast a ray into the scene to see where on the surface the tap actually occurred. This lets you position the game board just where they want it.
Add the following to the top of handleTap(recognizer:):
guard let touchLocation =
recognizer?.location(in: self.arView) else { return }
This gets the onscreen touch location from the gesture recognizer.
Tapping a Surface
Now, to perform the actual ray-cast into the AR scene.
Add the following code to bottom of handleTap(recognizer:):
let results = self.arView.raycast(
from: touchLocation,
allowing: .estimatedPlane,
alignment: .horizontal)
if let firstResult = results.first {
self.addGameBoardAnchor(transform: firstResult.worldTransform)
} else {
self.message.text = "[WARNING] No surface detected!"
}
This casts a ray into the scene, looking for the closest horizontal surface. When the ray finds a target, it adds the game board to the scene at the exact location where the ray hits the surface.
Tapping a Tile
OK, now that the game board is visible in the scene, what’s next? Well, when the user touches a tile, that tile should change to the player’s color.
To check if the user tapped a tile, you’ll piggyback on the tap gesture handler.
Add the following code to the top of handleTap(recognizer:), just after the guard statement:
if let hitEntity = self.arView.entity(at: touchLocation) {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
return
}
Instead of using another ray cast, this uses arView.entity(at:) to locate a touched entity in the scene. If it finds a hit, it simply updates the material color to the user’s color.
For arView.entity(at:) to successfully detect contact with entities in the AR scene, the entities must have a collision shape component. If you recall, you did that when you created the tile entity in initModelEntities().
Okay, enough coding for now, do a build and run to test out the current state of affairs.
When you tap the surface, the cool board game gets placed right where you tapped. When you choose Player 1 and then tap a tile, that tile turns blue. When you tap a tile after choosing Player 2, that tile turns red. You’ve essentially built a pass-and-play version of the old classic Tic-Tac-Toe game with a modern twist. Fantastic!
But playing with just one device is no fun. Next, you’ll let each player play on their own device.
Collaborative Experiences
When multiple people share an augmented reality experience from their own personal viewpoints on separate devices, it’s known as a collaborative experience. To achieve such an experience, all the devices should be connected to one another via a local network or Bluetooth. The devices share an AR world map, which localizes each device within the same space. During an active collaborative session, entities in the augmented space synchronize across all the other devices.
Thanks to the power of RealityKit, achieving a collaborative experience is actually easy. The first thing you need to do is to create a multi-peer network between the devices.
Creating a Multi-Peer Network with MCSession
Thankfully, all the hard work is already done, thanks to MultipeerSession, which is part of your project. It acts as a basic wrapper class for MCSession, which is the network session class that connects multiple peers.
The network session can browse for available hosts, and it can also advertise itself as an available host. The browser will search for advertisers. When it finds one, the browser then sends an invitation to join the network advertiser’s network session.
The advertiser then handles the invitation and connects the devices to the same network session. Once it establishes a connection, the network session will manage communication between the connected devices.
Note:
MultipeerSessionoffers various event handlers that you can tap into to control the flow of network session events.
Adding Multi-Peer Connectivity
Now that the network is ready, you’ll create the multi-peer session.
Open ViewController.swift and add the following variables under Properties:
var multipeerSession: MultipeerSession?
var peerSessionIDs = [MCPeerID: String]()
var sessionIDObservation: NSKeyValueObservation?
So what are these variables used for?
-
multipeerSession: Holds the instance of
MultipeerSessionthat you’ll create. - peerSessionIDs: A list of peer IDs (Strings) that will keep track of the connected peers. You’ll maintain this list of IDs manually.
- sessionIDObservation: Uses the observation pattern to monitor your own session ID, in case it changes over time.
Add the following function call to the bottom of viewDidAppear(_:):
initMultipeerSession()
This will generate an error, which you’ll resolve by adding the following code under Multipeer Session Functions:
func initMultipeerSession()
{
multipeerSession = MultipeerSession(
receivedDataHandler: receivedData,
peerJoinedHandler: peerJoined,
peerLeftHandler: peerLeft,
peerDiscoveredHandler: peerDiscovered)
}
func receivedData(_ data: Data, from peer: MCPeerID) {
}
func peerDiscovered(_ peer: MCPeerID) -> Bool {
}
func peerJoined(_ peer: MCPeerID) {
}
func peerLeft(_ peer: MCPeerID) {
}
This creates an instance of MultipeerSession and provides it with event handlers for all possible network session events.
Internally, MultipeerSession will start both a browser and an advertiser. It will operate in both modes, as a host and as a client connecting to other hosts. RealityKit requires this to perform the synchronization.
Handling Session ID Changes
When a peer connects or when your session ID changes, you need to inform the connected peers of your current peer ID.
Add the following helper function to Multipeer Session Functions:
private func sendARSessionIDTo(peers: [MCPeerID]) {
guard let multipeerSession = multipeerSession else { return }
let idString = arView.session.identifier.uuidString
let command = "SessionID:" + idString
if let commandData = command.data(using: .utf8) {
multipeerSession.sendToPeers(commandData,
reliably: true,
peers: peers)
}
}
This nifty helper function lets you send your own session ID to the other connected peers, making sure you keep them up-to-date.
Now, add the following to the top of initMultipeerSession():
sessionIDObservation = observe(\.arView.session.identifier,
options: [.new]) { object, change in
print("Current SessionID: \(change.newValue!)")
guard let multipeerSession = self.multipeerSession else
{ return }
self.sendARSessionIDTo(peers: multipeerSession.connectedPeers)
}
This uses the observer pattern to monitor your current session ID. Should it change, this will ensure the other connected peers are informed of your latest session ID.
You’ll handle the network session events next.
Handling the “Peer” Discovered Event
When the network session discovers a new peer, it triggers peerDiscovered(_:), asking it for permission to allow the new peer to connect.
Add the following to peerDiscovered(_:):
guard let multipeerSession = multipeerSession else
{ return false }
sendMessage("Peer discovered!")
if multipeerSession.connectedPeers.count > 2 {
sendMessage("[WARNING] Max connections reached!")
return false
} else {
return true
}
Here, you build in a restriction on the number of active connected peers allowed at once time. The code above simply checks that the total number of connected peers is under the allowed amount. If so, the peer is allowed to connect; otherwise, it’s rejected and the user gets a message that there are too many connections.
Handling the “Peer Joined” Event
When the peer is allowed to connect, the network session will trigger peerJoined(_:).
Add the following to peerJoined(_:):
sendMessage("Hold phones together...")
sendARSessionIDTo(peers: [peer])
As soon as a peer joins, it’s good time to inform the users to hold their phones close together. It’s also the perfect time to send your own session id to the peer who just joined so that they can also keep track of you in their list of peers.
Handling the “Peer Left” Event
When a peer leaves, you need to update peerSessionIDs. To do this, add the following to peerLeft(_:):
sendMessage("Peer left!")
peerSessionIDs.removeValue(forKey: peer)
This removes the peer from peerSessionIDs, maintaining the list at all times.
Configuring RealityKit for Collaboration
Well, that’s all you need to do to create a multi-peer network, but you’re not quite done yet. You still need to configure RealityKit for collaboration.
Enabling Collaboration
To use collaboration, you need to enable it when you create the AR configuration. Do this by adding the following line of code to initARView(), just before running the AR session:
arConfiguration.isCollaborationEnabled = true
Enabling collaboration will start sharing collaboration data with connected peers. Collaboration data contains information about detected surfaces, device positions and added anchors.
Setting the Synchronization Service
When you use RealityKit, you have to synchronize all of its entities and their components with all the connected peers.
Open MultipeerSession.swift and add the following extension to the bottom of the file:
extension MultipeerSession {
public var multipeerConnectivityService:
MultipeerConnectivityService? {
return try? MultipeerConnectivityService(
session: self.session)
}
}
Look at MultipeerSession and you’ll notice that that actual session instance, MCSession, is kept private. This extension allows you to create a multi-peer connectivity session, while still keeping the session private.
Next, back in ViewController.swift, add the following to the bottom of initMultipeerSession():
// 1
guard let multipeerConnectivityService =
multipeerSession!.multipeerConnectivityService else {
fatalError("[FATAL ERROR] Unable to create Sync Service!")
}
// 2
arView.scene.synchronizationService = multipeerConnectivityService
self.message.text = "Waiting for peers..."
Take a closer look:
-
With the extension function in place, this makes sure that you get a valid multi-peer session service from
MultipeerSession. -
This registers the synchronization service. Now, RealityKit will keep all the
Codableobjects in sync. This includes entities along with all their components.
Handling a Successful Connection
Now that everything’s in place, once a new peer successfully joins, RealityKit will create an ARParticipationAnchor for that peer.
Add the following function to Multipeer Session Functions:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let participantAnchor = anchor as? ARParticipantAnchor {
self.message.text = "Peer connected!"
let anchorEntity = AnchorEntity(anchor: participantAnchor)
arView.scene.addAnchor(anchorEntity)
}
}
}
Here, you use session(_:didAdd) — which is part of the ARSessionDelegate protocol — to check if a newly-added anchor is an ARParticipationAnchor. If it is, a peer has just successfully connected and an active collaborative experience is in progress. Excellent!
Requesting Network Permissions
Oh, you’re not quite done yet. There’s one last thing that you have to do and that’s to request network permissions.
Open Info.plist and add the following to it:
- Privacy — Local Network Usage Description: Set its value to something descriptive like: This app requires access to the network for Collaboration.
- Bonjour services: Add two sub-items to it. Set item 0 to _ar-collab._tcp and set item 1 to _ar-collab._udp.
The result should look like this:
The value ar-collab is a hard-coded string value defined within MultipeerSession that’s used when creating the browser and advertiser services.
Bonjour services gives permission to use that specific service type name.
Note: Your app will crash if you’ve failed to request network permission. Continue with caution!
Time to build, run and test out your collaborative experience.
The app starts and now asks for network permission. Nothing else has changed, except for the message at the top stating that it’s Waiting for peers…. Oh, of course — this is supposed to be a collaborative experience! :]
Before grabbing a friend, there’s one final thing you have to set up for the entire experience to function as intended.
Managing Ownership
During a collaborative experience, when you create an entity, you become the owner of that entity. Should another peer attempt to modify an entity that belongs to you, they’ll be blocked.
This is a great locking mechanism to control who’s allowed to modify entities within the AR scene. Without these controls, you’d have utter chaos.
However, this means you need to add ownership management to your app.
Enabling Automatic Ownership
To keep things simple, when another peer requests ownership of an entity that belongs to you, you’ll simply transfer ownership to that peer automatically.
Add the following line of code to the bottom of addGameBoardAnchor(_:), just before adding the anchorEntity to the scene:
anchorEntity.synchronization?.ownershipTransferMode = .autoAccept
This simply sets ownershipTransferMode to automatically accept ownership requests. Now, when a peer interacts with a tile, they first need to request ownership of that tile before trying to change its color.
Requesting Ownership
Now, you need to make sure you request ownership when you tap on a tile.
In handleTap(recognizer:), replace the previous entire hitEntity code block with this new one:
if let hitEntity = self.arView.entity(at: touchLocation) {
if hitEntity.isOwner {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
} else {
hitEntity.requestOwnership { result in
if result == .granted {
let modelEntity = hitEntity as! ModelEntity
modelEntity.model?.materials = [
SimpleMaterial(color: self.playerColor,
isMetallic: true)]
}
}
}
return
}
Previously, you simply modified the tile color. This time around, you first check to see if you’re the owner of the tile. If you are, no worries, you can change the tile color. If not, you first have to request ownership. Once granted, ownership of the tile now belongs to you and you can change the tile color.
Removing Anchors
As a final touch, when you’ve played a few games with a friend, it would be nice to clear out the playing field so that you can play some more.
Add the following helper function to Helper Functions:
func removeAnchors() {
guard let frame = arView.session.currentFrame else { return }
for anchor in frame.anchors {
arView.session.remove(anchor: anchor)
}
sendMessage("All anchors removed!")
}
Then add a call to it in clearButtonPressed_:):
removeAnchors()
And now do one final build and run. This time around, make sure that the app’s installed on more than one device.
The sequence of events should flow as follows:
-
Device A starts and states that it’s Waiting for peers….
-
Device B starts and also states that it’s Waiting for peers….
-
When the two devices come into close proximity to one another, both will indicate that they’ve Discovered a peer. At this point, hold the two devices close together to establish a collaborative session.
-
If all goes well, the collaborative session begins and both devices indicate that a Peer connected!
-
Device A chooses to be Player 1 and Device B chooses to be Player 2.
-
Either device can now add a game board to the scene by tapping a flat surface. The players can now take turns and play the ultimate game of Tic-Tac-Toe.
-
When the game board gets messy, simply Clear the space and place a new game board into the scene.
Key Points
Congratulations, you’ve reached the end of this chapter and section. You can find a copy of the project in its final state under final/XOXO.
Do a quick rewind and see what you learned:
- ECS: You learned about entity-component systems and how to create your very own entities, each with their own set of components that define the behavior of the entity.
- Predefined Entities: You learned that RealityKit comes with a whole collection of predefined entities that are commonly used within RealityKit-based AR experiences.
- Custom Entities: You also created your own custom entities and controlled which components you added to them.
- Cloning Entities: Cloning an entity is super simple and lightens a repetitive workload.
- Collaborative Experiences: Creating a collaborative experience for RealityKit is straightforward. All you need is an active peer-to-peer network session. Then you just configure a few basic settings and you’re up and running.
-
Synchronization: RealityKit will automatically synchronize all
Codableobjects for you over the network. This includes all entities with their components. - Ownership: When dealing with entities in a collaborative session, you have to request ownership of that entity before you can modify it. Luckily, there are a few settings to enable that make this a breeze.
Well done, grasshopper, you’ve earned a well-deserved break. Share your app with all your friends and enjoy some competitive Tic-Tac-Toe. See you on the flip side! :]