Vượt qua rào cản nhờ Gamepad API

Marcin Wichary
Marcin Wichary

Giới thiệu

Hãy để những người mới chơi giữ bàn phím cho các trò chơi phiêu lưu, đầu ngón tay đa chạm quý giá để cắt trái cây và các cảm biến chuyển động mới lạ, giả vờ nhảy múa như Michael Jackson. (Newsflash: Không thể làm vậy). Nhưng bạn thì khác. Bạn giỏi hơn. Bạn thật chuyên nghiệp. Đối với bạn, trò chơi bắt đầu và kết thúc với một tay điều khiển trò chơi trên tay.

Nhưng chờ đã. Liệu bạn có gặp may không nếu muốn hỗ trợ tay điều khiển trò chơi trong ứng dụng web của mình? Không còn nữa. Gamepad API hoàn toàn mới sẽ được cải tiến, cho phép bạn sử dụng JavaScript để đọc trạng thái của bất kỳ bộ điều khiển tay điều khiển trò chơi nào được đính kèm với máy tính của bạn. Hoàn toàn mới đến mức nó chỉ xuất hiện trong Chrome 21 tuần trước – và nó cũng đang trên đà được hỗ trợ trong Firefox (hiện có sẵn trong một bản dựng đặc biệt).

Đó có vẻ là thời điểm khá lý tưởng, bởi vì gần đây chúng tôi có cơ hội sử dụng tính năng này trong Hình tượng trưng trên Google 2012 của Hurdles 2012. Bài viết này sẽ giải thích ngắn gọn cách chúng tôi thêm Gamepad API vào hình tượng trưng và những điều chúng tôi rút ra được trong quá trình thực hiện.

Hình tượng trưng của Google trong thử thách Hurdles 2012
Hình tượng trưng trên Google năm 2012

Trình kiểm tra tay điều khiển trò chơi

Về mặt tạm thời, hình tượng trưng tương tác thường khá phức tạp. Để minh hoạ những gì đang nói dễ dàng hơn, chúng tôi đã lấy mã tay điều khiển trò chơi từ hình tượng trưng rồi tạo một công cụ kiểm thử tay điều khiển trò chơi đơn giản. Bạn có thể sử dụng để xem tay điều khiển USB có hoạt động chính xác hay không – đồng thời xem chi tiết để kiểm tra xem tay điều khiển trò chơi hoạt động như thế nào.

Những trình duyệt nào hiện hỗ trợ tính năng này?

Hỗ trợ trình duyệt

  • 21
  • 12
  • 29
  • 10.1

Nguồn

Có thể dùng tay điều khiển trò chơi nào?

Nhìn chung, mọi tay điều khiển trò chơi hiện đại được hệ thống của bạn hỗ trợ đều hoạt động bình thường. Chúng tôi đã thử nghiệm nhiều loại tay điều khiển trò chơi từ bộ điều khiển USB không chính hãng trên máy tính, từ tay điều khiển trò chơi PlayStation 2 được kết nối qua thiết bị bảo vệ phần mềm với máy Mac, cho đến tay điều khiển Bluetooth ghép nối với máy tính xách tay ChromeOS.

Tay cầm
Tay điều khiển trò chơi

Đây là ảnh chụp một số bộ điều khiển mà chúng tôi đã sử dụng để thử nghiệm hình tượng trưng của mình – "Phải, mẹ, đó thực sự là những gì tôi làm ở nơi làm việc". Nếu bộ điều khiển của bạn không hoạt động hoặc nếu các điều khiển được ánh xạ không chính xác, vui lòng báo cáo lỗi cho Chrome hoặc Firefox . (Vui lòng kiểm tra bằng phiên bản mới nhất của từng trình duyệt để đảm bảo trình duyệt chưa được khắc phục.)

Tính năng Phát hiện API Gamepad<

Đủ dễ dàng trong Chrome:

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

Có vẻ như điều này chưa được phát hiện trong Firefox – mọi thứ đều dựa trên sự kiện và tất cả các trình xử lý sự kiện cần được đính kèm vào cửa sổ. Điều này ngăn chặn kỹ thuật thông thường là phát hiện trình xử lý sự kiện hoạt động.

