עוברים את המכשולים עם Gamepad API

מארשין ויצ'ארי
מרסין ויצ'ארי

מבוא

נותנים לרקוד את המקלדת כדי לשחק במשחקי הרפתקאות, את קצות האצבעות היקרות שבאמצעותן הם נוגעים לחיתוך פירות ואת חיישני התנועה המפוארים שלהם ומעמידים פנים שהם יכולים לרקוד כמו מייקל ג'קסון. (NewsFlash: הם לא יכולים). אבל אתם שונים. כל טוב. מקצוענים. מבחינתכם, המשחקים מתחילים ומסתיימים בגיימפאד (גיימפאד) בידיים שלכם.

אבל רגע. נכון שאין לך מזל אם ברצונך לתמוך בגיימפאד באפליקציית האינטרנט שלך? לא יותר. ממשק ה-Gamepad API החדש ניצל את ההזדמנות ויעזור לכם להשתמש ב-JavaScript כדי לקרוא את המצב של כל שלט גיימפאד המחובר למחשב. היא כל כך חדשה מהעיתונות, שנוספה רק 21 ל-Chrome בשבוע שעבר – ובקרוב היא צפויה לקבל תמיכה גם ב-Firefox (כרגע יש תמיכה בגרסת build מיוחדת).

זה הפך לתזמון מעולה, כי יש לנו הזדמנות להשתמש בו לאחרונה ב-Hurdles 2012 של Google דודל. המאמר הזה מסביר בקצרה איך הוספנו את ממשק ה-API של Gamepad לציור, ומה למדנו במהלך התהליך.

דודל של Google לשנת 2012
דודל של Google לשנת 2012

בודק גיימפאד

שרבוטים אינטראקטיביים בדרך כלל די מורכבים מאחורי הקלעים. כדי שיהיה קל יותר להמחיש על מה אנחנו מדברים, לקחנו את קוד הגיימפאד מהדודל ויצרנו בודק פשוט של גיימפאד. אפשר להשתמש בו כדי לבדוק אם גיימפאד ה-USB פועל כראוי – וגם להסתכל מתחת למכסה המנוע כדי לבדוק איך הוא פועל.

אילו דפדפנים תומכים בו היום?

תמיכה בדפדפן

  • 21
  • 12
  • 29
  • 10.1

מקור

באילו בקרי משחקים אפשר להשתמש?

באופן כללי, כל גיימפאד מודרני שנתמך על ידי המערכת שלכם אמור לפעול. בדקנו מגוון גיימפאד של בקרי USB שאינם ממותגי מחשב, דרך גיימפאד של PlayStation 2 שמחוברים דרך מתאם ל-Mac ועד לבקרי Bluetooth שמותאמים למחשב נייד עם מערכת ההפעלה Chrome OS.

גיימפאד
מודעות משחקים

זו תמונה של כמה בקרים שבהם השתמשנו כדי לבדוק את הדודל – "כן, אימא, זה באמת מה שאני עושה בעבודה". אם הבקר לא פועל או אם הפקדים ממופים באופן שגוי, יש לדווח על באג ב-Chrome או ב-Firefox . (כדאי לבדוק בגרסה המוחלטת ביותר של כל דפדפן, כדי לוודא שהבעיה לא נפתרה.)

תכונה לזיהוי של ממשק ה-API של Gamepad<

קל מספיק ב-Chrome:

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

נראה שעדיין לא ניתן לזהות את זה ב-Firefox – הכול מבוסס-אירועים, וכל הגורמים המטפלים באירועים צריכים להיות מחוברים לחלון, מה שמונע שיטה אופיינית לזיהוי גורמים מטפלים באירועים.

אבל אנחנו בטוחים שזה זמני. ה-Modernizr הוא תמיד משחק מדהים שכבר כולל מידע על Gamepad API, כך שמומלץ להשתמש בו לכל צורכי הזיהוי שלכם בהווה ובעתיד:

var gamepadSupportAvailable = Modernizr.gamepads;

רוצה לקבל עוד מידע על לוחות משחקים מחוברים?

גם אם תחברו את הגיימפאד, הוא לא יתבטא בשום צורה אלא אם המשתמש ילחץ קודם על אחד מהלחצנים שלו. המטרה היא למנוע יצירה של טביעת אצבע דיגיטלית (fingerprinting), למרות העובדה שהיא מהווה אתגר בחוויית המשתמש: אי אפשר לבקש מהמשתמש ללחוץ על הלחצן או לספק הוראות ספציפיות לגיימפאד, כי אין לכם מושג אם הוא חיבר את השלט רחוק.

אחרי שתפתרו את המכשול (מצטערים...), תוכלו להמשיך לחכות.

סקרים

ההטמעה של ממשק ה-API ב-Chrome חושפת פונקציה – navigator.webkitGetGamepads() – שניתן להשתמש בה כדי לקבל רשימה של כל ה-גיימפאדים שמחוברים כרגע למערכת, וגם במצב הנוכחי שלהם (לחצנים + מקלות). ה-gamepad המחובר הראשון יוחזר כרשומה הראשונה במערך, וכן הלאה.

