鍍金池/ 教程/ iOS/ 游戲中的多點互聯(lián)
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

游戲中的多點互聯(lián)

多點互聯(lián) (Multipeer Connectivity,即 MPC) 是在 2013 年的 WDCC 中提出的,期間做過不少宣傳,但是卻很少有案例能夠成功有效地使用它。接下來,就讓我們來看一看如何正確使用 MPC,尤其是在游戲中的應(yīng)用。

什么是多點互聯(lián)

多點互聯(lián)是蘋果的一個傳輸無關(guān)的網(wǎng)絡(luò)框架,提供網(wǎng)絡(luò)的發(fā)現(xiàn)、創(chuàng)建和通信功能??梢哉f它是 Bonjour 的精神傳承者, Bonjour 可以在 LAN 和 Wi-Fi 的網(wǎng)絡(luò)下高效地識別設(shè)備。

MPC 的關(guān)鍵用途在于創(chuàng)建臨時網(wǎng)絡(luò)中的點對點連接,而不需要考慮天氣、無線、藍牙等各種因素,只需要有個人網(wǎng)絡(luò)就行。一旦創(chuàng)建之后,各個節(jié)點可以安全地共享消息、數(shù)據(jù)和文件資源。

絕大部分 MPC 的功能在更高層的 GameKit 框架中都可以找到。使用 GameKit 可以讓開發(fā)者接觸到有用的游戲概念,抽離底層的網(wǎng)絡(luò)協(xié)議。

大部分的游戲都更適合用 GameKit 開發(fā),它有很多直接使用 MPC 實現(xiàn)的游戲相關(guān)的封裝。不過作為 MPC 的進階手冊,本文主要涉及 MPC 的各種使用技巧。

什么時候該用

當你的游戲或應(yīng)用需要在近距離的多臺設(shè)備中進行連接的時候, MPC 可以大幅提高用戶體驗。不論你是想要建立一個遠程控制還是多人游戲, MPC 都可以幫助你減少用戶使用過程中的阻力,減少服務(wù)器的開銷,甚至可以減少網(wǎng)絡(luò)延時等問題。

比如一個遠程控制的應(yīng)用,如果它不需要用戶進行任何設(shè)置,而是在安裝后立即自動連接到被控制端上,那么應(yīng)用的品質(zhì)會得到很大的提升。不論這個遠程控制針對的是游戲、軟件展示、音頻播放還是其他東西,都是這樣。DeckRocket 就是一個很好的開源的例子,它是一個用來遠程遙控 DeckSet 幻燈片的 iOS 應(yīng)用。

多用戶游戲也可以從 MPC 的零配置和離線連接特性中受益。比如一個包含游戲邏輯、規(guī)則和存檔功能的卡牌類游戲,可以在不聯(lián)網(wǎng)的狀態(tài)下讓任意兩名玩家進行即時對戰(zhàn)。在這篇文章里,我們將會從 CardsAgainst 這個真實的應(yīng)用中選取一些例子進行說明。 CardsAgainst 是著名游戲 Cards Against Humanity 的開源 iOS 版本,完整的項目源代碼可以在 Github 獲取。

本文中的其他示例則選自 PeerKit,一個 Github 上的開源框架,用來構(gòu)建事件驅(qū)動且無需配置的 MPC 應(yīng)用。

發(fā)現(xiàn)設(shè)備的相關(guān)設(shè)置

有很多種方法可以把 MPC 的設(shè)備偵測概念整合到應(yīng)用中。接下來我們將介紹三種廣泛使用的設(shè)計模式。

方法一:默認方式

蘋果提供了一個內(nèi)置的 ViewController ,可以很方便地進行匹配和初始化連接。只需要設(shè)置好 serviceTypesession 并且彈出一個 MCBrowserViewController 即可,MPC 會幫你做好剩下的事情。注意,serviceType 最多是 15 位 ASCII 字符。使用方法通常像逆向的 DNS 標記一樣 (例如: io-objc-mpc):

