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

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

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

הדגמה

אם ברצונך ליצור סרטון, הנה גרסת 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 בדפדפן.

סיכום

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

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

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

עדיין אין מה לראות כאן!