├── .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 | ![手柄按钮对照](http://www.html5rocks.com/en/tutorials/doodles/gamepad/gamepad_diagram.png) 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 = $('
'); 36 | this.$el.text(this.gamepad.index).hide(); 37 | this.$el.css({'left': random(200, 500), 'top': random(100, 300)}); 38 | this.$el.addClass(this.bg); 39 | $container.append(this.$el); 40 | 41 | this._bindEvent(); 42 | 43 | // 轮询状态 44 | this.scheduleUpdate(); 45 | } 46 | 47 | $.extend(Node.prototype, { 48 | 49 | show: function () { 50 | this.$el.show(); 51 | }, 52 | 53 | remove: function () { 54 | this.$el.remove(); 55 | }, 56 | 57 | update: function () { 58 | 59 | this._moveHor(this.gamepad.axes[this.analogueHor]); 60 | this._moveVer(this.gamepad.axes[this.analogueVer]); 61 | 62 | // 进行下一次更新 63 | this.scheduleUpdate(); 64 | }, 65 | 66 | scheduleUpdate: function () { 67 | if(!start) { 68 | $('#stop').show(); 69 | return; 70 | } else { 71 | $('#stop').hide(); 72 | } 73 | if (window.requestAnimationFrame) { 74 | window.requestAnimationFrame(this.update.bind(this)); 75 | } else if (window.mozRequestAnimationFrame) { 76 | window.mozRequestAnimationFrame(this.update.bind(this)); 77 | } else if (window.webkitRequestAnimationFrame) { 78 | window.webkitRequestAnimationFrame(this.update.bind(this)); 79 | } 80 | }, 81 | 82 | // 绑定事件 83 | _bindEvent: function () { 84 | var _this = this; 85 | 86 | // 摇杆事件 87 | this.gamepad.axes[WebGamepad.AXES.LEFT_ANALOGUE_HOR].on('update', function () { 88 | _this.analogueHor = WebGamepad.AXES.LEFT_ANALOGUE_HOR; 89 | }); 90 | this.gamepad.axes[WebGamepad.AXES.LEFT_ANALOGUE_VERT].on('update', function () { 91 | _this.analogueVer = WebGamepad.AXES.LEFT_ANALOGUE_VERT; 92 | }); 93 | 94 | this.gamepad.axes[WebGamepad.AXES.RIGHT_ANALOGUE_HOR].on('update', function () { 95 | _this.analogueHor = WebGamepad.AXES.RIGHT_ANALOGUE_HOR; 96 | }); 97 | 98 | this.gamepad.axes[WebGamepad.AXES.RIGHT_ANALOGUE_VERT].on('update', function () { 99 | _this.analogueVer = WebGamepad.AXES.RIGHT_ANALOGUE_VERT; 100 | }); 101 | 102 | // 按钮事件 103 | this.gamepad.buttons[WebGamepad.BUTTONS.FACE_1].on('pressed', function () { 104 | _this._setBg('face1'); 105 | 106 | }).on('released', function () { 107 | _this._restoreBg(); 108 | }); 109 | 110 | this.gamepad.buttons[WebGamepad.BUTTONS.FACE_2].on('pressed', function () { 111 | _this._setBg('face2'); 112 | 113 | }).on('released', function () { 114 | _this._restoreBg(); 115 | }); 116 | 117 | this.gamepad.buttons[WebGamepad.BUTTONS.FACE_3].on('pressed', function () { 118 | _this._setBg('face3'); 119 | 120 | }).on('released', function () { 121 | _this._restoreBg(); 122 | }); 123 | 124 | this.gamepad.buttons[WebGamepad.BUTTONS.FACE_4].on('pressed', function () { 125 | _this._setBg('face4'); 126 | 127 | }).on('released', function () { 128 | _this._restoreBg(); 129 | }); 130 | 131 | this.gamepad.buttons[WebGamepad.BUTTONS.LEFT_SHOULDER].on('released', function() { 132 | _this.$el.css('border-radius', 0); 133 | }); 134 | 135 | this.gamepad.buttons[WebGamepad.BUTTONS.LEFT_SHOULDER_BOTTOM].on('released', function() { 136 | _this.$el.css('border-radius', '50%'); 137 | }); 138 | 139 | this.gamepad.buttons[WebGamepad.BUTTONS.RIGHT_SHOULDER].on('released', function () { 140 | _this.$el.css({'width': 50, 'height': 50, 'line-height': '50px'}); 141 | }); 142 | 143 | this.gamepad.buttons[WebGamepad.BUTTONS.RIGHT_SHOULDER_BOTTOM].on('released', function () { 144 | _this.$el.css({'width': 100, 'height': 100, 'line-height': '100px'}); 145 | }); 146 | 147 | this.gamepad.buttons[WebGamepad.BUTTONS.START].on('released', function () { 148 | start = !start; 149 | nodes.forEach(function (node) { 150 | if(node) { 151 | node.scheduleUpdate(); 152 | } 153 | }); 154 | }); 155 | 156 | }, 157 | 158 | _setBg: function (bg) { 159 | this.$el.removeClass(this.bg); 160 | this.bg = bg; 161 | this.$el.addClass(this.bg); 162 | }, 163 | 164 | _restoreBg: function () { 165 | this.$el.removeClass(this.bg); 166 | this.bg = 'face'; 167 | this.$el.addClass(this.bg); 168 | }, 169 | 170 | // 水平移动 171 | _moveHor: function (axes) { 172 | var left = (this.speed * axes.value) + parseInt(this.$el.css('left')); 173 | 174 | if(left < 0 || left > $container.width() - this.$el.width()) return; 175 | 176 | this.$el.css('left', left); 177 | 178 | }, 179 | 180 | // 垂直移动 181 | _moveVer: function (axes) { 182 | var top = (this.speed * axes.value) + parseInt(this.$el.css('top')); 183 | 184 | if(top < 0 || top > $container.height() - this.$el.height()) return; 185 | 186 | this.$el.css('top', top); 187 | } 188 | 189 | }); 190 | 191 | WebGamepad.listen({ 192 | socketServer: 'http://100.84.85.122:3000/' 193 | }); 194 | 195 | // 手柄连接后,添加一个节点 196 | WebGamepad.on('connected', function(gamepad) { 197 | var node = new Node(gamepad); 198 | nodes[gamepad.index] = node; 199 | node.show(); 200 | }); 201 | 202 | // 手柄断开连接后,删除一个节点 203 | WebGamepad.on('disconnected', function (gamepad) { 204 | var node = nodes[gamepad.index]; 205 | node.remove(); 206 | nodes[gamepad.index] = void 0; 207 | }); 208 | 209 | // 显示二维码 210 | $('#qrcode').attr('src', WebGamepad.getQrcode()); 211 | }); -------------------------------------------------------------------------------- /demo/events/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 事件测试 6 | 7 | 8 | 9 | 10 |
11 | 12 |
已禁止移动,请按 START 键开始移动
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/events/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 854px; 3 | height: 480px; 4 | background-color: #000; 5 | margin: 10px auto; 6 | position: relative; 7 | } 8 | 9 | .node { 10 | width: 50px; 11 | height: 50px; 12 | line-height: 50px; 13 | position: absolute; 14 | left: 420px; 15 | top: 240px; 16 | text-align: center; 17 | border-radius: 50%; 18 | background-color: #666; 19 | font-size: 24px; 20 | color: #fff; 21 | text-shadow: 1px 1px 1px #000; 22 | border: 1px solid #000; 23 | } 24 | 25 | .qrcode { 26 | width: 100px; 27 | height: 100px; 28 | } 29 | 30 | .stop { 31 | color: #fff; 32 | position: absolute; 33 | left: 0; 34 | bottom: 10px; 35 | } 36 | 37 | .face { 38 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #ffc56a), color-stop(100%, #f1efbc)); 39 | background-image: -moz-linear-gradient(bottom, #ffc56a, #f1efbc); 40 | background-image: -webkit-linear-gradient(bottom, #ffc56a, #f1efbc); 41 | background-image: linear-gradient(to top, #ffc56a, #f1efbc); 42 | } 43 | 44 | .face1 { 45 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #a1ca12), color-stop(100%, #e1f195)); 46 | background-image: -moz-linear-gradient(bottom, #a1ca12, #e1f195); 47 | background-image: -webkit-linear-gradient(bottom, #a1ca12, #e1f195); 48 | background-image: linear-gradient(to top, #a1ca12, #e1f195); 49 | } 50 | 51 | .face2 { 52 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #ef0909), color-stop(100%, #f47474)); 53 | background-image: -moz-linear-gradient(bottom, #ef0909, #f47474); 54 | background-image: -webkit-linear-gradient(bottom, #ef0909, #f47474); 55 | background-image: linear-gradient(to top, #ef0909, #f47474); 56 | } 57 | 58 | .face3 { 59 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #0e79cc), color-stop(100%, #43beeb)); 60 | background-image: -moz-linear-gradient(bottom, #0e79cc, #43beeb); 61 | background-image: -webkit-linear-gradient(bottom, #0e79cc, #43beeb); 62 | background-image: linear-gradient(to top, #0e79cc, #43beeb); 63 | } 64 | 65 | .face4 { 66 | background-image: -webkit-gradient(linear, 50% 100%, 50% 0%, color-stop(0%, #efa80e), color-stop(100%, #eed58a)); 67 | background-image: -moz-linear-gradient(bottom, #efa80e, #eed58a); 68 | background-image: -webkit-linear-gradient(bottom, #efa80e, #eed58a); 69 | background-image: linear-gradient(to top, #efa80e, #eed58a); 70 | } 71 | -------------------------------------------------------------------------------- /demo/tester/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #3a3a3a; 3 | } 4 | 5 | .tips { 6 | background-color: #f1f1f1; 7 | color: #666666; 8 | font-size: 14px; 9 | padding: 15px; 10 | border-radius: 5px; 11 | } 12 | 13 | .qrcode { 14 | display: block; 15 | width: 150px; 16 | height: 150px; 17 | position: fixed; 18 | right: 10px; 19 | top: 10px; 20 | border: 1px solid #ccc; 21 | background: #ccc; 22 | } 23 | 24 | .id { 25 | border-bottom: 1px solid #E5E190; 26 | padding-bottom: 10px; 27 | color: rgb(0, 187, 255); 28 | } 29 | 30 | .id span { 31 | color: rgb(255, 97, 0); 32 | padding: 0 10px; 33 | font-size: 28px; 34 | } 35 | 36 | ul { 37 | width: 80%; 38 | list-style: none; 39 | overflow: hidden; 40 | } 41 | 42 | li { 43 | padding: 5px 25px; 44 | float: left; 45 | margin: 10px 10px 0 0; 46 | background: #ccc; 47 | } 48 | 49 | li label { 50 | font-size: 14px; 51 | display: block; 52 | line-height: 19px; 53 | } 54 | 55 | li value { 56 | line-height: 25px; 57 | } 58 | 59 | li.active { 60 | background: red; 61 | color: #fff; 62 | } 63 | 64 | .axes li { 65 | min-width: 150px; 66 | } -------------------------------------------------------------------------------- /demo/tester/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Web gamepad tester 6 | 7 | 8 | 9 |

WeGamepad Tester

10 |

用手机扫描二维码连接手柄,也可以插入实体手柄。

11 |
实体手柄连接:按下任意键触发连接,如果连接不上,请重新插入,如果还不行,请重启浏览器。
12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/tester/js/main.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | basePath: './', 3 | paths: { 4 | 5 | // 配置 socket.io 的路径,一定要用这个名字 6 | socketio: 'http://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min', 7 | webgamepad: '../../../client/web-gamepad' 8 | } 9 | }); 10 | 11 | require(['tester'], function(app) { 12 | app.init(); 13 | }); -------------------------------------------------------------------------------- /demo/tester/js/tester.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by allenice on 15/3/17. 3 | */ 4 | (function () { 5 | 6 | var app = { 7 | 8 | init: function () { 9 | this._cacheDom(); 10 | this._bindEvent(); 11 | 12 | $('#qrcode').attr('src', WebGamepad.getQrcode()); 13 | }, 14 | 15 | _cacheDom: function () { 16 | this.$container = $('#container'); 17 | }, 18 | 19 | _bindEvent: function () { 20 | var _this = this; 21 | 22 | WebGamepad.listen({ 23 | socketServer: 'http://100.84.85.122:3000/' 24 | }); 25 | 26 | WebGamepad.on('connected', function (gamepad) { 27 | _this._createGamepad(gamepad); 28 | 29 | }).on('update', function (gamepad) { 30 | _this._upateGamepad(gamepad); 31 | 32 | }).on('disconnected', function (gamepad) { 33 | _this._removeGamepad(gamepad); 34 | }); 35 | }, 36 | 37 | _createGamepad: function (gamepad) { 38 | var $gamepadWrap = $('
'), 39 | $info = $('