let session = MCSession(peer: MCPeerID(displayName: "Mary"))
let serviceType = "io-objc-mpc" // 最多 15 ASCII 字符
window!.rootViewController = MCBrowserViewController(serviceType: serviceType, session: session)

http://wiki.jikexueyuan.com/project/objc/images/18-9.png" alt="" />

不過,我們無法輕易地對 MCBrowserViewController 進行自定義,而你有可能想設(shè)置自己的匹配原則,那么請移步下面的章節(jié)。

方法二:專門的公示者 (Advertiser) / 瀏覽者 (Browser)

如果你的游戲的匹配機制是先選取一個主節(jié)點來協(xié)調(diào)游戲邏輯,然后其他次節(jié)點和主節(jié)點進行連接,那么你應(yīng)該充分利用這些信息,只需要從主節(jié)點進行公示,然后次節(jié)點進行瀏覽即可:

http://wiki.jikexueyuan.com/project/objc/images/18-10.gif" alt="" />

// 主節(jié)點公示
advertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: discoveryInfo, serviceType: serviceType)
advertiser.delegate = self
advertiser.startAdvertisingPeer()

// 次節(jié)點瀏覽
mcBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: serviceType)
mcBrowser.delegate = self
mcBrowser.startBrowsingForPeers()

但是,總是有那么一些情況,最好能在應(yīng)用運行之前就建立好連接,而不用用戶進行任何操作。下面的章節(jié)就展示了如何實現(xiàn)這樣的功能。

方法三:零配置

MPC 能夠極大地減少用戶體驗的阻力。當你以正確的方式把它整合到應(yīng)用中時,你的用戶可以在安裝應(yīng)用之后立即開始通信,而不用任何配置。這會是一件大快所有人心的大好事。

http://wiki.jikexueyuan.com/project/objc/images/18-11.gif" alt="" />

為了實現(xiàn)這個功能,我們需要同時對會話進行公示和查看,我們把這種行為稱之為 收發(fā) (transceiving = transmitting and receiving)。

在多節(jié)點進行收發(fā)的時候,競爭問題是一個重大的挑戰(zhàn),因為可能會有很多節(jié)點同時嘗試連接彼此。這便是領(lǐng)袖選舉 (Leader Election) 問題,這個問題已經(jīng)被深入地討論和研究,并且有一些很好地解決方案。

下面介紹一種簡單而有效的方法。在邀請其他節(jié)點加入會話的時候,將每個節(jié)點的運行時間包含到元數(shù)據(jù) (metadata) 里,公示的節(jié)點總是加入到最早的會話中:

// 瀏覽者的委托代碼
func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) {
    var runningTime = -timeStarted.timeIntervalSinceNow
    let context = NSData(bytes: &runningTime, length: sizeof(NSTimeInterval))
    browser.invitePeer(peerID, toSession: mcSession, withContext: context, timeout: 30)
}

// 公示者的委托代碼
func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID: MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) {
    var runningTime = -timeStarted.timeIntervalSinceNow
    var peerRunningTime = NSTimeInterval()
    context.getBytes(&peerRunningTime)
    let isPeerOlder = (peerRunningTime > runningTime)
    invitationHandler(isPeerOlder, mcSession)
    if isPeerOlder {
        advertiser.stopAdvertisingPeer()
    }
}

發(fā)送和接受

MPC 提供了幾種發(fā)送和接收數(shù)據(jù)的方式,每種方式都有自己獨有的特點和取舍。

發(fā)送數(shù)據(jù)

當發(fā)送少量事件驅(qū)動的數(shù)據(jù) (最多幾 kb) 的時候,比如游戲事件 (開始/暫停/退出),使用這個方法:sendData(_:toPeers:withMode:error:)。

為了封裝傳輸?shù)臄?shù)據(jù),CardsAgainst 定義了一個游戲事件的枚舉類型,在接下來對隨行的數(shù)據(jù)進行序列化和反序列化的時候也會用到:

