Melewati rintangan dengan Gamepad API

Marcin Wichary
Marcin Wichary

Pengantar

Biarkan para pemula menggunakan keyboard mereka untuk game petualangan, ujung jari mereka yang berharga multi-sentuh untuk memotong buah, dan sensor gerakan canggih yang canggih untuk berpura-pura menari seperti Michael Jackson. (Newsflash: Mereka tidak bisa melakukannya.) Tapi Anda berbeda. Anda lebih baik. Anda seorang profesional. Untukmu, game dimulai dan diakhiri dengan gamepad di tangan.

Tapi tunggu. Bukankah Anda beruntung jika ingin mendukung gamepad di aplikasi web? Jangan lagi. Gamepad API baru hadir untuk membantu, memungkinkan Anda menggunakan JavaScript untuk membaca status pengontrol gamepad yang terpasang ke komputer Anda. Kabar ini terbilang baru, sehingga baru diluncurkan di Chrome 21 minggu lalu – dan hampir didukung di Firefox (saat ini tersedia dalam build khusus).

Waktu tersebut ternyata adalah waktu yang sangat tepat, karena kami mendapat kesempatan untuk menggunakannya baru-baru ini dalam doodle Google Herdles 2012. Artikel ini akan menjelaskan secara singkat bagaimana kami menambahkan Gamepad API ke doodle, dan apa yang kami pelajari selama proses tersebut.

Doodle Google Rintangan 2012
Google Doodle 2012 Hurdles

Penguji gamepad

Meskipun singkat, {i>doodle<i} interaktif cenderung cukup rumit di balik layar. Agar lebih mudah untuk mendemonstrasikan apa yang sedang kita bicarakan, kami mengambil kode gamepad dari doodle, dan membuat penguji gamepad sederhana. Anda bisa menggunakannya untuk melihat apakah gamepad USB berfungsi dengan benar – dan juga lihat di balik layar untuk memeriksa cara kerjanya.

Browser apa yang mendukungnya saat ini?

Dukungan Browser

  • 21
  • 12
  • 29
  • 10.1

Sumber

Gamepad apa yang bisa digunakan?

Umumnya, gamepad modern apa pun yang didukung oleh sistem Anda seharusnya berfungsi secara native. Kami menguji berbagai gamepad dari pengontrol USB yang tidak bermerek di PC, melalui gamepad PlayStation 2 yang terhubung melalui dongle ke Mac, hingga ke pengontrol Bluetooth yang dipasangkan dengan notebook Chrome OS.

Gamepad
Gamepad

Ini adalah foto beberapa pengontrol yang kami gunakan untuk menguji coretan kami – "Ya, ibu, itulah yang saya lakukan di tempat kerja." Jika pengontrol Anda tidak berfungsi, atau jika kontrol tidak dipetakan dengan benar, laporkan bug ke Chrome atau Firefox . (Ujilah pada versi yang benar-benar terbaru dari setiap browser untuk memastikan masalah ini belum diperbaiki.)

Fitur Mendeteksi Gamepad API<

Cukup mudah di Chrome:

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

Sepertinya belum mungkin untuk mendeteksi hal ini di Firefox – semuanya berbasis peristiwa, dan semua pengendali peristiwa perlu dilampirkan ke jendela, yang mencegah teknik umum untuk mendeteksi pengendali peristiwa.

Tapi kami yakin itu bersifat sementara. Modernizr yang selalu canggih sudah memberi tahu Anda tentang Gamepad API, jadi kami merekomendasikannya untuk semua kebutuhan pendeteksian saat ini dan mendatang:

var gamepadSupportAvailable = Modernizr.gamepads;

Mencari tahu tentang gamepad yang terhubung

Meskipun Anda menghubungkan gamepad, gamepad tidak akan muncul dengan cara apa pun kecuali pengguna menekan salah satu tombolnya terlebih dahulu. Hal ini untuk mencegah pelacakan sidik jari, meskipun terbukti menjadi sedikit tantangan bagi pengalaman pengguna: Anda tidak dapat meminta pengguna untuk menekan tombol atau memberikan petunjuk khusus gamepad karena Anda tidak tahu apakah mereka menghubungkan pengontrolnya atau tidak.

Setelah Anda menyingkirkan rintangan itu (maaf...), akan ada lebih banyak lagi yang menunggu.

Polling

Implementasi API di Chrome menampilkan fungsi – navigator.webkitGetGamepads() – yang dapat Anda gunakan untuk mendapatkan daftar semua gamepad yang saat ini dicolokkan ke sistem, beserta statusnya saat ini (tombol + stik). Gamepad pertama yang terhubung akan ditampilkan sebagai entri pertama dalam array, dan seterusnya.

