בניית רכיב בתפריט של משחק בתלת-ממד

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

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

הדגמה

אם אתם מעדיפים סרטון, הנה גרסה של YouTube לפוסט:

סקירה

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

HTML

תפריט משחק הוא רשימת לחצנים. הדרך הטובה ביותר לייצג את זה ב-HTML היא:

<ul class="threeD-button-set">
  <li><button>New Game</button></li>
  <li><button>Continue</button></li>
  <li><button>Online</button></li>
  <li><button>Settings</button></li>
  <li><button>Quit</button></li>
</ul>

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

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

CSS

העיצוב של רשימת הלחצנים מתחלק לשלבים הכלליים הבאים:

  1. הגדרת מאפיינים מותאמים אישית.
  2. פריסת Flexbox.
  3. לחצן בהתאמה אישית עם פסאודו אלמנטים דקורטיביים.
  4. הצבת אלמנטים במרחב בתלת-ממד.

סקירה כללית של מאפיינים מותאמים אישית

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

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

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);

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

.threeD-button-set {
  --y:;
  --x:;
  --distance: 1px;
  --theme: hsl(180 100% 50%);
  --theme-bg: hsl(180 100% 50% / 25%);
  --theme-bg-hover: hsl(180 100% 50% / 40%);
  --theme-text: white;
  --theme-shadow: hsl(180 100% 10% / 25%);

  --_max-rotateY: 10deg;
  --_max-rotateX: 15deg;
  --_btn-bg: var(--theme-bg);
  --_btn-bg-hover: var(--theme-bg-hover);
  --_btn-text: var(--theme-text);
  --_btn-text-shadow: var(--theme-shadow);
  --_bounce-ease: cubic-bezier(.5, 1.75, .75, 1.25);

  @media (--dark) {
    --theme: hsl(255 53% 50%);
    --theme-bg: hsl(255 53% 71% / 25%);
    --theme-bg-hover: hsl(255 53% 50% / 40%);
    --theme-shadow: hsl(255 53% 10% / 25%);
  }

  @media (--HDcolor) {
    @supports (color: color(display-p3 0 0 0)) {
      --theme: color(display-p3 .4 0 .9);
    }
  }
}

רקעים של חרוטים בעיצוב בהיר ועיצוב כהה

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

