使用Node.js開(kāi)發(fā)多人玩的HTML 5游戲
譯文【51CTO精選譯文】有一天,幾個(gè)朋友來(lái)我家,給我介紹幾個(gè)很酷的iPad游戲。其中一個(gè)游戲是《星噬》(Osmos),開(kāi)發(fā)這款游戲的是加拿大一家獨(dú)立開(kāi)發(fā)商,名叫Hemisphere Games。你可以控制在二維空間漂浮的一個(gè)小小的星團(tuán)。小星團(tuán)唯一能做的事就是往某個(gè)特定的方向噴射自己,結(jié)果往相反的方向推動(dòng)星團(tuán)。游戲規(guī)則很簡(jiǎn)單;主要規(guī)則就是,兩個(gè)星團(tuán)碰撞時(shí),大的那個(gè)會(huì)吞噬掉小的那個(gè)。其余規(guī)則基本上直接來(lái)自質(zhì)能守恒。
《星噬》確實(shí)引起了我的興趣,因?yàn)樗芎?jiǎn)單,但玩法很吸引人,不過(guò)明顯缺少支持多人玩的功能。我一下子來(lái)了勁,想解決這個(gè)問(wèn)題。于是,osMUs(MU指多人玩)應(yīng)運(yùn)而生,這是一款基于瀏覽器的多人玩的《星噬》克隆版游戲。
工作原理
瀏覽器瀏覽到osmus登錄頁(yè)面后,服務(wù)器會(huì)將宇宙的當(dāng)前狀態(tài)發(fā)送給新的客戶端,這個(gè)宇宙由多個(gè)速度隨機(jī)的星團(tuán)組成。這時(shí)候,客戶端可以被動(dòng)地關(guān)注游戲進(jìn)度;但是當(dāng)然了,也可以作為玩家控制的星團(tuán),加入游戲。一旦玩家加入,他就可以點(diǎn)擊或在移動(dòng)設(shè)備上快速按下畫布(canvas),射出新的星團(tuán)。
隨著游戲不斷進(jìn)行,服務(wù)器決定某人(可能是其中一個(gè)獨(dú)立自主的星團(tuán))何時(shí)獲勝;這時(shí),玩家們接到通知,游戲重新開(kāi)始。
本文其余部分介紹了與開(kāi)發(fā)有關(guān)的一些具體內(nèi)容。所以,如果你想試一下,盡管試好了。不過(guò)要注意一點(diǎn):osmus在Chrome穩(wěn)定版(版本13)和iPad上運(yùn)行。
游戲架構(gòu)
我編寫osmus,是為了分成不同的、松散耦合的組件,既為了讓其他代碼貢獻(xiàn)者更容易獲得代碼庫(kù),又為了便于嘗試可以互換的技術(shù)。
osmus使用一個(gè)共享的游戲引擎(Game Engine),該引擎既可以在瀏覽器中運(yùn)行,又可以在服務(wù)器上運(yùn)行。引擎是一個(gè)簡(jiǎn)單的狀態(tài)機(jī),其主要功能就是使用里面定義的物理規(guī)則,計(jì)算出與時(shí)間有關(guān)的下一個(gè)游戲狀態(tài)。
- Game.prototype.computeState = function(delta) {
- var newState = {};
- // Compute a bunch of stuff based on this.state
- return newState;
- }
這是游戲引擎很狹窄的定義。在游戲開(kāi)發(fā)領(lǐng)域,游戲引擎的含意通常涵蓋渲染器、聲音播放器和網(wǎng)絡(luò)層等方面。這種情況下,我在這些組件之間作了非常明確的劃分,osmus游戲的核心僅僅包括物理狀態(tài)機(jī),那樣客戶端和服務(wù)器都能計(jì)算出下一個(gè)狀態(tài),因而在時(shí)間上做到很合理的同步。
客戶端有三個(gè)主要部件組成:渲染器、輸入管理器和聲音管理器。我制作了一個(gè)非常簡(jiǎn)單的基于畫布的渲染器,將星團(tuán)畫成紅圓圈,將玩家星團(tuán)畫成綠圓圈。我的同事Arne Roomann-Kurrik編寫了一個(gè)替代的基于three.js的渲染器,使用了一些壯麗的著色器和陰影。
聲音管理器處理回放聲音效果和背景音樂(lè)(來(lái)自8-bit Magic)的工作。目前實(shí)現(xiàn)的方法使用了音頻標(biāo)簽,有兩個(gè)元素,一個(gè)用于背景音樂(lè)通道,另一個(gè)用于聲音效果通道。這個(gè)方法存在已知的局限性,但考慮到我實(shí)現(xiàn)的方法具有模塊性,聲音實(shí)現(xiàn)方法可以換成使用其他API的方法,比如使用Chrome的Web Audio API。
最后,輸入管理器負(fù)責(zé)處理鼠標(biāo)事件,但是可以換成改而使用觸摸操作的管理器,用于移動(dòng)版本。在移動(dòng)情況下,可能有必要使用CSS3轉(zhuǎn)換而不是使用畫布,因?yàn)镃SS3在iOS上是硬件加速的,而HTML5畫布仍然不是,也沒(méi)有實(shí)現(xiàn)WebGL。
說(shuō)到移動(dòng),我驚喜地發(fā)現(xiàn),osmus在iPad上玩起來(lái)很順暢,尤其是在運(yùn)行最新iOS版本的iPad 2上。這太好了,也是為開(kāi)放互聯(lián)網(wǎng)編寫游戲的其中一個(gè)實(shí)際好處。
#p#
聯(lián)網(wǎng)很難
從聯(lián)網(wǎng)的角度來(lái)看,游戲是一個(gè)相當(dāng)宏偉龐大的項(xiàng)目,需要客戶端之間實(shí)現(xiàn)無(wú)縫實(shí)時(shí)同步。正由于如此,客戶端/服務(wù)器的雙向通信必不可少。在現(xiàn)代互聯(lián)網(wǎng)架構(gòu)中,這種通信機(jī)制由Web Sockets來(lái)提供,它在TCP上提供了薄薄的一層,把許多繁瑣的細(xì)節(jié)隱藏起來(lái),不讓實(shí)現(xiàn)者看到。為進(jìn)一步隱藏網(wǎng)絡(luò)堆棧方面的細(xì)節(jié),我使用了socket.io庫(kù),該庫(kù)為整個(gè)游戲提供了一種異常簡(jiǎn)單的事件驅(qū)動(dòng)抽象層。遺憾的是,目前不支持二進(jìn)制數(shù)據(jù),不然可以大大壓縮消息大小——拿《星噬》來(lái)說(shuō),壓縮后也許可以減少一兩個(gè)數(shù)量級(jí)。
經(jīng)過(guò)一番研究,包括我與知名的HTML5開(kāi)發(fā)專家Rob Hawkes進(jìn)行的那次深入討論后,清楚地發(fā)現(xiàn):要獲得任何一種共享體驗(yàn),最簡(jiǎn)單的模式就是在服務(wù)器上有真正的游戲狀態(tài),讓客戶端定期與它進(jìn)行同步。這方面需要取舍的主要是同步質(zhì)量與所需的網(wǎng)絡(luò)流量。
在一個(gè)極端情況下,如果游戲邏輯完全在服務(wù)器上,以每秒60幀的速度將更新內(nèi)容(或者可能僅僅是屏幕截圖)發(fā)送到客戶端,就可以編寫游戲,但是由于這種模式需要數(shù)量龐大的帶寬,所以這個(gè)做法一般行不通。在相反的極端情況下,你可以設(shè)想這種網(wǎng)絡(luò)架構(gòu):客戶端連接,獲得初始狀態(tài),然后基本上各自獨(dú)立自主。
實(shí)際上,有一種很好的折衷方法——許多支持多人玩的游戲采用這種方法,那就意味著復(fù)制客戶端和服務(wù)器中的重要代碼。幸好,由于我們處在無(wú)所不在的JavaScript時(shí)代,再也不需要復(fù)制功能,而是只要用JavaScript編寫游戲引擎就可以共享代碼,然后在客戶端上的瀏覽器中和服務(wù)器上的node.js中運(yùn)行即可。
共享的JS模塊
如前所述,osmus使用在客戶端與服務(wù)器之間共享的物理引擎。因而有人可能會(huì)想:在兩者之間共享JavaScript代碼會(huì)易如反掌,實(shí)際上不是那么容易。
模塊加載器有一大堆。有CommonJS規(guī)范、RequireJS庫(kù)和node.js require方法,沒(méi)有一個(gè)可以很好地協(xié)同使用。如果你不用模塊加載器,就想在客戶端和服務(wù)器之間共享代碼(這是服務(wù)器上JS的一大優(yōu)點(diǎn)),那么你可以使用這個(gè)有點(diǎn)變通的模式:
- (function(exports) {
- var MyClass = function() { /* ... */ };
- var myObject = {};
- exports.MyClass = MyClass;
- exports.myObject = MyObject;
- })(typeof global === "undefined" ? window : exports);
這個(gè)變通方法靠的是這一點(diǎn):node.js定義了global(全局)對(duì)象,而瀏覽器沒(méi)有定義。有了這個(gè)變通方法,node.js require()會(huì)很高興,你還可以在<script>標(biāo)簽中加入文件,不會(huì)污染你的名稱空間,當(dāng)然假設(shè)沒(méi)有其他JS以window.global對(duì)象污染你的名稱空間!
遺憾的是,這個(gè)方法只適用于一個(gè)共享模塊。一旦你有了多個(gè)彼此依賴的模塊(通過(guò)node-land中的require方法和browser-land中的global對(duì)象),節(jié)點(diǎn)的名稱空間與瀏覽器的加入之間的差異會(huì)變得異常明顯,需要更多的變通方法。
另一個(gè)方法是使用browserify,捆綁所有JS,在瀏覽器里面模擬require。這種方法依賴node.js來(lái)提供生成的JS,這并不理想,因?yàn)殪o態(tài)文件應(yīng)該由專門為該用途優(yōu)化的web服務(wù)器來(lái)提供。不過(guò),node.js+ browserify可以進(jìn)行配置,以便編譯可以靜態(tài)提供的JS,不必依賴節(jié)點(diǎn)來(lái)提供。這個(gè)方法帶來(lái)了一些開(kāi)銷:
1. 多出了構(gòu)建這個(gè)步驟,以便部署。
2. 無(wú)論browserify使用什么機(jī)制來(lái)支持require()調(diào)用,都需要性能開(kāi)銷。
總的來(lái)說(shuō),這個(gè)方法在我看來(lái)比較好,我希望在將來(lái)編寫的osmus版本中試用一下。
原文:Developing Multiplayer HTML5 Games with Node.js
【編輯推薦】