├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── client └── web-gamepad.js ├── demo ├── events │ ├── app.js │ ├── index.html │ └── style.css └── tester │ ├── css │ └── style.css │ ├── index.html │ ├── js │ ├── main.js │ └── tester.js │ ├── requirejs.html │ └── seajs.html ├── package.json ├── public ├── config.rb ├── css │ └── index.css ├── images │ ├── bg.png │ ├── icon.ico │ └── icon.png ├── index.html ├── js │ ├── app │ │ ├── WebGamepad.js │ │ ├── app.js │ │ └── utils.js │ ├── index.js │ ├── lib │ │ └── socket.io.min.js │ └── main.js └── sass │ ├── common │ ├── _const.scss │ └── _mixin.scss │ └── index.scss └── server ├── index.js ├── mime.js └── socket.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea 31 | 32 | .sass-cache 33 | *.map -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | requirejs: { 5 | compile: { 6 | options: { 7 | baseUrl: "public/js/", 8 | name: 'app/app', 9 | optimize: 'none', 10 | out: 'public/js/index.js' 11 | } 12 | } 13 | }, 14 | watch: { 15 | dev: { 16 | files: ['public/js/app/**/*.js'], 17 | tasks: ['requirejs'] 18 | } 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 23 | grunt.loadNpmTasks('grunt-contrib-watch'); 24 | 25 | grunt.registerTask('default', ['requirejs', 'watch']); 26 | 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Allenice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-gamepad 2 | web-gamepad 是一个运行在手机浏览器的游戏手柄。游戏引用 client 里面的 web-gamepad.js, 玩家通过扫描二维码连接手柄。web-gamepad.js 还支持真实手柄。建议使用 web-gamepad 之前,请先阅读 [Web gamepad api](https://dvcs.w3.org/hg/gamepad/raw-file/default/gamepad.html) 3 | 4 | ## 在线 demo: 5 | 6 | - [tester](http://demo.allenice233.com/web-gamepad/demo/tester/); 7 | - [events](http://demo.allenice233.com/web-gamepad/demo/events/) 8 | 9 | ## 安装 10 | ```bash 11 | # 克隆代码到本地 12 | git clone git@github.com:Allenice/web-gamepad.git 13 | 14 | # 安装依赖包 15 | npm install 16 | 17 | # 启动 socket 服务器, 建议使用 pm2,supervisor 等工具运行 18 | node server/index.js 19 | ``` 20 | 21 | ### 运行 demo 22 | demo 不需要与 socket 服务器 同域或同端口,启动 socket 服务器后,运行 demo 只需将 client 和 demo 两个文件夹复制到你其他的 http 服务器里面运行。 23 | 24 | ## 游戏接入 25 | 游戏需要支持手柄功能,只需要简单配置即可使用。 26 | ```javascript 27 | // 引用 client 文件夹里面的 web-gamepad.js 后, 配置 28 | WebGamepad.listen({ 29 | socketServer: 'http://yourdomain.com:3000' 30 | }); 31 | 32 | // 显示二维码, 配置了 socketServer 之后才能获得二维码 33 | $('#qrcode').attr('src', WebGamepad.getQrcode()); 34 | 35 | ``` 36 | 如果不配置 socket 服务器的话,只支持真实手柄接入。如果使用 requirejs 或 seajs 的话,请查考 [tester demo](https://github.com/Allenice/web-gamepad/tree/master/demo/tester); 37 | 38 | ## client api 39 | 40 | ### WebGamepad 41 | ```javascript 42 | // 版本 43 | WebGamepad.VERSION = '0.1.1'; 44 | 45 | // 已连接的手柄, 前四个是真实手柄,后面是 web 手柄 46 | WebGamepad.gamepads = []; 47 | 48 | // 获取已连接的手柄,过滤掉 undefined 49 | WebGamepad.getGamepads = function(){}; 50 | 51 | // 获取连接二维码 52 | WebGamepad.getQrcode = function(){}; 53 | 54 | // 事件 55 | WebGamepad.on('connected', function(gamepad) { 56 | console.log('手柄已连接', gamepad); 57 | }).on('update', function(gamepad) { 58 | console.log('手柄状态更新', gamepad); 59 | }).on('disconnected', function(gamepad) { 60 | console.log('手柄断开连接', gamepad); 61 | }); 62 | ``` 63 | 64 | ### 手柄按钮和摇杆常量 65 | ```javascript 66 | // 按钮和摇杆对应的索引值 67 | WebGamepad.BUTTONS = { 68 | FACE_1: 0, // 按钮 1,2,3,4 69 | FACE_2: 1, 70 | FACE_3: 2, 71 | FACE_4: 3, 72 | LEFT_SHOULDER: 4, // L1 73 | RIGHT_SHOULDER: 5, // R1 74 | LEFT_SHOULDER_BOTTOM: 6, // L2 75 | RIGHT_SHOULDER_BOTTOM: 7, // R2 76 | SELECT: 8, 77 | START: 9, 78 | LEFT_ANALOGUE_STICK: 10, // 左摇杆按下的按钮(目前没做) 79 | RIGHT_ANALOGUE_STICK: 11, 80 | PAD_TOP: 12, // 上 81 | PAD_BOTTOM: 13, // 下 82 | PAD_LEFT: 14, // 左 83 | PAD_RIGHT: 15 // 右 84 | }; 85 | 86 | WebGamepad.AXES = { 87 | LEFT_ANALOGUE_HOR: 0, // 左摇杆水平方向 88 | LEFT_ANALOGUE_VERT: 1, // 左摇杆垂直方向 89 | RIGHT_ANALOGUE_HOR: 2, 90 | RIGHT_ANALOGUE_VERT: 3 91 | }; 92 | ``` 93 | 可以看下图对照一下 94 |  95 | 96 | ### WebGamepad.Event 97 | 简单的事件支持,提供 on, off, trigger 三个方法。 98 | ```javascript 99 | var obj = {}; 100 | WebGamepad.utils.extend(obj, WebGamepad.Event); 101 | 102 | // 绑定事件 103 | obj.on('eventName', function() { 104 | console.log('event trigger'); 105 | }); 106 | 107 | // 触发事件 108 | obj.trigger('eventName'); 109 | 110 | // 解除绑定 111 | obj.off('eventName'); 112 | ``` 113 | 114 | ### WebGamepad.GamepadButton, WebGamepad.GamepadAxes 115 | GamepadButton 和 GamepadAxes 的 api 是一样的,只是两个代表的意思不一样,Axes 是摇杆的轴。一个手柄有两个杆,每个杆有 X,Y 两个轴。手柄连接后,可以对按钮和轴进行监听。 116 | ``` 117 | // 手柄连接 118 | WebGamepad.on('connected', function(gamepad) { 119 | // 按钮1按下 120 | gamepad.buttons[WebGamepad.BUTTONS.FACE_1].on('pressed', function() { 121 | console.log('face1 button pressed'); 122 | 123 | // gamepad: 按钮所属的手柄,value: 新值,oldValue: 旧值 124 | console.log(this.gamepad, this.value, this.oldValue); 125 | 126 | }).on('released', function() { 127 | 128 | console.log('face1 button released'); 129 | }); 130 | 131 | // 左摇杆的 x 轴值改变 132 | gamepad.axes[WebGamepad.AXES.LEFT_ANALOGUE_HOR].on('update', function(){ 133 | console.log('update'); 134 | }) 135 | }); 136 | ``` 137 | 138 | ### WebGamepad.Gamepad 139 | 手柄类 140 | ```javascript 141 | { 142 | // 手柄 id,用于区分手柄 143 | id: ''; 144 | 145 | // 手柄数组索引, 0-3 只给真实手柄 146 | index: 0, 147 | 148 | // 状态更新的时间戳 149 | timestamp: new Date(), 150 | 151 | // 手柄按钮 152 | buttons: [], 153 | 154 | // 轴 155 | axes: [] 156 | } 157 | 158 | // 事件 159 | WebGamepad.on('connected', function(gamepad) { 160 | gamepad.on('update', function() { 161 | console.log('gamepad update'); 162 | }); 163 | }); 164 | 165 | WebGamepad.on('disconnected', function(gamepad) { 166 | console.log('gamepad disconnected', gamepad); 167 | }); 168 | ``` -------------------------------------------------------------------------------- /client/web-gamepad.js: -------------------------------------------------------------------------------- 1 | /* 2 | * web-gamepad.js 0.1.1 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Allenice 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | * */ 25 | 26 | (function (root, factory) { 27 | 28 | // amd 支持 29 | if(typeof define === 'function' && define.amd) { 30 | define(['socketio'], function(io) { 31 | root.WebGamepad = factory(root, {}, io); 32 | return root.WebGamepad; 33 | }); 34 | 35 | // cmd 支持 36 | } else if(typeof define === 'function' && define.cmd){ 37 | define(function (require, exports, module) { 38 | var io = require('socketio'); 39 | root.WebGamepad = factory(root, exports, io); 40 | }); 41 | } else { 42 | root.WebGamepad = factory(root, {}, root.io); 43 | } 44 | })(this, function (root, WebGamepad, io) { 45 | io = io || window.io; 46 | /* 47 | * init 48 | * ------- 49 | * */ 50 | var utils = WebGamepad.utils = { 51 | 52 | slice: Array.prototype.slice, 53 | 54 | isArray: function (arr) { 55 | return Object.prototype.toString.call(arr) === '[object Array]'; 56 | }, 57 | 58 | uuid: (function() { 59 | function s4() { 60 | return Math.floor((1 + Math.random()) * 0x10000) 61 | .toString(16) 62 | .substring(1); 63 | } 64 | return function() { 65 | return s4() + s4() + '' + s4() + '' + s4() + '' + 66 | s4() + '' + s4() + s4() + s4(); 67 | }; 68 | })(), 69 | 70 | extend: function (target/*,source...*/) { 71 | var length = arguments.length; 72 | 73 | if(length > 1) { 74 | for(var i = 1; i < length; i++) { 75 | var source = arguments[i]; 76 | for(var key in source) { 77 | target[key] = source[key]; 78 | } 79 | } 80 | } 81 | return target; 82 | } 83 | } 84 | 85 | var uid = 'u' + utils.uuid(), 86 | qrcodeSrc = 'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=', 87 | socketServer; 88 | 89 | // 版本 90 | WebGamepad.VERSION = '0.1.1'; 91 | 92 | // 按钮和摇杆对应的索引值 93 | WebGamepad.BUTTONS = { 94 | FACE_1: 0, // 按钮 1,2,3,4 95 | FACE_2: 1, 96 | FACE_3: 2, 97 | FACE_4: 3, 98 | LEFT_SHOULDER: 4, // L1 99 | RIGHT_SHOULDER: 5, // R1 100 | LEFT_SHOULDER_BOTTOM: 6, // L2 101 | RIGHT_SHOULDER_BOTTOM: 7, // R2 102 | SELECT: 8, 103 | START: 9, 104 | LEFT_ANALOGUE_STICK: 10, // 左摇杆按下的按钮(目前没做) 105 | RIGHT_ANALOGUE_STICK: 11, 106 | PAD_TOP: 12, // 上 107 | PAD_BOTTOM: 13, // 下 108 | PAD_LEFT: 14, // 左 109 | PAD_RIGHT: 15 // 右 110 | }; 111 | 112 | WebGamepad.AXES = { 113 | LEFT_ANALOGUE_HOR: 0, // 左摇杆水平方向 114 | LEFT_ANALOGUE_VERT: 1, // 左摇杆垂直方向 115 | RIGHT_ANALOGUE_HOR: 2, 116 | RIGHT_ANALOGUE_VERT: 3 117 | }; 118 | 119 | // 按钮数量(0-15) 120 | WebGamepad.TYPICAL_BUTTON_COUNT = 16; 121 | 122 | // 轴数量,一个摇杆有两个轴,x 轴和 y 轴,目前支持两个摇杆 123 | WebGamepad.TYPICAL_AXES_COUNT = 4; 124 | 125 | 126 | // 存储已连接的手柄 127 | WebGamepad.gamepads = []; 128 | 129 | // 浏览器是否支持实体手柄 130 | WebGamepad.gamepadSupport = navigator.getGamepads || 131 | !!navigator.webkitGetGamepads || 132 | !!navigator.webkitGamepads; 133 | 134 | // 获取连接二维码 135 | WebGamepad.getQrcode = function () { 136 | return qrcodeSrc; 137 | }; 138 | 139 | // 获取已经连接的手柄 140 | WebGamepad.getGamepads = function () { 141 | return WebGamepad.gamepads.filter(function (gamepad) { 142 | return typeof gamepad != 'undefined'; 143 | }); 144 | }; 145 | 146 | /* 147 | * WebGamepad.GamepadEvent 148 | * 简单的事件支持 149 | * --------- 150 | * */ 151 | 152 | var Events = WebGamepad.GamepadEvents = {}; 153 | 154 | // 绑定事件 155 | Events.on = function (name, callback) { 156 | 157 | this._callbacks = (function(callbacks){ 158 | callbacks[name] = callbacks[name] || []; 159 | callbacks[name].push(callback); 160 | return callbacks; 161 | })(this._callbacks || {}); 162 | 163 | return this; 164 | }; 165 | 166 | // 解除事件 167 | Events.off = function (name) { 168 | if(this._callbacks) { 169 | for(var key in this._callbacks) { 170 | if(key === name) delete this._callbacks[key]; 171 | } 172 | } 173 | return this; 174 | }; 175 | 176 | // 触发事件 177 | Events.trigger = function (name) { 178 | var _this = this, 179 | args = arguments; 180 | if(this._callbacks) { 181 | for(var key in this._callbacks) { 182 | if(key === name && utils.isArray(this._callbacks[key])) { 183 | this._callbacks[key].forEach(function(callback) { 184 | callback.apply(_this, utils.slice.call(args, 1)); 185 | }); 186 | } 187 | } 188 | } 189 | return this; 190 | }; 191 | 192 | // 使用 WebGamepad 对象具有事件功能 193 | utils.extend(WebGamepad, Events); 194 | 195 | /* 196 | * WebGamepad.GamepadButton 197 | * 手柄按钮 198 | * ------- 199 | * */ 200 | 201 | var GamepadButon = WebGamepad.GamepadButton = function () {}; 202 | 203 | utils.extend(GamepadButon.prototype, Events, { 204 | value: 0, 205 | oldValue: 0, 206 | gamepad: null, // 所属手柄 207 | 208 | setValue: function (value) { 209 | this.oldValue = this.value; 210 | this.value = value; 211 | 212 | if(this.value > 0.9 && this.oldValue <= 0.1) { 213 | this.trigger('pressed'); 214 | this.gamepad.trigger('update'); 215 | WebGamepad.trigger('update', this.gamepad); 216 | } 217 | 218 | if(this.value <= 0.1 && this.oldValue > 0.9) { 219 | this.trigger('released'); 220 | this.gamepad.trigger('update'); 221 | WebGamepad.trigger('update', this.gamepad); 222 | } 223 | } 224 | }); 225 | 226 | /* 227 | * WebGamepad.GamepadAxes 228 | * 手柄的轴(一个摇杆有两个轴,x 轴和 y 轴) 229 | * */ 230 | 231 | var GamepadAxes = WebGamepad.GamepadAxes = function(){}; 232 | 233 | utils.extend(GamepadAxes.prototype, Events, { 234 | value: 0, 235 | oldValue: 0, 236 | 237 | setValue: function (value) { 238 | this.oldValue = this.value; 239 | this.value = parseFloat(value.toFixed(2)); 240 | 241 | if(this.value != this.oldValue) { 242 | this.trigger('update'); 243 | this.gamepad.trigger('update'); 244 | WebGamepad.trigger('update', this.gamepad); 245 | } 246 | } 247 | }); 248 | 249 | /* 250 | * WebGamepad.Gamepad 251 | * 手柄 252 | * */ 253 | 254 | var Gamepad = WebGamepad.Gamepad = function(){ 255 | 256 | this.id = ''; 257 | 258 | // 用于区分手柄 259 | this.index = 0; 260 | 261 | // 按钮 262 | this.buttons = []; 263 | 264 | // 上一次状态更新的时间 265 | this.timestamp = new Date().getTime(), 266 | 267 | // 轴 268 | this.axes = []; 269 | 270 | // 初始化按钮和轴 271 | for(var i = 0; i < WebGamepad.TYPICAL_BUTTON_COUNT; i ++) { 272 | var button = new GamepadButon(); 273 | button.gamepad = this; 274 | this.buttons.push(button); 275 | } 276 | 277 | for(var i = 0; i < WebGamepad.TYPICAL_AXES_COUNT; i ++) { 278 | var axes = new GamepadAxes(); 279 | axes.gamepad = this; 280 | this.axes.push(axes); 281 | } 282 | }; 283 | 284 | utils.extend(Gamepad.prototype, Events, { 285 | 286 | // 更新状态 287 | update: function (gamepadData) { 288 | this.id = gamepadData.id; 289 | this.index = gamepadData.index; 290 | this.timestamp = gamepadData.timestamp; 291 | 292 | this.buttons.forEach(function (button, index) { 293 | var btn = gamepadData.buttons[index]; 294 | 295 | // 兼容某些实体手柄,button 的值是:{value: 0|false, pressed: true|false} 296 | var value = typeof btn === 'object' ? btn.value : btn; 297 | 298 | if(typeof value != 'undefined') { 299 | button.setValue(value); 300 | } 301 | }); 302 | 303 | this.axes.forEach(function (axes, index) { 304 | var value = gamepadData.axes[index]; 305 | 306 | if(typeof value != 'undefined') { 307 | axes.setValue(value); 308 | } 309 | }); 310 | 311 | } 312 | }); 313 | 314 | /* 315 | * 连接事件相关回调 316 | * ---- 317 | * */ 318 | 319 | // 有手柄连接 320 | function onGamepadConnected(data) { 321 | var gamepad = new WebGamepad.Gamepad(); 322 | gamepad.update(data); 323 | WebGamepad.gamepads[gamepad.index] = gamepad; 324 | WebGamepad.trigger('connected', gamepad); 325 | } 326 | 327 | // 手柄断开连接 328 | function onGamepadDisconnected(data) { 329 | var gamepad = WebGamepad.gamepads[data.index]; 330 | WebGamepad.gamepads[data.index] = void 0; 331 | WebGamepad.trigger('disconnected', gamepad); 332 | } 333 | 334 | // 手柄状态更新 335 | function onGamepadUpdate(data) { 336 | var gamepad = WebGamepad.gamepads[data.index]; 337 | gamepad.update(data); 338 | } 339 | 340 | 341 | /* 342 | * 连接真实手柄 343 | * ------------ 344 | * */ 345 | 346 | var gamepadSupport = { 347 | ticking: false, 348 | 349 | init: function () { 350 | if (WebGamepad.gamepadSupport) { 351 | // 判断是否支持 gamepadconnected/gamepaddisconnected 事件 352 | if ('ongamepadconnected' in window) { 353 | window.addEventListener('gamepadconnected', 354 | gamepadSupport.onGamepadConnect, false); 355 | window.addEventListener('gamepaddisconnected', 356 | gamepadSupport.onGamepadDisconnect, false); 357 | } else { 358 | // 如果不支持这两个事件就一直轮询查看手柄连接状态 359 | gamepadSupport.startPolling(); 360 | } 361 | } 362 | }, 363 | 364 | // 手柄连接 365 | onGamepadConnect: function (event) { 366 | onGamepadConnected(event.gamepad); 367 | gamepadSupport.startPolling(); 368 | }, 369 | 370 | // 手柄断开连接,如果没有真实手柄连接,停止轮询 371 | onGamepadDisconnect: function (event) { 372 | var gamepads = WebGamepad.gamepads.slice(0,4), 373 | flag = true; 374 | 375 | onGamepadDisconnected(event.gamepad); 376 | 377 | // 检查还有没有实体手柄连接,没有的话就停止轮询 378 | for(var i = 0; i < gamepads.length; i++) { 379 | if(gamepads[i]) { 380 | flag = false; 381 | break; 382 | } 383 | } 384 | 385 | if(flag) gamepadSupport.stopPolling(); 386 | }, 387 | 388 | startPolling: function () { 389 | if (!gamepadSupport.ticking) { 390 | gamepadSupport.ticking = true; 391 | gamepadSupport.tick(); 392 | } 393 | }, 394 | 395 | stopPolling: function() { 396 | gamepadSupport.ticking = false; 397 | }, 398 | 399 | tick: function () { 400 | gamepadSupport.pollStatus(); 401 | gamepadSupport.scheduleNextTick(); 402 | }, 403 | 404 | scheduleNextTick: function () { 405 | if (gamepadSupport.ticking) { 406 | if (window.requestAnimationFrame) { 407 | window.requestAnimationFrame(gamepadSupport.tick); 408 | } else if (window.mozRequestAnimationFrame) { 409 | window.mozRequestAnimationFrame(gamepadSupport.tick); 410 | } else if (window.webkitRequestAnimationFrame) { 411 | window.webkitRequestAnimationFrame(gamepadSupport.tick); 412 | } 413 | } 414 | }, 415 | 416 | // 轮询手柄连接状态 417 | pollStatus: function () { 418 | var rawGamepads = 419 | (navigator.getGamepads && navigator.getGamepads()) || 420 | (navigator.webkitGetGamepads && navigator.webkitGetGamepads()); 421 | 422 | for(var i = 0; i < rawGamepads.length; i++) { 423 | var data = rawGamepads[i]; 424 | 425 | if(!data) { 426 | 427 | // 如果是 undefined, 而且 WebGamepad.gamepads 存在这个手柄,表示现在已经断开连接 428 | if(WebGamepad.gamepads[i]) { 429 | onGamepadDisconnected(WebGamepad.gamepads[i]); 430 | 431 | } 432 | // 继续检查下一个 433 | continue; 434 | }; 435 | 436 | // 如果手柄已经添加到 WebGamepad.gamepads 的话,更新数据,否则是新连接的手柄 437 | if(WebGamepad.gamepads[data.index]) { 438 | WebGamepad.gamepads[data.index].update(data); 439 | } else { 440 | onGamepadConnected(data); 441 | } 442 | } 443 | } 444 | 445 | }; 446 | 447 | /* 448 | * 手柄连接相关 449 | * ----------- 450 | * */ 451 | 452 | // 监听 gamepad 连接 453 | WebGamepad.listen = function (options) { 454 | options = options || {}; 455 | socketServer = options.socketServer; 456 | qrcodeSrc = qrcodeSrc + (socketServer + '?uid=' + uid); 457 | 458 | // 如果配置了 socket 服务器就连接 459 | if(socketServer) { 460 | var socket = io.connect(socketServer); 461 | 462 | // 连接到 socket 服务器 463 | socket.on('server-connected', function (data) { 464 | socket.emit('connected', {uid: uid}); 465 | }); 466 | 467 | // 有手柄连接到游戏 468 | socket.on('gamepad-connected', function (data) { 469 | onGamepadConnected(data); 470 | }); 471 | 472 | // 手柄状态更新 473 | socket.on('gamepad-update', function (data) { 474 | onGamepadUpdate(data); 475 | }); 476 | 477 | // 手柄断开连接 478 | socket.on('gamepad-disconnected', function (data) { 479 | onGamepadDisconnected(data); 480 | }); 481 | 482 | // 如果与服务器断开连接,则触发 web 手柄的 disconnected 事件 483 | socket.on('disconnect', function () { 484 | for(var i = 4; i < WebGamepad.gamepads.length; i++) { 485 | var gamepad = WebGamepad.gamepads[i]; 486 | if(gamepad) WebGamepad.trigger('disconnected', gamepad); 487 | } 488 | }); 489 | 490 | } 491 | 492 | // 实体手柄支持 493 | gamepadSupport.init(); 494 | }; 495 | 496 | // export to global 497 | return WebGamepad; 498 | 499 | }); -------------------------------------------------------------------------------- /demo/events/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 手柄操作节点的位置,颜色,大小, 目的是测试摇杆和按钮事件 3 | * */ 4 | 5 | $(function() { 6 | 7 | function random(min, max) { 8 | if (max == null) { 9 | max = min; 10 | min = 0; 11 | } 12 | return min + Math.floor(Math.random() * (max - min + 1)); 13 | } 14 | 15 | var nodes = [], 16 | start = false, 17 | $container = $('#container'); 18 | 19 | // 节点类,手柄操作节点的位置,颜色,大小 20 | function Node (gamepad) { 21 | this.gamepad = gamepad; 22 | 23 | // 移动速度 24 | this.speed = 4; 25 | 26 | // 标记水平方向上哪个摇杆在控制 27 | this.analogueHor = WebGamepad.AXES.LEFT_ANALOGUE_HOR; 28 | 29 | // 标记垂直方向上哪个摇杆在控制 30 | this.analogueVer = WebGamepad.AXES.LEFT_ANALOGUE_VERT; 31 | 32 | // 初始背景 33 | this.bg = 'face'; 34 | 35 | this.$el = $('