(Panggilan fungsi ini baru saja menggantikan array yang dapat Anda akses secara langsung – navigator.webkitGamepads[]. Mulai awal Agustus 2012, mengakses array ini masih diperlukan di Chrome 21, sementara panggilan fungsi berfungsi di Chrome 22 dan yang lebih baru. Selanjutnya, panggilan fungsi adalah cara yang direkomendasikan untuk menggunakan API, dan akan perlahan-lahan masuk ke semua browser Chrome yang terinstal.)

Bagian spesifikasi yang sejauh ini diterapkan mengharuskan Anda untuk terus memeriksa status gamepad yang terhubung (dan membandingkannya dengan gamepad sebelumnya jika perlu), bukan memicu peristiwa saat terjadi perubahan. Kami mengandalkan requestAnimationFrame() untuk menyiapkan polling dengan cara yang paling efisien dan ramah baterai. Untuk doodle, meskipun kami sudah memiliki loop requestAnimationFrame() untuk mendukung animasi, kami membuat loop kedua yang benar-benar terpisah – lebih sederhana untuk dikodekan dan tidak akan memengaruhi performa dengan cara apa pun.

Berikut adalah kode dari penguji:

/**
 * 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.)
},

Jika Anda hanya mempedulikan satu gamepad, mendapatkan datanya mungkin semudah:

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

Jika Anda ingin sedikit lebih pintar, atau mendukung lebih dari satu pemain secara bersamaan, Anda perlu menambahkan beberapa baris kode lagi untuk menanggapi skenario yang lebih kompleks (dua atau lebih gamepad terhubung, beberapa di antaranya terputus di tengah jalan, dll.). Anda dapat melihat kode sumber penguji kami, fungsi pollGamepads(), untuk satu pendekatan tentang cara menyelesaikan ini.

Peristiwa

Firefox menggunakan cara alternatif yang lebih baik yang dijelaskan dalam spesifikasi Gamepad API. Alih-alih meminta Anda untuk melakukan polling, Firefox menampilkan dua peristiwa – MozGamepadConnected dan MozGamepadDisconnected – yang diaktifkan setiap kali gamepad dicolokkan (atau, lebih tepatnya, dicolokkan dan "diumumkan" dengan menekan salah satu tombolnya) atau dicabut. Objek gamepad yang akan terus mencerminkan status mendatangnya diteruskan sebagai parameter .gamepad objek peristiwa.

Dari kode sumber penguji:

/**
 * 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);
},

Ringkasan

Pada akhirnya, fungsi inisialisasi dalam penguji, yang mendukung kedua pendekatan tersebut, terlihat seperti ini:

/**
 * 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();
    }
    }
},

Info gamepad

Setiap gamepad yang terhubung ke sistem akan diwakili oleh objek yang terlihat seperti ini:

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

Info dasar

Beberapa kolom teratas merupakan metadata sederhana:

  • id: deskripsi tekstual gamepad
  • index: bilangan bulat yang berguna untuk membedakan berbagai gamepad yang terpasang ke satu komputer
  • timestamp: stempel waktu pembaruan terakhir status tombol/sumbu (saat ini hanya didukung di Chrome)

Kancing dan stik

Gamepad masa kini tidak sama persis dengan yang digunakan kakek Anda untuk menyimpan putri raja di kastil yang salah – biasanya memiliki setidaknya enam belas tombol terpisah (beberapa terpisah, beberapa tombol analog), selain dua stik analog. Gamepad API akan memberi tahu Anda tentang semua tombol dan stick analog yang dilaporkan oleh sistem operasi.

Setelah mendapatkan status saat ini dalam objek gamepad, Anda dapat mengakses tombol melalui .buttons[] dan sticky melalui array .axes[]. Berikut adalah ringkasan visual terkait hal tersebut:

Diagram Gamepad
Diagram Gamepad

Spesifikasi ini meminta browser untuk memetakan enam belas tombol dan empat sumbu pertama ke:

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
};

Tombol dan sumbu ekstra akan ditambahkan ke tombol dan sumbu di atas. Perlu diketahui bahwa tidak ada enam belas tombol atau empat sumbu yang dijamin – siap untuk beberapa di antaranya yang tidak ditentukan.

Tombol dapat mengambil nilai dari 0.0 (tidak ditekan) hingga 1.0 (ditekan sepenuhnya). Sumbu-sumbu pergi dari -1.0 (benar-benar kiri atau atas) melalui 0.0 (tengah) ke 1.0 (sepenuhnya kanan atau bawah).

Analog atau diskret?

Tampaknya, setiap tombol bisa menjadi tombol analog – ini agak umum untuk tombol bahu, misalnya. Oleh karena itu, yang terbaik adalah menetapkan ambang batas daripada hanya membandingkannya dengan terus-menerus dengan 1,00 (bagaimana jika tombol analog terlihat sedikit kotor? Mungkin tidak pernah mencapai 1,00). Dalam {i>doodle<i} kami, kami melakukannya seperti ini:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Anda dapat melakukan hal yang sama untuk mengubah stik analog menjadi joystick digital. Tentu, selalu ada pad digital (d-pad), tetapi gamepad Anda mungkin tidak memilikinya. Berikut adalah kode kami untuk menangani hal tersebut:

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;
    }
};

Menekan tombol dan gerakan stik

Peristiwa

Dalam beberapa kasus, seperti game simulator penerbangan, terus-menerus memeriksa dan bereaksi terhadap posisi tongkat atau penekanan tombol lebih masuk akal... tetapi untuk hal-hal seperti coretan Rintangan 2012? Anda mungkin bertanya-tanya: Mengapa saya harus memeriksa tombol di setiap bingkai? Mengapa saya tidak bisa mendapatkan peristiwa seperti yang saya lakukan untuk keyboard atau mouse atas/bawah?

Berita baiknya adalah, Anda bisa. Kabar buruknya – di masa mendatang. ID ini ada dalam spesifikasi, tetapi belum diimplementasikan di browser apa pun.

Polling

Sementara itu, jalan keluar Anda adalah membandingkan status saat ini dan sebelumnya, serta memanggil fungsi jika Anda melihat perbedaan. Contoh:

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);
}

Pendekatan {i>keyboard-first<i} dalam {i>doodle<i} Hurdles 2012

Karena tanpa gamepad, metode input pilihan kami untuk coretan hari ini adalah keyboard, jadi kami memutuskan untuk meniru gamepad itu dengan lebih cermat. Ini berarti tiga keputusan:

  1. Doodle hanya memerlukan tiga tombol – dua untuk berlari dan satu untuk melompat – tetapi gamepad kemungkinan akan memiliki lebih banyak tombol. Oleh karena itu, kami memetakan keenam belas tombol yang diketahui dan dua tongkat yang diketahui ke dalam tiga fungsi logika tersebut dengan cara yang menurut kami paling masuk akal, sehingga orang-orang dapat menjalankannya dengan: bergantian tombol A/B, tombol bahu bolak-balik, menekan kiri/kanan pada d-pad, atau mengayunkan tongkat ke kiri dan kanan dengan keras (beberapa di antaranya, tentu saja, lebih efisien daripada yang lain). Contoh:

    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. Kita memperlakukan setiap input analog sebagai input terpisah, menggunakan fungsi ambang batas yang dijelaskan sebelumnya.

  3. Kita telah memasukkan input gamepad ke dalam doodle, bukan memasukkannya dalam corat-coret – loop polling sebenarnya menyintesis peristiwa keydown dan keyup yang diperlukan (dengan keyCode yang tepat) dan mengirimkannya kembali ke 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);

Dan itu saja!

Tips dan trik

  • Ingatlah bahwa gamepad sama sekali tidak akan terlihat di browser Anda sebelum tombol ditekan.
  • Jika Anda menguji gamepad di browser yang berbeda secara bersamaan, perhatikan bahwa hanya satu gamepad yang dapat merasakan pengontrol. Jika Anda tidak menerima peristiwa apa pun, pastikan untuk menutup halaman lain yang mungkin menggunakannya. Selain itu, berdasarkan pengalaman kami, terkadang browser dapat "memegang" gamepad meskipun Anda menutup tab atau menutup browser itu sendiri. Memulai ulang sistem terkadang merupakan satu-satunya cara untuk memperbaiki sesuatu.
  • Seperti biasa, gunakan Chrome Canary dan versi yang setara untuk browser lain guna memastikan Anda mendapatkan dukungan terbaik – dan lakukan tindakan yang sesuai jika Anda melihat versi lama memiliki perilaku yang berbeda.

Masa Depan

Kami harap ini bisa membantu menjelaskan API baru ini – masih sedikit sulit, tapi sudah sangat menyenangkan.

Selain bagian API yang hilang (misalnya event) dan dukungan browser yang lebih luas, kami juga berharap pada akhirnya menemukan hal-hal seperti rumble control, akses ke giroskop bawaan, dll. Selain itu, dukungan lainnya untuk berbagai jenis gamepad – harap laporkan bug di Chrome dan/atau laporkan bug terhadap Firefox jika Anda melihat ada yang tidak berfungsi dengan baik atau tidak sama sekali.

Tapi sebelum itu, ayo mainkan doodleHurdles 2012 kami dan lihat betapa serunya di gamepad ini. Oh, barusan kamu bilang kalau kamu bisa melakukan lebih dari 10,7 detik? Lakukan saja.

Bacaan lebih lanjut