Oyun Kumandası API'siyle engelleri aşma

Marcin Wichary
Marcin Wichary

Giriş

Yeni oyuncular macera oyunları için klavyelerini, meyve kesmek için çok dokunuşlu, kıymetli parmak uçlarını ve Michael Jackson gibi dans edebiliyorlar gibi hareket edebilecekleri yeni hareket sensörlerini kullanma imkanı buluyorlar. (Böyle bir şey yapamazlar.) Ama sen farklısın. Daha iyisin. Tam bir uzmansınız. Sizin için oyunlar elinizde bir oyun kumandasıyla başlayıp bitiyor.

Ama, bir saniye. Web uygulamanızda bir oyun kumandasını desteklemek istemeniz mümkün değil mi? Ama artık değil. Yepyeni Oyun Kumandası API'si imdadınıza yetişerek bilgisayarınıza bağlı herhangi bir oyun kumandasının durumunu okumak için JavaScript'i kullanmanıza olanak tanır. Baskıdan o kadar yeni çıktı ki yalnızca geçen hafta Chrome 21'deydı. Aynı zamanda, Firefox'ta da (şu anda özel yapıda mevcuttur) desteklenmenin eşiğinde.

Bu zamanlama çok iyi oldu çünkü son zamanlarda Hurdles 2012 Google doodle'ında kullanma şansımız oldu. Bu makalede Oyun Kumandası API'sini doodle'a nasıl eklediğimizi ve bu süreçte neler öğrendiğimizi kısaca açıklayacağız.

Engeller 2012 Google doodle'ı
Engeller 2012 Google doodle

Oyun kumandası test kullanıcısı

Geçici doodle'lar, arka planda oldukça karmaşık olma eğilimindedir. Bahsettiğimiz konuyu daha kolay gösterebilmek için doodle'dan oyun kumandası kodunu aldık ve basit bir oyun kumandası test aracını oluşturduk. Bu çubuğu kullanarak USB oyun kumandanızın düzgün çalışıp çalışmadığını kontrol edebilir, ayrıca nasıl çalıştığını görmek için gelişmiş sisteme bakabilirsiniz.

Bugün hangi tarayıcılar destekliyor?

Tarayıcı Desteği

  • 21
  • 12
  • 29
  • 10.1

Kaynak

Hangi oyun kumandaları kullanılabilir?

Genel olarak, sisteminizin yerel olarak desteklediği tüm modern oyun kumandalarının çalışması gerekir. PC'de marka olmayan USB denetleyicilerden çeşitli oyun kumandalarını, donanım anahtarıyla Mac'e bağlanan PlayStation 2 oyun kumandalarını ve Chrome OS dizüstü bilgisayarla eşlenmiş Bluetooth denetleyicileri test ettik.

Oyun Kumandaları
Oyun Kumandaları

Bu, doodle'ımızı test etmek için kullandığımız bazı kumandaların fotoğrafı. "Evet anne, işte ben gerçekten bu." Kumandanız çalışmıyorsa veya kontroller yanlış eşlenmişse lütfen Chrome veya Firefox'ta hata bildiriminde bulunun . (Düzeltilmemiş olduğundan emin olmak için lütfen her tarayıcının en yeni sürümünü test edin.)

Oyun Kumandası API'sini Algılama Özelliği<

Chrome'da yeterince kolay:

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

Bu durum henüz Firefox'ta tespit edilememektedir. Her şey olay tabanlıdır ve tüm etkinlik işleyicilerin pencereye eklenmesi gerekir. Bu, etkinlik işleyicilerin algılandığı tipik bir tekniğin çalışmasını engeller.

Ancak, bunun geçici olduğundan eminiz. Her zaman muhteşem Modernizr size Gamepad API'den bahsediyor. Mevcut ve gelecekteki tüm algılama ihtiyaçlarınız için bunu ö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 oyun kumandası hiçbir şekilde görünmez. Bu, parmak izi almayı önlemek amacıyla yapılır. Ancak kullanıcı deneyimi açısından biraz zorlayıcı olabilmektedir: Kumandasını bağlayıp bağlamadığını bilmediğiniz için kullanıcıdan düğmeye basmasını isteyemez veya oyun kumandasına özgü talimatlar veremezsiniz.

Bu engeli çözdükten sonra (özür dileriz...) daha fazlası sizi bekliyor.

Anket

Chrome'un API uygulaması bir işlevi (navigator.webkitGetGamepads()) görüntüler. Bu işlevi, geçerli durumlarıyla (düğmeler ve çubuklar) birlikte sisteme bağlı durumdaki tüm oyun kumandalarının listesini almak için kullanabilirsiniz. İlk bağlı oyun kumandası, dizideki ilk giriş olarak döndürülür ve bu şekilde devam eder.

