Oyun Kumandası API'siyle engelleri aşma

Marcin Wichary
Marcin Wichary

Giriş

Yeni başlayanlar macera oyunları için klavyelerini, meyve kesmek için çok dokunaklı parmaklarını ve Michael Jackson gibi dans edebileceklerini iddia etmek için yeni moda hareket sensörlerini kullanabilir. (Haber: Bunu yapamaz.) Ama siz farklısınız. İyisiniz. Siz bir Pro'sunuz. Sizin için oyunlar, elinizde bir gamepad ile başlar ve biter.

Ama bekleyin. Web uygulamanızda gamepad desteği sunmak istiyorsanız şansınız yok mu? Artık değil. Yeni Gamepad API, bilgisayarınıza bağlı herhangi bir gamepad kontrol cihazının durumunu okumak için JavaScript'i kullanmanıza olanak tanıyarak yardımınıza koşar. Bu özellik henüz yeni kullanıma sunulduğu için yalnızca geçen hafta Chrome 21'e eklendi. Firefox'ta da desteklenmesine az kaldı (şu anda özel bir derlemede kullanılabilir).

Bu, 2012 Engeller Günü Google doodle'ında kullanma fırsatı bulduğumuz için oldukça iyi bir zamanlama oldu. Bu makalede, Gamepad API'yi doodle'a nasıl eklediğimiz ve bu süreçte neler öğrendiğimiz kısaca açıklanmaktadır.

Engeller 2012 Google doodle'ı
Engeller 2012 Google doodle'ı

Oyun kumandası test kullanıcısı

Etkileşimli doodle'lar geçici olsa da arka planda oldukça karmaşıktır. Ne hakkında konuştuğumuzu daha kolay göstermek için doodle'daki gamepad kodunu alıp basit bir gamepad test cihazı oluşturduk. USB oyun kumandanızın düzgün çalışıp çalışmadığını görmek için bu aracı kullanabilir ve nasıl çalıştığını incelemek için de ayrıntılı inceleme yapabilirsiniz.

Şu anda hangi tarayıcılar bu özelliği destekliyor?

Tarayıcı desteği

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

Kaynak

Hangi gamepad'ler kullanılabilir?

Genellikle, sisteminiz tarafından doğal olarak desteklenen tüm modern oyun kumandaları çalışır. PC'de markalı olmayan USB kontrol cihazlarından, Mac'e dongle ile bağlanan PlayStation 2 oyun kumandaları ve Chrome OS dizüstü bilgisayarla eşlenen Bluetooth kontrol cihazlarına kadar çeşitli oyun kumandaları test ettik.

Oyun kumandası
Oyun kumandası

Bu, karalamayı test etmek için kullandığımız bazı kontrol cihazlarının fotoğrafıdır. "Evet anne, işte gerçekten bunu yapıyorum." Kumandanız çalışmıyorsa veya kontroller yanlış eşlenmişse lütfen Chrome ya da Firefox için hata kaydı oluşturun . (Sorunun düzeltilmediğinden emin olmak için lütfen her tarayıcının en yeni sürümünde test edin.)

Gamepad API'sini Özellik Algılama<

Chrome'da bu işlemi yapmak oldukça kolaydır:

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

Bunu Firefox'ta henüz algılamak mümkün değil. Her şey etkinliğe dayalı ve tüm etkinlik işleyicilerinin pencereye eklenmesi gerekiyor. Bu da etkinlik işleyicileri algılamayla ilgili tipik bir tekniğin çalışmasını engelliyor.

Ancak bunun geçici bir durum olduğunun farkındayız. Her zaman harika olan Modernizr, Gamepad API hakkında size bilgi veriyor. Bu nedenle, mevcut ve gelecekteki tüm algılama ihtiyaçlarınız için bunu kullanmanızı öneririz:

var gamepadSupportAvailable = Modernizr.gamepads;

Bağlı oyun kumandaları hakkında bilgi edinme

Oyun kumandasını bağlasanız bile kullanıcı düğmelerinden birine basmadığı sürece hiçbir şekilde kendini göstermez. Bu, parmak izi bırakmayı önlemek içindir ancak kullanıcı deneyimi açısından biraz zor olabilir: Denetleyiciyi bağlayıp bağlamadıklarını bilmediğiniz için kullanıcıdan düğmeye basmasını isteyemez veya gamepad'e özel talimatlar sağlayamazsınız.

Ancak bu engeli aştıktan sonra (özür dileriz) daha fazlası sizi bekliyor.

Anket

Chrome'un API'yi uygulamasında, şu anda sisteme takılı olan tüm gamepad'lerin listesini ve mevcut durumlarını (düğmeler + çubuklar) almak için kullanabileceğiniz bir işlev (navigator.webkitGetGamepads()) bulunur. İlk bağlanan gamepad, diziye ilk giriş olarak döndürülür ve bu şekilde devam eder.

