Xây dựng thành phần trình đơn trò chơi 3D

Thông tin tổng quan cơ bản về cách tạo một trình đơn trò chơi 3D có khả năng thích ứng, phản hồi nhanh và dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo một thành phần trình đơn trò chơi 3D. Hãy dùng thử bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video, thì đây là phiên bản video của bài đăng này trên YouTube:

Tổng quan

Trò chơi điện tử thường mang đến cho người dùng một trình đơn sáng tạo và độc đáo, có ảnh động và ở không gian 3D. Đây là một cách phổ biến trong các trò chơi thực tế tăng cường/thực tế ảo mới để làm cho trình đơn có vẻ như đang trôi nổi trong không gian. Hôm nay, chúng ta sẽ tạo lại những yếu tố cơ bản của hiệu ứng này nhưng có thêm nét đặc sắc của bảng phối màu thích ứng và các lựa chọn cho người dùng thích chế độ chuyển động giảm.

HTML

Trình đơn trò chơi là một danh sách các nút. Cách tốt nhất để biểu thị điều này trong HTML là như sau:

<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> 

Một danh sách các nút sẽ thông báo rõ ràng cho các công nghệ trình đọc màn hình và hoạt động mà không cần JavaScript hoặc CSS.

một danh sách dấu đầu dòng trông rất chung chung với các nút thông thường làm mục.

CSS

Việc tạo kiểu cho danh sách nút được chia thành các bước cấp cao sau:

  1. Thiết lập thuộc tính tuỳ chỉnh.
  2. Bố cục hộp linh hoạt.
  3. Một nút tuỳ chỉnh có các phần tử giả trang trí.
  4. Đặt các phần tử vào không gian 3D.

Tổng quan về thuộc tính tuỳ chỉnh

Các thuộc tính tuỳ chỉnh giúp phân biệt rõ ràng các giá trị bằng cách đặt tên có ý nghĩa cho các giá trị có vẻ ngẫu nhiên, tránh mã lặp lại và chia sẻ các giá trị giữa các thành phần con.

Dưới đây là các truy vấn phương tiện được lưu dưới dạng biến CSS, còn được gọi là phương tiện tuỳ chỉnh. Đây là các biến toàn cục và sẽ được dùng trong nhiều bộ chọn để giữ cho mã ngắn gọn và dễ đọc. Thành phần trình đơn trò chơi sử dụng các lựa chọn ưu tiên về chuyển động, bảng phối màu của hệ thống và khả năng dải màu của màn hình.

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

Các thuộc tính tuỳ chỉnh sau đây quản lý bảng phối màu và giữ các giá trị vị trí chuột để làm cho trình đơn trò chơi tương tác khi di chuột. Việc đặt tên cho các thuộc tính tuỳ chỉnh giúp mã dễ đọc hơn vì nó cho biết trường hợp sử dụng cho giá trị hoặc tên thân thiện cho kết quả của giá trị.

.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);     }   } } 

Nền hình nón cho giao diện sáng và tối

Giao diện sáng có chế độ chuyển màu hình nón cyan sang deeppink rực rỡ, còn giao diện tối có chế độ chuyển màu hình nón tối tinh tế. Để biết thêm về những việc có thể làm với gradient hình nón, hãy xem conic.style.

html {   background: conic-gradient(at -10% 50%, deeppink, cyan);    @media (--dark) {     background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);   } } 
Minh hoạ việc thay đổi nền giữa các lựa chọn ưu tiên về màu sáng và tối.

Bật chế độ xem phối cảnh 3D

Để các phần tử tồn tại trong không gian 3D của một trang web, bạn cần khởi tạo một khung nhìn có góc nhìn. Tôi chọn đặt góc nhìn trên phần tử body và sử dụng các đơn vị khung nhìn để tạo kiểu mà tôi thích.

body {   perspective: 40vw; } 

Đây là loại tác động mà góc nhìn có thể mang lại.

Tạo kiểu cho danh sách nút <ul>

Phần tử này chịu trách nhiệm về bố cục macro tổng thể của danh sách nút, cũng như là một thẻ nổi 3D và có khả năng tương tác. Sau đây là một cách để đạt được mục tiêu đó.

Bố cục nhóm nút

Flexbox có thể quản lý bố cục vùng chứa. Thay đổi hướng mặc định của flex từ hàng thành cột bằng flex-direction và đảm bảo mỗi mục có kích thước bằng nội dung của mục đó bằng cách thay đổi từ stretch thành start cho 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; } 

