Search code examples
swift4arkitcodableios12decodable

AR Multi User Projectiles (Swift4)


I'm currently sharing an AR world map across a multiplier session successfully, and I am trying to send individual projectile data (position, angular, velocity, type) across the network from both peer clients but I'm not quite sure how to serialize these data types SCNVector3 using Codable, nor can I find any documentation on doing so.. which is why I'm here. I need to be able to unarchive the data with the following method

let shouldSend = MovementData(velocity: data.velocity, angular: Float(data.angular), position: posStr , x:Float(data.x), y:Float(data.y),z:Float(data.z),type:"Striker")
        guard let sendData = try? NSKeyedArchiver.archivedData(withRootObject: shouldSend, requiringSecureCoding: true)
            else { fatalError("can't encode movementData") }

I am attempting to use the following to encode the data

import SceneKit

open class MovementData: NSObject, NSSecureCoding {
   public static var supportsSecureCoding: Bool = true

var velocity = CGPoint.zero,
angular = Float(0),
position:String = "SCNVector3(x:0,y:0.5,z:0)",
orientation = SCNVector3(),
x = Float(0),
y = Float(0),
z = Float(0),
type = String()

enum Key:String {
    case velocity = "velocity"
    case angular = "angular"
    case position = "position"
    case x = "x"
    case y = "y"
    case z = "z"
    case type = "type"
}

public func encode(with aCoder: NSCoder) {
    aCoder.encode(velocity as CGPoint, forKey: Key.velocity.rawValue)
    aCoder.encode(angular as Float, forKey: Key.angular.rawValue)
     aCoder.encode(position as String, forKey: Key.position.rawValue)
    aCoder.encode(x as Float, forKey: Key.x.rawValue)
     aCoder.encode(y as Float, forKey: Key.y.rawValue)
     aCoder.encode(z as Float, forKey: Key.z.rawValue)
    aCoder.encode(type as String, forKey: Key.type.rawValue)


}

public convenience required init?(coder aDecoder: NSCoder) {
    let _velocity = aDecoder.decodeCGPoint(forKey: Key.velocity.rawValue)
    let _angular = aDecoder.decodeFloat(forKey: Key.angular.rawValue)
    let _position = aDecoder.decodeObject(forKey: Key.position.rawValue)
       let _x = aDecoder.decodeFloat(forKey: Key.x.rawValue)
        let _y = aDecoder.decodeFloat(forKey: Key.y.rawValue)
        let _z = aDecoder.decodeFloat(forKey: Key.z.rawValue)
    let _type = aDecoder.decodeObject(forKey: Key.type.rawValue)



    self.init(velocity: _velocity, angular: _angular, position: _position as! String, x:_x,y:_y,z:_z,type:_type as! String)
}

init(velocity: CGPoint, angular: Float, position:String, x:Float,y:Float,z:Float,type:String) {
    self.velocity = velocity
    self.angular = angular
    self.position = position
    self.x = x
    self.y = y
    self.z = z
    self.type = type
}}

I am using the following model

//
//  Striker.swift


import Foundation
import SceneKit

public struct StrikerData: CustomStringConvertible {
      var velocity = CGPoint.zero,
 angular = CGFloat(0),
  //    position = SCNVector3()
x = CGFloat(0),
y = CGFloat(0.5),
z = CGFloat(0)
mutating func reset() {
    velocity = CGPoint.zero
    angular = 0
    x = 0
    y = 0.5
    z = 0
}

   public var description: String { 
      return "StrikerData(velocity: \(velocity), angular: \(angular),         position:  SCNVector3(x:\(x),y:\(y),z:\(z)))"
    }
}
struct Striker {
    var name: String
    var x: String
    var y: String
    var z: String
    var orientation: String
    var velocity:String
    init(name: String, orientation: String, velocity: String,x: String,     y: String,z:String) {
         self.name = name
        self.orientation = orientation
       self.velocity = velocity
       self.x = x
        self.y = y
        self.z = z
    }
  }