(Bu işlev çağrısı, kısa bir süre önce doğrudan erişebileceğiniz bir dizinin yerini aldı: navigator.webkitGamepads[]. Ağustos 2012'nin başlarından itibaren, Chrome 21'de bu diziye erişmek hâlâ gerekliyken, işlev çağrısı Chrome 22 ve daha yeni sürümlerde çalışmaktadır. Bundan sonra, API'yi kullanmak için önerilen yöntem işlev çağrısı olacak ve yavaş yavaş tüm yüklü Chrome tarayıcılara eklenecektir.)

Spesifikasyonun şu ana kadar uygulanan bölümü, işler değiştiğinde etkinlikleri tetiklemek yerine, bağlı oyun kumandalarının durumunu sürekli olarak kontrol etmenizi (ve gerekirse bir öncekiyle karşılaştırmanızı) gerektirir. Yoklamayı en verimli ve pil dostu şekilde ayarlamak için requestAnimationFrame() işlevini kullandık. Doodle'ımız için, animasyonları destekleyecek bir requestAnimationFrame() döngüsü olmasına rağmen tamamen ayrı bir ikinci döngü oluşturduk. Bu döngüyü kodlamak daha basitti ve performansı hiçbir şekilde etkilememeliydi.

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

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

Sadece tek bir oyun kumandasıyla ilgileniyorsanız verilerini almak şu kadar basit olabilir:

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

Biraz daha zeki olmak veya aynı anda birden fazla oyuncuyu desteklemek istiyorsanız daha karmaşık senaryolara tepki vermek için birkaç satır daha kod eklemeniz gerekir (iki veya daha fazla oyun kumandası bağlanır, bazılarının oyun ortasında bağlantısı kesilir vb.). Bu sorunun nasıl çözüleceğine ilişkin bir yaklaşım için test kullanıcımızın pollGamepads() işlevi olan 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 oyun kumandası takıldığında (veya daha kesin bir şekilde, takıldığında ve düğmelerinden birine basılarak "duyurulduğunda") veya çıkarıldığında tetiklenen iki etkinlik (MozGamepadConnected ve MozGamepadDisconnected) gösterir. Gelecekteki durumunu yansıtmaya devam edecek olan oyun kumandası nesnesi, etkinlik nesnesinin .gamepad parametresi olarak aktarılır.

Test kullanıcısı 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 kullanıcısındaki her iki yaklaşımı da destekleyen ilk kullanıma hazırlama işlevimiz aşağıdaki gibi 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 oyun kumandası, aşağıdaki gibi bir nesne ile 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

En üstteki birkaç alan basit meta verilerdir:

  • id: Oyun kumandasının yazılı bir açıklaması
  • index: Bir bilgisayara bağlı farklı oyun kumandalarını 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 desteklenmektedir)

Düğmeler ve çubuklar

Bugünün oyun kumandaları tam olarak büyükbabanızın prenseyi yanlış şatoda kurtarmak için kullanmış olabileceğiyle aynı değil. Genellikle iki analog çubuğa ek olarak en az on altı ayrı düğme (bazıları ayrı, bazıları analog) vardır. Oyun Kumandası API'sı, işletim sistemi tarafından bildirilen tüm düğmeler ve analog çubukları size bildirir.

Oyun kumandası nesnesindeki mevcut durumu aldıktan sonra düğmelere .buttons[] aracılığıyla erişebilirsiniz. Ayrıca, .axes[] dizileri aracılığıyla düğmelere erişebilirsiniz. Aşağıda bunların ne anlama geldiğinin görsel bir özetini bulabilirsiniz:

Oyun Kumandası Diyagramı
Oyun Kumandası Diyagramı

Spesifikasyon, tarayıcıdan ilk on altı düğmeyi ve dört ekseni aşağıdakilere 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
};

Fazladan düğmeler ve eksenler yukarıdakilerin sonuna eklenir. Bununla birlikte, ne on altı düğmenin ne de dört eksenin garanti edilmediğini lütfen unutmayın. Bazılarının tanımsız kalmasına hazır olun.

Düğmeler 0,0 (basılmamış) ile 1,0 (tamamen basıldı) arasındaki değerleri alabilir. Eksenler -1,0 (tamamen sola veya yukarı) ile 0,0 (orta) ile 1,0 (tamamen sağ veya aşağı) arasında değişir.

Analog mu yoksa ayrı mı?