// 所有的游戲事件
enum Event: String {
    case StartGame = "StartGame",
    Answer = "Answer",
    CancelAnswer = "CancelAnswer",
    Vote = "Vote",
    NextCard = "NextCard",
    EndGame = "EndGame"
}

// 可靠地 (使用 .Reliable 模式) 向節(jié)點發(fā)送事件,有可能有隨行數(shù)據(jù)
func sendEvent(event: Event, object: AnyObject? = nil, toPeers peers: [MCPeerID] = session.connectedPeers as [MCPeerID]) {
    if peers.count == 0 {
        return
    }
    var rootObject: [String: AnyObject] = ["event": event.rawValue]
    if let object = object {
        rootObject["object"] = object
    }
    let data = NSKeyedArchiver.archivedDataWithRootObject(rootObject)
    session.sendData(data, toPeers: peers, withMode: .Reliable, error: nil)
}

// 使用例
sendEvent(.StartGame, ["initialData": "hello objc.io!"])

具體內(nèi)容可以參考 ConnectionManager.swift 的源代碼。

可靠傳輸 和 不可靠傳輸

就像是 TCP/UDP 一樣,MPC 有可靠傳輸和不可靠傳輸兩種模式。MCSessionSendDataMode 包含了這兩種模式。

如果要在可靠模式 (.Reliable) 下發(fā)送數(shù)據(jù):

let message = "Hello objc.io!"
let data = message.dataUsingEncoding(NSUTF8StringEncoding)!
var error: NSError? = nil
if !session.sendData(data, toPeers: peers, withMode: .Reliable, error: &error) {
    println("error: \(error!)")
}

如果你發(fā)送的數(shù)據(jù)十分關(guān)鍵,直接關(guān)系到你的游戲能否正常運行,比如開始或者暫停游戲,使用可靠模式 (.Reliable):

如果與準確性和有序性相比,速度的優(yōu)先級更高,比如發(fā)送傳感器的數(shù)據(jù),那么不可靠模式 (.Unreliable) 可能更適合。務(wù)必權(quán)衡利弊,在考慮好的情況下,選擇最適合你的方案。

發(fā)送文件

當你發(fā)送大量數(shù)據(jù) (幾百 kB 甚至幾 MB) 的時候,比如文件,應(yīng)該使用 sendResourceAtURL(_:withName:toPeer:withCompletionHandler:) 方法。它可以通過 NSProgress 對象讓發(fā)送方和接收方同時監(jiān)控傳輸進度。

這是 DeckRocket 中的例子:

pdfProgress = session!.sendResourceAtURL(url, withName: filePath.lastPathComponent, toPeer: peer) { error in
    dispatch_async(dispatch_get_main_queue()) {
        self.pdfProgress!.removeObserver(self, forKeyPath: "fractionCompleted", context: &ProgressContext)
        if error != nil {
            HUDView.show("Error!\n\(error.localizedDescription)")
        } else {
            HUDView.show("Success!")
        }
    }
}
pdfProgress!.addObserver(self, forKeyPath: "fractionCompleted", options: .New, context: &ProgressContext)

對于流數(shù)據(jù),比如傳感器的讀數(shù)或者持續(xù)更新的用戶坐標信息等等,可以使用 startStreamWithName(_:toPeer:error:) 方法把數(shù)據(jù)寫到 NSOutputStream 中。接收者則通過 NSInputStream 讀取數(shù)據(jù)流:

// 接收者
public func session(session: MCSession!, didReceiveStream stream: NSInputStream!, withName streamName: String!, fromPeer peerID: MCPeerID!) {
    // 假設(shè)是一個 UInt8 的流
    var buffer = [UInt8](count: 8, repeatedValue: 0)

    stream.open()

    // 讀取單個字節(jié)
    if stream.hasBytesAvailable {
        let result: Int = stream.read(&buffer, maxLength: buffer.count)
        println("result: \(result)")
    }
}

