مروری اساسی بر نحوه ساخت یک منوی بازی سهبعدی واکنشگرا، تطبیقپذیر و در دسترس.
در این پست میخواهم ایدههایم را در مورد روشی برای ساخت یک کامپوننت منوی بازی سهبعدی به اشتراک بگذارم. نسخه آزمایشی را امتحان کنید.
اگر ویدیو را ترجیح میدهید، نسخه یوتیوب این پست در اینجا آمده است:
نمای کلی
بازیهای ویدیویی اغلب منویی خلاقانه و غیرمعمول، متحرک و در فضای سهبعدی را به کاربران ارائه میدهند. در بازیهای جدید AR/VR، ایجاد منویی که در فضا شناور به نظر برسد، رایج است. امروز ما اصول اولیه این جلوه را بازسازی خواهیم کرد، اما با اضافه کردن یک طرح رنگی تطبیقی و امکاناتی برای کاربرانی که حرکت کمتر را ترجیح میدهند.
اچتیامال
منوی بازی لیستی از دکمهها است. بهترین روش برای نمایش آن در 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> فهرستی از دکمهها به خوبی خود را به فناوریهای صفحهخوان معرفی میکند و بدون جاوا اسکریپت یا CSS کار میکند.

سیاساس
استایلدهی به لیست دکمهها به مراحل سطح بالای زیر تقسیم میشود:
- تنظیم ویژگیهای سفارشی.
- یک طرحبندی فلکسباکس.
- یک دکمه سفارشی با شبه عناصر تزئینی.
- قرار دادن عناصر در فضای سه بعدی
نمای کلی از ویژگیهای سفارشی
ویژگیهای سفارشی با دادن نامهای معنادار به مقادیری که در غیر این صورت تصادفی به نظر میرسند، به رفع ابهام مقادیر کمک میکنند و از تکرار کد و اشتراکگذاری مقادیر بین فرزندان جلوگیری میکنند.
در زیر کوئریهای رسانهای ذخیره شده به عنوان متغیرهای 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 را از ردیفها به ستونها با 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 را تنظیم کنید تا مطمئن شوید که کارت بیش از چرخشهای خوانا نمیچرخد. توجه داشته باشید که مقدار میانی برای clamp یک ویژگی سفارشی است، این مقادیر --x و --y بعداً از طریق جاوا اسکریپت هنگام تعامل ماوس تنظیم میشوند.
.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 روی transforms، درونیابی را فعال کنید. این گذار زمانی رخ میدهد که ماوس با کارت تعامل داشته باشد و انتقالهای نرم به تغییرات چرخش را امکانپذیر کند. انیمیشن یک انیمیشن در حال اجرا است که فضای سهبعدی کارت را نشان میدهد، حتی اگر ماوس نتواند یا نخواهد با کامپوننت تعامل داشته باشد.
@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; } 
شبه عناصر دکمه
حاشیههای دکمه، حاشیههای سنتی نیستند، بلکه شبهعنصرهایی با موقعیت مطلق و حاشیه هستند.

این عناصر در نمایش نمای سهبعدی ایجاد شده بسیار مهم هستند. یکی از این شبهعناصر از دکمه دور میشود و دیگری به کاربر نزدیکتر میشود. این تأثیر در دکمههای بالا و پایین بیشتر قابل توجه است.
.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 باید برای تغییر آماده باشد و یک گذار برای ویژگیهای 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 را برای انجام موارد زیر اعمال میکند:
- رنگ پسزمینهی شناور را اعمال کنید.
- فاصله را بیشتر کنید.
- یک افکت bounce ease اضافه کنید.
- انتقال شبهعنصرها را به صورت متناوب انجام دهید.
.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 حرکت، هنوز هم واقعاً زیبا بود. عناصر بالا و پایین، این اثر را به شکلی ظریف و زیبا نشان میدهند.
پیشرفتهای کوچک با جاوا اسکریپت
این رابط کاربری از قبل با کیبورد، صفحهخوان، گیمپد، صفحه لمسی و ماوس قابل استفاده است، اما میتوانیم برای سهولت در چند سناریو، کمی جاوا اسکریپت به آن اضافه کنیم.
کلیدهای جهتنمای پشتیبانیکننده
کلید تب (tab) روش خوبی برای پیمایش در منو است، اما انتظار داشتم که پد جهتدار یا جویاستیکها فوکوس را روی یک گیمپد جابجا کنند. کتابخانه roving-ux که اغلب برای رابطهای GUI Challenge استفاده میشود، کلیدهای جهتنما را برای ما مدیریت میکند. کد زیر به کتابخانه میگوید که فوکوس را درون .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 تقسیم کردم، ممکن است راه بهتری برای انجام این کار وجود داشته باشد. اگر از ابتدا به خاطر داشته باشید، props --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 که مرورگر ارائه میدهد، ندارند.
نتیجهگیری
حالا که فهمیدید چطور این کار را کردم، شما چطور انجام میدهید‽ 🙂 آیا میتوانید تعامل با شتابسنج را به منو اضافه کنید، طوری که با کاشیکاری کردن گوشی، منو بچرخد؟ آیا میتوانیم تجربه بدون حرکت را بهبود بخشیم؟
بیایید رویکردهایمان را متنوع کنیم و تمام روشهای ساخت در وب را یاد بگیریم. یک نسخه آزمایشی ایجاد کنید، لینکها را برای من توییت کنید ، و من آن را به بخش ریمیکسهای انجمن در زیر اضافه خواهم کرد!
ریمیکسهای انجمن
اینجا هنوز چیزی برای دیدن نیست!