HTML5開(kāi)發(fā)Kinect體感游戲的實(shí)例應(yīng)用
HTML5開(kāi)發(fā)Kinect體感游戲的實(shí)例應(yīng)用
一、簡(jiǎn)介
我們要做的是怎樣一款游戲?
在前不久成都TGC2016展會(huì)上,我們開(kāi)發(fā)了一款《火影忍者手游》的體感游戲,主要模擬手游章節(jié)《九尾襲來(lái) 》,用戶化身四代,與九尾進(jìn)行對(duì)決,吸引了大量玩家參與。 表面上看,這款游戲與其它體感體驗(yàn)無(wú)異,實(shí)際上,它一直運(yùn)行于瀏覽器Chrome下,也就是說(shuō),我們只需要掌握前端相應(yīng)技術(shù),就可以開(kāi)發(fā)基于Kinect的網(wǎng)頁(yè)體感游戲。
二、實(shí)現(xiàn)原理
實(shí)現(xiàn)思路是什么?
使用H5開(kāi)發(fā)基于Kinect的體感游戲,其實(shí)工作原理很簡(jiǎn)單,由Kinect采集到玩家及環(huán)境數(shù)據(jù),比如人體骨骼,使用某種方式,使瀏覽器可以訪問(wèn)這些數(shù)據(jù)。
1、采集數(shù)據(jù)
Kinect有三個(gè)鏡頭,中間鏡頭類(lèi)似普通攝像頭,獲取彩色圖像。左右兩邊鏡頭則是通過(guò)紅外線獲取深度數(shù)據(jù)。我們使用微軟提供的SDK去讀取以下類(lèi)型數(shù)據(jù):
- 色彩數(shù)據(jù):彩色圖像;
- 深度數(shù)據(jù):顏色嘗試信息;
- 人體骨骼數(shù)據(jù):基于以上數(shù)據(jù)經(jīng)計(jì)算,獲取到人體骨骼數(shù)據(jù)。
2、使瀏覽器可訪問(wèn)到Kinect數(shù)據(jù)
我嘗試和了解過(guò)的框架,基本上是以socket讓瀏覽器進(jìn)程與服務(wù)器進(jìn)行通信 ,進(jìn)行數(shù)據(jù)傳輸:
- Kinect-HTML5 用C#搭建服務(wù)端,色彩數(shù)據(jù)、嘗試數(shù)據(jù)、骨骼數(shù)據(jù)均有提供;
- ZigFu 支持H5、U3D、Flash進(jìn)行開(kāi)發(fā),API較為完整,貌似收費(fèi);
- DepthJS 以瀏覽器插件形式提供數(shù)據(jù)訪問(wèn);
- Node-Kinect2 以Nodejs搭建服務(wù)器端,提供數(shù)據(jù)比較完整,實(shí)例較多。
我最終選用Node-Kinect2,雖然沒(méi)有文檔,但是實(shí)例較多,使用前端工程師熟悉的Nodejs,另外作者反饋比較快。

- Kinect: 捕獲玩家數(shù)據(jù),比如深度圖像、彩色圖像等;
- Node-Kinect2: 從Kinect獲取相應(yīng)數(shù)據(jù),并進(jìn)行二次加工;
- 瀏覽器: 監(jiān)聽(tīng)node應(yīng)用指定接口,獲取玩家數(shù)據(jù)并完成游戲開(kāi)發(fā)。
三、準(zhǔn)備工作
先得買(mǎi)個(gè)Kinect啊
1、系統(tǒng)要求:
這是硬性要求,我曾在不符合要求的環(huán)境下浪費(fèi)太多時(shí)間。
- USB3.0
- 支持DX11的顯卡
- win8及以上系統(tǒng)
- 支持Web Sockets的瀏覽器
- 當(dāng)然Kinect v2傳感器是少不了的
2、環(huán)境搭建流程:
- 連接上Kinect v2
- 安裝 KinectSDK-v2.0
- 安裝 Nodejs
- 安裝 Node-Kinect2
npm install kinect2
四、實(shí)例演示
說(shuō)什么都不如給我一個(gè)例子!
如下圖所示,我們演示如何獲取人體骨骼,并標(biāo)識(shí)脊椎中段及手勢(shì):