Tuy nhiên, chúng tôi chắc chắn rằng đó chỉ là giải pháp tạm thời. Công cụ Currentizr tuyệt vời này đã cho bạn biết về API Gamepad, vì vậy chúng tôi đề xuất API này cho mọi nhu cầu phát hiện hiện tại và trong tương lai của bạn:

var gamepadSupportAvailable = Modernizr.gamepads;

Tìm hiểu về tay điều khiển trò chơi đã kết nối

Ngay cả khi bạn kết nối tay điều khiển trò chơi, tay điều khiển trò chơi sẽ không tự biểu hiện theo bất kỳ cách nào trừ phi người dùng nhấn nút bất kỳ trước. Điều này nhằm ngăn việc tạo vân tay số, mặc dù trải nghiệm người dùng khá khó khăn: bạn không thể yêu cầu người dùng nhấn nút hoặc cung cấp hướng dẫn dành riêng cho tay điều khiển trò chơi vì bạn không biết liệu họ có kết nối tay điều khiển của mình hay không.

Tuy nhiên, sau khi bạn đã loại bỏ trở ngại đó (xin lỗi...), sẽ có nhiều điều khác chờ đợi bạn.

Cuộc thăm dò ý kiến

Quá trình triển khai API của Chrome sẽ hiển thị một hàm – navigator.webkitGetGamepads() – bạn có thể sử dụng để lấy danh sách tất cả tay điều khiển trò chơi hiện đã được cắm vào hệ thống cùng với trạng thái hiện tại của các tay điều khiển đó (nút + cần). Tay điều khiển trò chơi đã kết nối đầu tiên sẽ được trả về dưới dạng mục nhập đầu tiên trong mảng, v.v.

(Lệnh gọi hàm này vừa mới thay thế một mảng mà bạn có thể truy cập trực tiếp – navigator.webkitGamepads[]. Kể từ đầu tháng 8 năm 2012, bạn vẫn cần truy cập vào mảng này trong Chrome 21 trong khi lệnh gọi hàm hoạt động trong Chrome 22 trở lên. Từ giờ trở đi, lệnh gọi hàm là cách được đề xuất để sử dụng API và sẽ dần nhỏ dần tới tất cả trình duyệt Chrome đã cài đặt.)

Phần được triển khai từ nay về thông số kỹ thuật yêu cầu bạn liên tục kiểm tra trạng thái của các tay điều khiển đã kết nối (và so sánh với tay điều khiển trước đó nếu cần), thay vì kích hoạt các sự kiện khi mọi thứ thay đổi. Chúng tôi dựa vào requestAnimationFrame() để thiết lập tính năng thăm dò ý kiến theo cách hiệu quả và tiết kiệm pin nhất. Đối với hình tượng trưng, mặc dù chúng ta đã có vòng lặp requestAnimationFrame() để hỗ trợ ảnh động, nhưng chúng ta đã tạo một vòng lặp thứ hai hoàn toàn riêng biệt – mã này đơn giản hơn và không ảnh hưởng đến hiệu suất.

Dưới đây là mã từ người kiểm thử:

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

Nếu bạn chỉ quan tâm đến một tay điều khiển trò chơi, việc lấy dữ liệu có thể đơn giản như sau:

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

Nếu muốn thông minh hơn một chút hoặc hỗ trợ nhiều người chơi cùng lúc, bạn sẽ cần thêm một vài dòng mã để phản ứng với những tình huống phức tạp hơn (hai hoặc nhiều tay điều khiển trò chơi được kết nối, một số tay điều khiển bị ngắt kết nối giữa chừng, v.v.). Bạn có thể xem mã nguồn của hàm kiểm thử, hàm pollGamepads(), để biết một phương pháp về cách giải quyết vấn đề này.

Sự kiện

Firefox sử dụng một cách khác và hiệu quả hơn được mô tả trong phần thông số kỹ thuật của Gamepad API. Thay vì yêu cầu bạn thăm dò ý kiến, trình duyệt này sẽ hiển thị 2 sự kiện – MozGamepadConnectedMozGamepadDisconnected – được kích hoạt bất cứ khi nào tay điều khiển trò chơi được cắm (hoặc chính xác hơn là cắm và "thông báo" bằng cách nhấn vào nút bất kỳ) hoặc rút phích cắm. Đối tượng tay điều khiển trò chơi sẽ tiếp tục phản ánh trạng thái trong tương lai được truyền dưới dạng tham số .gamepad của đối tượng sự kiện.

