Melewati rintangan dengan Gamepad API

Marcin Wichary
Marcin Wichary

Pengantar

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

Tapi tunggu. Apakah Anda tidak beruntung jika ingin mendukung gamepad di aplikasi web? Jangan khawatir. Gamepad API yang baru hadir untuk membantu, memungkinkan Anda menggunakan JavaScript untuk membaca status pengontrol gamepad yang terpasang ke komputer. Fitur ini baru saja dirilis, sehingga hanya tersedia di Chrome 21 minggu lalu – dan juga akan segera didukung di Firefox (saat ini tersedia dalam build khusus).

Ternyata itu adalah waktu yang tepat, karena kami baru-baru ini mendapatkan kesempatan untuk menggunakannya dalam Google doodle Hambatan 2012. Artikel ini akan menjelaskan secara singkat cara kami menambahkan Gamepad API ke doodle, dan hal-hal yang kami pelajari selama prosesnya.

Google doodle 2012 tentang rintangan
Google doodle Hambatan 2012

Penguji gamepad

Meskipun bersifat sementara, doodle interaktif cenderung cukup rumit di balik layar. Agar lebih mudah untuk mendemonstrasikan apa yang kita bicarakan, kami mengambil kode gamepad dari doodle, dan menyusun pengujian gamepad sederhana. Anda dapat menggunakannya untuk melihat apakah gamepad USB berfungsi dengan benar – dan juga melihat di balik layar untuk memeriksa cara kerjanya.

Browser apa yang mendukungnya saat ini?

Dukungan Browser

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

Sumber

Gamepad apa yang dapat digunakan?

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

Gamepad
Gamepad

Ini adalah foto beberapa pengontrol yang kami gunakan untuk menguji doodle kami – "Ya, Bu, itulah yang saya lakukan di kantor". Jika pengontrol tidak berfungsi, atau jika kontrol dipetakan secara salah, harap laporkan bug ke Chrome atau Firefox . (Harap uji di versi terbaru setiap browser untuk memastikan masalahnya belum diperbaiki.)

Fitur Mendeteksi Gamepad API<

Cukup mudah di Chrome:

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

Sepertinya belum mungkin mendeteksinya di Firefox – semuanya berbasis peristiwa, dan semua pengendali peristiwa harus dilampirkan ke jendela, yang mencegah teknik umum mendeteksi pengendali peristiwa berfungsi.

Namun, kami yakin hal ini bersifat sementara. Modernizr yang sangat luar biasa sudah memberi tahu Anda tentang Gamepad API, jadi sebaiknya gunakan ini untuk semua kebutuhan deteksi Anda saat ini dan mendatang:

var gamepadSupportAvailable = Modernizr.gamepads;

Mencari tahu gamepad yang terhubung

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

Namun, setelah Anda melewati rintangan tersebut (maaf…), masih ada tantangan lainnya.

Polling

Implementasi API Chrome mengekspos fungsi – navigator.webkitGetGamepads() – yang dapat Anda gunakan untuk mendapatkan daftar semua gamepad yang saat ini terhubung 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, sedangkan panggilan fungsi berfungsi di Chrome 22 dan yang lebih baru. Ke depannya, panggilan fungsi adalah cara yang direkomendasikan untuk menggunakan API, dan akan perlahan-lahan diterapkan ke semua browser Chrome yang diinstal.)

Bagian spesifikasi yang telah diimplementasikan sejauh ini mengharuskan Anda untuk terus memeriksa status gamepad yang terhubung (dan membandingkannya dengan gamepad sebelumnya jika perlu), bukan memicu peristiwa saat ada perubahan. Kami mengandalkan requestAnimationFrame() untuk menyiapkan polling dengan cara yang paling efisien dan hemat baterai. Untuk doodle, meskipun sudah memiliki loop requestAnimationFrame() untuk mendukung animasi, kita membuat loop kedua yang benar-benar terpisah – kodenya lebih sederhana 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 tertarik dengan satu gamepad, mendapatkan datanya mungkin semudah:

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

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

Acara

Firefox menggunakan cara alternatif yang lebih baik yang dijelaskan dalam spesifikasi Gamepad API. Alih-alih meminta Anda untuk melakukan polling, Firefox mengekspos 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 statusnya di masa mendatang 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 kita di penguji, yang mendukung kedua pendekatan tersebut, akan 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 direpresentasikan 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 adalah metadata sederhana:

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

Tombol dan stik

Gamepad saat ini tidak sama dengan yang mungkin digunakan kakek Anda untuk menyelamatkan putri di kastil yang salah – gamepad biasanya memiliki setidaknya enam belas tombol terpisah (beberapa terpisah, beberapa analog), selain dua stik analog. Gamepad API akan memberi tahu Anda tentang semua tombol dan stik analog yang dilaporkan oleh sistem operasi.

Setelah mendapatkan status saat ini di objek gamepad, Anda dapat mengakses tombol melalui .buttons[] dan stik melalui array .axes[]. Berikut adalah ringkasan visual tentang kesesuaiannya:

