پریدن از موانع با Gamepad API

مقدمه

بگذارید تازه‌کارها صفحه‌کلیدشان را برای بازی‌های ماجراجویی، نوک انگشتان چندلمسی گران‌بهایشان را برای برش میوه، و حسگرهای حرکتی جدید و فانتزی‌شان را برای تظاهر به اینکه می‌توانند مثل مایکل جکسون برقصند نگه دارند. (خبر فوری: نمی‌توانند.) ولی تو فرق داری. شما بهتر هستید. تو یک حرفه‌ای هستی. برای شما، بازی‌ها با دسته بازی در دستانتان شروع می‌شود و پایان می‌یابد.

ولی صبر کنید. اگر بخواهید از دسته بازی در برنامه وب خود پشتیبانی کنید، آیا بدشانس نیستید؟ دیگر نه. Gamepad API جدید به کمک شما می‌آید و به شما امکان می‌دهد از جاوا اسکریپت برای خواندن وضعیت هر کنترل‌کننده دسته بازی متصل به رایانه خود استفاده کنید. این فناوری آنقدر جدید است که همین هفته گذشته در Chrome 21 عرضه شد – و همچنین در آستانه پشتیبانی در Firefox قرار دارد (درحال‌حاضر در یک نسخه ویژه دردسترس است).

این زمان‌بندی بسیار عالی بود، چون اخیراً فرصت استفاده از آن را در گوگل‌وارهٔ Google دو با مانع ۲۰۱۲ پیدا کردیم. این مقاله به‌طور خلاصه توضیح می‌دهد که چگونه Gamepad API را به doodle اضافه کردیم و درطول این فرایند چه چیزهایی یاد گرفتیم.

گوگل‌وارهٔ Google دو با مانع ۲۰۱۲
گوگل‌وارهٔ Google دو با مانع ۲۰۱۲

آزمایش‌گر دسته بازی

هرچند Doodleهای تعاملی زودگذر هستند، اما معمولاً در پس‌زمینه بسیار پیچیده هستند. برای اینکه نشان دادن آنچه درباره‌اش صحبت می‌کنیم آسان‌تر شود، کد دسته بازی را از Doodle گرفتیم و یک آزمایش‌کننده دسته بازی ساده را کنار هم قرار دادیم. می‌توانید از آن برای بررسی اینکه دسته بازی USB شما به‌درستی کار می‌کند یا نه استفاده کنید – و همچنین نگاهی به پشت صحنه بیندازید تا ببینید چگونه انجام می‌شود.

امروزه کدام مرورگرها از آن پشتیبانی می‌کنند؟

Browser Support

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

Source

از چه دسته‌های بازی‌ای می‌توان استفاده کرد؟

به‌طور کلی، هر دسته بازی مدرنی که به‌طور بومی توسط سیستم شما پشتیبانی می‌شود باید کار کند. ما دسته‌های بازی مختلفی را آزمایش کردیم، از کنترل‌کننده‌های USB بدون مارک در رایانه، تا دسته‌های بازی PlayStation 2 که ازطریق دانگل به Mac متصل شده‌اند، و کنترل‌کننده‌های بلوتوثی که با نوت‌بوک Chrome OS جفت شده‌اند.

دسته بازی
دسته بازی

این عکسی از برخی کنترل‌کننده‌هایی است که برای آزمایش Doodle خود استفاده کردیم – «بله، مامان، واقعاً این کاری است که من در محل کار انجام می‌دهم.» اگر کنترل‌کننده شما کار نمی‌کند یا اگر کنترل‌ها به‌درستی نگاشت نشده‌اند، لطفاً اشکالی را برای Chrome یا Firefox ثبت کنید. (لطفاً در جدیدترین نسخه هر مرورگر آزمایش کنید تا مطمئن شوید که قبلاً برطرف نشده باشد.)

ویژگی تشخیص Gamepad API<

در Chrome:

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

به‌نظر می‌رسد هنوز امکان تشخیص این موضوع در Firefox وجود ندارد – همه چیز مبتنی بر رویداد است و همه گرداننده‌های رویداد باید به پنجره متصل شوند، که مانع از کارکرد تکنیک معمول تشخیص گرداننده‌های رویداد می‌شود.

اما مطمئنیم که این موقتی است. Modernizr فوق‌العاده ازقبل درباره Gamepad API به شما می‌گوید، بنابراین ما این را برای همه نیازهای شناسایی فعلی و آینده شما توصیه می‌کنیم:

