如何利用Leap Motion創建支持手勢操控的Web游戲
譯文有了Leap Motion,大家可以跟游戲手柄說拜拜了。
現代智能手機憑借著以手勢與觸控為核心的新型用戶界面為我們帶來前所未有的使用體驗。隨著時間的不斷推移,我們幾乎可以肯定地認為,在計算的未來中觸控技術將扮演愈發重要的關鍵性角色。不過話雖如此,一系列艱難挑戰仍然橫亙在開發者面前、使他們很難在臺式設備上充分發揮觸控機制的巨大優勢。
深度相機與3D位置追蹤器的出現隱隱給觸控式互動操作的迅速普及指出了一條光明的發展道路。雖然目前的最新一代技術仍然遠遠無法與《少數派報告》中的展示效果相提并論,但其中蘊含的可觀潛力卻絕對不容置疑。
就在去年,我們曾經對Leap Motion控制器進行過一番評測——這款設備利用高精度動作捕捉與手指追蹤技術成功在任何標準化臺式機上實現了手勢輸入機制。作為一套經由標準USB端口連接的設備,Leap Motion控制器通過其內置攝像頭與紅外LED來捕捉用戶手指與手部的細微活動。Leap Motion軟件會對圖像數據加以處理,并將這些信息翻譯成手勢以及觸控事件。
Leap Motion提供一套指向性極廣的SDK,旨在幫助開發人員更輕松地在自己的應用程序當中實現對該設備的支持。它支持跨越多種平臺的一系列不同編程語言——除了原生桌面軟件之外,Leap Motion還提供一套JavaScript庫,從而通過構建Leap兼容性網站的方式將開發成果在Web瀏覽器上加以呈現。
Leap Motion SDK非常容易上手,掌握之后能為我們帶來極為豐厚的回報。它以抽象化方式消除了控制器所固有的大部分復雜因素,通過高級別API實現了對手部及手指追蹤數據的捕捉。在今天的文章中,我們將共同探討如何構建起一款能夠充分發揮Leap Motion追蹤功能優勢的前端Web應用程序。作為初始內容,我們先來說明如何在HTML Canvas元素上渲染手指定位點,而后再一步步討論怎樣利用Pixi.js圖形庫來配合Leap Motion控制機制開發出簡單的2D游戲。
正式開始
Leap Motion JavaScript庫依賴于WebSockets將來自控制器的數據提交并顯示在用戶的Web瀏覽器之上。WebSockets標準的設計目標在于允許JavaScript代碼運行在網頁當中,從而確保網頁與遠程服務器之間始終保持有長效連接——這一特性通常被用于創建基于瀏覽器的聊天客戶端以及其它一些實時類Web應用程序。
當用戶部署好了自己的Leap Motion設備、并為其安裝了附帶的軟件及驅動程序之后,其中作為內置軟件組件之一的輕量級WebSocket服務器就會運行在用戶計算機的后臺進程當中。由Leap Motion控制器捕捉到的數據會被發往WebSocket服務器,這是為了能夠讓相關數據直接交由Web瀏覽器使用、而無需另行安裝額外的瀏覽器插件。Leap Motion JavaScript庫與本地WebSocket服務器相連,負責捕捉數據并利用部分簡單API進行打包以保證其易于使用。
作為開發工作的第一步,讓我們首先創建一個網頁,并在其中載入Leap Motion JavaScript庫、獲取來自設備的數據并記錄下瀏覽器調試控制臺中的部分數據:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- </head>
- <body>
- </body>
- <script type="text/javascript">
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0)
- console.log(frame.pointables);
- });
- </script> </html>
上述代碼的head元素中包含一個script標簽,它的作用是從公司的CDN處下載Leap Motion JavaScript庫。Leap Motion針對生產使用環境推出一套精簡版本,此外還針對開發用途提供非精簡版本。在這里我們使用的是非精簡版本,這是為了能夠在需要使用瀏覽器的JavaScript調試器時能夠更輕松地對代碼進行單步調試。大家可以點擊此處訪問Leap Motion官方網站,并在這里下載到前面提到的這兩種版本。
在第二個script標簽中,也就是頁面body之下,我們利用Leap.loop方法對來自設備的數據進行捕捉。Leap Motion驅動程序會發出數據“幀(frame)”,這些幀也就是經過處理的控制器視頻流快照。該軟件每秒大約會產生30幀數據,從而持續不斷地為應用程序的運行提供必要信息。被傳遞至該loop中的匿名函數在每次接收到新幀時都會執行一次。
Leap Motion API大大簡化了對手部、手指以及工具位置的檢測流程。這里的“工具”被認定為一種延長狀物體,例如鉛筆,其中一端由用戶把持在手中。在Leap Motion的表述體系當中,通用術語“指向物(pointable)”被用于描述作為工具或者手指存在的對象。幀對象當中包含一項名為pointables的屬性,用于顯示一系列顯示在幀內的指向物對象。
在前面的示例中,每一幀內所包含的一系列pointables都被輸出至控制臺當中。如果大家檢查這些被推送至控制臺的指向物對象,就會發現其中有多項屬性被用于描述顯示信息——例如指向物的長度與寬度、指向物末端的空間坐標以及該指向物末端的移動速度等。
在火狐開發者控制臺中查看指向物對象。
計算標準化手指位置
利用指向物數據,我們將建立一套簡單的演示范例,用戶可以利用它通過移動手指控制屏幕上某個元素的位置。第一步,我們需要獲取指尖的具體位置。下面的示例代碼為如何獲取單個指向物的原始坐標:
- Leap.loop(function(frame)
- { if (frame.pointables.length > 0)
- { var position = frame.pointables[0].tipPosition;
- console.log("X: " + position[0] + " Y: " + position[1]);
- }
- });
在多數情況下,大家都會優先使用Leap Motion軟件所提供的自動穩定功能,而不太可能直接使用原始tipPosition數據。Leap Motion控制器會以極高的精度對圖像中的活動對象加以檢測,并從中甄別出使用者手部幾乎難以察覺的細微搖晃及活動。另一項名為stabilizedTipPosition的備選屬性則允許我們收集同樣的數據,但在結果中過濾掉上述細微活動。現在我們已經獲得了用戶手指的物理坐標,接下來要做的就是將其與瀏覽器窗口內的位置進行關聯。
在Leap Motion的術語體系中,由控制器加以追蹤的大型虛擬空間被稱為互動框(interaction box)。幀對象的interactionBox屬性當中包含多種方法與屬性,它們負責提供與互動框及其維度相關的具體信息。它采用一種便捷的標準化方法,即將某個物理點的原始坐標換算成能夠代表該點在互動框中相對位置的等效數值。
標準化定位機制非常實用,因為它能幫助我們獲取手指的原始位置、并將其與應用程序窗口中的對應點加以映射。標準化坐標使用浮點數值格式,具體數值在0到1之間浮動。要想獲取瀏覽器窗口中對應點的相對數值,大家只需將X與Y值同目標區域的高度與寬度相乘即可:
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0) {
- var position = frame.pointables[0].stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var x = window.innerWidth * normalized[0];
- var y = window.innerHeight * (1 - normalized[1]);
- console.log("X: " + x + " Y: " + y);
- }
- });
在屏幕上繪制手指位置
利用標準化定位機制,現在我們已經可以在網頁上繪制手指的具體位置。為了簡單起見,這篇文章將只向大家展示如何利用DOM元素實現這一目標。只需創建一個具備絕對位置的div,而后將其top與left屬性分別設置為對應的X與Y坐標,我們就能將Leap Motion loop中的標準化函數用于定位:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- <style type="text/css">
- #position {
- width: 25px;
- height: 25px;
- position: absolute;
- background-color: blue;
- }
- </style>
- </head>
- <body>
- <div id="position"></div>
- </body>
- <script type="text/javascript">
- Leap.loop(function(frame) {
- if (frame.pointables.length > 0) {
- var position = frame.pointables[0].stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var element = document.getElementById("position");
- element.style.left = window.innerWidth * normalized[0];
- element.style.top = window.innerHeight * (1 - normalized[1]);
- }
- });
- </script>
- </html>
這樣一來,當用戶在Leap Motion控制器前方移動自己的手指時,屏幕上的div元素也將隨之發生位移。在目前為止的示例中,我們只涉及單一手指的處理,也就是使用pointables數組中的第一個元素。在接下來的示例中,我們將逐步探尋數組中第一個元素的作用、從而使每根手指都成為可操作對象。這一次,我們將在HTML 5 Canvas上描繪手指位置、而不再使用DOM元素:
- <html>
- <head>
- <script src="http://js.leapmotion.com/leap-0.4.2.js"></script>
- </head>
- <body>
- <canvas id="canvas" width="800" height="600"></canvas>
- </body>
- <script type="text/javascript">
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext("2d");
- Leap.loop({frameEventName: "animationFrame"}, function(frame) {
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
- frame.pointables.forEach(function(pointable) {
- var position = pointable.stabilizedTipPosition;
- var normalized = frame.interactionBox.normalizePoint(position);
- var x = ctx.canvas.width * normalized[0];
- var y = ctx.canvas.height * (1 - normalized[1]);
- ctx.beginPath();
- ctx.rect(x, y, 20, 20);
- ctx.fill();
- });
- });
- </script>
- </html>
在上面的示例中,我們利用forEach方法對pointable項目進行了遍歷。它會對每一個標準化位置進行識別,而后將其作為矩形繪制在屏幕當中。此外,clearRect方法在處理每一幀圖像時都會被調用一次,旨在確保前一幀所繪制的矩形切實得到清除。
上述示例當中還引入了另一項新功能,即frameEventName選項。在默認狀態下,Leap.loop回調將被調用至每一個提取自Leap Motion控制器的幀數據。對frameEventName選項中的“animationFrame”值進行設置會改變這一行為機制,從而令Leap.loop與瀏覽器的繪制周期保持一致。它會利用瀏覽器的requestAnimationFrame API,從而確保該回調只在瀏覽器準備進行繪制時被調用。
檢測手勢
除了手指位置,Leap Motion SDK還能夠識別出其它幾種手勢動作,其中包括掃動與點觸。Leap Motion幀對象當中包含一個手勢屬性,能夠提取從幀數據中檢測出的一系列手勢信息。以下示例代碼顯示了如何對掃動手勢進行迭代、對其起始與結束位置進行標準化處理并最終將結果繪制在Canvs當中:
- var canvas = document.getElementById("canvas");
- var ctx = canvas.getContext("2d");
- var options = {
- enableGestures: true, frameEventName: "animationFrame" };
- Leap.loop(options, function(frame) {
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
- frame.gestures.forEach(function(gesture) {
- if (gesture.type != "swipe") return;
- var start = frame.interactionBox.normalizePoint(gesture.startPosition);
- var end = frame.interactionBox.normalizePoint(gesture.position);
- var startX = ctx.canvas.width * start[0];
- var startY = ctx.canvas.width * (1 - start[1]);
- var endX = ctx.canvas.width * end[0];
- var endY = ctx.canvas.width * (1 - end[1]);
- ctx.beginPath();
- ctx.moveTo(startX, startY);
- ctx.lineTo(endX, endY);
- ctx.stroke();
- });
- });
Leap Motion SDK在默認狀態下不會顯示手勢。為了獲取手勢數據,我們必須將選項對象的enableGestures屬性設為true,而后再將其傳遞至Leap.loop方法當中。如果該選項沒有經過設置,那么frame.gestures數組將直接為空。
每一種手勢都具備一項type屬性,用于描述該手勢的相關性質。在forEach loop當中,首先利用條件表達式來忽略不屬于掃動的手勢動作。接下來,利用interactionBox對手勢的起始與結束位置進行標準化處理。最后,利用標準Canvas繪制API來描繪由起始位置到結束位置的線條。
在面對Leap Motion設備進行手部掃動操作時,大家應該能夠在Canvas上看到自己的動作軌跡線條。手勢檢測在更為廣義的互動操作之下起著非常關鍵的作用。舉例來說,大家可以通過向左或者向右的掃動操作幫助用戶導航至相冊界面的上一個或者下一個顯示條目當中。
為了找到更多明確的黑色方塊,大家往往盡快得焦頭爛額。
下頁精彩內容繼續
#p#
利用Leap Motion與Pixi.js創建一款簡單游戲
現在我們已經介紹了關于Leap Motion開發的基礎知識,是時候嘗試建立一些更具現實意義的實驗性成果了。在接下來的示例中,我們將一步步幫助大家構建起一款能夠與Leap Motion控制器相對接的互動游戲。在該游戲中,大家可以移動自己的手指來控制屏幕上的宇宙飛船。玩家必須小心駕駛自己的飛船,從而避免與敵人的船只相撞。
我們利用Pixi.js——一套簡單的2D圖形庫——進行游戲創建,它能夠幫助大家輕松創建并操作HTML Canvas元素。底層Leap Motion控制方案則基于文章前面提到的單手指操作示例。在游戲中,經過標準化X與Y坐標處理的手指位置將被映射到畫面中的飛船之上,也就是玩家所需要操作的對象。
為了幫助大家更輕松地遵照示例進行,我們在代碼當中穿插注釋來幫助各位讀者順利理解。下面來看游戲代碼:
- <html>
- <head>
- <script src="assets/pixi.dev.js" type="text/javascript">
- </script>
- <script src="http://js.leapmotion.com/leap-0.4.2.js">
- </script>
- <style type="text/css">
- #game {
- margin-left: auto;
- margin-right: auto;
- width: 800px;
- height: 600px;
- }
- </style>
- </head>
- <body>
- <div id="game"></div>
- </body>
- <script type="text/javascript">
- var stageWidth = 800;
- var stageHeight = 600;
- //創建關卡與渲染引擎
- var stage = new PIXI.Stage(0xFFFFFF);
- var render = new PIXI.autoDetectRenderer(stageWidth, stageHeight); document.getElementById("game").appendChild(render.view);
- // 創建動態星空背景
- var spacebg = PIXI.Texture.fromImage("assets/spacebg.png");
- var space = new PIXI.TilingSprite(spacebg, stageWidth, stageHeight);
- stage.addChild(space);
- // 創建飛船對象
- var rocket = PIXI.Sprite.fromImage("assets/rocketship.png");
- stage.addChild(rocket);
- //這條變量用于控制游戲狀態。
- //當飛船與短文碰撞時,此變量將被設置為“true”,
- //這時draw loop中止并顯示“游戲結束”字樣
- var collided = false;
- //在計時器中設置初始值
- //更多關于時間與游戲速度的詳細信息
- var timer = window.performance.now();
- //設置Leap Motion控制器loop
- var options = {frameEventName: "animationFrame"};
- var controller = Leap.loop(options, function(frame) {
- //我希望游戲中的各個元素能夠始終以同樣的速度進行移動。
- //如果激活了draw loop,
- //游戲速度將根據幀速率進行變化。
- //我利用渲染時間差來確定實際距離
- //場景中的每個元素都應該以每一幀為單位進行移動。
- var now = window.performance.now();
- var delta = Math.min(now - timer, 100);
- timer = now;
- //讓星空背景保持移動
- space.tilePosition.x -= 0.2 * delta;
- //檢查“游戲結束”是否處于活動狀態
- if (collided) {
- //創建并顯示“游戲結束”標題
- var caption = new PIXI.Text("Game Over", {
- font: "50px Helvetica", fill: "red"
- });
- caption.x = stageWidth / 2 - caption.width / 2;
- caption.y = stageHeight / 2;
- stage.addChild(caption);
- //渲染該幀并調用返回結果
- //場景中的各元素保持靜態
- return render.render(stage);
- }
- //遍歷關卡當中的每一艘敵方船只
- space.children.forEach(function(child) {
- // 使敵方船只向前移動
- child.x -= 0.2 * delta;
- //測宇宙飛船的中心點是否處于敵方船只的邊界當中。
- //這是一種非常簡單的處理方式。
- //大家也可以選擇采用更為智能的多碰撞檢測機制
- //即利用“hitArea”設置更為精確的碰撞邊界。
- //如果用戶與敵機接觸,則變更“collided”值以結束游戲
- if (child.getBounds().contains(rocket.x, rocket.y))
- collided = true;
- //移除移動至屏幕邊緣以外的敵方船只
- if (child.x < -child.width)
- space.removeChild(child);
- });
- //找到剛剛被添加到關卡當中的宇宙飛船
- var last = space.children[space.children.length - 1];
- //如果屏幕上不存在宇宙飛船,則加入一艘新的船只
- //如果剛剛添加的船只距離右側邊緣超過250px,則再增加一艘新的船只。
- //這就是我們在游戲中派遣敵方船只的方式。在實際游戲中,
- //大家可以進一步調低250px這一空間設置以增加游戲難度
- if (space.children.length == 0 || last.x < (stageWidth - 250)) {
- var item = PIXI.Sprite.fromImage("assets/enemy.png");
- item.y = Math.floor((Math.random() * (stageHeight - 100)));
- item.x = stageWidth;
- space.addChild(item);
- }
- if (frame.pointables.length > 0) {
- //獲取標準化手指位置
- var pos = frame.pointables[0].stabilizedTipPosition;
- var normPos = frame.interactionBox.normalizePoint(pos, true);
- //將宇宙飛船移動至標準化手指位置
- rocket.x = stageWidth * normPos[0];
- rocket.y = stageHeight * (1 - normPos[1]);
- }
- // 渲染場景
- render.render(stage);
- });
- </script>
- </html>
如大家所見,Leap Motion SDK讓創建實時手勢交互游戲變得相對簡單了一些。利用代碼示例中所涉及的開發技術,大家可以很輕松地創建出多種不同類型的游戲或者應用程序。如果大家擁有自己的Leap Motion控制器,不妨嘗試利用沿X軸進行手指操控的方式開發“打磚塊”或者“是男人就下一百層”之類的游戲。大家還可以通過多種方式對上述代碼進行深入擴展,從而添加更多更為豐富的功能。舉例來說,我們可以利用指向物的touchZone屬性讓宇宙飛船在識別到用戶的指向劃動操作時發射激光武器。
“手指?在向前行進時,我們不需要手指。”
我們在GitHub資源庫中發布了本篇文章中所用到的全部代碼內容,感興趣的朋友可以點擊此處進行查看。除此之外,作為獎勵內容我們還利用Three.js制作了一份粗糙的WebGL示例,它能夠顯示出手指位置在三維空間中的追蹤軌跡——點擊此處查看。
游戲中所使用的圖像資源來自Daniel Cook發布的SpaceCute圖像合集,大家可以點擊此處進行下載。如果各位希望使用其它用于游戲原型設計的免費圖形資源,也不妨點擊此處看看Cook同志收集的其它好東西。背景圖像來自OpenGameArt的宇宙飛行游戲入門素材,點擊此處查看。
總結
Leap Motion硬件也許僅僅算是邁向未來的第一步(但卻絕對堪稱是‘人類的一大步’),很明顯其實際表現與《少數派報告》等電影中展示的效果還存在著巨大的差距,不過它的出現已經標志著我們向手勢驅動類計算系統吹響了進軍的號角。由于引入了三維空間概念,這扇剛剛開啟的大門完全有可能引領我們走入更為豐富也更為復雜的交互新時代——這要比我們習以為常的平面觸控機制更加激動人心。
這份教程當中涉及到了JavaScript使用場景,不過Leap Motion還提供面向更多其它不同編程環境的多種SDK。大家既可以輕松創建原生應用,也可以根據實際需要使用腳本化語言。
另外值得一提的是,除了本篇教程中所提到的功能之外、Leap Motion還提供更多有待發掘的珍貴資源。要詳細了解Leap Motion SDK所支持的各項功能,我們強烈建議大家點擊此處查看官方發布的說明文檔。
請大家繼續期待我們本系列專題中的下一篇文章,屆時我們將向大家展示如何利用Pixi.js創建一款更為完整的游戲成品。我們將向其中添加更多游戲性功能——包括更為深入的卷軸滾動周期以及游戲循環機制。此外,我們還會探討如何利用新的HTML 5 Gamepad API讓玩家能夠通過傳統的游戲主機手柄進行操作。
核子可樂譯,點擊查看原文。