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

Marcin Wichary
Marcin Wichary

מבוא

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

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

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

דודל של Google בנושא משוכות 2012
הדודל של Google ב-2012 בנושא מכשולים

בודק של גיימפאד

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

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

Browser Support

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

Source

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

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

בקרי משחקים
גיימפדים

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

Feature Detecting the Gamepad API<

קל לעשות זאת ב-Chrome:

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

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

אבל אנחנו בטוחים שזה זמני. הספרייה הנהדרת Modernizr כבר מספרת לכם על Gamepad API, לכן אנחנו ממליצים עליה לכל הצרכים הנוכחיים והעתידיים שלכם לזיהוי:

var gamepadSupportAvailable = Modernizr.gamepads;

מידע על משחקי מחשב מחוברים

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

עם זאת, אחרי שתעברו את המשוכה הזו (סליחה…), יש עוד דברים שצריך לעשות.

סקרים

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

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

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

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

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

אנלוגי או דיסקרטי?

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

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

לחיצות על לחצנים ותנועות של מוטות

אירועים

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

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

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

    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.

אבל לפני כן, כדאי לשחק בהדודלי של 2012 בנושא מכשולים ולראות כמה כיף יותר לשחק בו עם שלט המשחק. אוי, אמרת שאפשר לעשות טוב יותר מ-10.7 שניות? קדימה.

קריאה נוספת