var gamepadSupportAvailable = Modernizr.gamepads;

درحال کسب اطلاعات درباره دسته‌های بازی متصل‌شده

حتی اگر دسته بازی را متصل کنید، تا زمانی که کاربر یکی از دکمه‌های آن را فشار ندهد، دسته بازی به هیچ شکلی ظاهر نخواهد شد. این کار برای جلوگیری از انگشت‌نگاری است، اگرچه ثابت شده است که برای تجربه کاربری کمی چالش‌برانگیز است: نمی‌توانید از کاربر بخواهید دکمه را فشار دهد یا دستورالعمل‌های خاص دسته بازی را ارائه دهد زیرا نمی‌دانید که آیا کنترل‌کننده خود را متصل کرده است یا نه.

اما وقتی از این مانع عبور کردید (ببخشید…)، موانع بیشتری در انتظارتان است.

نظرسنجی

پیاده‌سازی Chrome از API تابعی را نشان می‌دهد – navigator.webkitGetGamepads() – که می‌توانید از آن برای دریافت فهرستی از تمام دسته‌های بازی که درحال‌حاضر به سیستم متصل هستند، همراه با وضعیت فعلی آن‌ها (دکمه‌ها + دسته‌ها) استفاده کنید. اولین دسته بازی متصل‌شده به‌عنوان اولین ورودی در آرایه برگردانده می‌شود و به همین ترتیب.

(این فراخوانی تابع به‌تازگی جایگزین آرایه‌ای شده است که می‌توانستید مستقیماً به آن دسترسی داشته باشید – navigator.webkitGamepads[]. از اوایل اوت ۲۰۱۲، دسترسی به این آرایه هنوز در Chrome 21 ضروری است، درحالی‌که فراخوانی تابع در Chrome 22 و نسخه‌های جدیدتر کار می‌کند. در آینده، فراخوانی تابع روش توصیه‌شده برای استفاده از API خواهد بود و به‌تدریج در همه مرورگرهای Chrome نصب‌شده اعمال خواهد شد.)

بخش پیاده‌سازی‌شده تاکنون از مشخصات، شما را ملزم می‌کند که به‌طور مداوم وضعیت دسته‌های بازی متصل را بررسی کنید (و درصورت لزوم آن را با وضعیت قبلی مقایسه کنید)، به‌جای اینکه هنگام تغییرات رویدادها را فعال کنید. ما به requestAnimationFrame() تکیه کردیم تا نظرسنجی را به کارآمدترین و باتری‌پسندترین روش راه‌اندازی کنیم. برای Doodle ما، اگرچه ازقبل یک حلقه requestAnimationFrame() برای پشتیبانی از پویانمایی‌ها داشتیم، یک حلقه کاملاً جداگانه دیگر ایجاد کردیم – کدنویسی آن ساده‌تر بود و نباید به هیچ وجه بر عملکرد تأثیر بگذارد.

کد ارائه‌شده ازسوی آزمایش‌گر در اینجا آمده است:

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

اگر فقط به یک دسته بازی اهمیت می‌دهید، دریافت داده‌های آن می‌تواند به همین سادگی باشد:

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

اگر می‌خواهید کمی هوشمندانه‌تر عمل کنید یا از بیش‌از یک بازیکن به‌طور هم‌زمان پشتیبانی کنید، باید چند خط کد دیگر اضافه کنید تا به سناریوهای پیچیده‌تر واکنش نشان دهید (اتصال دو یا چند دسته بازی، قطع شدن اتصال برخی از آن‌ها در میانه بازی، و غیره). می‌توانید به کد منبع آزمایش‌گر ما، تابع pollGamepads()، نگاه کنید تا با یکی از روش‌های حل این مشکل آشنا شوید.

رویدادها

Firefox از روش جایگزین و بهتری استفاده می‌کند که در مشخصات Gamepad API شرح داده شده است. به‌جای اینکه از شما بخواهد نظرسنجی کنید، دو رویداد (MozGamepadConnected و MozGamepadDisconnected) را نشان می‌دهد که هرگاه دسته بازی وصل شود (یا به‌طور دقیق‌تر، وصل شود و با فشار دادن هریک از دکمه‌هایش «اعلام» شود) یا جدا شود، فعال می‌شوند. شیء دسته بازی که همچنان وضعیت آینده‌اش را منعکس می‌کند به‌عنوان پارامتر .gamepad شیء رویداد ارسال می‌شود.

