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 dùng bàn phím cho các trò chơi phiêu lưu, dùng ngón tay đa chạm quý giá để cắt trái cây và dùng cảm biến chuyển động mới lạ để giả vờ rằng họ có thể nhảy như Michael Jackson. (Tin nhanh: Họ không thể.) Nhưng bạn thì khác. Bạn đã tiến bộ hơn. Bạn là một chuyên gia. Đối với bạn, trò chơi bắt đầu và kết thúc bằng tay điều khiển trò chơi trong tay.

Nhưng chờ đã. Có phải bạn sẽ không thể hỗ trợ tay điều khiển trò chơi trong ứng dụng web của mình không? Giờ thì không còn chuyệ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 kết nối với máy tính của bạn. Tính năng này mới ra mắt nên chỉ mới 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).

Thời điểm đó hoá ra lại rất phù hợp, vì gần đây chúng tôi có cơ hội sử dụng nó trong hình tượng trưng của Google về môn Chạy vượt rào 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 hình tượng trưng 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ề môn chạy vượt rào năm 2012
Hình tượng trưng của Google về môn chạy vượt rào năm 2012

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

Dù chỉ xuất hiện trong thời gian ngắn, nhưng các hình tượng trưng tương tác thường có cấu trúc 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 trò chơi từ hình tượng trưng và kết hợp thành một trình kiểm tra tay điều khiển trò chơi đơn giản. Bạn có thể sử dụng ứng dụng này để xem tay điều khiển trò chơi USB có hoạt động đúng cách hay không, đồng thời tìm hiểu sâu hơn để xem cách hoạt động của ứng dụng này.

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

Browser Support

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

Source

Có thể sử dụng nhữ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ợ sẵn đều có thể hoạt động. Chúng tôi đã thử nghiệm nhiều tay điều khiển trò chơi, từ bộ điều khiển USB không có thương hiệu trên máy tính, tay điều khiển trò chơi PlayStation 2 kết nối qua khoá phần cứng với máy Mac, cho đến bộ điều khiển Bluetooth được ghép nối với máy tính xách tay ChromeOS.

Tay điều khiển trò chơi
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 đã dùng để thử nghiệm hình tượng trưng – "Mẹ ơi, đây thực sự là công việc của con đấy". Nếu tay điều khiển của bạn không hoạt động hoặc nếu các nút điều khiển được liên kết không chính xác, vui lòng gửi lỗi cho Chrome hoặc Firefox . (Vui lòng kiểm tra trong phiên bản mới nhất của từng trình duyệt để đảm bảo rằng vấn đề chưa được khắc phục.)

Phát hiện tính năng của Gamepad API<

Trong Chrome:

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

Có vẻ như chưa thể phát hiện đ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 cần được đính kèm vào cửa sổ, điều này ngăn kỹ thuật phát hiện trình xử lý sự kiện thông thường hoạt động.

Nhưng chúng tôi chắc chắn rằng đây chỉ là tạm thời. Modernizr tuyệt vời đã cho bạn biết về Gamepad API, vì vậy, bạn nên sử dụng API này cho tất cả nhu cầu phát hiện hiện tại và 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 trò chơi, tay điều khiển này cũng sẽ không tự biểu hiện theo bất kỳ cách nào trừ phi người dùng nhấn vào bất kỳ nút nào trước. Điều này là để ngăn chặn việc lấy vân tay số, mặc dù có thể gây ra một chút khó khăn 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 trò chơi vì bạn không biết liệu họ có kết nối tay điều khiển hay không.

Tuy nhiên, khi vượt qua được rào cản đó (xin lỗi…), bạn sẽ gặp nhiều rào cản khác.

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

Cách triển khai API của Chrome cho thấy một hàm – navigator.webkitGetGamepads() – bạn có thể 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 chúng (nút + cần điều khiển). Tay điều khiển trò chơi được kết nối đầu tiên sẽ được trả về dưới dạng mục đầu tiên trong mảng, v.v.

(Lệnh gọi hàm này vừa thay thế một mảng mà bạn có thể truy cập trực tiếp – navigator.webkitGamepads[]. Tính đến đầ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. Trong tương lai, lệnh gọi hàm là cách nên dùng để sử dụng API và sẽ dần được triển khai cho tất cả trình duyệt Chrome đã cài đặt.)