1、服務(wù)器端
創(chuàng)建web服務(wù)器,并將骨骼數(shù)據(jù)發(fā)送到瀏覽器端,代碼如下:
var Kinect2 = require('../../lib/kinect2'),
express = require('express'),
app = express(),
server = require('http').createServer(app),
io = require('socket.io').listen(server);
var kinect = new Kinect2();
// 打開(kāi)kinect
if(kinect.open()) {
// 監(jiān)聽(tīng)8000端口
server.listen(8000);
// 指定請(qǐng)求指向根目錄
app.get('/', function(req, res) {
res.sendFile(__dirname + '/public/index.html');
});
// 將骨骼數(shù)據(jù)發(fā)送給瀏覽器端
kinect.on('bodyFrame', function(bodyFrame){
io.sockets.emit('bodyFrame', bodyFrame);
});
// 開(kāi)始讀取骨骼數(shù)據(jù)
kinect.openBodyReader();
}
2、瀏覽器端
瀏覽器端獲取骨骼數(shù)據(jù),并用canvas描繪出來(lái),關(guān)鍵代碼如下:
var socket = io.connect('/');
var ctx = canvas.getContext('2d');
socket.on('bodyFrame', function(bodyFrame){
ctx.clearRect(0, 0, canvas.width, canvas.height);
var index = 0;
// 遍歷所有骨骼數(shù)據(jù)
bodyFrame.bodies.forEach(function(body){
if(body.tracked) {
for(var jointType in body.joints) {
var joint = body.joints[jointType];
ctx.fillStyle = colors[index];
// 如果骨骼節(jié)點(diǎn)為脊椎中點(diǎn)
if(jointType == 1) {
ctx.fillStyle = colors[2];
}
ctx.fillRect(joint.depthX * 512, joint.depthY * 424, 10, 10);
}
// 識(shí)別左右手手勢(shì)
updateHandState(body.leftHandState, body.joints[7]);
updateHandState(body.rightHandState, body.joints[11]);
index++;
}
});
});
很簡(jiǎn)單的幾行代碼,我們便完成了玩家骨骼捕獲,有一定 javascript基礎(chǔ)的同學(xué)應(yīng)該很容易能看明白,但不明白的是我們能獲取哪些數(shù)據(jù)?如何獲???骨骼節(jié)點(diǎn)名稱(chēng)分別是什么?而node-kienct2并沒(méi)有文檔告訴我們這些。
五、開(kāi)發(fā)文檔
Node-Kinect2并沒(méi)有提供文檔,我將我測(cè)試總結(jié)的文檔整理如下:
1、服務(wù)器端能提供的數(shù)據(jù)類(lèi)型;
kinect.on('bodyFrame', function(bodyFrame){}); //還有哪些數(shù)據(jù)類(lèi)型呢?
| bodyFrame | 骨骼數(shù)據(jù) |
| infraredFrame | 紅外數(shù)據(jù) |
| longExposureInfraredFrame | 類(lèi)似infraredFrame,貌似精度更高,優(yōu)化后的數(shù)據(jù) |
| rawDepthFrame | 未經(jīng)處理的景深數(shù)據(jù) |
| depthFrame | 景深數(shù)據(jù) |
| colorFrame | 彩色圖像 |
| multiSourceFrame | 所有數(shù)據(jù) |
| audio | 音頻數(shù)據(jù),未測(cè)試 |
2、骨骼節(jié)點(diǎn)類(lèi)型
body.joints[11] // joints包括哪些呢?
| 節(jié)點(diǎn)類(lèi)型 | JointType | 節(jié)點(diǎn)名稱(chēng) |
| 0 | spineBase | 脊椎基部 |
| 1 | spineMid | 脊椎中部 |
| 2 | neck | 頸部 |
| 3 | head | 頭部 |
| 4 | shoulderLeft | 左肩 |
| 5 | elbowLeft | 左肘 |
| 6 | wristLeft | 左腕 |
| 7 | handLeft | 左手掌 |
| 8 | shoulderRight | 右肩 |
| 9 | elbowRight | 右肘 |
| 10 | wristRight | 右腕 |
| 11 | handRight | 右手掌 |
| 12 | hipLeft | 左屁 |
| 13 | kneeLeft | 左膝 |
| 14 | ankleLeft | 左踝 |
| 15 | footLeft | 左腳 |
| 16 | hipRight | 右屁 |
| 17 | kneeRight | 右膝 |
| 18 | ankleRight | 右踝 |
| 19 | footRight | 右腳 |
| 20 | spineShoulder | 頸下脊椎 |
| 21 | handTipLeft | 左手指(食中無(wú)?。?/td> |
| 22 | thumbLeft | 左拇指 |
| 23 | handTipRight | 右手指 |
| 24 | thumbRight | 右拇指 |
3、手勢(shì),據(jù)測(cè)識(shí)別并不是太準(zhǔn)確,在精度要求不高的情況下使用
| 0 | unknown | 不能識(shí)別 |
| 1 | notTracked | 未能檢測(cè)到 |
| 2 | open | 手掌 |
| 3 | closed | 握拳 |
| 4 | lasso | 剪刀手,并合并中食指 |
4、骨骼數(shù)據(jù)
body [object] {
bodyIndex [number]:索引,允許6人
joints [array]:骨骼節(jié)點(diǎn),包含坐標(biāo)信息,顏色信息
leftHandState [number]:左手手勢(shì)
rightHandState [number]:右手手勢(shì)
tracked [boolean]:是否捕獲到
trackingId
}
5、kinect對(duì)象
| on | 監(jiān)聽(tīng)數(shù)據(jù) |
| open | 打開(kāi)Kinect |
| close | 關(guān)閉 |
| openBodyReader | 讀取骨骼數(shù)據(jù) |
| open**Reader | 類(lèi)似如上方法,讀取其它類(lèi)型數(shù)據(jù) |
六、實(shí)戰(zhàn)總結(jié)
火影體感游戲經(jīng)驗(yàn)總結(jié)
接下來(lái),我總結(jié)一下TGC2016《火影忍者手游》的體感游戲開(kāi)發(fā)中碰到的一些問(wèn)題。
1、講解之前,我們首先需要了解下游戲流程。
|
1.1、通過(guò)手勢(shì)觸發(fā)開(kāi)始游戲 |
1.2、玩家化身四代,左右跑動(dòng)躲避九尾攻擊 |
|
1.3、擺出手勢(shì)“奧義”,觸發(fā)四代大招 |
1.4、用戶掃描二維碼獲取自己現(xiàn)場(chǎng)照片 |
2、服務(wù)器端
游戲需要玩家骨骼數(shù)據(jù)(移動(dòng)、手勢(shì)),彩色圖像數(shù)據(jù)(某一手勢(shì)下觸發(fā)拍照),所以我們需要向客戶端發(fā)送這兩部分?jǐn)?shù)據(jù)。值得注意的是,彩色圖像數(shù)據(jù)體積過(guò)大,需要進(jìn)行壓縮。
var emitColorFrame = false;
io.sockets.on('connection', function (socket){
socket.on('startColorFrame', function(data){
emitColorFrame = true;
});
});
kinect.on('multiSourceFrame', function(frame){
// 發(fā)送玩家骨骼數(shù)據(jù)
io.sockets.emit('bodyFrame', frame.body);
// 玩家拍照
if(emitColorFrame) {
var compression = 1;
var origWidth = 1920;
var origHeight = 1080;
var origLength = 4 * origWidth * origHeight;
var compressedWidth = origWidth / compression;
var compressedHeight = origHeight / compression;
var resizedLength = 4 * compressedWidth * compressedHeight;
var resizedBuffer = new Buffer(resizedLength);
// ...
// 照片數(shù)據(jù)過(guò)大,需要壓縮提高傳輸性能
zlib.deflate(resizedBuffer, function(err, result){
if(!err) {
var buffer = result.toString('base64');
io.sockets.emit('colorFrame', buffer);
}
});
emitColorFrame = false;
}
});
kinect.openMultiSourceReader({
frameTypes: Kinect2.FrameType.body | Kinect2.FrameType.color
});
3、客戶端
客戶端業(yè)務(wù)邏輯較復(fù)雜,我們提取關(guān)鍵步驟進(jìn)行講解。
3.1、用戶拍照時(shí),由于處理的數(shù)據(jù)比較大,為防止頁(yè)面出現(xiàn)卡頓,我們需要使用web worker
(function(){
importScripts('pako.inflate.min.js');
var imageData;
function init() {
addEventListener('message', function (event) {
switch (event.data.message) {
case "setImageData":
imageData = event.data.imageData;
break;
case "processImageData":
processImageData(event.data.imageBuffer);
break;
}
});
}
function processImageData(compressedData) {
var imageBuffer = pako.inflate(atob(compressedData));
var pixelArray = imageData.data;
var newPixelData = new Uint8Array(imageBuffer);
var imageDataSize = imageData.data.length;
for (var i = 0; i < imageDataSize; i++) {
imageData.data[i] = newPixelData[i];
}
for(var x = 0; x < 1920; x++) {
for(var y = 0; y < 1080; y++) {
var idx = (x + y * 1920) * 4;
var r = imageData.data[idx + 0];
var g = imageData.data[idx + 1];
var b = imageData.data[idx + 2];
}
}
self.postMessage({ "message": "imageReady", "imageData": imageData });
}
init();
})();
3.2、接投影儀后,如果渲染面積比較大,會(huì)出現(xiàn)白屏,需要關(guān)閉瀏覽器硬件加速。