Muhtemelen her düğme analog olabilir. Örneğin, kenar düğmeleri için bu yaygın bir durumdur. Bu nedenle, açık bir şekilde 1,00 ile karşılaştırmak yerine bir eşik ayarlamak en iyisidir (analog düğme biraz kirliyse ne olur? 1,00'e asla ulaşmayabilir). 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);
};

Aynı işlemi analog çubukları dijital kumanda çubuğuna dönüştürmek için de yapabilirsiniz. Elbette, her zaman dijital pad (d-pad) vardır, ancak oyun kumandanızda böyle bir şey olmayabilir. Bu sorunu çözmek için aşağıdaki kodu kullanabilirsiniz:

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 basma ve düğme hareketleri

Etkinlikler

Bazı durumlarda, örneğin bir uçuş simülatörü oyunu, çubuk konumlarını veya düğmeye basılanları sürekli kontrol etmek ve bunlara tepki vermek daha mantıklı. Peki Engelli 2012 doodle'ı gibi şeyler için? "Neden her karede düğmeleri kontrol etmem gerekiyor?" sorusunu sorabilirsiniz. Klavye veya fare yukarı/aşağı için yaptığım gibi etkinlikleri neden alamıyorum?

İyi haber, bunu yapabilirsiniz. Ancak gelecekte kötü bir haberimiz var. Bu spesifikasyonda var, ancak henüz hiçbir tarayıcıda uygulanmadı.

Anket

Bu sırada, mevcut ve önceki durumu karşılaştırır ve herhangi bir farklılık görürseniz işlevleri çağırırsınız. Ö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);
}

Engelli 2012 doodle'ında klavye öncelikli yaklaşım

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

  1. Doodle'da iki düğme koşmak ve zıplamak için yalnızca üç düğme gerekiyor. Ancak oyun kumandasında muhtemelen çok daha fazlası var. Dolayısıyla, bilinen on altı düğmenin ve bilinen iki çubuğun tümünü, en mantıklı şekilde düşündüğümüz şekilde bu üç mantıksal işlevle eşleştirdik: İnsanların koşarak koşabilmesini sağlamak için: A/B düğmelerini, alternatif omuz düğmelerini, d-pad'de sola/sağa basmayı veya çubuğu sert bir şekilde sola ve sağa sallama (bu öğelerin 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 bir analog girişi ayrı bir giriş olarak değerlendirdik.

  3. Oyun kumandası girişini doodle'a eklemek yerine doodle'ın üzerine vidayla bağladık. Yoklama döngümüz aslında gerekli keydown ve keyup etkinliklerini (uygun bir keyCode ile) sentezleyip DOM'ye geri gönderiyor:

    // 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ı

  • Düğmeye basılmadan önce oyun kumandasının tarayıcınızda görünmeyeceğini unutmayın.
  • Oyun kumandasını aynı anda farklı tarayıcılarda test ediyorsanız yalnızca birinin kumandayı algılayabildiğini unutmayın. Herhangi bir etkinlik almıyorsanız etkinliği kullanıyor olabilecek diğer sayfaları kapattığınızdan emin olun. Ayrıca, tecrübelerimize göre tarayıcı, bazen sekmeyi kapatsanız veya tarayıcıdan çıksanız bile oyun kumandasını "tutabilir". Bazen sorunları düzeltmenin tek yolu sistemi yeniden başlatmaktır.
  • Her zaman olduğu gibi, en iyi desteği aldığınızdan emin olmak için Chrome Canary'yi ve diğer tarayıcılara ait eşdeğerlerini kullanın. Daha sonra, eski sürümlerin farklı davrandığını görürseniz gerekli işlemleri yapın.

Gelecek

Bu açıklamanın, biraz riskli olsa da şimdiden çok eğlenceli olan yeni API'ye ışık tutmaya yardımcı olacağını umuyoruz.

API'nin eksik parçalarına (ör. etkinlikler) ve daha kapsamlı tarayıcı desteğine ek olarak, titreme kontrolü, yerleşik jiroskoplara erişim vb. gibi özellikleri de ileride görmeyi umuyoruz. Farklı oyun kumandası türleri için daha fazla destek. Hatalı çalışan veya hiç çalışmayan bir oyun görürseniz lütfen Chrome'da hata bildiriminde bulunun ve/veya Firefox'ta hata bildirin.

Ama öncesinde Engelli 2012 doodle'ımızla oyun oynayarak oyun kumandasında ne kadar daha eğlenceli olduğunu görün. 10, 7 saniyeden daha iyi performans gösterebileceğinizi mi söylediniz? Getir onu.

Daha fazla bilgi