html {
  background: conic-gradient(at -10% 50%, deeppink, cyan);

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
הדגמה של שינוי הרקע בין העדפות של צבע בהיר לצבע כהה.

הפעלת נקודת מבט תלת-ממדית

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

body {
  perspective: 40vw;
}

זה סוג ההשפעה של נקודת המבט.

עיצוב רשימת הלחצנים <ul>

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

פריסה של קבוצת לחצנים

עם Flexbox אפשר לנהל את פריסת הקונטיינרים. משנים את כיוון ברירת המחדל של הגדרת הגמישות משורות לעמודות באמצעות flex-direction ומוודאים שכל פריט הוא בגודל התוכן שלו על ידי שינוי מ-stretch ל-start בשביל align-items.

.threeD-button-set {
  /* remove <ul> margins */
  margin: 0;

  /* vertical rag-right layout */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2.5vh;
}

בשלב הבא, מגדירים את הקונטיינר בתור הקשר תלת-ממדי של שטח, ומגדירים פונקציות clamp() של CSS כדי לוודא שהכרטיס לא מסתובב מעבר לסיבובים הקריאים. שימו לב שהערך האמצעי של ההצמדה הוא מאפיין מותאם אישית. הערכים האלה של --x ו---y יוגדרו מ-JavaScript אחרי אינטראקציה עם העכבר מאוחר יותר.

.threeD-button-set {
  …

  /* create 3D space context */
  transform-style: preserve-3d;

  /* clamped menu rotation to not be too extreme */
  transform:
    rotateY(
      clamp(
        calc(var(--_max-rotateY) * -1),
        var(--y),
        var(--_max-rotateY)
      )
    )
    rotateX(
      clamp(
        calc(var(--_max-rotateX) * -1),
        var(--x),
        var(--_max-rotateX)
      )
    )
  ;
}

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

@media (--motionOK) {
  .threeD-button-set {
    /* browser hint so it can be prepared and optimized */
    will-change: transform;

    /* transition transform style changes and run an infinite animation */
    transition: transform .1s ease;
    animation: rotate-y 5s ease-in-out infinite;
  }
}

האנימציה rotate-y מגדירה רק את תמונת המפתח האמצעית ב-50%, כי הדפדפן יקבע כברירת מחדל את 0% ואת 100% כסגנון ברירת המחדל של הרכיב. זהו קיצור של אנימציות שמתחלפות, וצריכות להתחיל ולהסתיים באותו מיקום. זוהי דרך נהדרת לנסח אנימציות מתחלפות אינסופית.

@keyframes rotate-y {
  50% {
    transform: rotateY(15deg) rotateX(-6deg);
  }
}

עיצוב הרכיבים של <li>

כל פריט ברשימה (<li>) מכיל את הלחצן ואת רכיבי הגבול שלו. הסגנון display השתנה כך שבפריט לא מוצג ::marker. הסגנון position מוגדר ל-relative, כך שהפסאודו-אלמנטים של הלחצנים הבאים יכולים למקם את עצמם באזור המלא שהלחצן צורך.

.threeD-button-set > li {
  /* change display type from list-item */
  display: inline-flex;

  /* create context for button pseudos */
  position: relative;

  /* create 3D space context */
  transform-style: preserve-3d;
}

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

עיצוב הרכיבים של <button>

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

<button> סגנונות ראשוניים

בהמשך מפורטים סגנונות הבסיס שיתמכו במדינות האחרות.

.threeD-button-set button {
  /* strip out default button styles */
  appearance: none;
  outline: none;
  border: none;

  /* bring in brand styles via props */
  background-color: var(--_btn-bg);
  color: var(--_btn-text);
  text-shadow: 0 1px 1px var(--_btn-text-shadow);

  /* large text rounded corner and padded*/
  font-size: 5vmin;
  font-family: Audiowide;
  padding-block: .75ch;
  padding-inline: 2ch;
  border-radius: 5px 20px;
}

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

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

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

צילום מסך של חלונית הרכיבים של Chrome Devtools, עם לחצן שמוצג בו הרכיבים
: before ו-::after

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

.threeD-button button {
  …

  &::after,
  &::before {
    /* create empty element */
    content: '';
    opacity: .8;

    /* cover the parent (button) */
    position: absolute;
    inset: 0;

    /* style the element for border accents */
    border: 1px solid var(--theme);
    border-radius: 5px 20px;
  }

  /* exceptions for one of the pseudo elements */
  /* this will be pushed back (3x) and have a thicker border */
  &::before {
    border-width: 3px;

    /* in dark mode, it glows! */
    @media (--dark) {
      box-shadow:
        0 0 25px var(--theme),
        inset 0 0 25px var(--theme);
    }
  }
}

סגנונות טרנספורמציה בתלת-ממד

הערך של מתחת ל-transform-style מוגדר ל-preserve-3d, כדי שהילדים יוכלו לצאת בעצמם לציר z. השדה transform מוגדר למאפיין המותאם אישית --distance, שיוגדל לאחר העברת העכבר מעל והתמקדות.

.threeD-button-set button {
  …

  transform: translateZ(var(--distance));
  transform-style: preserve-3d;

  &::after {
    /* pull forward in Z space with a 3x multiplier */
    transform: translateZ(calc(var(--distance) / 3));
  }

  &::before {
    /* push back in Z space with a 3x multiplier */
    transform: translateZ(calc(var(--distance) / 3 * -1));
  }
}

סגנונות אנימציה מותנית

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

.threeD-button-set button {
  …

  @media (--motionOK) {
    will-change: transform;
    transition:
      transform .2s ease,
      background-color .5s ease
    ;

    &::before,
    &::after {
      transition: transform .1s ease-out;
    }

    &::after    { transition-duration: .5s }
    &::before { transition-duration: .3s }
  }
}

סגנונות אינטראקציה של העברת עכבר והתמקדות

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

  • מחילים את צבע הרקע של העברת העכבר.
  • הגדלת המרחק .
  • הוספת אפקט של קלי קלות.
  • מניעים את המעברים המדומים.
.threeD-button-set button {
  …

  &:is(:hover, :focus-visible):not(:active) {
    /* subtle distance plus bg color change on hover/focus */
    --distance: 15px;
    background-color: var(--_btn-bg-hover);

    /* if motion is OK, setup transitions and increase distance */
    @media (--motionOK) {
      --distance: 3vmax;

      transition-timing-function: var(--_bounce-ease);
      transition-duration: .4s;

      &::after  { transition-duration: .5s }
      &::before { transition-duration: .3s }
    }
  }
}

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

שיפורים קטנים באמצעות JavaScript

אפשר להשתמש בממשק ממקלדות, קוראי מסך, גיימפאד, מסך מגע ועכבר, אבל אפשר להוסיף כמה נגיעות קלות של JavaScript כדי להקל על כמה תרחישים.

מקשי החיצים תומכים

מקש ה-Tab הוא דרך טובה לנווט בתפריט, אבל ציפיתי שהמקלדת או הג'ויסטיקים יזיזו את המיקוד לבקר משחקים. הספרייה roving-ux שמשמשת לעיתים קרובות לממשקים של אתגר GUI תטפל במקשי החיצים בשבילנו. הקוד הבא מנחה את הספרייה לתעד את המיקוד בתוך .threeD-button-set ולהעביר את המיקוד לצאצאים של הלחצנים.

import {rovingIndex} from 'roving-ux'

rovingIndex({
  element: document.querySelector('.threeD-button-set'),
  target: 'button',
})

אינטראקציה עם פרלקס של עכבר

מעקב אחר העכבר והטיית התפריט נועדו לדמות ממשקים של משחקי וידאו ב-AR ו-VR, שבהם יש מצביע וירטואלי במקום עכבר. זה יכול להיות כיף כשאלמנטים מאוד מודעים למצביע.

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

const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

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

const getAngles = (clientX, clientY) => {
  const { x, y, width, height } = menuRect

  const dx = clientX - (x + 0.5 * width)
  const dy = clientY - (y + 0.5 * height)

  return {dx,dy}
}

לבסוף, בודקים את הזזת העכבר, מעבירים את המיקום לפונקציה getAngles() ומשתמשים בערכי דלתא בתור סגנונות של מאפיינים מותאמים אישית. חילקתי ב-20 כדי להסיר את הדלתא כך שיהיה פחות אינטנסיבי, אולי יש דרך טובה יותר לעשות זאת. אם אתם זוכרים מההתחלה, מציבים את המאפיינים --x ו---y באמצע הפונקציה clamp(), כדי למנוע ממיקום העכבר לסובב את הכרטיס יותר מדי למצב לא קריא.

if (motionOK) {
  window.addEventListener('mousemove', ({target, clientX, clientY}) => {
    const {dx,dy} = getAngles(clientX, clientY)

    menu.attributeStyleMap.set('--x', `${dy / 20}deg`)
    menu.attributeStyleMap.set('--y', `${dx / 20}deg`)
  })
}

תרגומים ומסלולים

הייתה תוצאה אחת שבדקתי את תפריט המשחק בשפות ובמצבי כתיבה אחרים.

לרכיבי <button> יש סגנון !important ל-writing-mode בגיליון הסגנונות של סוכן המשתמש. המשמעות היא שה-HTML של תפריט המשחק היה צריך להשתנות כדי להתאים לעיצוב הרצוי. שינוי רשימת הלחצנים לרשימת קישורים מאפשר למאפיינים לוגיים לשנות את כיוון התפריט, כי לרכיבי <a> אין את הסגנון !important של הדפדפן.

סיכום

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

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

רמיקסים קהילתיים

אין כאן שום דבר עדיין לראות!