(קריאת הפונקציה הזו החליפה לאחרונה מערך שניתן לגשת אליו ישירות – navigator.webkitGamepads[]. מתחילת אוגוסט 2012, עדיין יש צורך בגישה למערך הזה ב-Chrome 21, ואילו הבקשה להפעלת הפונקציה פועלת ב-Chrome בגרסה 22 ואילך. בהמשך מומלץ להשתמש ב-API על ידי קריאה לפונקציה, והיא תוצג לאט-לאט בכל דפדפני Chrome המותקנים).

הקטע במפרט שהטמעתם ככה רחוק, מצריך לבדוק ברציפות את המצב של הגיימפאדים המחוברים (ומשווים אותו לקוד הקודם במקרה הצורך), במקום להפעיל אירועים כשדברים משתנים. הסתמכנו על requestAnimationFrame() כדי להגדיר סקרים בדרך הכי יעילה וידידותית לסוללה. עבור הדודל שלנו, על אף שכבר יש לנו לולאה 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 משתמש בדרך חלופית וטובה יותר המתוארת במפרט של ממשק ה-API של Gamepad. במקום לבקש מכם לבצע סקר, הוא חושף שני אירועים – MozGamepadConnected ו-MozGamepadDisconnected – שמופעלים בכל פעם ש-gamepad מחובר (או ליתר דיוק, מחובר ו "מפורסם" על ידי לחיצה על אחד מהלחצנים שלו) או כשהוא מנותק מהחשמל. אובייקט הגיימפאד שימשיך לשקף את המצב העתידי מועבר כפרמטר .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)

לחצנים ומקלות

לוחות המשחק של היום הם לא בדיוק מה שסבא שלכם היה יכול להשתמש בו כדי להציל את הנסיכה בטירה הלא נכונה – בדרך כלל יש בהם לפחות 16 כפתורים נפרדים (חלקם נפרדים, חלקם אנלוגיים), בנוסף לשני מקלות אנלוגיים. ממשק ה-API של Gamepad ידווח לכם על כל הלחצנים והמקלות האנלוגיים שדווחו על ידי מערכת ההפעלה.

אחרי שמקבלים את המצב הנוכחי באובייקט ה-gamepad, אפשר לגשת ללחצנים דרך .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
};

הלחצנים והצירים הנוספים יתווספו ללחצנים שלמעלה. עם זאת, חשוב לדעת שלא מובטח ש-16 לחצנים ו-4 צירים יהיו מוכנים לכך שחלק מהם פשוט לא יהיו מוגדרים.

הלחצנים יכולים לקבל ערכים מ-0.0 (לא לחוצים) עד 1.0 (לחצים במלואם). הצירים נעים מ-1.0- (משמאל לחלוטין או למעלה) ל-0.0 (מרכז) ל-1.0 (ימין לגמרי או למטה).

אנלוגי או נפרד?

למעשה, כל לחצן יכול להיות אנלוגיה - זהו מצב נפוץ במידה מסוימת עבור לחצני שוליים, למשל. לכן, עדיף להגדיר סף ולא רק להשוות אותו באופן עקום ל-1.00 (מה קורה אם לחצן אנלוגי הוא במקרה שהוא קצת מלוכלך? ייתכן שהוא אף פעם לא יגיע ל-1.00). בציור שלנו אנחנו עושים זאת כך:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

תוכל לעשות את אותו הדבר כדי להפוך מקלות אנלוגיים לג'ויסטיקים דיגיטליים. בוודאי, תמיד יש לחצני החיצים (d-pad), אבל יכול להיות שאין בגיימפאד שלכם לחצן כזה. אפשר להשתמש בקוד שלנו כדי לטפל בבעיה:

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

לחיצות כפתור ותנועות מודבקות

אירועים

במקרים מסוימים, כמו במשחק של סימולטור טיסה, הגיוני יותר לבדוק ולהגיב באופן רציף למיקומי הדבקה או לחיצות לחצנים... אבל בדברים כמו ציור של משוכות 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);
}

הגישה הראשונה למקלדת בציור של משוכות 2012

מכיוון שללא גיימפאד, שיטת הקלט המועדפת עלינו בציור כיום היא המקלדת, לכן החלטנו שהגיימפאד יחקה אותה מקרוב. המשמעות הייתה שלוש החלטות:

  1. יש צורך בשלושה לחצנים בלבד בציור - שניים לריצה ואחד לקפיצה, אבל סביר להניח שב-גיימפאד יהיו עוד הרבה לחצנים. לכן מיפינו את כל שישה-עשר הלחצנים הידועים ושני הסיכות הידועות האלה לשלוש הפונקציות הלוגיות האלו, באופן שנראה לנו הגיוני, כך שאנשים יוכלו לפעול באמצעות: לחצני A/B מתחלפים, לחצני כתפיים מתחלפים, לחיצה שמאלה/ימינה על מקשי החיצים (D-pad) או נדנדה אלימה אחת שמאלה וימינה (חלק מהם, כמובן, יהיו יעילים יותר מהאחרים). לדוגמה:

    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. השקענו את כוחו בקלט של הגיימפאד לתוך הדודל, במקום להטמיע אותו, לולאת הסקרים שלנו למעשה מסנכרנת את אירועי המקשים הנדרשים ואירועי Keyup הנדרשים (עם קוד מפתח מתאים) ושולחת אותם חזרה ל-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 בדודל שלנו ולראות כמה כיף הוא על הגיימפאד. אמרת שאפשר לעשות יותר טוב מ-10.7 שניות? הבא!

קריאה נוספת