איך לשחק במשחק Chrome Dino באמצעות הגיימפאד

כאן מוסבר איך משתמשים ב-Gamepad API כדי לשדרג את משחקי האינטרנט.

ביצת ההפתעה בדף האופליין של Chrome היא אחד הסודות הכי גלויים בהיסטוריה ([citation needed], אבל הטענה הזו נועדה ליצור אפקט דרמטי). אם לוחצים על מקש הרווח או, במכשירים ניידים, מקישים על הדינוזאור, הדף במצב אופליין הופך למשחק ארקייד שאפשר לשחק בו. יכול להיות שאתם יודעים שלא צריך להתנתק מהאינטרנט כדי לשחק: ב-Chrome, אפשר פשוט לעבור אל about://dino או, אם אתם חובבי טכנולוגיה, אל about://network-error/-106. אבל ידעתם שבכל חודש משחקים 270 מיליון משחקים של דינוזאור Chrome?

דף האופליין של Chrome עם המשחק Chrome Dino.
כדי לשחק, לוחצים על מקש הרווח.

עובדה נוספת שאולי לא ידעתם עליה, ואפשר לטעון שהיא שימושית יותר, היא שבמצב ארקייד אפשר לשחק במשחק באמצעות גיימפד. התמיכה בבקר משחקים נוספה לפני כשנה, בזמן כתיבת המאמר הזה, בcommit של Reilly Grant. כפי שאפשר לראות, המשחק, כמו שאר פרויקט Chromium, הוא קוד פתוח לחלוטין. בפוסט הזה אני רוצה להראות לכם איך להשתמש ב-Gamepad API.

שימוש ב-Gamepad API

זיהוי תכונות ותמיכה בדפדפן

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

if ('getGamepads' in navigator) {
  // The API is supported!
}

איך הדפדפן מייצג גיימפאד

הדפדפן מייצג את הגיימפדים כאובייקטים Gamepad. ל-Gamepad יש את המאפיינים הבאים:

  • id: מחרוזת זיהוי של הגיימפד. המחרוזת הזו מזהה את המותג או הסגנון של בקר משחקים מחובר.
  • displayId: VRDisplay.displayId של VRDisplay משויך (אם רלוונטי).
  • index: האינדקס של הגיימפד בכלי הניווט.
  • connected: מציין אם הגיימפד עדיין מחובר למערכת.
  • hand: סוג enum שמגדיר באיזו יד מחזיקים את בקר המשחק, או באיזו יד הכי סביר שיחזיקו אותו.
  • timestamp: הפעם האחרונה שבה עודכנו הנתונים של בקר המשחקים הזה.
  • mapping: מיפוי הכפתורים והצירים שנמצא בשימוש במכשיר הזה, "standard" או "xr-standard".
  • pose: אובייקט GamepadPose שמייצג את נתוני התנוחה שמשויכים לבקר WebVR.
  • axes: מערך של ערכים לכל הצירים של הגיימפד, עם נורמליזציה לינארית לטווח של -1.0 עד 1.0.
  • buttons: מערך של מצבי הכפתורים של כל הכפתורים בגיימפאד.

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

  • pressed: המצב הלחוץ של הלחצן (true אם הלחצן לחוץ, ו-false אם הוא לא לחוץ).
  • touched: מצב הלחיצה של הכפתור. אם הלחצן יכול לזהות מגע, המאפיין הזה הוא true אם נוגעים בלחצן, ו-false אחרת.
  • value: עבור לחצנים עם חיישן אנלוגי, המאפיין הזה מייצג את מידת הלחיצה על הלחצן, בנורמליזציה לינארית לטווח של 0.0 עד 1.0.
  • hapticActuators: מערך שמכיל אובייקטים של GamepadHapticActuator, שכל אחד מהם מייצג חומרה של משוב הפטי שזמינה בבקר.

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

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

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

סקירה כללית סכמטית של מיפוי הכפתורים והצירים של גיימפד נפוץ.
ייצוג חזותי של פריסת לחצנים סטנדרטית בבקר משחקים (מקור).

התראה כשגיימפאד מתחבר