挑戰(zhàn)

雖然 MPC 很強大,但同時也面臨不少挑戰(zhàn)。下面列舉一下你可能會遇到的問題。

可用性

MPC 只能用于 iOS 7、iOS 8 和 OS X 10.10 ,所以如果不是蘋果的設(shè)備,或者不是最新的 OS X 發(fā)行版的話,那么請忘了 MPC 吧。跨平臺的應(yīng)用或者游戲需要依賴別的替代品。

可靠性

盡管在 iOS 7 之后蘋果對 MPC 的可靠性做了很大的提升,可靠性依舊是 MPC 的痛處。不得不考慮到連接失敗的情況,而且為了盡可能覆蓋很多邊界情況,還需要做不少額外的功課。

同步性和競爭條件

撇開因無線連接的損耗所導(dǎo)致的網(wǎng)絡(luò)延時不談,編寫即時型網(wǎng)絡(luò)的代碼有點像是寫本地的多線程代碼。在假設(shè)事件發(fā)送成功之前,務(wù)必在合適的位置對關(guān)鍵傳輸加鎖,從而確保所有節(jié)點確認接收關(guān)鍵事件。

游戲常常需要共享狀態(tài),比如游戲是否開始或暫停,玩家是否退出等等。如果玩家在對手即將發(fā)動致命一擊的時候暫停游戲了會怎么樣? MPC 將異步的游戲邏輯競爭留給開發(fā)者來決定。使用 GameKit 這樣的框架對集中邏輯很有幫助,但是同時也犧牲了一些靈活性作為代價。

替代選擇

用 MPC 來寫一個復(fù)雜的游戲無疑充滿了挑戰(zhàn)性。你可以了解一下其他選擇再做決定。

GameKit

蘋果在 GameKit 中投入了很多想法。盡管它強制要求使用指定的模型和結(jié)構(gòu)模式,并且還需要放棄會話連接過程中的一些控制,但是它確實抽離了很多底層的工作,減輕了工作量。

用 GameKit 開發(fā)游戲可以同時滿足點對點模式和傳統(tǒng)網(wǎng)絡(luò)連接模式的需求。

Websockets

WebSocket 協(xié)議 (RFC 6455) 允許服務(wù)器端和客戶端之間進行雙向通信。每個節(jié)點需要建立一個新的 websocket 連接。該協(xié)議建立在 TCP 的基礎(chǔ)上,所以不提供類似 MPC 的 .Unreliable 信息發(fā)送模式。不像 MPC,websocket 不提供任何網(wǎng)絡(luò)創(chuàng)建或者設(shè)備檢測功能,所以服務(wù)器端和客戶端都必須連接在同一個網(wǎng)絡(luò)上。它常用于和 Bonjour 關(guān)聯(lián)使用。

如果是構(gòu)建跨平臺游戲或應(yīng)用,那么 WebSocket 可以說是極具吸引力的,不過它需要一個有自定義后臺的連接。

目前Swift (starscream) 和 Objective-C (SocketRocket、jetfire) 都有不少現(xiàn)成的 WebSocket 類庫可供使用。

總結(jié)

把 MPC 整合到你的游戲或者應(yīng)用中的過程不會很復(fù)雜,但是卻能極大的提升用戶體驗,希望讀完本文你也認同此觀點。

如果想了解關(guān)于 MPC 的更多內(nèi)容,下面的資料可能會有所幫助。

資料

Multipeer Connectivity Reference

Multipeer Connectivity WWDC 2013 Session

GameKit Reference

NSHipster Article on Multipeer Connectivity

PeerKit: An open-source Swift framework for building event-driven, zero-config MPC apps

CardsAgainst: An open-source iOS game built with MPC

DeckRocket: An open-source presentation remote control app for iOS/OSX built with MPC

上一篇:UI 測試下一篇:音頻 API 一覽