個案研究 -《Inside World Wide Maze》

Saqoosha
Saqoosha

World Wide Maze 遊戲:為了達成目標,使用者可使用智慧型手機在 3D 迷宮上瀏覽從網站製作的 3D 迷宮球球。

World Wide Maze

這個遊戲會大量使用 HTML5 功能。舉例來說,DeviceOrientation 事件會從智慧型手機擷取傾斜資料,接著再透過 WebSocket 傳送至電腦,讓玩家能透過 WebGLWeb Worker 建立的 3D 空間。

本文將詳細說明這些功能的使用方式、整體開發程序,以及最佳化的重點。

DeviceOrientation

DeviceOrientation 事件 (範例) 可用於從智慧型手機擷取傾斜資料。將 addEventListenerDeviceOrientation 事件搭配使用時,系統會按照固定間隔叫用含有 DeviceOrientationEvent 物件的回呼做為引數。間隔時間會因使用的裝置而異。例如,在 iOS + Chrome 和 iOS + Safari 中,系統大約每 1/20 秒叫用一次回呼,而在 Android 4 + Chrome 中,系統大約會在每 1/10 秒叫用一次回呼。

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

DeviceOrientationEvent 物件包含每個 XYZ 軸的傾斜資料 (非弧度) 資料 (詳情請參閱 HTML5Rocks)。不過,傳回的值也會因裝置和瀏覽器的組合而有所不同。實際傳回值的範圍如下表所示:

裝置螢幕方向。

上方以藍色標明的值是按照 W3C 規格定義的值。以綠色標示的圖示符合相關規格,以紅色標示的標誌代表尺寸相符。令人驚訝的是,只有 Android 與 Firefox 的組合才會傳回符合規格的值。然而,如果需要進行實作,則更適合採用經常發生的值。因此,World Wide Maze 會使用 iOS 傳回值做為標準,並據此調整 Android 裝置。

if android and event.gamma > 180 then event.gamma -= 360

不過,這項做法並不支援 Nexus 10。雖然 Nexus 10 傳回的值範圍與其他 Android 裝置相同,但仍有一項錯誤會反轉 beta 和 Gamma 值。這將另外處理。(是否預設為橫向?)

如同此範例,即使涉及實體裝置的 API 已設定規格,也無法保證傳回的值符合這些規格。因此,請務必在所有潛在裝置上測試這些程式。這也表示您可能會輸入非預期的值,而必須建立解決方法。不過,World Wide Maze 會提示第一位玩家按照教學課程的步驟 1 校正裝置,但如果玩家收到非預期的傾斜值值,則無法正確校準零的位置。因此,這類元件有內部時間限制,如果無法在時限內校正,系統會提示玩家切換到鍵盤控制項。

WebSocket

在 World Wide Maze 中,你的智慧型手機和電腦會透過 WebSocket 連線。更準確來說,它們是經由智慧型手機之間的轉發伺服器 (例如智慧型手機對伺服器對電腦) 進行連線。這是因為 WebSocket 無法讓瀏覽器彼此直接連線。(使用 WebRTC 資料管道可進行點對點連線,不需轉發伺服器,但實作時只能搭配 Chrome Canary 和 Firefox 夜間使用)。

我選擇使用 Socket.IO (0.9.11) 程式庫實作,內含在連線逾時或中斷連線時重新連線的功能。我搭配使用了 NodeJS 和 NodeJS,因為這個 NodeJS 和 Socket.IO 的組合顯示出在數個 WebSocket 實作測試中達到最佳伺服器端效能。

正在以數字配對

  1. 您的電腦會連線至伺服器。
  2. 伺服器會隨機產生一組數字,並記住數字和 PC 的組合。
  3. 在行動裝置上指定電話號碼,並連線至伺服器。
  4. 如果指定的號碼與連線的電腦相同,則行動裝置會與該電腦配對。
  5. 如果沒有指定的電腦,就會發生錯誤。
  6. 行動裝置提供的資料會傳送至與手機配對的電腦,反之亦然。

您也可以改用行動裝置進行初始連線。在這種情況下,裝置會進行反向操作。

分頁同步功能

Chrome 專屬的「分頁同步功能」功能可簡化配對程序。換句話說,在電腦上開啟的網頁可以在行動裝置上輕鬆開啟 (反之亦然)。電腦擷取伺服器核發的連線號碼,並使用 history.replaceState 將該號碼附加至網頁的網址。

