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

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

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

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

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

שימוש ב-Gamepad API

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

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

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

איך הדפדפן מייצג שלט משחק

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

  • ‫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. הוא מאפשר שני סוגים של אפקטים של רטט:

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

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

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

התראה כששלט משחק מתחבר

כדי לדעת מתי שלט משחק מחובר, צריך להאזין לאירוע 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
  */
});

שלט המשחק ב-game loop

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

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

אם שלט משחק אחד או יותר מחוברים, אבל navigator.getGamepads() עדיין מדווח על null פריטים, יכול להיות שצריך "להעיר" כל שלט משחק על ידי לחיצה על אחד מהלחצנים שלו. אחר כך אפשר לבדוק את המצבים של שלט המשחק ב-game loop, כמו בקוד הבא.

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.0 עד 1.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>

הדגמה (דמו)

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

בונוס: משחקים ב-Chrome Dino ב-web.dev

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

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

תודות

המסמך הזה נבדק על ידי François Beaufort ו-Joe Medley. מפרט Gamepad API נערך על ידי סטיב אגוסטון, ג'יימס הולייר ומאט ריינולדס. עורכי המפרט הקודמים הם ברנדון ג'ונס, סקוט גרהאם וטד מיילצ'ארק. המפרט של Gamepad Extensions נערך על ידי ברנדון ג'ונס.