از کد منبع آزمایش‌کننده:

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

خلاصه

درنهایت، تابع مقداردهی اولیه ما در آزمایش‌کننده که از هر دو رویکرد پشتیبانی می‌کند، به این شکل است:

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

اطلاعات دسته بازی

هر دسته بازی متصل به سیستم با شیئی شبیه به این نشان داده می‌شود:

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

اطلاعات اصلی

چند فیلد اول فراداده‌های ساده هستند:

  • ‫id: شرح نوشتاری دسته بازی
  • ‫index: عدد صحیحی که برای تشخیص دسته‌های بازی مختلف متصل به یک رایانه مفید است
  • timestamp: مُهر زمان آخرین به‌روزرسانی وضعیت دکمه/محورها (درحال‌حاضر فقط در Chrome پشتیبانی می‌شود)

دکمه‌ها و دسته‌ها

دسته بازی‌های امروزی دقیقاً همان چیزی نیستند که پدربزرگتان ممکن است برای نجات شاهزاده خانم در قلعه اشتباهی استفاده کرده باشد – آن‌ها معمولاً حداقل شانزده دکمه جداگانه (برخی گسسته، برخی آنالوگ)، علاوه‌بر دو دسته آنالوگ دارند. Gamepad API همه دکمه‌ها و دسته‌های آنالوگی را که سیستم‌عامل گزارش کرده است به شما اطلاع می‌دهد.

وقتی وضعیت فعلی را در شیء دسته بازی دریافت کردید، می‌توانید ازطریق آرایه‌های .buttons[] به دکمه‌ها و ازطریق آرایه‌های .axes[] به دسته‌های بازی دسترسی پیدا کنید. در اینجا خلاصه‌ای تصویری از آنچه با آن مطابقت دارند آورده شده است:

نمودار دسته بازی
نمودار دسته بازی

مشخصات از مرورگر می‌خواهد که شانزده دکمه اول و چهار محور را به موارد زیر نگاشت کند:

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

دکمه‌ها و محورهای اضافی به موارد بالا اضافه خواهد شد. لطفاً توجه داشته باشید که نه شانزده دکمه و نه چهار محور تضمین نمی‌شوند – آماده باشید که برخی‌از آن‌ها به‌سادگی تعریف‌نشده باشند.

دکمه‌ها می‌توانند مقادیری از ۰٫۰ (فشرده‌نشده) تا ۱٫۰ (کاملاً فشرده‌شده) داشته باشند. محورها از ‎-۱٫۰ (کاملاً چپ یا بالا) تا ۰٫۰ (مرکز) و ‎۱٫۰ (کاملاً راست یا پایین) می‌روند.

آنالوگ یا گسسته؟

ظاهراً هر دکمه‌ای می‌تواند آنالوگ باشد – برای مثال، این امر برای دکمه‌های شانه‌ای تا حدودی رایج است. بنابراین، بهتر است آستانه‌ای تنظیم کنید تا اینکه فقط آن را مستقیماً با ۱٫۰۰ مقایسه کنید (اگر دکمه آنالوگ کمی کثیف باشد چه؟ ممکن است هرگز به ۱٫۰۰ نرسد). در Doodle ما این کار را به این صورت انجام می‌دهیم:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

می‌توانید همین کار را برای تبدیل دسته‌های آنالوگ به دسته بازی دیجیتال انجام دهید. البته همیشه پد دیجیتال (پد کنترل) وجود دارد، اما ممکن است دسته بازی شما آن را نداشته باشد. کد ما برای رسیدگی به این موضوع در زیر آمده است:

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

فشار دادن دکمه و حرکت دادن دسته

رویدادها

در برخی موارد، مانند بازی شبیه‌ساز پرواز، بررسی مداوم و واکنش به موقعیت‌های دسته بازی یا فشار دادن دکمه‌ها منطقی‌تر است… اما برای چیزهایی مانند دودل Hurdles 2012؟ شاید بپرسید: چرا باید در هر فریم دکمه‌ها را بررسی کنم؟ چرا نمی‌توانم رویدادها را مانند آنچه برای صفحه‌کلید یا موشواره بالا/پایین انجام می‌دهم دریافت کنم؟

خبر خوب این است که می‌توانید. خبر بد این است که – در آینده. در مشخصات وجود دارد، اما هنوز در هیچ مرورگری پیاده‌سازی نشده است.