Phần thông số kỹ thuật đã triển khai cho đến nay 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 trạng thái đó với trạng thái trước đó nếu cần), thay vì kích hoạt sự kiện khi có 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 hình tượng trưng, mặc dù đã có vòng lặp requestAnimationFrame() để hỗ trợ ảnh động, nhưng chúng tôi đã tạo một vòng lặp thứ hai hoàn toàn riêng biệt – việc này giúp lập trình đơn giản hơn và không ảnh hưởng đến hiệu suất theo bất kỳ cách nào.

Sau đâ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 trò chơi, thì 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 (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 trình kiểm thử, hàm pollGamepads(), để biết một cách giải quyết vấn đề này.

Sự kiện

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

Trong 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 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 đượ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

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 về 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 khác nhau được kết nối với 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 gạt

Tay điều khiển trò chơi ngày nay không giống với những gì ông bạn có thể đã dùng để cứu công chúa trong nhầm lâu đài – chúng thường có ít nhất 16 nút riêng biệt (một số nút rời, một số nút analog), ngoài hai cần điều khiển analog. Gamepad API sẽ cho bạn biết về tất cả các nút và cần điều khiển analog mà 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 mảng .buttons[] và cần điều khiển thông qua mảng .axes[]. Dưới đây là thông tin tóm tắt trực quan về những gì các thành phần này tương ứng:

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 đầu tiên và 4 trục 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 phải lúc nào cũng được đảm bảo có 16 nút hoặc 4 trục, vì vậy, hãy chuẩn bị cho trường hợp một số nút hoặc trục không được xác định.

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

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

Bề ngoài, mọi nút đều có thể là nút analog – đ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 ngưỡng thay vì chỉ so sánh trực tiếp với 1.00 (điều gì sẽ xảy ra nếu nút analog bị bẩn một chút? Giá trị này có thể không bao giờ đạt đến 1). Trong hình tượng trưng, chúng tôi thực hiện theo cách này:

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ố. Tất nhiên, luôn có bàn phím 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 phím này. Dưới đây là mã để 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à di chuyển cần gạt

Sự kiện

Trong một số trường hợp, chẳng hạn như chế độ mô phỏng máy bay, việc liên tục kiểm tra và phản ứng với vị trí 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ư hình tượng trưng 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 trên mỗi khung hình? Tại sao tôi không thể nhận được các sự kiện như khi nhấn phím hoặc nhấp chuột lên/xuống?

Tin vui là bạn có thể làm được điều đó. Tin xấu là – trong tương lai. Thuộc tính 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 đó, rồi gọi các hàm nếu thấy có 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 tượng trưng Hurdles 2012

Vì không có tay điều khiển trò chơi, nên phương thức nhập ưu tiên của hình tượng trưng hôm nay là bàn phím. Chúng tôi quyết định để tay điều khiển trò chơi mô phỏng bàn phím một cách gần giống nhất. Điều này có nghĩa là chúng tôi phải đưa ra 3 quyết định:

  1. Hình tượng trưng chỉ cần 3 nút (2 nút để chạy và 1 nút để nhảy), nhưng tay điều khiển trò chơi 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 chức năng logic đó theo cách mà chúng tôi cho là hợp lý nhất, để người chơi có thể chạy bằng cách: nhấn luân phiên nút A/B, nhấn luân phiên nút vai, nhấn nút trái/phải trên d-pad hoặc lắc 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 những 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 tôi coi mỗi dữ liệu đầu vào tương tự là một dữ liệu riêng biệt, sử dụng các hàm ngưỡng đã mô tả trước đó.

  3. Chúng tôi đã tiến xa đến mức gắn đầu vào của tay điều khiển trò chơi vào hình tượng trưng, thay vì tích hợp sẵn – 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 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);

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 của bạn trước khi bạn nhấn nút.
  • Nếu bạn đang thử nghiệm tay điều khiển trò chơi trong nhiều trình duyệt cùng một lúc, hãy lưu ý rằng chỉ một trong số các trình duyệt đó 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ữ" 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ư thường lệ, hãy sử dụng Chrome Canary và các phiên bản tương đương cho các trình duyệt khác để đảm bảo bạn đang được 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.

Tương lai

Chúng tôi hy vọng bài viết này sẽ giúp bạn hiểu rõ hơn về API mới này – tuy vẫn còn một số điểm chưa ổn định nhưng đã mang lại nhiều điều 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 những 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 cũng hy vọng sẽ có thêm nhiều loại tay điều khiển trò chơi được hỗ trợ. 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 tay điều khiển trò chơi hoạt động không chính xác hoặc không hoạt động.

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

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