history.replaceState(null, null, '/maze/' + connectionNumber)

啟用分頁同步功能後,網址會在數秒後同步處理,方便使用者在行動裝置上開啟相同網頁。行動裝置會檢查開啟的網頁網址。如果有加上數字,則會立即開始連線。這樣就不必手動輸入數字,或使用相機掃描 QR code。

延遲時間

由於中繼伺服器位於美國,因此從日本存取伺服器時,智慧型手機的傾斜資料傳送到電腦前,經過約 200 毫秒的時間。與開發時使用的本機環境相比,回應時間明顯遲緩,但插入低傳遞濾波器 (我使用了 EMA) 這類程式碼後,回應時間就不會造成乾擾。(在實務上,我們也需要低壓濾鏡來展示簡報;傾斜感應器傳回的值會加入大量雜訊,因此照原樣套用這些值到螢幕上,會造成大量晃動。)這不適用於跳躍動作,因為看起來緩慢,但是沒有辦法解決這個問題。

因為我從一開始就預期會發生延遲問題,所以我考慮在全球各地設置轉發伺服器,讓用戶端能連線至最接近的可用伺服器 (進而縮短延遲時間)。不過,我最後使用了只在美國使用 Google Compute Engine (GCE),所以這是不可能發生的。

Nagle 演算法問題

Nagle 演算法通常會隨著 TCP 層級的緩衝而納入作業系統中,以確保通訊效率,但我發現啟用此演算法後,無法即時傳送資料。(尤其是與 TCP 延遲確認合併使用時)。即使沒有延遲 ACK,如果 ACK 因伺服器位於海外等因素而延遲到特定程度,仍會發生相同問題。

