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

Marcin Wichary
Marcin Wichary

מבוא

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

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

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

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

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

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

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

תמיכה בדפדפנים

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

מקור

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

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

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

משחקי ה-Gamepad של היום הם לא בדיוק מה שהסבא שלכם השתמש בו כדי להציל את הנסיכה בטירה הלא נכונה – בדרך כלל יש בהם לפחות 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 בנושא מכשולים

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

  1. לציור ה-Doodle נדרשים רק שלושה לחצנים – שניים לריצה ואחד לקפיצה – אבל ב-Gamepad צפויים להיות הרבה יותר לחצנים. לכן מיפינו את כל שש עשרה הלחצנים הידועים ואת שני הסטיקרים הידועים לשלוש הפונקציות הלוגיות האלה באופן שחשבנו שהוא הגיוני ביותר, כדי שאנשים יוכלו לרוץ באמצעות: לחיצה לסירוגין על הלחצנים 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 הנחוצים (עם קוד מקש מתאים) ושולחת אותם בחזרה ל-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 שניות? אשמח לקבל אותו.

קריאה נוספת