Tiếp theo, hãy thiết lập vùng chứa làm ngữ cảnh không gian 3D và thiết lập các hàm clamp() CSS để đảm bảo thẻ không xoay quá mức xoay có thể đọc được. Lưu ý rằng giá trị ở giữa của clamp là một thuộc tính tuỳ chỉnh, các giá trị --x--y này sẽ được đặt từ JavaScript khi có tương tác bằng chuột sau nà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)       )     )   ; } 

Tiếp theo, nếu người dùng truy cập không gặp vấn đề gì với chuyển động, hãy thêm một gợi ý vào trình duyệt rằng chế độ biến đổi của mục này sẽ liên tục thay đổi theo will-change. Ngoài ra, hãy bật tính năng nội suy bằng cách đặt transition trên các phép biến đổi. Hiệu ứng chuyển đổi này sẽ xảy ra khi chuột tương tác với thẻ, cho phép chuyển đổi mượt mà sang các thay đổi về hướng xoay. Ảnh động này là một ảnh động chạy liên tục, minh hoạ không gian 3D mà thẻ nằm trong đó, ngay cả khi chuột không thể hoặc không tương tác với thành phần.

@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;   } } 

Ảnh động rotate-y chỉ đặt khung hình chính ở giữa tại 50% vì trình duyệt sẽ mặc định 0%100% thành kiểu mặc định của phần tử. Đây là cách viết tắt cho các ảnh động thay thế, cần bắt đầu và kết thúc ở cùng một vị trí. Đây là một cách hiệu quả để thể hiện ảnh động luân phiên vô hạn.

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

Tạo kiểu cho các phần tử <li>

Mỗi mục trong danh sách (<li>) đều chứa nút và các phần tử đường viền của nút. Kiểu display được thay đổi để mục không hiển thị ::marker. Kiểu position được đặt thành relative để các phần tử giả của nút sắp tới có thể tự định vị trong toàn bộ khu vực mà nút sử dụng.

.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; } 

Ảnh chụp màn hình danh sách được xoay trong không gian 3D để thể hiện góc nhìn, và mỗi mục trong danh sách không còn dấu đầu dòng.

Tạo kiểu cho các phần tử <button>

Việc tạo kiểu cho các nút có thể là một công việc khó khăn, có rất nhiều trạng thái và loại tương tác cần tính đến. Những nút này nhanh chóng trở nên phức tạp do phải cân bằng các phần tử giả, ảnh động và lượt tương tác.

Kiểu <button> ban đầu

Dưới đây là các kiểu cơ bản sẽ hỗ trợ các trạng thái khác.

.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; } 

Ảnh chụp màn hình danh sách nút ở chế độ xem 3D, lần này có các nút được tạo kiểu.

Phần tử giả của nút

Đường viền của nút không phải là đường viền truyền thống, mà là các phần tử giả có vị trí tuyệt đối với đường viền.

Ảnh chụp màn hình bảng điều khiển Elements (Phần tử) của Chrome DevTools, trong đó một nút có các phần tử ::before và ::after xuất hiện.

Những phần tử này đóng vai trò quan trọng trong việc thể hiện góc nhìn 3D đã được thiết lập. Một trong những phần tử giả này sẽ bị đẩy ra khỏi nút và một phần tử sẽ được kéo gần hơn về phía người dùng. Hiệu ứng này dễ nhận thấy nhất ở các nút trên cùng và dưới cùng.

.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);     }   } } 

Kiểu biến đổi 3D

Phần tử transform-style được đặt thành preserve-3d để các thành phần con cháu có thể tự sắp xếp khoảng cách trên trục z. transform được đặt thành thuộc tính tuỳ chỉnh --distance, thuộc tính này sẽ tăng lên khi di chuột và lấy tiêu điểm.

.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));   } } 

Kiểu ảnh động có điều kiện

Nếu người dùng chấp nhận chuyển động, nút sẽ gợi ý cho trình duyệt rằng thuộc tính biến đổi phải sẵn sàng thay đổi và một hiệu ứng chuyển đổi được đặt cho các thuộc tính transformbackground-color. Hãy lưu ý sự khác biệt về thời lượng, tôi cảm thấy điều này tạo ra một hiệu ứng so le tinh tế.

.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 }   } } 

Kiểu tương tác di chuột và lấy tiêu điểm

Mục tiêu của ảnh động tương tác là trải các lớp tạo nên nút xuất hiện phẳng. Để thực hiện việc này, hãy đặt biến --distance, ban đầu thành 1px. Bộ chọn xuất hiện trong ví dụ về mã sau đây sẽ kiểm tra để xem nút có đang được di chuột hoặc được một thiết bị có thể nhìn thấy chỉ báo tiêu điểm lấy tiêu điểm hay không, và không được kích hoạt. Nếu có, CSS sẽ áp dụng để thực hiện những việc sau:

  • Áp dụng màu nền khi di chuột.
  • Tăng khoảng cách .
  • Thêm hiệu ứng chuyển động nảy.
  • Làm trễ các hiệu ứng chuyển đổi của phần tử giả.