//MARK: - OpponentStriker
open class OpponentStriker:  SCNView {
var striker: Striker!

var trackingHandler: ((StrikerData) -> ())?
var beginHandler: (() -> Void)?
var stopHandler: (() -> Void)?
var substrate: OpponentStriker!

private var tracking = false
private(set) var data = StrikerData()






required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

@objc func listen() {

    if tracking {
        trackingHandler?(data)
    }
}

//MARK: - Overrides
    open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    if let touch = touches.first{

        let striker = touch.location(in: self)
        tracking = true
        beginHandler?()

    }
}

    open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

    for touch: AnyObject in touches {
        let location = touch.location(in: self)

        guard tracking else {
            return
        }

      print("Stopped here FIX THIS!!!!!!")
    }
}

    open override func touchesEnded(_ touches: Set<UITouch>, with event:     UIEvent?) {
    resetStriker()
}

    open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    resetStriker()
}

// CustomStringConvertible protocol
open override var description: String {
    return "OpponentStriker(data: \(data))"
    print("OpponentStriker(data:\(data)")
}

// private methods
private func resetStriker() {
    tracking = false
        let moveToBack = SCNAction.move(to: SCNVector3(x:0, y:1, z: 0),     duration: TimeInterval(0.1))
    moveToBack.timingMode = .easeOut

    data.reset()
    stopHandler?();
    }
}

typealias 🕹 = OpponentStriker

I then send the data from the view controller when creating the node with the following function :

    guard let tableScene = SCNScene(named: "art.scnassets/table.scn"),
        let strikerOne = tableScene.rootNode.childNode(withName:    "Striker", recursively: false)
        //            let strikerTwo =      tableScene.rootNode.childNode(withName: "Striker", recursively: false)
        else { print("Cant find strikerOne node")
            return
    }
    let nodeScale = SCNVector3(0.5,1.25,0.75)
    strikerOne.position = SCNVector3(x,y,z)
    strikerOne.physicsBody = SCNPhysicsBody(type: .kinematic , shape:    SCNPhysicsShape(node: strikerOne,  options:  [SCNPhysicsShape.Option.scale:   nodeScale]))
        strikerOne.physicsBody?.categoryBitMask =    BitMaskCategory.striker.rawValue
        strikerOne.physicsBody?.contactTestBitMask = BitMaskCategory.puck.rawValue
        self.strikerOneNode = strikerOne
        if player == "One" {
        self.selectedNode = strikerOne
        }

        self.sceneView.scene.rootNode.addChildNode(strikerOne)
    let mat4 = strikerOne.transform

    let f4x4 = simd_float4x4(mat4)

    let anchor = ARAnchor(name: "StrikerOne", transform: f4x4 )
    sceneView.session.add(anchor: anchor)

        // Send the anchor info to peers, so they can place the same    content.
        guard let data = try?    NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
            else { fatalError("can't encode anchor") }
        self.multipeerSession.sendToAllPeers(data)
        print("Striker One added")
        //        guard let data = try? NSKeyedArchiver.archivedData(withRootObject: strikerOne, requiringSecureCoding: true)
      self.sceneView.scene.rootNode.addChildNode(strikerTwo)

and I am trying to deserialize the data using NSKeyedUnarchiver

 if let movement = try  NSKeyedUnarchiver.unarchivedObject(ofClass:MovementData.self, from: data) {
                     print(" movement DATA FROM \(peer) with data  \(data)")
                    print(movement)


        }

The error I receive is :

can't decode data recieved from <MCPeerID: 0x28294b120 DisplayName = Montreaux’s iPhone>

I am using the following Demo/Readme for creating an AR Multi user experience:

Apple ARKit MultiUser Demo

All assistance is very much appreciated,

Cheers!