Từ mã nguồn của người kiểm thử:

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

Tóm tắt

Cuối cùng, hàm khởi chạy của chúng ta trong trình kiểm thử (hỗ trợ cả hai phương pháp) sẽ có dạng như sau:

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

Thông tin về tay điều khiển trò chơi

Mọi tay điều khiển trò chơi kết nối với hệ thống sẽ được biểu thị bằng một đối tượng có dạng như sau:

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

Thông tin cơ bản

Một vài trường trên cùng là siêu dữ liệu đơn giản:

  • id: nội dung mô tả bằng văn bản của tay điều khiển trò chơi
  • index: một số nguyên hữu ích để phân biệt các tay điều khiển trò chơi được gắn vào một máy tính
  • timestamp: dấu thời gian của lần cập nhật gần đây nhất trạng thái nút/Trục (hiện chỉ được hỗ trợ trong Chrome)

Nút và gậy

Bàn chơi game ngày nay không hẳn là thứ mà ông có thể đã sử dụng để cứu công chúa vào sai lâu đài – chúng thường có ít nhất 16 nút riêng biệt (một số nút riêng biệt, một số nút tương tự), ngoài hai cần tương tự. Gamepad API sẽ cho bạn biết về tất cả các nút và cần tương tự được hệ điều hành báo cáo.

Sau khi nhận được trạng thái hiện tại trong đối tượng tay điều khiển trò chơi, bạn có thể truy cập vào các nút thông qua .buttons[] và thẻ thông qua các mảng .axes[]. Dưới đây là tóm tắt trực quan về những gì tương ứng với các từ khoá đó:

Sơ đồ tay điều khiển trò chơi
Sơ đồ tay điều khiển trò chơi

Thông số kỹ thuật này yêu cầu trình duyệt liên kết 16 nút và 4 trục đầu tiên với:

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

Các nút và trục bổ sung sẽ được nối thêm vào các nút và trục ở trên. Xin lưu ý rằng cả 16 nút và 4 trục đều không được đảm bảo. Hãy chuẩn bị để một vài nút trong số đó có thể không xác định được.

Các nút có thể nhận giá trị từ 0.0 (không được nhấn) đến 1.0 (nhấn hoàn toàn). Các trục đi từ -1.0 (hoàn toàn bên trái hoặc lên) đến 0.0 (ở giữa) đến 1.0 (hoàn toàn bên phải hoặc hướng xuống).

Tương tự hay rời rạc?

Nhìn chung, mọi nút đều có thể là một nút tương tự – ví dụ như điều này khá phổ biến đối với các nút ở vai. Do đó, tốt nhất là bạn nên đặt một ngưỡng thay vì chỉ so sánh thẳng thừng với 1,00 (điều gì sẽ xảy ra nếu nút tương tự bị bẩn một chút? Con số này có thể không bao giờ đạt đến 1.00). Trong hình tượng trưng, chúng tôi thực hiện theo cách sau:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

Bạn có thể làm tương tự để biến cần điều khiển analog thành cần điều khiển kỹ thuật số. Chắc chắn rồi, luôn có bàn di chuột kỹ thuật số (d-pad), nhưng tay điều khiển trò chơi có thể không có. Dưới đây là mã của chúng tôi để xử lý việc đó:

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

Thao tác nhấn nút và chuyển động của gậy

Sự kiện

Trong một số trường hợp, như trò chơi mô phỏng máy bay, việc liên tục kiểm tra và phản ứng với các vị trí dính hoặc nhấn nút có ý nghĩa hơn... nhưng đối với những thứ như hình tượng trưng của Hurdles 2012 thì sao? Bạn có thể thắc mắc: Tại sao tôi cần kiểm tra các nút cho từng khung hình? Tại sao tôi không nhận được các sự kiện như tôi làm cho bàn phím hoặc chuột lên/xuống?

Tin vui là bạn có thể. Tin xấu là – trong tương lai. Việc này nằm trong thông số kỹ thuật nhưng chưa được triển khai trong bất kỳ trình duyệt nào.