نظرسنجی

در این فاصله، راه حل شما مقایسه وضعیت فعلی و قبلی و فراخوانی توابع درصورت مشاهده هرگونه تفاوت است. برای مثال:

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

رویکرد اولویت صفحه‌کلید در Doodle «دو با مانع ۲۰۱۲»

ازآنجایی‌که بدون دسته بازی، روش ورودی ترجیحی Doodle امروز ما صفحه‌کلید است، تصمیم گرفتیم دسته بازی را طوری طراحی کنیم که عملکردی بسیار شبیه به صفحه‌کلید داشته باشد. این یعنی سه تصمیم:

  1. این doodle فقط به سه دکمه نیاز دارد – دو دکمه برای دویدن و یک دکمه برای پریدن – اما دسته بازی احتمالاً دکمه‌های بیشتری دارد. بنابراین، هر شانزده دکمه شناخته‌شده و دو دسته شناخته‌شده را به آن سه عملکرد منطقی به روشی که فکر می‌کردیم منطقی‌تر است نگاشت کردیم تا افراد بتوانند با این روش‌ها بدوند: دکمه‌های A/B متناوب، دکمه‌های شانه‌ای متناوب، فشار دادن چپ/راست روی پد کنترل، یا تکان دادن شدید هریک از دسته‌ها به چپ و راست (البته برخی از این روش‌ها کارآمدتر از بقیه خواهند بود). برای مثال:

    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. ما هر ورودی آنالوگ را به‌عنوان یک ورودی گسسته درنظر گرفتیم و از توابع آستانه که قبلاً توضیح داده شد استفاده کردیم.

  3. ما تا آنجا پیش رفتیم که ورودی دسته بازی را به doodle متصل کردیم، به‌جای اینکه آن را در آن تعبیه کنیم – حلقه نظرسنجی ما درواقع رویدادهای ضروری keydown و keyup را (با keyCode مناسب) ترکیب می‌کند و آن‌ها را به 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);

و این همه چیزی است که باید بدانید!

نکته‌ها و ترفندها

  • به‌یاد داشته باشید که دسته بازی تا زمانی که دکمه‌ای فشار داده نشود، در مرورگر شما قابل‌مشاهده نخواهد بود.
  • اگر دسته بازی را به‌طور هم‌زمان در مرورگرهای مختلف آزمایش می‌کنید، توجه داشته باشید که فقط یکی از آن‌ها می‌تواند کنترل‌کننده را حس کند. اگر هیچ رویدادی دریافت نمی‌کنید، حتماً صفحه‌های دیگری را که ممکن است از آن استفاده کنند ببندید. به‌علاوه، طبق تجربه ما، گاهی اوقات مرورگر می‌تواند دسته بازی را «نگه دارد» حتی اگر برگه را ببندید یا از خود مرورگر خارج شوید. گاهی اوقات بازراه‌اندازی سیستم تنها راه برطرف کردن مشکلات است.
  • مثل همیشه، از Chrome Canary و معادل‌های آن برای مرورگرهای دیگر استفاده کنید تا مطمئن شوید بهترین پشتیبانی را تجربه می‌کنید – و سپس اگر دیدید نسخه‌های قدیمی‌تر رفتار متفاوتی دارند، اقدام مناسب را انجام دهید.

آینده

امیدواریم این توضیحات به روشن شدن این API جدید کمک کند – هنوز کمی ناپایدار است، اما درحال‌حاضر بسیار سرگرم‌کننده است.

علاوه‌بر بخش‌های مفقود API (مثلاً رویدادها) و پشتیبانی گسترده‌تر مرورگر، امیدواریم درنهایت چیزهایی مثل کنترل لرزش، دسترسی به ژیروسکوپ‌های داخلی، و غیره را ببینیم. و پشتیبانی بیشتر از انواع مختلف دسته بازی – لطفاً اگر دسته بازی‌ای پیدا کردید که درست کار نمی‌کند یا اصلاً کار نمی‌کند، یک اشکال برای Chrome ثبت کنید و/یا یک اشکال برای Firefox ثبت کنید.

اما قبل‌از آن، بروید و با دودل Hurdles 2012 ما بازی کنید و ببینید که چقدر با دسته بازی لذت‌بخش‌تر است. اوه، همین الان گفتی می‌توانی بهتر از ۱۰٫۷ ثانیه انجام دهی؟ بیاورید.

مطالعه بیشتر