Solution

  • If you are using the example provided by Apple, I was able to get your code work by doing the following.

    Firstly when both devices where connected, I used the following function to send some test data:

    /// Send A Movement Data Object To Our Peers
    @IBAction func pressToSend(){
    
        let shouldSend = MovementData(velocity: CGPoint.zero,
                                      angular: Float(10),
                                      position: "StackOverflow",
                                      x:Float(10), y:Float(20),z:Float(30),
                                      type:"BlackMirrorz")
    
        guard let sendData = try? NSKeyedArchiver.archivedData(withRootObject: shouldSend, requiringSecureCoding: true) else { fatalError("can't encode movementData") }
    
        cloudSession.sendDataToUsers(sendData)
    }
    

    With the cloudSession sendDataToUsers(_ data: Data) function being called in the Multipeer Class:

    //--------------------------
    // MARK: - MCSessionDelegate
    //--------------------------
    
    extension ARCloudShare: MCSessionDelegate {
    
        func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { }
    
        func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
            receivedDataHandler(data, peerID)
        }
    
        func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
            fatalError("This Service Does Not Send Or Receive Streams")
        }
    
        func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
            fatalError("This Service Does Not Send Or Receive Resources")
        }
    
        func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
            fatalError("This Service Does Not Send Or Receive Resources")
        }
    
    }
    
    //---------------------------------------
    // MARK: - MCNearbyServiceBrowserDelegate
    //---------------------------------------
    
    extension ARCloudShare: MCNearbyServiceBrowserDelegate {
    
        public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
    
            //Invite A New User To The Session
            browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
        }
    
        public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { }
    
    }
    
    //------------------------------------------
    // MARK: - MCNearbyServiceAdvertiserDelegate
    //------------------------------------------
    
    extension ARCloudShare: MCNearbyServiceAdvertiserDelegate {
    
        //----------------------------------------------------------
        // MARK: - Allows The User To Accept The Invitation To Share
        //----------------------------------------------------------
    
        func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
    
            //Allow The User To Accept The Invitation & Join The Twunkl Session
            invitationHandler(true, self.session)
        }
    
    }
    
    class ARCloudShare: NSObject{
    
        static let serviceType = "arcloud-share"
    
        let myPeerID = MCPeerID(displayName: UIDevice.current.name)
        var session: MCSession!
        var serviceAdvertiser: MCNearbyServiceAdvertiser!
        var serviceBrowser: MCNearbyServiceBrowser!
        let receivedDataHandler: (Data, MCPeerID) -> Void
    
        //-----------------------
        // MARK: - Initialization
        //-----------------------
    
        init(receivedDataHandler: @escaping (Data, MCPeerID) -> Void ) {
    
            self.receivedDataHandler = receivedDataHandler
    
            super.init()
    
            session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
            session.delegate = self
    
            serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: ARCloudShare.serviceType)
            serviceAdvertiser.delegate = self
            serviceAdvertiser.startAdvertisingPeer()
    
            serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: ARCloudShare.serviceType)
            serviceBrowser.delegate = self
            serviceBrowser.startBrowsingForPeers()
        }
    
        //---------------------
        // MARK: - Data Sending
        //---------------------
    
        func sendDataToUsers(_ data: Data) {
            do {
                try session.send(data, toPeers: session.connectedPeers, with: .reliable)
            } catch {
                print("Error Sending Data To Users: \(error.localizedDescription)")
            }
        }
    
        //----------------------
        // MARK: - Peer Tracking
        //----------------------
    
        var connectedPeers: [MCPeerID] { return session.connectedPeers }
    }
    

    Then in our main ViewController handling the data like so:

    //----------------------
    // MARK: - Data Handling
    //----------------------
    
    /// Handles The Data Received From Our ARMultipeer Session
    ///
    /// - Parameters:
    ///   - data: Data
    ///   - peer: MCPeerID
    func receivedData(_ data: Data, from peer: MCPeerID) {
    
        if let unarchivedData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data){
    
        if unarchivedData is MovementData, let data = unarchivedData as? MovementData{
    
                print(data.orientation)
                print(data.position)
    
            }
    
            else {
                print("Unknown Data Recieved From = \(peer)")
    
            }
        }
    
    }
    

    Which yields something like this:

    enter image description here

    An example of sending multiple data types can be seen here: ARWorldMaps

    Hope it points you in the right direction...