Melewati rintangan dengan Gamepad API

Marcin Wichary
Marcin Wichary

Pengantar

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

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

Ternyata waktunya sangat tepat, karena kami berkesempatan menggunakannya baru-baru ini di Google Doodle Lari Gawang 2012. Artikel ini akan menjelaskan secara singkat cara kami menambahkan Gamepad API ke doodle, dan apa yang kami pelajari selama proses tersebut.

Google doodle Lari Gawang 2012
Google Doodle Lari Gawang 2012

Penguji gamepad

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

Browser apa yang mendukungnya saat ini?

Browser Support

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

Source

Gamepad apa yang dapat digunakan?

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

Gamepad
Gamepad

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

Mendeteksi fitur Gamepad API<

Di Chrome:

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

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

Namun, kami yakin hal ini bersifat sementara. Modernizr yang sangat luar biasa sudah memberi tahu Anda tentang Gamepad API, jadi kami merekomendasikannya untuk semua kebutuhan deteksi Anda saat ini dan di masa 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 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.

Namun, setelah Anda melewati rintangan itu (maaf…), masih banyak rintangan lain yang menanti.

Polling

Implementasi API Chrome mengekspos 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, sedangkan panggilan fungsi berfungsi di Chrome 22 dan yang lebih baru. Ke depannya, panggilan fungsi adalah cara yang direkomendasikan untuk menggunakan API, dan akan diterapkan secara bertahap 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 status sebelumnya jika perlu), bukan memicu peristiwa saat terjadi perubahan. Kami mengandalkan requestAnimationFrame() untuk menyiapkan polling dengan cara yang paling efisien dan hemat baterai. Untuk doodle kami, meskipun kami sudah memiliki loop requestAnimationFrame() untuk mendukung animasi, kami membuat loop kedua yang benar-benar terpisah – lebih mudah untuk membuat kode 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 mementingkan satu gamepad, mendapatkan datanya mungkin sesederhana:

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

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

Acara

Firefox menggunakan cara alternatif yang lebih baik yang dijelaskan dalam spesifikasi Gamepad API. Alih-alih meminta Anda 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 dari 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, 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 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 adalah metadata sederhana:

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

Tombol dan tongkat

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 diskret, 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 ringkasan visual tentang hal yang sesuai:

Diagram Gamepad
Diagram Gamepad

Spesifikasi ini 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. Namun, perlu diperhatikan bahwa enam belas tombol atau empat sumbu tidak dijamin – bersiaplah jika beberapa di antaranya tidak ditentukan.

Tombol dapat mengambil nilai dari 0,0 (tidak ditekan) hingga 1,0 (ditekan sepenuhnya). Sumbu bergerak dari -1,0 (sepenuhnya ke kiri atau ke atas) melalui 0,0 (tengah) hingga 1,0 (sepenuhnya ke kanan atau ke bawah).

Analog atau diskret?

Tampaknya, setiap tombol bisa berupa tombol analog – hal ini agak umum untuk tombol bahu, misalnya. Oleh karena itu, sebaiknya tetapkan nilai minimum daripada hanya membandingkannya secara langsung dengan 1,00 (bagaimana jika tombol analog kebetulan sedikit kotor? Nilainya mungkin tidak akan pernah mencapai 1,00). Dalam doodle, kami 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 stik analog menjadi joystick digital. Tentu, selalu ada pad digital (d-pad), tetapi gamepad Anda mungkin tidak memilikinya. Berikut kode kita 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;
    }
};

Penekanan tombol dan gerakan stick

Acara

Dalam beberapa kasus, seperti game simulator penerbangan, terus-menerus memeriksa dan bereaksi terhadap posisi stick atau penekanan tombol lebih masuk akal… tetapi untuk hal-hal seperti doodle Lari Gawang 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 up/down?

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

Polling

Sementara itu, jalan 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 yang mengutamakan keyboard dalam doodle Lari Gawang 2012

Karena tanpa gamepad, metode input pilihan doodle hari ini adalah keyboard, kami memutuskan agar gamepad mengemulasikannya dengan cukup mirip. Artinya, 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 keenam belas tombol yang diketahui dan dua stik yang diketahui ke tiga fungsi logis tersebut dengan cara yang menurut kami paling masuk akal, sehingga orang dapat berlari dengan: menekan tombol A/B secara bergantian, menekan tombol bahu secara bergantian, menekan tombol kiri/kanan pada d-pad, atau mengayunkan salah satu stik dengan keras ke kiri dan ke 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. Kami memperlakukan setiap input analog sebagai input diskret, menggunakan fungsi nilai minimum yang dijelaskan sebelumnya.

  3. Kami bahkan memasang input gamepad ke doodle, bukan menyatukannya – 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

  • Ingat bahwa gamepad tidak akan terlihat di browser sama sekali sebelum tombol ditekan.
  • Jika Anda menguji gamepad di berbagai browser 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 "mempertahankan" 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

Semoga informasi ini membantu menjelaskan API baru ini – masih agak rentan, tetapi sudah sangat menyenangkan.

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

Namun sebelum itu, coba mainkan Doodle Lari Gawang 2012 kami dan lihat betapa serunya bermain dengan gamepad. Oh, apakah kamu baru saja mengatakan bahwa kamu bisa melakukan lebih baik dari 10,7 detik? Lakukan saja.

Bacaan lebih lanjut