3.3、現(xiàn)場(chǎng)光線較暗,其它玩家干擾,在追蹤玩家運(yùn)動(dòng)軌跡的過(guò)程中,可能會(huì)出現(xiàn)抖動(dòng)的情況,我們需要去除干擾數(shù)據(jù)。(當(dāng)突然出現(xiàn)很大位移時(shí),需要將數(shù)據(jù)移除)
var tracks = this.tracks;
var len = tracks.length;
// 數(shù)據(jù)過(guò)濾
if(tracks[len-1] !== window.undefined) {
if(Math.abs(n - tracks[len-1]) > 0.2) {
return;
}
}
this.tracks.push(n);
3.4、當(dāng)玩家站立,只是左右少量晃動(dòng)時(shí),我們認(rèn)為玩家是站立狀態(tài)。
// 保留5個(gè)數(shù)據(jù)
if(this.tracks.length > 5) {
this.tracks.shift();
} else {
return;
}
// 位移總量
var dis = 0;
for(var i = 1; i < this.tracks.length; i++) {
dis += this.tracks[i] - this.tracks[i-1];
}
if(Math.abs(dis) < 0.01) {
this.stand();
} else {
if(this.tracks[4] > this.tracks[3]) {
this.turnRight();
} else {
this.turnLeft();
}
this.run();
}
七、展望
1、使用HTML5開(kāi)發(fā)Kinect體感游戲,降低了技術(shù)門(mén)檻,前端工程師可以輕松的開(kāi)發(fā)體感游戲;
2、大量的框架可以應(yīng)用,比如用JQuery、CreateJS、Three.js(三種不同渲染方式);
3、無(wú)限想象空間,試想下體感游戲結(jié)合webAR,結(jié)合webAudio、結(jié)合移動(dòng)設(shè)備,太可以挖掘的東西了……想想都激動(dòng)不是么!
如有疑問(wèn)請(qǐng)留言或者到本站社區(qū)交流討論,感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
微信小程序tabBar組件切換與下拉刷新實(shí)現(xiàn)詳解
tabBar相對(duì)而言用的還是比較多的,但是用起來(lái)并沒(méi)有難,下面這篇文章主要給大家介紹了關(guān)于微信小程序全局配置之tabBar的相關(guān)資料,文中通過(guò)圖文以及示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10
基于JavaScript實(shí)現(xiàn)下拉列表左右移動(dòng)代碼
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)下拉列表左右移動(dòng)效果,代碼簡(jiǎn)單易懂非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
微信小程序textarea層級(jí)過(guò)高的解決方法
這篇文章主要給大家介紹了關(guān)于微信小程序textarea層級(jí)過(guò)高問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
js 純數(shù)字不重復(fù)排列的另類(lèi)方法
有一組數(shù)據(jù),大概10萬(wàn)個(gè)左右,每一單位的值不會(huì)大于30000,要求按照由大到小的順序不重復(fù)輸出。2010-07-07
創(chuàng)建js對(duì)象和js類(lèi)的方法匯總
這篇文章主要介紹了2種創(chuàng)建js對(duì)象和js類(lèi)的方法,十分的簡(jiǎn)單,推薦給大家。2014-12-12
使用JavaScript和CSS實(shí)現(xiàn)文本隔行換色的方法
這篇文章主要介紹了使用JavaScript和CSS實(shí)現(xiàn)文本隔行換色的方法,當(dāng)然最普通的也可以單純用CSS實(shí)現(xiàn),需要的朋友可以參考下2015-11-11