在 Android 版 Google Chrome 的 WebSocket 中,(包括停用 Nagle 的 TCP_NODELAY 選項) 不會發生 Nagle 延遲問題,但 iOS 版 Google Chrome 中使用的 WebKit WebSocket 才發生了問題,但 iOS 版 Google Chrome 並未啟用這個選項。(Safari 使用相同的 WebKit 也發生問題,這是透過 Google 向 Apple 回報的問題,在 WebKit 的開發版本中顯然已解決

發生這個問題時,每 100 毫秒傳送的傾斜資料會合併為多個區塊,每 500 毫秒就僅會傳送到電腦。遊戲無法在這些情況下運作,因此伺服器端的短暫間隔 (每 50 毫秒以上) 會傳送資料,避免發生這種延遲。我認為短時間內接收 ACK 會欺騙 Nagle 演算法,因而誤以為可以傳送資料。

Nagle 演算法 1

上方圖表會以圖表形式呈現實際接收資料的間隔情況。這代表封包之間的時間間隔;綠色代表輸出間隔,紅色代表輸入間隔。最小值為 54 毫秒,最大值為 158 毫秒,中間接近 100 毫秒。我使用 iPhone 透過位於日本的中繼伺服器。輸出和輸入皆為約 100 毫秒,且作業的流暢度。

Nagle 演算法 2

相較之下,這張圖表則顯示美國使用該伺服器的結果。雖然綠色輸出間隔保持穩定 100 毫秒,但是輸入間隔會在 0 毫秒低至 500 毫秒之間出現波動,代表電腦正在以區塊的形式接收資料。

ALT_TEXT_HERE

最後,這張圖表顯示的是伺服器傳送預留位置資料,避免發生延遲的結果。雖然執行日文伺服器的效能不如日本伺服器,但我們可以清楚地在 100 毫秒左右的輸入間隔時間相對穩定。

發生錯誤?

儘管 Android 4 (ICS) 的預設瀏覽器具有 WebSocket API,但無法連線,因此會導致 Socket.IO connect_failed 事件。發生在內部逾時,而伺服器端也無法驗證連線。(我沒有隻使用 WebSocket 測試這個功能,所以這可能是 Socket.IO 問題)。

調度轉發伺服器資源

由於轉發伺服器的角色並非複雜,因此只要確保同一電腦和行動裝置一律連線至同一伺服器,要擴充及增加伺服器數量並不容易。

物理學

運用 3D 物理模擬器,就能玩球裡的球 (滾滾坡、與地面碰撞)、在牆壁碰撞、收集物品等。我利用 Ammo.js 這個 Bullet 物理引擎的移植於 JavaScript,並採用 Emscripten,同時利用 Physijs 做為「網路工作人員」。

網路工作處理序

「網路工作處理序」是一個 API,可在個別執行緒中執行 JavaScript。以網路工作處理器形式啟動的 JavaScript 與最初呼叫它的執行緒不同,可在不回應網頁回應的情況下執行繁重的工作。Physijs 使用 Web Worker,協助通常大量耗用的 3D 物理引擎順利執行。World Wide Maze 會以完全不同的影格速率處理物理引擎和 WebGL 圖片算繪作業,因此即使影格速率因 WebGL 算繪負載過高,所以畫面更新率下降,物理引擎本身仍會調整 60 fps 或不受遊戲控制項影響。

FPS

這張圖片顯示 Lenovo G570 產生的影格速率。上圖顯示的是 WebGL (圖片轉譯) 的畫面更新率,下方方塊則為物理引擎的畫面更新率。GPU 是經過整合的 Intel HD Graphics 3000 晶片,因此圖片轉譯影格速率未達預期的 60 fps。不過,由於物理引擎達到預期的影格速率,因此遊戲過程與高規格機器的效能並無太大差異。

由於含有運作中的網路工作站的執行緒沒有主控台物件,因此資料必須透過 postMessage 傳送至主執行緒,才能產生偵錯記錄。使用 console4Worker 會建立與 worker 中相符的主控台物件,讓偵錯程序更加簡單。

Service Worker

新版 Chrome 可讓您在啟動 Web Worker 時設定中斷點,這對於偵錯作業也相當實用。您可以前往開發人員工具的「Workers」面板查看這項資訊。

效能

多邊形數量較多的階段有時會超過 100,000 個多邊形,但即使完全產生為 Physijs.ConcaveMesh (項目符號中的 btBvhTriangleMeshShape),效能也不會特別嚴重。

初期,隨著需要衝突偵測的物件數量增加,影格速率也隨之下降,不過 Physijs 中消除不必要的處理機制後,能改善了效能。這項改善是原始 Physijs 的分支

幽靈物體

具備衝突偵測功能但不影響衝突,因此不會對其他物件造成任何影響的物件在 Bullet 中稱為「幽靈物體」。雖然 Physijs 並未正式支援 ghost 物件,但您可以在產生 Physijs.Mesh 後試著使用旗標來建立這些物件。World Wide Maze 會使用幽靈物體偵測物品和目標點的碰撞情形。

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

對於 collision_flags,1 為 CF_STATIC_OBJECT,4 為 CF_NO_CONTACT_RESPONSE。請嘗試搜尋公告論壇Stack Overflow項目符號說明文件。由於 Physijs 是 Ammo.js 和 Ammo.js 的包裝函式,且基本上與 Bullet 相同,因此大部分可在 Bullet 中完成的工作。

Firefox 18 相關問題

Firefox 17 版更新為 18 版改變了 Web Worker 交換資料的方式,Physijs 則因此停止運作。這個問題已於 GitHub 回報,並於數天後解決。雖然這個開放原始碼的效率對我印象深刻,但這個事件也提醒我,World Wide Maze 是如何由數個不同的開放原始碼架構組成。我們撰寫此文章是為了提供一些意見。

asm.js

雖然這未必直接涉及 World Wide Maze,但 Ammo.js 已在 Mozilla 最近宣布推出 asm.js (令人驚訝的是,因為 asm.js 是建立用來加快 JavaScript 速度的開發原因,Emscripten 的建立者也是 Ammo.js 的建立者)。如果 Chrome 也支援 asm.js,物理引擎的運算量應會大幅下降。測試 Firefox Nightly 時的速度明顯提升。也許最好是在 C/C++ 中編寫需要更多速度的區段,然後使用 Emscripten 將這些區段移植到 JavaScript?

WebGL

針對 WebGL 實作,我使用最積極開發的程式庫 three.js (r53)。雖然修訂版本 57 已在第二開發階段發布,但 API 已做出重大變更,因此無法順利發布原本的版本。

光暈效果

如要對球核心和項目添加光暈效果,是透過稱為「Kawase Method MGF」的簡易版本進行。雖然 Kawase Method 讓所有明亮區域都開始開花,World Wide Maze 則會針對需要發光的區域分別建立轉譯目標。這是因為網站螢幕截圖必須用於舞台紋理;如果只擷取所有明亮的區域,如果圖片是白色背景,可能會導致整個網站發光。我也考慮以 HDR 格式處理所有事務,但這次決定不要採用,因為實作方式會變得相當複雜。

光暈

左上角顯示第一張票證,其中光暈區域已分開轉譯,然後套用模糊效果。右下方顯示第 2 張票證,其中圖片大小減少了 50%,再套用模糊效果。右上方顯示第三張票證,圖中再次將圖像縮小 50%,然後再模糊處理。接著將三張合成圖片疊在一起,完成最後的合成圖片,圖片在左下角。至於我使用的模糊處理部分,我使用了 VerticalBlurShaderHorizontalBlurShader (在 3.js 中),有足夠空間進行進一步的最佳化調整。

反光球

球體的反射機制則是根據由 Three.js 的範例製成。所有方向都是從球體的位置呈現,並做為環境地圖使用。每次球移動時,環境地圖就必須更新,但由於以每秒 60 個影格為 60 的更新相當密集,因此每隔 3 個影格就會更新一次。得到的結果並不像更新每個影格那樣順暢,但除非特別指出差異,否則差異幾乎完全不會讓人察覺。

著色器、著色器、著色器...

WebGL 需要著色器 (頂點著色器、片段著色器),才能進行所有轉譯。雖然 Three.js 中包含的著色器已經支援廣泛的效果,但自行撰寫廣告素材是難以製作更精細的陰影和最佳化。World Wide Maze 讓 CPU 越來越忙碌,能夠透過自家的物理引擎處理,因此我嘗試以著色語言 (GLSL) 盡可能寫入 GPU,而盡可能改用 GPU,即使 CPU 處理 (透過 JavaScript) 處理並非如此。海洋波特效自然受到著色器的作用,就像是達成目標點時的煙火一樣,球上也會出現網狀效應。

著色器球

以上是測試球體顯示時使用的網狀效果。左側是遊戲內使用的多邊形,由 320 個多邊形組成。中央的多邊形約使用 5,000 個多邊形,右側的多邊形則使用約 300,000 個多邊形。即使使用這麼多個多邊形,使用著色器處理時,仍可維持 30 fps 的影格速率。

著色器網格

散佈在舞台上的小物全都會整合到一個網格中,而個人動作仰賴著色器來移動每個多邊形提示。這是進行測試,看看在現有大量物件中,效能是否會受到影響。這裡列出約 5,000 個物件,由大約 20,000 個多邊形組成。效能完全沒有影響。

poly2tri

系統會根據從伺服器接收的輪廓資訊形成階段,然後以 JavaScript 進行多邊形。三角法是這個程序的重要部分,他們在 3.js 中實作方式欠佳,且通常會失敗。因此,我決定自行整合名為 poly2tri 的不同三角化程式庫。結果顯示,Three.js 可看出過去是否嘗試同樣的做法,因此我只要加上註解,就能執行這項工作。錯誤由此大幅減少,讓使用者有更多可遊戲階段。偶發性錯誤持續發生,由於某些原因,poly2tri 會藉由發出快訊處理錯誤,因此我已修改內容,改為擲回例外狀況。

poly2tri

以上所示的藍色外框如何進行三角形計算,並產生紅色的多邊形。

非均質過濾

由於標準的獨立 MIP 會使水平與垂直軸上的圖片縮小,所以檢視斜角多邊形的多邊形會使 World Wide Maze 的最遠端紋理看起來像是水平加長的低解析度紋理。這個維基百科網頁的右上方圖片就是很好的範例。實務上,您需要使用更多水平解析度,WebGL (OpenGL) 即用一種稱為非均質篩選的方法來解析。在 Three.js 中,如果為 THREE.Texture.anisotropy 設定大於 1 的值,就能啟用非均質篩選。不過,這項功能屬於擴充功能,不一定適用於所有 GPU。

最佳化

如同這篇 WebGL 最佳做法一文所述,改善 WebGL (OpenGL) 效能最重要的方法,就是盡量減少繪製呼叫。World Wide Maze 的初始開發階段,所有遊戲內島嶼、橋樑和護欄都是不同的物件。有時系統會產生超過 2,000 次繪製呼叫,讓複雜的階段變得雜亂無章。不過,一旦將相同類型的物件全部封裝到一個網格後,繪製呼叫就會下降至五十左右,大幅改善效能。

我使用了 Chrome 追蹤功能,進一步進行最佳化。Chrome 開發人員工具中的分析器能夠判斷整體方法處理時間,或判斷各部分需要多少時間,但追蹤記錄會顯示每個部分所需時間 (最小為一秒 1/1000)。如要進一步瞭解如何使用追蹤功能,請參閱這篇文章

最佳化

以上是為球形反射建立環境地圖的追蹤記錄結果。將 console.timeconsole.timeEnd 插入三.js 中看起來相關的位置,就可以得到像這樣的圖表。時間從左到右流,而每個圖層就像呼叫堆疊一樣。在 console.time 內建立 console.time 結構可進一步測量。上方圖表為「預先最佳化」,底部則顯示最佳化後。如上圖所示,在預先最佳化期間,系統分別呼叫了 updateMatrix (但該字詞遭到截斷)。但我已修改這個檔案,讓系統只呼叫一次,因為只有物件變更位置或方向時,才需要執行這項程序。

追蹤記錄程序本身會佔用資源,因此過度插入 console.time 可能會導致實際效能出現明顯偏差,更難以找出需要最佳化的區域。

成效調整工具

由於網際網路的性質的關係,這款遊戲的系統規格可能不盡相同,《尋找奧茲之旅》於 2 月初推出,使用名為 IFLAutomaticPerformanceAdjust 的類別,根據畫面更新率的波動調整背光效果,確保播放過程順暢。World Wide Maze 以相同的 IFLAutomaticPerformanceAdjust 類別建構而成,並會以下列順序縮放返回效果,盡可能確保遊戲過程順暢:

  1. 如果影格速率低於 45 fps,環境地圖就會停止更新。
  2. 如果仍低於 40 fps,轉譯解析度會降低至 70% (佔表率的 50%)。
  3. 如果仍低於 40 fps,就會排除 FXAA (反鋸齒) 模式。
  4. 如果仍低於 30 fps,就會排除亮光效果。

記憶體流失

簡潔地刪除物件就像 3.js 一樣簡單。但不要去這些東西,顯然將導致記憶體流失,因此我設計出了以下方法。「@renderer」是指 THREE.WebGLRenderer。(3.js 的最新修訂版本使用略為不同的交易定位方法,因此可能無法正常運作)。

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

我個人認為,WebGL 應用程式最棒的一點就是能夠設計 HTML 的網頁版面配置。建構 2D 介面,如 Flash 或 openFrameworks (OpenGL) 中的分數或文字顯示畫面是相當麻煩的。Flash 至少具備 IDE,但如果您不使用 OpenFrameworks 就不太容易 (使用 Cocos2D 這類工具可簡化這項作業)。另一方面,HTML 也允許透過 CSS 精確控制所有前端設計層面,就像建立網站時一樣。儘管將粒子壓縮成標誌之類的複雜特效是不可能的,但 CSS Transforms 功能仍有部分 3D 效果。World Wide Maze 的「目標」和「時間起伏」文字效果是以 CSS 轉場效果 (搭配大眾運輸功能導入) 使用,以動畫呈現。(背景升級顯然會使用 WebGL)。

遊戲中的每個頁面 (標題、結果、RANKING 等) 都有專屬的 HTML 檔案,且一旦載入為範本後,系統就會在適當時機使用合適的值呼叫 $(document.body).append()。發生問題的原因是,滑鼠和鍵盤事件無法在附加前設定,因此嘗試在附加前嘗試 el.click (e) -> console.log(e) 就無法運作。

國際化 (i18n)

處理 HTML 也方便建立英文版本。我選擇使用 i18next (Web i18n 程式庫) 來滿足國際化需求,無需經過修改就能原封不動地使用。

在 Google 文件的試算表中,完成遊戲內文字的編輯及翻譯作業。i18next 需要 JSON 檔案,因此我將試算表匯出至 TSV,然後以自訂轉換工具進行轉換。我在推出應用程式前已進行了許多更新,因此將「Google 文件」試算表的匯出程序自動化,可以比以往輕鬆許多。

由於網頁是使用 HTML 建構而成,因此 Chrome 的自動翻譯功能也會正常運作。然而,有時可能無法正確偵測語言,而是以完全不同的語言將語言最小化 (例如越南文),因此這項功能目前為停用狀態。(您可以使用中繼標記停用此功能)。

RequireJS

我選擇了 RequireJS 做為我的 JavaScript 模組系統。這個遊戲的 10,000 行原始碼分為 60 個類別 (= 咖啡檔案),然後編譯成個別的 js 檔案。RequireJS 會根據依附元件,以適當順序載入這些個別檔案。

define ->
  class Hoge
    hogeMethod: ->

上述定義的類別 (hoge.coffee) 可用如下:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

如要運作,hoge.js 必須在 moge.js 之前載入。此外,由於將「hoge」指定為「definition」(定義) 的第一個引數,hoge.js 一律會先載入 (hoge.js 載入完成後再呼叫)。這項機制稱為 AMD。只要第三方程式庫支援 AMD,就能用於相同類型的回呼。只要已預先指定付款方式,即使是未 (例如 Three.js) 的廣告活動,也能以類似的方式執行成效。

這與匯入 AS3 的做法類似,因此看起來會不出奇怪。如果您最終存在更多相依檔案,是可能的解決方案。

r.js

RequireJS 包含稱為 r.js 的最佳化工具。這會將主要 js 和所有相依 js 檔案合併成一個,然後使用 UglifyJS (或 Closure Compiler) 將其壓縮。這樣可以減少瀏覽器需要載入的檔案數量和總量。World Wide Maze 的 JavaScript 總檔案大小約為 2 MB,且採用 r.js 最佳化功能可縮減至約 1 MB。如果遊戲可以使用 gzip 發行,將進一步縮減為 250 KB。(GAE 的問題不支援傳輸 1 MB 以上 gzip 檔案,因此遊戲目前未壓縮為 1 MB 的純文字檔案)。

舞台建構工具

階段資料產生方式如下,完全在美國境內的 GCE 伺服器中執行:

  1. 要轉換成階段的網站網址會透過 WebSocket 傳送。
  2. PhantomJS 會擷取螢幕截圖,然後擷取 div 和 img 標記位置並以 JSON 格式輸出。
  3. 根據步驟 2 的螢幕截圖及放置 HTML 元素資料的位置,自訂 C++ (OpenCV、Boost) 程式會刪除不必要的區域、產生島嶼、連結島與橋樑、計算護欄與項目位置、設定目標點等等。結果會以 JSON 格式輸出並傳回至瀏覽器。

PhantomJS

PhantomJS 是一款不需要螢幕的瀏覽器。這項功能可以在不開啟視窗的情況下載入網頁,可用於自動測試或在伺服器端擷取螢幕截圖。該公司的瀏覽器引擎是 WebKit,與 Chrome 和 Safari 採用相同的引擎,因此其版面配置和 JavaScript 的執行結果,也與標準瀏覽器大致相同。

使用 PhantomJS、JavaScript 或 CoffeeScript 編寫要執行的程序。擷取螢幕截圖非常簡單,如此範例所示。我使用的是 Linux 伺服器 (CentOS),所以需要安裝字型才能顯示日文 (M+ 字型)。儘管如此,字型的顯示方式與 Windows 或 Mac OS 的處理方式不同,所以同一個字型在其他電腦上的外觀也可能有所不同 (雖然差異很小)。

擷取 img 和 div 標記位置的方式基本上與標準網頁中相同,您也可以使用 jQuery。

stage_builder

我一開始考慮使用更以 DOM 為基礎的方法產生階段 (類似於 Firefox 3D 檢查器),並嘗試在 PhantomJS 中進行 DOM 分析。最後,我決定採取圖片處理做法。為了達成這個目標,我編寫了一個名為「stage_builder」的 C++ 程式,以及使用 OpenCV 和 Boost 的程式。它會執行以下作業:

  1. 載入螢幕截圖和 JSON 檔案。
  2. 將圖片和文字轉換成「島嶼」。
  3. 建立橋樑連接這些島嶼。
  4. 刪除不必要的橋樑,形成迷宮。
  5. 放置大型項目。
  6. 放置小項目。
  7. 地點護欄。
  8. 這個外掛程式能以 JSON 格式輸出定位資料。

以下詳述每個步驟。

載入螢幕截圖和 JSON 檔案

一般會使用 cv::imread 載入螢幕截圖。我測試了多個 JSON 檔案程式庫,但 picojson 似乎最容易使用。

將圖片和文字轉換成「獨立」

階段建構

以上是 aid-dcc.com「新聞」部分的螢幕截圖 (按一下即可查看實際大小)。圖片和文字元素都必須轉換為島嶼。為區隔這些部分,我們應刪除白色的背景顏色,也就是螢幕截圖中最常見的顏色。完成之後看起來會像這樣:

階段建構

白色的部分是指可能的島嶼。

文字太過清晰,因此我們將使用 cv::dilatecv::GaussianBlurcv::threshold 加長。此外,由於 PhantomJS 的 img 標記資料輸出,我們仍會在這些區域填滿白色區域。產生的圖片如下所示:

階段建構

文字現已形成合適的線索,而每張圖片都是一個合適的島嶼。

建立橋樑連接兩座島嶼

島嶼準備就緒後,它們就會與橋樑連接。每個島都會尋找左、右、上、下方和相鄰的島嶼,然後將橋樑連接至最近島嶼的最接近點,如下所示:

階段建構

消除不必要的橋樑,建設迷宮

保留所有橋樑會使舞台不易瀏覽,因此必須刪除一些橋樑才能形成迷宮。系統會選擇將一個島上 (例如左上角的) 做為起點,然後刪除連結至該島上的所有小島 (例如隨機選取)。接著,在另一座連接其他橋樑的島上做了同樣的事。這條路線一路抵達死路或回到先前造訪過的島嶼時,就會回到重新通往新島嶼的途中。所有島嶼都以這種方式處理後,您就可以完成迷宮。

階段建構

放置大型項目

每座島嶼上放置一或多個大型物品 (視其尺寸而定),從島嶼邊緣最遠的點中選擇。雖然不太清楚,但這些點是以紅色顯示:

階段建構

從以上所有點中,左上方的是起點 (紅色圓圈),右下角的一個設為目標 (綠色圓圈),其餘最多 6 個點則被選擇用於大型項目 (紫色圓圈)。

放置小物品

階段建構

適合在離島邊緣的定線位置放置適合的小型物品。上圖 (不是來自 aid-dcc.com) 的圖片將投影的放置線以灰色顯示,並與島嶼邊緣的固定間隔顯示位置。紅點表示小型項目的位置。由於這張圖片是開發中版本,項目會以直線排列,但最終版本會稍微疏散到灰色線條兩側。

設置護欄

護欄基本上就是沿著各島的外圍界線,但必須在橋樑上切斷才能進入。事實證明,Boost 幾何圖形程式庫能滿足這一點,簡化幾何計算作業,例如判斷島嶼邊界資料與橋接器兩側的線條重疊的位置。

階段建構

島嶼周圍的綠線就是護欄。要在這張圖片中看得很困難,但因為沒有綠線。這是用於偵錯的最終圖片,其中所有需要輸出至 JSON 的物件都會包含在內。淺藍點是小型項目,灰點則代表重新啟動點。當球落入海邊時,遊戲會從最近的重新啟動點恢復計時。重新啟動點的排列方式會大或較少,與小型項目相同的方式,在離島邊緣的固定距離內定時間隔。

以 JSON 格式輸出定位資料

我也使用 picojson 提供輸出。它會將資料寫入標準輸出,而呼叫端 (Node.js) 才會收到輸出資料。

在 Mac 上建立 C++ 程式,以便在 Linux 中執行

遊戲是在 Mac 上開發,並部署在 Linux 上,但由於 OpenCV 和 Boost 同時適用於這兩種作業系統,因此只要建立編譯環境,開發本身就相當困難。我使用 Xcode 的指令列工具在 Mac 上對版本進行偵錯,然後使用 automake/autoconf 建立設定檔,以便在 Linux 中編譯這個版本然後我只需在 Linux 中使用「configure & make& Make」選項,即可建立可執行檔案。因為編譯器版本差異,我遇到 Linux 特有的錯誤,但使用 gdb 可以相對輕鬆解決。

結論

這種遊戲可以透過 Flash 或 Unity 製作,帶來許多優勢。不過,這個版本不需要外掛程式,而 HTML5 + CSS3 的版面配置功能已證明非常強大。因此,每項工作都應使用合適的工具是件非常重要的事。我個人感到驚訝的是,這款遊戲完全是以 HTML5 的形式製作而成,但目前許多領域仍缺乏這項技術,我很期待未來能見證這項技術的發展。