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

Marcin Wichary
Marcin Wichary

Giới thiệu

Hãy để người mới chơi sử dụng bàn phím cho các trò chơi phiêu lưu, các đầu ngón tay cảm ứng đa điể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ư họ có thể nhảy như Michael Jackson. (Tin mới: Họ không thể.) Nhưng bạn thì khác. Bạn đã khoẻ 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 bằng tay cầm trong tay.

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

Đó là một thời điểm khá tuyệt vời, vì gần đây chúng tôi có cơ hội sử dụng hình ảnh đó trong Doodle của Google về các rào cản năm 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 bức vẽ nguệch ngoạc và những điều chúng tôi học được trong quá trình này.

Hình tượng trưng của Google về các rào cản năm 2012
Google doodle về các rào cản năm 2012

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

Mặc dù ngắn ngủi, nhưng các hình vẽ nguệch ngoạc tương tác lại khá phức tạp. Để dễ dàng minh hoạ những gì chúng ta đang nói, chúng tôi đã lấy mã tay điều khiển từ bức vẽ nguệch ngoạc và tạo một trình kiểm thử tay điều khiển đơn giản. Bạn có thể sử dụng công cụ này để xem tay điều khiển USB có hoạt động đúng cách hay không, đồng thời xem xét cách thực hiện.

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

Hỗ trợ trình duyệt

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 10.1.

Nguồn

Tôi có thể sử dụng tay điều khiển nào?

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

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

Đây là ảnh chụp một số tay điều khiển mà chúng tôi dùng để kiểm thử ứng dụng vẽ nguệch ngoạc – "Vâng, mẹ ơi, đó thực sự là công việc của con ở cơ quan". Nếu tay điều khiển không hoạt động hoặc các nút điều khiển được liên kết không chính xác, vui lòng báo cáo lỗi về Chrome hoặc Firefox . (Vui lòng kiểm thử trong phiên bản mới nhất của từng trình duyệt để đảm bảo vấn đề chưa được khắc phục.)

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

Bạn có thể dễ dàng thực hiện việc này trong Chrome:

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

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

Nhưng chúng tôi chắc chắn rằng đó chỉ là vấn đề tạm thời. Modernizr tuyệt vời đã cho bạn biết về API tay điều khiển trò chơi, vì vậy, bạn nên sử dụng API này cho mọi nhu cầu phát hiện hiện tại và trong tương lai:

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, tay điều khiển đó sẽ không hiển thị theo bất kỳ cách nào trừ phi người dùng nhấn bất kỳ nút nào của tay điều khiển trước. Điều này là để ngăn chặn việc tạo vân tay số, mặc dù gây ra một chút thách thức cho trải nghiệm người dùng: 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 vì bạn không biết liệu họ đã kết nối tay điều khiển hay chưa.

Tuy nhiên, sau khi vượt qua rào cản đó (xin lỗi…), bạn sẽ phải đối mặt với nhiều thử thách khác.

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

Việc triển khai API của Chrome sẽ hiển thị một hàm – navigator.webkitGetGamepads() – mà 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 đang đượ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 đầu tiên được kết nối sẽ được trả về dưới dạng mục đầu tiên trong mảng, v.v.

(Gần đây, lệnh gọi hàm này đã 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, bạn nên sử dụng lệnh gọi hàm để dùng API và dần dần áp dụng cho tất cả trình duyệt Chrome đã cài đặt.)

Phần đã triển khai của 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 tay điều khiển trò chơi đã kết nối (và so sánh với trạng thái trước đó nếu cần), thay vì kích hoạt 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ả nhất và tiết kiệm pin nhất. Đối với bức vẽ nguệch ngoạc, mặc dù đã có một 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 – vòng lặp này đơn giản hơn để lập trình và không ảnh hưởng đến hiệu suất theo bất kỳ cách nào.