Diagram Gamepad
Diagram Gamepad

Spesifikasi meminta browser untuk memetakan enam belas tombol pertama dan empat sumbu 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 tambahan akan ditambahkan ke tombol dan sumbu di atas. Perhatikan bahwa enam belas tombol atau empat sumbu tidak dijamin, jadi bersiaplah jika beberapa di antaranya tidak ditentukan.

Tombol dapat menggunakan nilai dari 0,0 (tidak ditekan) hingga 1,0 (ditekan sepenuhnya). Sumbunya berkisar dari -1,0 (sepenuhnya ke kiri atau atas) hingga 0,0 (tengah) hingga 1,0 (sepenuhnya ke kanan atau bawah).

Analog atau diskret?

Secara umum, setiap tombol dapat berupa tombol analog – misalnya, ini cukup umum untuk tombol bahu. Oleh karena itu, sebaiknya tetapkan nilai minimum, bukan hanya membandingkannya secara langsung dengan 1,00 (bagaimana jika tombol analog sedikit kotor? Nilai ini mungkin tidak pernah mencapai 1,00). Dalam doodle, kita melakukannya dengan cara 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 tongkat analog menjadi joystick digital. Tentu saja, selalu ada tombol digital (d-pad), tetapi gamepad Anda mungkin tidak memilikinya. Berikut kode kita untuk menanganinya:

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

Penekanan tombol dan gerakan stik

Acara

Dalam beberapa kasus, seperti game simulator penerbangan, terus memeriksa dan bereaksi terhadap posisi tongkat atau penekanan tombol akan lebih masuk akal… tetapi untuk hal-hal seperti doodle Hurdles 2012? Anda mungkin bertanya-tanya: Mengapa saya perlu memeriksa tombol di setiap frame? Mengapa saya tidak bisa mendapatkan peristiwa seperti yang saya lakukan untuk keyboard atau mouse atas/bawah?

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

Polling

Sementara itu, cara keluarnya 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 keyboard-first dalam doodle Hurdles 2012

Karena tanpa gamepad, metode input yang diinginkan untuk doodle hari ini adalah keyboard, kami memutuskan agar gamepad mengemulasikannya dengan cukup dekat. Hal ini berarti ada tiga keputusan:

  1. Doodle hanya memerlukan tiga tombol – dua untuk berlari, dan satu untuk melompat – tetapi gamepad kemungkinan memiliki lebih banyak tombol. Oleh karena itu, kami memetakan semua enam belas tombol yang diketahui dan dua tongkat yang diketahui ke tiga fungsi logis tersebut dengan cara yang menurut kami paling masuk akal, sehingga orang dapat berlari dengan: bergantian tombol A/B, bergantian tombol bahu, menekan kiri/kanan pada d-pad, atau mengayunkan salah satu tongkat dengan keras ke kiri dan kanan (beberapa di antaranya tentu saja akan 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 diskret, menggunakan fungsi nilai minimum yang dijelaskan sebelumnya.

  3. Kami bahkan memasang input gamepad ke doodle, bukan memasukkannya – loop polling kami 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);

Selesai.

Tips dan trik

  • Perlu diingat bahwa gamepad tidak akan terlihat di browser Anda sama sekali sebelum tombol ditekan.
  • Jika Anda menguji gamepad di browser yang berbeda secara bersamaan, perhatikan bahwa hanya satu browser yang dapat mendeteksi pengontrol. Jika Anda tidak menerima peristiwa apa pun, pastikan untuk menutup halaman lain yang mungkin menggunakannya. Selain itu, dari pengalaman kami, terkadang browser dapat "memegang" gamepad meskipun Anda menutup tab atau keluar dari browser itu sendiri. Memulai ulang sistem terkadang merupakan satu-satunya cara untuk memperbaiki masalah.
  • Seperti biasa, gunakan Chrome Canary dan yang setara untuk browser lain guna memastikan Anda mendapatkan dukungan terbaik, lalu bertindaklah dengan tepat jika Anda melihat versi lama berperilaku berbeda.

Masa Depan

Kami harap informasi ini dapat membantu menjelaskan API baru ini – masih sedikit tidak stabil, tetapi sudah sangat menyenangkan.

Selain bagian API yang tidak ada (misalnya, peristiwa) dan dukungan browser yang lebih luas, kami juga berharap pada akhirnya akan melihat hal-hal seperti kontrol getaran, akses ke giroskop bawaan, dll. Selain itu, dukungan lebih lanjut untuk berbagai jenis gamepad – harap laporkan bug di Chrome dan/atau laporkan bug di Firefox jika Anda menemukan bug yang tidak berfungsi dengan benar atau tidak berfungsi sama sekali.

Namun, sebelum itu, mainkan Doodle Hurdles 2012 kami dan lihat betapa menyenangkannya bermain dengan gamepad. Oh, apakah Anda baru saja mengatakan bahwa Anda bisa melakukannya lebih cepat dari 10,7 detik? Ayo.

Bacaan lebih lanjut