כדי לדעת מתי גיימפאד מחובר, צריך להאזין לאירוע gamepadconnected שמופעל באובייקט window. כשהמשתמש מחבר בקר משחקים, באמצעות USB או Bluetooth, מופעל GamepadEvent עם פרטי בקר המשחקים במאפיין gamepad שנקרא בשם מתאים. בדוגמה הבאה אפשר לראות בקר של Xbox 360 שהיה לי בבית (כן, אני אוהב משחקים רטרו).

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

התראה כשגיימפאד מתנתק

ההתראה על ניתוק של גיימפד מתקבלת באופן דומה לזיהוי של חיבורים. הפעם האפליקציה מאזינה לאירוע gamepaddisconnected. שימו לב שבדוגמה הבאה ‫connected הוא עכשיו false כשמנתקים את בקר Xbox 360.

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

הגיימפאד בלולאת המשחק

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

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

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

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

מפעיל הרטט

המאפיין vibrationActuator מחזיר אובייקט GamepadHapticActuator, שמתאים להגדרה של מנועים או מפעילים אחרים שיכולים להפעיל כוח למטרות משוב הפטי. כדי להפעיל אפקטים של משוב מישוש, צריך לקרוא ל-Gamepad.vibrationActuator.playEffect(). סוגי האפקטים התקינים היחידים הם 'dual-rumble' ו-'trigger-rumble'.

אפקטים נתמכים של רטט

if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
  // Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
  // Dual rumble supported.
} else {
  // Rumble effects aren't supported.
}

רעידות כפולות

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

  • duration: הגדרת משך הזמן של אפקט הרטט באלפיות השנייה.
  • startDelay: הגדרת משך ההשהיה עד להפעלת הרטט.
  • strongMagnitude ו-weakMagnitude: מגדירים את רמות עוצמת הרטט של המנועים הכבדים והקלים יותר של מסות סיבוביות אקסצנטריות, בנורמליזציה לטווח 0.01.0.
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

הפעלת רטט

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

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

שילוב עם מדיניות ההרשאות

במפרט של Gamepad API מוגדרת תכונה בשליטת מדיניות שמזוהה על ידי המחרוזת "gamepad". ערך ברירת המחדל של allowlist הוא "self". מדיניות ההרשאות של מסמך קובעת אם תוכן כלשהו במסמך יכול לגשת אל navigator.getGamepads(). אם משביתים את התכונה הזו במסמך כלשהו, אי אפשר להשתמש ב-navigator.getGamepads() בשום תוכן במסמך, והאירועים gamepadconnected ו-gamepaddisconnected לא יופעלו.

<iframe src="index.html" allow="gamepad"></iframe>

הדגמה (דמו)

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

בונוס: משחק Chrome dino באתר web.dev

אתם יכולים לשחק בChrome dino באמצעות בקר המשחקים שלכם באתר הזה. קוד המקור זמין ב-GitHub. אפשר לעיין בהטמעה של סקר בקר המשחקים בכתובת trex-runner.js ולראות איך היא מדמה לחיצות על מקשים.

כדי שהדמו של בקר המשחקים של Chrome Dino יעבוד, הוצאתי את המשחק Chrome Dino מליבת הפרויקט של Chromium (עדכון של מאמץ קודם של Arnelle Ballane), הצבתי אותו באתר עצמאי, הרחבתי את ההטמעה הקיימת של Gamepad API על ידי הוספת אפקטים של הנמכה ורטט, יצרתי מצב מסך מלא ו-Mehul Satardekar תרם הטמעה של מצב כהה. שיהיה לכם משחק מהנה!

תודות

המסמך הזה נבדק על ידי François Beaufort וJoe Medley. מפרט Gamepad API נערך על ידי Steve Agoston,‏ James Hollyer ו-Matt Reynolds. העורכים הקודמים של המפרט הם ברנדון ג'ונס, סקוט גרהאם וטד מילצ'ארק. מפרט התוספים של הגיימפאד נערך על ידי ברנדון ג'ונס. תמונה ראשית (Hero) מאת Laura Torrent Puig.