.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 }     }   } } 

Góc nhìn 3D vẫn rất gọn gàng đối với lựa chọn ưu tiên chuyển động reduced. Các phần tử trên cùng và dưới cùng cho thấy hiệu ứng một cách tinh tế.

Một số điểm cải tiến nhỏ bằng JavaScript

Giao diện này có thể sử dụng được trên bàn phím, trình đọc màn hình, tay cầm chơi game, màn hình cảm ứng và chuột, nhưng chúng ta có thể thêm một số thao tác nhỏ bằng JavaScript để đơn giản hoá một số trường hợp.

Hỗ trợ các phím mũi tên

Phím tab là một cách hay để di chuyển trong trình đơn, nhưng tôi muốn các phím định hướng hoặc cần điều khiển di chuyển tiêu điểm trên tay cầm chơi game. Thư viện roving-ux thường được dùng cho các giao diện Thử thách GUI sẽ xử lý các phím mũi tên cho chúng ta. Đoạn mã bên dưới hướng dẫn thư viện bẫy tiêu điểm trong .threeD-button-set và chuyển tiêu điểm đến các nút con.

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

Tương tác thị sai bằng chuột

Việc theo dõi chuột và để chuột nghiêng trình đơn là nhằm mô phỏng giao diện trò chơi điện tử AR và VR, trong đó thay vì chuột, bạn có thể có một con trỏ ảo. Có thể sẽ rất thú vị khi các phần tử nhận biết rõ con trỏ.

Vì đây là một tính năng nhỏ bổ sung, nên chúng ta sẽ đặt hoạt động tương tác này sau một truy vấn về lựa chọn ưu tiên chuyển động của người dùng. Ngoài ra, trong quá trình thiết lập, hãy lưu trữ thành phần danh sách nút vào bộ nhớ bằng querySelector và lưu trữ tạm thời ranh giới của phần tử vào menuRect. Sử dụng các ranh giới này để xác định độ lệch xoay được áp dụng cho thẻ dựa trên vị trí chuột.

const menu = document.querySelector('.threeD-button-set') const menuRect = menu.getBoundingClientRect()  const { matches:motionOK } = window.matchMedia(   '(prefers-reduced-motion: no-preference)' ) 

Tiếp theo, chúng ta cần một hàm chấp nhận vị trí xy của chuột, đồng thời trả về một giá trị mà chúng ta có thể dùng để xoay thẻ. Hàm sau đây sử dụng vị trí chuột để xác định vị trí của chuột ở phía nào của hộp và khoảng cách là bao nhiêu. Delta được trả về từ hàm.

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} } 

Cuối cùng, hãy theo dõi chuyển động của chuột, truyền vị trí đến hàm getAngles() và sử dụng các giá trị delta làm kiểu thuộc tính tuỳ chỉnh. Tôi chia cho 20 để tăng khoảng cách giữa các giá trị và giảm độ giật, có thể có cách tốt hơn để làm việc đó. Nếu bạn nhớ từ đầu, chúng ta sẽ đặt các thuộc tính --x--y ở giữa hàm clamp(). Điều này ngăn vị trí chuột xoay thẻ quá mức thành vị trí khó đọc.

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`)   }) } 

Bản dịch và đường đi

Có một điểm cần lưu ý khi kiểm thử trình đơn trò chơi ở các chế độ và ngôn ngữ viết khác.

Các phần tử <button> có kiểu !important cho writing-mode trong biểu định kiểu của tác nhân người dùng. Điều này có nghĩa là HTML của trình đơn trò chơi cần thay đổi để phù hợp với thiết kế mong muốn. Việc thay đổi danh sách nút thành danh sách các đường liên kết cho phép các thuộc tính logic thay đổi hướng của trình đơn, vì các phần tử <a> không có kiểu !important do trình duyệt cung cấp.

Kết luận

Giờ bạn đã biết cách tôi làm, vậy bạn có thể làm được không‽ 🙂 Bạn có thể thêm tương tác gia tốc kế vào trình đơn để khi bạn nghiêng điện thoại, trình đơn sẽ xoay không? Chúng tôi có thể cải thiện trải nghiệm không chuyển động không?

Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách để xây dựng trên web. Hãy tạo một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó vào phần bản phối lại của cộng đồng bên dưới!

Bản phối lại của cộng đồng

Chưa có nội dung nào ở đây!