Dưới đây là mã của 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, việc lấy dữ liệu của tay điều khiển đó 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 cần thêm một vài dòng mã để phản ứng với các tình huống phức tạp hơn (2 hoặc nhiều tay điều khiển đượ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 trình kiểm thử, hàm pollGamepads(), để biết một phương pháp giải quyết vấn đề này.

Sự kiện

Firefox sử dụng một cách thay thế tốt hơn được mô tả trong thông số kỹ thuật của Gamepad API. Thay vì yêu cầu bạn thăm dò ý kiến, Firefox hiển thị hai sự kiện – MozGamepadConnectedMozGamepadDisconnected – được kích hoạt bất cứ khi nào tay điều khiển được cắm (hoặc chính xác hơn là được cắm và "công bố" bằng cách nhấn bất kỳ nút nào của tay điều khiển) hoặc rút phích cắm. Đối tượng tay điều khiển 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 trình 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 được 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

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 về tay điều khiển
  • 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 về trạng thái nút/trục (hiện chỉ được hỗ trợ trong Chrome)

Nút và cần điều khiển

Tay điều khiển ngày nay không giống như tay điều khiển mà ông của bạn có thể đã dùng để cứu công chúa trong lâu đài sai – tay điều khiển 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 điều khiển tương tự. Gamepad API sẽ cho bạn biết về tất cả các nút và cần điều khiển tương tự do 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, bạn có thể truy cập vào các nút thông qua .buttons[] và cần điều khiển thông qua các mảng .axes[]. Dưới đây là thông tin tóm tắt trực quan về những giá trị này:

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

Thông số kỹ thuật 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 thêm vào các nút và trục ở trên. Tuy nhiên, xin lưu ý rằng không có 16 nút hay 4 trục nào được đảm bảo – hãy sẵn sàng cho việc một số nút hoặc trục chỉ đơn giản là không được xác định.

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

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

Rõ ràng là mọi nút đều có thể là nút tương tự – điều này khá phổ biến đối với các nút vai, chẳng hạn. Do đó, tốt nhất bạn nên đặt ngưỡng thay vì chỉ so sánh ngưỡ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? Chỉ số này có thể không bao giờ đạt đến 1.00). Trong bức vẽ nguệch ngoạc của mình, chúng ta sẽ thực hiện như 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ự để chuyển đổi cần điều khiển analog thành cần điều khiển kỹ thuật số. Chắc chắn là 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ủa bạn có thể không có bàn di chuột. Dưới đây là mã để xử lý vấn đề đó:

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à di chuyển cần điều khiển

Sự kiện

Trong một số trường hợp, chẳng hạn như trò chơi mô phỏng chuyến bay, việc liên tục kiểm tra và phản ứng với vị trí của cần điều khiển hoặc thao tác nhấn nút sẽ hợp lý hơn… nhưng đối với những trò chơi như 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 trong từng khung hình? Tại sao tôi không nhận được sự kiện như khi nhấn phím hoặc di chuyển chuột lên/xuống?

Tin vui là bạn có thể làm được. Tin không vui là – trong tương lai. Tính năng này có 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 nhận thấy sự khác biệt. 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 ưu tiên bàn phím trong hình minh hoạ Hurdles 2012

Vì không có tay điều khiển, phương thức nhập ưu tiên của bức vẽ nguệch ngoạc hôm nay là bàn phím, nên chúng tôi quyết định tay điều khiển sẽ mô phỏng bàn phím một cách khá sát sao. Điều này có nghĩa là chúng tôi phải đưa ra 3 quyết định:

  1. Doodle chỉ cần 3 nút – 2 nút để chạy và 1 nút để nhảy – nhưng tay điều khiển có thể có nhiều nút hơn. Do đó, chúng tôi đã liên kết tất cả 16 nút và 2 cần điều khiển đã biết với 3 hàm logic đó theo cách chúng tôi cho là hợp lý nhất để mọi người có thể chạy bằng: nút A/B luân phiên, nút vai luân phiên, nhấn trái/phải trên d-pad hoặc vung mạnh cần điều khiển sang trái và phải (tất nhiên, một số cách sẽ hiệu quả hơn các cách 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 ta đã coi mỗi đầu vào tương tự là một đầu vào rời rạc, bằng cách sử dụng các hàm ngưỡng đã mô tả trước đó.

  3. Chúng tôi đã đi xa đến mức gắn chặt phương thức nhập bằng tay điều khiển vào hình vẽ nguệch ngoạc, thay vì tích hợp phương thức này – vòng lặp thăm dò ý kiến của chúng tôi thực sự tổng hợp các sự kiện nhấn phím và nhả phím cần thiết (với keyCode thích hợp) và gửi các sự kiện đó 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);

Chỉ vậy thôi!

Mẹo và thủ thuật

  • Hãy nhớ rằng tay điều khiển trò chơi sẽ không xuất hiện trong trình duyệt trước khi bạn nhấn một nút.
  • Nếu bạn đang kiểm thử tay điều khiển trong nhiều trình duyệt cùng lúc, hãy 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 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ữ lại" 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 sự cố.
  • Như mọi khi, hãy sử dụng Chrome Canary và các trình duyệt tương đương để đảm bảo bạn nhận được dịch vụ hỗ trợ tốt nhất, sau đó hành động thích 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 bài viết này giúp bạn hiểu rõ hơn về API mới này – vẫn còn hơi rủi ro nhưng rất thú vị.

Ngoài các phần còn thiếu của API (ví dụ: sự kiện) và khả năng 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ác tính năng như điều khiển rung, quyền truy cập vào con quay hồi chuyển tích hợp, v.v. Ngoài ra, chúng tôi sẽ hỗ trợ nhiều hơn cho nhiều loại tay điều khiển trò chơi – vui lòng gửi lỗi cho Chrome và/hoặc gửi lỗi cho Firefox nếu bạn thấy một lỗi không hoạt động đúng cách hoặc không hoạt động.

Nhưng trước đó, hãy chơi với bức vẽ nguệch ngoạc Hurdles 2012 của chúng tôi và xem trò chơi này thú vị hơn như thế nào trên tay điều khiển. Ồ, bạn vừa nói rằng bạn có thể làm tốt hơn 10,7 giây phải không? Cứ thử đi.

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