נעילת המצביע ולחצני יריות בגוף ראשון

John McCutchan
John McCutchan

מבוא

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

Pointer Lock API מאפשר לאפליקציה לבצע את הפעולות הבאות:

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

כתוצאה מהפעלת נעילת הסמן, סמן העכבר מוסתר. כך תוכלו לבחור לצייר סמן ספציפי לאפליקציה, או להשאיר את סמן העכבר מוסתר כדי שהמשתמש יוכל להזיז את המסגרת בעזרת העכבר. תנועת העכבר היחסית היא ההפרש בין מיקום סמן העכבר לבין המיקום שלו בפריים הקודם, ללא קשר למיקום המוחלט. לדוגמה, אם סמן העכבר עבר מ-(640, 480) אל (520, 490), התנועה היחסית הייתה (-120, 10). בהמשך מופיעה דוגמה אינטראקטיבית שמציגה דלתא של מיקום העכבר בצורתו הגולמית.

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

תאימות דפדפן

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

  • Chrome: 37.
  • Edge: ‏ 13.
  • Firefox: ‏ 50.
  • Safari: 10.1.

מקור

המנגנון של נעילה של מצביע העכבר

זיהוי תכונות

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

var havePointerLock = 'pointerLockElement' in document ||
    'mozPointerLockElement' in document ||
    'webkitPointerLockElement' in document;

בשלב הזה, נעילת הסמן זמינה רק ב-Firefox וב-Chrome. עדיין אין תמיכה ב-Opera וב-IE.

מופעל

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

element.requestPointerLock = element.requestPointerLock ||
                 element.mozRequestPointerLock ||
                 element.webkitRequestPointerLock;
// Ask the browser to lock the pointer
element.requestPointerLock();

// Ask the browser to release the pointer
document.exitPointerLock = document.exitPointerLock ||
               document.mozExitPointerLock ||
               document.webkitExitPointerLock;
document.exitPointerLock();

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

סרגל המידע של נעילת הסמן ב-Chrome.
סרגל המידע של נעילת הסמן ב-Chrome.

טיפול באירועים

יש שני אירועים שהאפליקציה צריכה להוסיף להם מאזינים. הראשון הוא pointerlockchange, שמופעל בכל פעם שמתרחש שינוי במצב נעילת הסמן. השני הוא mousemove, שמופעל בכל פעם שהעכבר זז.

// Hook pointer lock state change events
document.addEventListener('pointerlockchange', changeCallback, false);
document.addEventListener('mozpointerlockchange', changeCallback, false);
document.addEventListener('webkitpointerlockchange', changeCallback, false);

// Hook mouse move events
document.addEventListener("mousemove", this.moveCallback, false);

בתוך פונקציית ה-callback של pointerlockchange, צריך לבדוק אם הסמן נעול או נעול. קל לבדוק אם נעילת הסמן מופעלת: בודקים אם document.pointerLockElement שווה לרכיב שבו נעילת הסמן נדרשה. אם הוא נעול, סימן שהאפליקציה נעלה את הסמן בהצלחה. אם הוא לא נעול, סימן שהמשתמש או הקוד שלכם ביטלו את הנעילה של הסמן.

if (document.pointerLockElement === requestedElement ||
  document.mozPointerLockElement === requestedElement ||
  document.webkitPointerLockElement === requestedElement) {
  // Pointer was just locked
  // Enable the mousemove listener
  document.addEventListener("mousemove", this.moveCallback, false);
} else {
  // Pointer was just unlocked
  // Disable the mousemove listener
  document.removeEventListener("mousemove", this.moveCallback, false);
  this.unlockHook(this.element);
}

כשנעילה של מצביע העכבר מופעלת, הערכים של clientX, ‏ clientY, ‏ screenX ו-screenY נשארים קבועים. הערכים של movementX ו-movementY מתעדכנים במספר הפיקסלים שהסמן היה צריך לנוע מאז שליחת האירוע האחרון. בקוד מדומה:

event.movementX = currentCursorPositionX - previousCursorPositionX;
event.movementY = currentCursorPositionY - previousCursorPositionY;

בתוך פונקציית ה-callback של mousemove, אפשר לחלץ נתונים של תנועת העכבר היחסית מהשדות movementX ו-movementY של האירוע.

function moveCallback(e) {
  var movementX = e.movementX ||
      e.mozMovementX          ||
      e.webkitMovementX       ||
      0,
  movementY = e.movementY ||
      e.mozMovementY      ||
      e.webkitMovementY   ||
      0;
}

איך לתפוס שגיאות

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

document.addEventListener('pointerlockerror', errorCallback, false);
document.addEventListener('mozpointerlockerror', errorCallback, false);
document.addEventListener('webkitpointerlockerror', errorCallback, false);

האם נדרש מצב מסך מלא?

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

דוגמה לאמצעי בקרה במשחקי יריות בגוף ראשון

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

אמצעי הבקרה במשחקי יריות בגוף ראשון מבוססים על ארבעה מנגנונים מרכזיים:

  • תנועה קדימה ואחורה לאורך וקטור המבט הנוכחי
  • תנועה שמאלה וימינה לאורך וקטור הצד הנוכחי
  • סיבוב התצוגה (שמאלה וימינה)
  • סיבוב התצוגה (למעלה ולמטה)

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

תנועה

השלב הראשון הוא תנועה. בהדגמה שבהמשך, התנועה ממופה למקשים הרגילים W, ‏ A, ‏ S ו-D. מקשי W ו-S גורמים למצלמה לנוע קדימה ואחורה. מקשי A ו-D מזיזים את המצלמה שמאלה וימינה. קל להזיז את המצלמה קדימה ואחורה:

// Forward direction
var forwardDirection = vec3.create(cameraLookVector);
// Speed
var forwardSpeed = dt * cameraSpeed;
// Forward or backward depending on keys held
var forwardScale = 0.0;
forwardScale += keyState.W ? 1.0 : 0.0;
forwardScale -= keyState.S ? 1.0 : 0.0;
// Scale movement
vec3.scale(forwardDirection, forwardScale * forwardSpeed);
// Add scaled movement to camera position
vec3.add(cameraPosition, forwardDirection);

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

// Strafe direction
var strafeDirection = vec3.create();
vec3.cross(cameraLookVector, cameraUpVector, strafeDirection);

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

השלב הבא הוא סיבוב התצוגה.

פה

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

// Extract camera look vector
var frontDirection = vec3.create();
vec3.subtract(this.lookAtPoint, this.eyePoint, frontDirection);
vec3.normalize(frontDirection);
var q = quat4.create();
// Construct quaternion
quat4.fromAngleAxis(deltaAngle, axis, q);
// Rotate camera look vector
quat4.multiplyVec3(q, frontDirection);
// Update camera look vector
this.lookAtPoint = vec3.create(this.eyePoint);
vec3.add(this.lookAtPoint, frontDirection);

גובה צליל

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

סיכום

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

  • הוספת פונקציית מעקב אירועים pointerlockchange כדי לעקוב אחרי המצב של נעילת הסמן
  • שליחת בקשה לנעילת הסמן לרכיב ספציפי
  • הוספת פונקציית event listener של mousemove כדי לקבל עדכונים

הדגמות חיצוניות

קובצי עזר