Cuộc thăm dò ý kiến

Trong thời gian chờ đợi, bạn có thể so sánh trạng thái hiện tại và trạng thái trước đó, đồng thời gọi các hàm nếu thấy bất kỳ sự khác biệt nào. Ví dụ:

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

Phương pháp tiếp cận ưu tiên bàn phím trong hình tượng trưng của Hurdles năm 2012

Vì không có tay điều khiển trò chơi, phương thức nhập ưa thích của hình tượng trưng hiện nay là bàn phím, chúng tôi quyết định mô phỏng tay điều khiển trò chơi tương đối chặt chẽ. Điều này có nghĩa là có ba quyết định:

  1. Hình tượng trưng chỉ cần ba nút – hai nút để chạy và một nút để nhảy – nhưng tay điều khiển trò chơi có thể còn có nhiều nút khác nữa. Do đó, chúng tôi liên kết cả 16 nút đã biết và 2 thanh đã biết vào 3 hàm logic đó theo cách mà chúng tôi cho là hợp lý nhất, để mọi người có thể thao tác bằng cách: xen kẽ các nút A/B, xen kẽ các nút vai, nhấn trái/phải trên d-pad hoặc vung gậy sang trái và phải mạnh mẽ (tất nhiên một số nút sẽ hiệu quả hơn các nút khác). Ví dụ:

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. Chúng tôi đã xử lý mỗi đầu vào tương tự là một đầu vào riêng biệt, sử dụng các hàm ngưỡng được mô tả trước đó.

  3. Chúng ta đã đi sâu vào việc cố định đầu vào tay điều khiển trò chơi vào hình tượng trưng, thay vì tích hợp – vòng lặp thăm dò của chúng tôi thực sự tổng hợp các sự kiện keydown và keyup cần thiết (bằng một keyCode phù hợp) và gửi chúng trở lại DOM:

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

Đó là tất cả những gì bạn cần làm!

Mẹo và thủ thuật

  • Hãy nhớ rằng tay điều khiển trò chơi sẽ không hiển thị trong trình duyệt trước khi bạn nhấn nút.
  • Nếu bạn đang kiểm thử đồng thời tay điều khiển trò chơi trên nhiều trình duyệt, thì xin lưu ý rằng chỉ một trong số đó có thể cảm nhận được tay điều khiển. Nếu bạn không nhận được bất kỳ sự kiện nào, hãy nhớ đóng các trang khác có thể đang sử dụng sự kiện đó. Ngoài ra, theo kinh nghiệm của chúng tôi, đôi khi trình duyệt có thể "giữ" tay điều khiển trò chơi ngay cả khi bạn đóng thẻ hoặc thoát khỏi trình duyệt. Đôi khi, khởi động lại hệ thống là cách duy nhất để khắc phục vấn đề.
  • Như thường lệ, hãy sử dụng Chrome Canary và các trình duyệt tương đương cho các trình duyệt khác để đảm bảo bạn được hỗ trợ tốt nhất – sau đó xử lý phù hợp nếu bạn thấy các phiên bản cũ hoạt động khác đi.

Tương lai

Chúng tôi hy vọng thông tin này sẽ giúp bạn hiểu thêm về API mới này – tuy vẫn còn một chút bấp bênh nhưng cũng rất thú vị.

Ngoài các phần còn thiếu của API (ví dụ: sự kiện) và hỗ trợ trình duyệt rộng hơn, chúng tôi cũng hy vọng cuối cùng sẽ thấy được những tính năng như kiểm soát rumble, quyền truy cập vào con quay hồi chuyển tích hợp, v.v. Và hỗ trợ thêm cho các loại tay điều khiển trò chơi khác – vui lòng gửi lỗi với Chrome và/hoặc gửi lỗi với Firefox nếu bạn thấy một lỗi hoạt động không chính xác hoặc hoàn toàn không hoạt động.

Nhưng trước đó, hãy chơi với hình tượng trưng của YouTube, thử thách năm 2012 và xem trên tay điều khiển trò chơi thú vị hơn nhiều đến mức nào. Ồ, bạn vừa nói có thể làm tốt hơn 10,7 giây phải không? Mang đi.

Tài liệu đọc thêm