Статьи

Использование iPhone в качестве 3D-мыши с многопользовательским подключением в Swift

Мой недавний эксперимент с  CoreMotionCoreMotion Controlled 3D Sketching на iPhone с Swift , заставил меня задуматься о том, можно ли использовать iPhone в качестве 3D-мыши для управления другим приложением на отдельном устройстве. Оказывается, с платформой Apple  Multipeer Connectivity  это не только возможно, но и довольно круто!

Структура Multipeer Connectivity обеспечивает одноранговую связь между устройствами iOS через Wi-Fi и Bluetooth. Помимо предоставления устройствам возможности отправлять отдельные пакеты информации, он также поддерживает потоковую передачу, и это то, что мне нужно, чтобы мой iPhone передавал непрерывный поток данных, описывающих его положение (поворот, наклон и рыскание) в трехмерном пространстве.

Я не буду вдаваться в подробности фреймворка, они прекрасно объясняются в трех основных статьях, которые я использовал, чтобы научить меня быстро:

Моя единая кодовая база выполняет работу как приложения iPad «Вращающийся куб», которое отображает куб, плавающий в пространстве, так и приложения iPhone «3D Mouse», которое контролирует трехмерное вращение куба. Поскольку это скорее проект для проверки концепции, а не кусок производственного кода, все в  одном контроллере представления , это не очень хорошая архитектура, но при быстром переключении между двумя «режимами» это было очень быстро работать в.

IPad «Вращающееся приложение Cube»

Приложения, использующие Multipeer Connectivity, могут либо рекламировать сервис, либо искать сервис. В моем проекте  приложение Rotating Cube  берет на себя роль рекламодателя, поэтому мой контроллер представления реализует протокол MCNearbyServiceAdvertiserDelegate. После того, как я начинаю рекламу:

    func initialiseAdvertising()
    {
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)

        serviceAdvertiser.delegate = self
        serviceAdvertiser.startAdvertisingPeer()

    }

… метод advertiser протокола () вызывается, когда он получает приглашение от партнера. Я хочу автоматически принять это:

    func advertiser(advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: NSData?, invitationHandler: (Bool, MCSession) -> Void)
    {
        invitationHandler(true, self.session)

    }

IPhone «3D Mouse App»

Поскольку  приложение Rotating Cube  является рекламодателем, мое  приложение 3D Mouse  — это браузер. Таким образом, мой монолитный контроллер представления также реализует MCNearbyServiceBrowserDelegate и, как и рекламодатель, начинает просмотр:

    func initialiseBrowsing()
    {
        serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)

        serviceBrowser.delegate = self
        serviceBrowser.startBrowsingForPeers()

    }

… и как только он найдет одноранговый узел, он отправляет приглашение, которое мы видели выше, присоединиться к сеансу:

    func browser(browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?)
    {
        streamTargetPeer = peerID

        browser.invitePeer(peerID, toSession: session, withContext: nil, timeout: 120)

        displayLink = CADisplayLink(target: self, selector: Selector("step"))
        displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

    }

Здесь я также создаю экземпляр CADisplayLink для вызова метода step () с каждым кадром. step () делает две вещи: он использует streamTargetPeer, который я определил выше, чтобы попытаться запустить потоковую сессию …

        outputStream =  try session.startStreamWithName("MotionControlStream", toPeer: streamTargetPeer)

        outputStream?.scheduleInRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

        outputStream?.open()

… и, если этот сеанс потоковой передачи доступен, отправляет отношение iPhone в трехмерном пространстве (полученное с помощью  CoreMotion ) через поток:

    if let attitude = attitude where outputStream.hasSpaceAvailable
    {
        self.label.text = "stream: \(attitude.roll.radiansToDegrees()) | \(attitude.pitch.radiansToDegrees()) | \(attitude.yaw.radiansToDegrees())"

        outputStream.write(attitude.toBytes(), maxLength: 12)

    }

Сериализация и десериализация значений с плавающей запятой

Структура att (типа MotionControllerAttitude) содержит три значения с плавающей запятой для roll, pitch и yaw, но поток поддерживает только байты UInt8. Чтобы сериализовать и десериализовать эти данные, я нашел эти две функции  Rintaro  в StackOverflow,  которые принимают любой тип и преобразуют его в массивы UInt8 и из них:

    func fromByteArray(value: [UInt8], _: T.Type) -> T {
        return value.withUnsafeBufferPointer {
            return UnsafePointer<T>($0.baseAddress).memory
        }
    }

    func toByteArray(var value: T) -> [UInt8] {
        return withUnsafePointer(&value) {
            Array(UnsafeBufferPointer(start: UnsafePointer<UInt8>($0), count: sizeof(T)))
        }

    }

Моя структура MotionControllerAttitude имеет метод toBytes (), который использует toByteArray () с flatMap () для создания массива UInt8, который может использовать outputStream.write:

    func toBytes() -> [UInt8]
    {
        let composite = [roll, pitch, yaw]

        return composite.flatMap(){toByteArray($0)}

    }

… и, наоборот, также имеет init () для создания экземпляра самого себя из массива UInt8 с использованием fromByteArray ():

    init(fromBytes: [UInt8])
    {
        roll = fromByteArray(Array(fromBytes[0...3]), Float.self)
        pitch = fromByteArray(Array(fromBytes[4...7]), Float.self)
        yaw = fromByteArray(Array(fromBytes[8...11]), Float.self)

    }

Это довольно хрупкий код — опять же, это просто доказательство концепции!

Вращающийся куб

Вернувшись в  приложение Rotating Cube , поскольку контроллер представления также выступает в качестве NSStreamDelegate для steam (теперь вы можете видеть, что вещи реорганизуются!), Метод stream () вызывается, когда iPad получает пакет данных.

Мне нужно проверить, что входящий поток на самом деле является NSInputStream, и у него есть доступные байты. Если это так, то я использую приведенный выше код для создания экземпляра MotionControllerAttitude из входящих данных и просто устанавливаю углы Эйлера на моем кубе:

    func stream(stream: NSStream, handleEvent eventCode: NSStreamEvent)
    {
        if let inputStream = stream as? NSInputStream where eventCode == NSStreamEvent.HasBytesAvailable
        {
            var bytes = [UInt8](count:12, repeatedValue: 0)
            inputStream.read(&bytes, maxLength: 12)

            let streamedAttitude = MotionControllerAttitude(fromBytes: bytes)

            dispatch_async(dispatch_get_main_queue())
            {
                self.label.text = "stream in: \(streamedAttitude.roll.radiansToDegrees()) | \(streamedAttitude.pitch.radiansToDegrees()) | \(streamedAttitude.yaw.radiansToDegrees())"

                self.geometryNode?.eulerAngles = SCNVector3(x: -streamedAttitude.pitch, y: streamedAttitude.yaw, z: streamedAttitude.roll)

            }
        }
    }

В заключении

Этот проект демонстрирует мощь Multipeer Connectivity: независимо от того, создаете ли вы игры или приложения для создания контента, несколько устройств iOS могут работать вместе и передавать данные любого типа быстро и надежно. Можно предположить, что целый ряд iPad может быть подключен как одноранговый и выступать в роли  фермы рендеринга  или огромного дисплея с несколькими устройствами.

Как всегда, исходный код этого проекта доступен в  моем репозитории GitHub здесь .

Я не рассмотрел код CoreMotion, все это обсуждается в  CoreMotion Управляемые 3D-эскизы на iPhone с Swift .