(Bu işlev çağrısı, doğrudan erişebileceğiniz bir diziyi (navigator.webkitGamepads[]) kısa süre önce değiştirdi. Ağustos 2012'nin başlarından itibaren Chrome 21'de bu diziye erişmek hâlâ gereklidir. İşlev çağrısı ise Chrome 22 ve sonraki sürümlerde çalışır. Bundan sonra, API'yi kullanmanın önerilen yolu işlev çağrısıdır ve bu yöntem, yüklü tüm Chrome tarayıcılarına kademeli olarak sunulacaktır.)

Spesifikasyonun şu ana kadar uygulanmış olan kısmı, bir değişiklik olduğunda etkinlik tetiklemek yerine bağlı oyun kumandanlarının durumunu sürekli olarak kontrol etmenizi (ve gerekirse öncekiyle karşılaştırmanızı) gerektirir. Anketi en verimli ve pil dostu şekilde ayarlamak için requestAnimationFrame() işlevini kullandık. Doodle'ımız için, animasyonları destekleyen bir requestAnimationFrame() döngüm olsa da tamamen ayrı ikinci bir döngü oluşturduk. Bu döngünün kodlaması daha basitti ve performansı hiçbir şekilde etkilemeyecekti.

Test kullanıcısının kodu:

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

Yalnızca bir gamepad'i ilgilendiren verileri almak için şunları yapmanız yeterlidir:

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

Biraz daha akıllıca davranmak veya aynı anda birden fazla oyuncuyu desteklemek istiyorsanız daha karmaşık senaryolara (bağlı iki veya daha fazla gamepad, bazılarının oyunun ortasında bağlantısının kesilmesi vb.) tepki vermek için birkaç satır daha kod eklemeniz gerekir. Bu sorunu çözmek için kullanabileceğiniz bir yaklaşım için test cihazımızın (pollGamepads() işlevi) kaynak koduna bakabilirsiniz.

Etkinlikler

Firefox, Gamepad API spesifikasyonunda açıklanan alternatif ve daha iyi bir yöntem kullanır. Sizden anket yapmanızı istemek yerine, bir gamepad takıldığında (veya daha doğrusu takıldığında ve düğmelerinden birine basılarak "duyurulurken") veya prizden çıkarıldığında tetiklenen iki etkinlik (MozGamepadConnected ve MozGamepadDisconnected) gösterir. Gelecekteki durumunu yansıtmaya devam edecek gamepad nesnesi, etkinlik nesnesinin .gamepad parametresi olarak iletilir.

Test kullanıcısının kaynak kodundan:

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

Özet

Sonuç olarak, test cihazındaki her iki yaklaşımı da destekleyen başlatma işlevimiz şu şekilde görünür:

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

Oyun kumandası bilgileri

Sisteme bağlı her gamepad, aşağıdaki gibi görünen bir nesneyle temsil edilir:

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

Temel bilgiler

İlk birkaç alan basit meta verilerdir:

  • id: Oyun kumandasının metin açıklaması
  • index: Bir bilgisayara bağlı farklı gamepad'leri ayırt etmek için yararlı olan bir tam sayı
  • timestamp: Düğme/eksen durumunun son güncellemesinin zaman damgası (şu anda yalnızca Chrome'da desteklenir)

Düğmeler ve çubuklar

Günümüzde gamepad'ler, dedenizin yanlış kaledeki prensesi kurtarmak için kullandığı cihazlardan çok farklı. Genellikle iki analog çubuğa ek olarak en az on altı ayrı düğme (bazı ayrı, bazıları analog) bulunur. Gamepad API, işletim sistemi tarafından bildirilen tüm düğmeler ve analog çubuklar hakkında bilgi verir.

Gamepad nesnesinde mevcut durumu aldıktan sonra düğmelere .buttons[], çubuklara ise .axes[] dizileri aracılığıyla erişebilirsiniz. Bu metriklerin ne anlama geldiğine dair görsel bir özeti aşağıda bulabilirsiniz:

Oyun kumandası şeması
Oyun kumandası şeması

Spesifikasyon, tarayıcının ilk on altı düğmeyi ve dört ekseni aşağıdakilerle eşlemesini ister:

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

Ek düğmeler ve eksenler yukarıdakilere eklenir. Ancak on altı düğme veya dört eksenin garanti edilmediğini lütfen unutmayın. Bazılarının tanımlanmamış olabileceğini göz önünde bulundurun.

Düğmeler 0,0 (basılmamış) ile 1,0 (tamamen basılmış) arasında değerler alabilir. Eksenler -1,0 (tamamen solda veya yukarıda) ile 0,0 (ortada) ve 1,0 (tamamen sağda veya aşağıda) arasındadır.

Analog mu yoksa ayrık mı?

Her düğme analog olabilir. Örneğin, omuz düğmeleri genellikle analogdur. Bu nedenle, değeri doğrudan 1,00 ile karşılaştırmak yerine bir eşik belirlemek en iyisidir (Analog bir düğme biraz kirliyse ne olur? 1,00'a hiç ulaşamayabilir). Doodle'ımızda bunu şu şekilde yapıyoruz:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Analog çubukları dijital kontrol çubuklarına dönüştürmek için de aynı işlemi yapabilirsiniz. Elbette dijital ped (d-pad) her zaman vardır ancak gamepad'inizde olmayabilir. Bu sorunu çözmek için kullandığımız kod:

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

Düğme basışları ve çubuk hareketleri

Etkinlikler

Uçuş simülasyon oyunu gibi bazı durumlarda, kontrol çubuğu konumlarını veya düğme basma işlemlerini sürekli olarak kontrol etmek ve bunlara tepki vermek daha mantıklı olabilir. Peki 2012 Hurdles doodle'ı gibi şeyler için? "Neden her karede düğmeleri kontrol etmem gerekiyor?" diye düşünebilirsiniz. Klavye veya fare yukarı/aşağı tuşları için olduğu gibi neden etkinlik alamıyorum?

İyi bir haberimiz var. Kötü haber şu: Gelecekte. Bu özellik spesifikasyonda yer alsa da henüz hiçbir tarayıcıda uygulanmamıştır.

Anket

Bu sırada, mevcut durumu önceki durumla karşılaştırabilir ve fark görürseniz işlevleri çağırabilirsiniz. Örneğin:

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

Hurdles 2012 doodle'ında klavyeye öncelik veren yaklaşım

Bugünkü doodle'ımızda oyun kumandası olmadan tercih edilen giriş yöntemi klavye olduğundan, oyun kumandasının klavyeyi oldukça yakından taklit etmesine karar verdik. Bu, üç karar anlamına geliyordu:

  1. Karalama defteri için yalnızca üç düğmeye (iki koşmak için, bir de atlamak için) ihtiyaç duyulur ancak gamepad'de çok daha fazla düğme olabilir. Bu nedenle, bilinen on altı düğmeyi ve iki çubuğu bu üç mantıksal işlevle eşledik. Böylece kullanıcılar A/B düğmelerini, omuz düğmelerini, d-pad'de sola/sağa basmayı veya çubukları şiddetle sola ve sağa sallayarak koşabilir (Bunlardan bazıları elbette diğerlerinden daha verimli olacaktır). Örneğin:

    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. Daha önce açıklanan eşik işlevlerini kullanarak her analog girişi ayrı bir giriş olarak ele aldık.

  3. Gamepad girişini, yerleşik hale getirmek yerine Doodle'a sabitledik. Anket döngümüzde gerekli keydown ve keyup etkinlikleri (uygun bir keyCode ile) sentezlenip DOM'a geri gönderilir:

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

Hepsi bu kadar.

İpuçları ve püf noktaları

  • Bir düğmeye basmadan önce oyun kumandasının tarayıcınızda hiç görünmeyeceğini unutmayın.
  • Oyun kumandasını aynı anda farklı tarayıcılarda test ediyorsanız yalnızca birinin denetleyiciyi algılayabileceğini unutmayın. Herhangi bir etkinlik almıyorsanız bu özelliği kullanabilecek diğer sayfaları kapattığınızdan emin olun. Ayrıca, deneyimlerimizden yola çıkarak, bazen sekmeyi kapatsanız veya tarayıcıdan çıksanız bile tarayıcının gamepad'i "tutmaya devam edebileceğini" söyleyebiliriz. Bazen sorunları düzeltmenin tek yolu sistemi yeniden başlatmaktır.
  • Her zaman olduğu gibi, en iyi desteği deneyimlediğinizden emin olmak için Chrome Canary'ı ve diğer tarayıcıların eşdeğer sürümlerini kullanın. Ardından, eski sürümlerin farklı davrandığını görürseniz uygun şekilde hareket edin.

Gelecek

Bu makalenin, henüz biraz riskli olsa da çok eğlenceli olan bu yeni API'ye biraz ışık tutmasını umuyoruz.

API'nin eksik parçalarına (ör. etkinlikler) ve daha geniş tarayıcı desteğine ek olarak, titreşim kontrolü, yerleşik jiroskoplara erişim gibi özellikleri ve farklı gamepad türleri için daha fazla desteği de zaman içinde sunmayı umuyoruz. Yanlış çalışan veya hiç çalışmayan bir gamepad görürseniz lütfen Chrome için hata bildirin ve/veya Firefox için hata bildirin.

Ancak öncesinde Engeller 2012 doodle'ımızla oynayarak gamepad'de oynamanın ne kadar eğlenceli olduğunu keşfedin. 10, 7 saniyeden daha iyisini yapabileceğinizi mi söylediniz? Getirin.

Daha fazla bilgi