Nghiên cứu điển hình – Mê cung Inside World

World Wide Mê cung là trò chơi trong đó bạn sử dụng điện thoại thông minh để điều hướng một quả bóng lăn qua các mê cung 3D được tạo từ các trang web nhằm cố gắng đạt được điểm mục tiêu.

Mê cung trên toàn thế giới

Trò chơi sử dụng nhiều tính năng HTML5. Ví dụ: sự kiện DeviceOrientation sẽ truy xuất dữ liệu về độ nghiêng từ điện thoại thông minh. Sau đó, dữ liệu này được gửi đến máy tính thông qua WebSocket, nơi người chơi tìm cách di chuyển qua không gian 3D do WebGLWeb Workers xây dựng.

Trong bài viết này, tôi sẽ giải thích chính xác cách sử dụng các tính năng này, quá trình phát triển tổng thể và các điểm chính để tối ưu hoá.

DeviceOrientation

Sự kiện DeviceOrientation (ví dụ) được dùng để truy xuất dữ liệu về độ nghiêng từ điện thoại thông minh. Khi addEventListener được dùng với sự kiện DeviceOrientation, lệnh gọi lại có đối tượng DeviceOrientationEvent sẽ được gọi dưới dạng một đối số theo định kỳ. Khoảng thời gian này khác nhau tuỳ theo thiết bị được sử dụng. Ví dụ: trong iOS + Chrome và iOS + Safari, lệnh gọi lại được gọi khoảng 1/20 giây một lần, trong khi ở Android 4 + Chrome, lệnh gọi lại được gọi khoảng 1/10 giây một lần.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

Đối tượng DeviceOrientationEvent chứa dữ liệu độ nghiêng cho từng trục X, YZ theo độ (không phải radian) (Tìm hiểu thêm trên HTML5Rocks). Tuy nhiên, các giá trị trả về cũng thay đổi theo kết hợp thiết bị và trình duyệt được sử dụng. Phạm vi của giá trị trả về thực tế được trình bày trong bảng dưới đây:

Hướng thiết bị.

Các giá trị ở phần trên cùng được đánh dấu bằng màu xanh dương là các giá trị được xác định trong các thông số kỹ thuật của W3C. Những thông số được đánh dấu bằng màu xanh lục phù hợp với các thông số kỹ thuật này, còn những thông số được đánh dấu bằng màu đỏ bị lệch đi. Điều đáng ngạc nhiên là chỉ kết hợp Android-Firefox mới trả về giá trị khớp với thông số kỹ thuật. Tuy nhiên, khi nói đến việc triển khai, bạn nên điều chỉnh các giá trị xảy ra thường xuyên. Do đó, World Wide Mê cung sử dụng giá trị trả về iOS làm tiêu chuẩn và điều chỉnh cho các thiết bị Android cho phù hợp.

if android and event.gamma > 180 then event.gamma -= 360

Tuy nhiên, tính năng này vẫn không hỗ trợ Nexus 10. Mặc dù Nexus 10 trả về cùng một phạm vi giá trị như các thiết bị Android khác, nhưng vẫn có một lỗi đảo ngược giá trị beta và gamma. Vấn đề này đang được giải quyết riêng. (Có lẽ nó được đặt mặc định thành hướng ngang?)

Như điều này cho thấy, ngay cả khi các API liên quan đến thiết bị thực tế đã có thông số kỹ thuật cụ thể, cũng không có gì đảm bảo rằng các giá trị được trả về sẽ khớp với các thông số kỹ thuật đó. Vì vậy, việc thử nghiệm các phiên bản này trên tất cả các thiết bị tiềm năng là rất quan trọng. Điều này cũng có nghĩa là bạn có thể nhập các giá trị không mong muốn, nên cần có một giải pháp thay thế. World Wide Mê cung nhắc người chơi lần đầu hiệu chỉnh thiết bị của họ ở bước 1 trong hướng dẫn, nhưng sẽ không hiệu chỉnh đúng vị trí 0 nếu nhận được giá trị độ nghiêng không mong muốn. Do đó, trò chơi có giới hạn thời gian nội bộ và nhắc người chơi chuyển sang dùng các nút điều khiển bằng bàn phím nếu không thể hiệu chỉnh trong giới hạn thời gian đó.

WebSocket

Trong World Wide Mê cung, điện thoại thông minh và máy tính của bạn được kết nối qua WebSocket. Chính xác hơn, các thiết bị này được kết nối qua một máy chủ chuyển tiếp, tức là từ điện thoại thông minh đến máy chủ đến máy tính. Điều này là do WebSocket thiếu khả năng kết nối các trình duyệt trực tiếp với nhau. (Việc sử dụng kênh dữ liệu WebRTC cho phép kết nối ngang hàng và bạn không cần phải có máy chủ chuyển tiếp, nhưng tại thời điểm triển khai, phương thức này chỉ dùng được với Chrome Canary và Firefox Nightly.)

Tôi đã chọn triển khai bằng cách sử dụng thư viện có tên Socket.IO (v0.9.11), bao gồm các tính năng để kết nối lại trong trường hợp hết thời gian chờ hoặc ngắt kết nối. Tôi đã sử dụng nó cùng với NodeJS, vì tổ hợp NodeJS + Socket.IO này cho thấy hiệu suất phía máy chủ tốt nhất trong một số thử nghiệm triển khai WebSocket.

Ghép nối theo số

  1. Máy tính của bạn đã kết nối với máy chủ.
  2. Máy chủ cung cấp cho máy tính của bạn một số được tạo ngẫu nhiên và ghi nhớ tổ hợp số đó và máy tính.
  3. Trên thiết bị di động của bạn, hãy chỉ định một số rồi kết nối với máy chủ.
  4. Nếu số được chỉ định giống với số từ một máy tính được kết nối thì thiết bị di động của bạn sẽ được ghép nối với máy tính đó.
  5. Nếu không có máy tính nào được chỉ định, thì sẽ xảy ra lỗi.
  6. Khi dữ liệu đến từ thiết bị di động của bạn, dữ liệu đó sẽ được gửi đến máy tính mà dữ liệu được ghép nối và ngược lại.

Bạn cũng có thể thực hiện kết nối ban đầu từ thiết bị di động. Trong trường hợp đó, bạn chỉ cần đảo ngược các thiết bị.

Đồng bộ hoá thẻ

Tính năng Đồng bộ hoá thẻ dành riêng cho Chrome giúp quá trình ghép nối thậm chí còn dễ dàng hơn. Nhờ công cụ này, những trang đang mở trên máy tính có thể mở trên thiết bị di động một cách dễ dàng (và ngược lại). Máy tính sẽ lấy số kết nối do máy chủ cấp và thêm vào URL của một trang bằng history.replaceState.

history.replaceState(null, null, '/maze/' + connectionNumber)

Nếu tính năng Đồng bộ hoá thẻ đang bật, thì URL sẽ được đồng bộ hoá sau vài giây và người dùng có thể mở chính trang đó trên thiết bị di động. Thiết bị di động kiểm tra URL của trang đang mở và nếu một số được thêm vào thì nó sẽ bắt đầu kết nối ngay lập tức. Theo đó, bạn sẽ không cần phải nhập số theo cách thủ công hoặc quét mã QR bằng camera.

Độ trễ

Vì máy chủ chuyển tiếp nằm ở Hoa Kỳ, nên việc truy cập máy chủ từ Nhật Bản sẽ dẫn đến độ trễ khoảng 200 mili giây trước khi dữ liệu độ nghiêng của điện thoại thông minh đến được máy tính. Thời gian phản hồi rõ ràng là chậm so với môi trường cục bộ được sử dụng trong quá trình phát triển, nhưng việc chèn một cái gì đó như bộ lọc thông thấp (tôi đã sử dụng EMA) đã giúp cải thiện mức độ không phô trương. (Trong thực tế, chúng ta cũng cần dùng một bộ lọc thông thấp cho mục đích trình bày; các giá trị trả về từ cảm biến độ nghiêng bao gồm một lượng nhiễu đáng kể, nên việc áp dụng các giá trị đó cho màn hình dẫn đến rất nhiều rung lắc.) Cách này không hiệu quả với các bước nhảy, vốn rõ ràng là rất chậm, nhưng không thể giải quyết vấn đề này.

Vì tôi đã dự đoán vấn đề về độ trễ ngay từ đầu, nên tôi đã cân nhắc việc thiết lập các máy chủ chuyển tiếp trên toàn thế giới để các máy khách có thể kết nối với máy chủ gần nhất hiện có (do đó giảm thiểu độ trễ). Tuy nhiên, tôi quyết định sử dụng Google Compute Engine (GCE), một nền tảng chỉ có ở Hoa Kỳ vào thời điểm đó, vì vậy điều này là không thể.

Bài toán của giải thuật Nagle

Thuật toánNagle thường được tích hợp vào các hệ điều hành để giao tiếp hiệu quả bằng cách lưu vào bộ đệm ở cấp độ TCP, nhưng tôi nhận thấy rằng mình không thể gửi dữ liệu theo thời gian thực khi thuật toán này được bật. (Cụ thể khi kết hợp với xác nhận chậm về TCP. Ngay cả khi không ACK bị trễ, vấn đề tương tự vẫn xảy ra nếu ACK bị trễ đến một mức độ nhất định do các yếu tố như máy chủ ở nước ngoài.)

Sự cố độ trễ Nagle không xảy ra với WebSocket trong Chrome dành cho Android, bao gồm tuỳ chọn TCP_NODELAY để tắt Nagle, nhưng đã xảy ra với WebKit WebSocket được sử dụng trong Chrome dành cho iOS mà không bật tuỳ chọn này. (Safari, sử dụng cùng một WebKit, cũng gặp sự cố này. Sự cố này đã được báo cáo cho Apple qua Google và có vẻ như đã được giải quyết trong phiên bản phát triển của WebKit.

Khi sự cố này xảy ra, dữ liệu độ nghiêng được gửi đi cứ 100 mili giây sẽ được kết hợp thành các đoạn chỉ tiếp cận được máy tính sau mỗi 500 mili giây. Trò chơi không thể hoạt động trong những điều kiện như vậy, vì vậy, trò chơi có thể tránh được độ trễ này bằng cách yêu cầu phía máy chủ gửi dữ liệu trong khoảng thời gian ngắn (khoảng 50 mili giây một lần). Tôi tin rằng việc nhận ACK trong các khoảng thời gian ngắn sẽ đánh lừa thuật toán Nagle nghĩ rằng việc gửi dữ liệu đi là bình thường.

Thuật toán Nagle 1

Phần trên vẽ đồ thị các khoảng thời gian của dữ liệu thực tế nhận được. Nó biểu thị khoảng thời gian giữa các gói, màu xanh lục biểu thị khoảng thời gian đầu ra và màu đỏ biểu thị khoảng thời gian đầu vào. Tối thiểu là 54 mili giây, tối đa là 158 mili giây và mức giữa gần 100 mili giây. Ở đây, tôi sử dụng một chiếc iPhone có máy chủ chuyển tiếp đặt tại Nhật Bản. Cả đầu ra và đầu vào đều khoảng 100 mili giây và hoạt động rất mượt mà.

Thuật toán Nagle 2

Ngược lại, biểu đồ này cho thấy kết quả sử dụng máy chủ tại Hoa Kỳ. Trong khi khoảng đầu ra màu xanh lục giữ ổn định ở 100 mili giây, khoảng đầu vào dao động giữa mức thấp 0 mili giây và mức cao 500 mili giây, chỉ ra rằng máy tính đang nhận dữ liệu theo các đoạn.

ALT_TEXT_HERE

Cuối cùng, biểu đồ này cho thấy kết quả tránh được độ trễ bằng cách yêu cầu máy chủ gửi dữ liệu giữ chỗ. Mặc dù hoạt động không tốt như khi sử dụng máy chủ Nhật Bản, nhưng rõ ràng là khoảng thời gian đầu vào vẫn tương đối ổn định ở khoảng 100 mili giây.

Bạn gặp phải lỗi?

Mặc dù trình duyệt mặc định trong Android 4 (ICS) có API WebSocket nhưng lại không thể kết nối, dẫn đến sự kiện Socket.IO connect_failed. Trong nội bộ, nó hết thời gian chờ và phía máy chủ cũng không thể xác minh kết nối. (Tôi chưa thử nghiệm việc này với WebSocket, vì vậy nó có thể là sự cố Socket.IO.)

Mở rộng quy mô máy chủ chuyển tiếp

Vì vai trò của máy chủ chuyển tiếp không phức tạp nên việc mở rộng và tăng số lượng máy chủ sẽ không khó, miễn là bạn đảm bảo rằng cùng một máy tính và thiết bị di động luôn được kết nối với cùng một máy chủ.

Vật lý

Chuyển động của bóng trong trò chơi (xuống dốc, va chạm với mặt đất, va chạm với tường, thu thập vật phẩm, v.v.) đều được thực hiện qua một trình mô phỏng thực tế dạng 3D. Tôi đã sử dụng Ammo.js – một cổng của công cụ vật lý Bullet được sử dụng rộng rãi vào JavaScript bằng cách sử dụng Emscripten – cùng với Physijs để sử dụng công cụ đó làm "Web Worker".

Trình chạy web

Web Workers là một API để chạy JavaScript trong các luồng riêng biệt. JavaScript được khởi chạy dưới dạng Web Worker chạy dưới dạng một luồng riêng biệt với luồng ban đầu được gọi, vì vậy có thể thực hiện các tác vụ nặng trong khi vẫn duy trì khả năng phản hồi của trang. Physijs sử dụng Web Workers một cách hiệu quả để giúp công cụ vật lý 3D thường chuyên sâu hoạt động trơn tru. World Wide Mê cung xử lý công cụ vật lý và kết xuất hình ảnh WebGL ở tốc độ khung hình hoàn toàn khác nhau, vì vậy ngay cả khi tốc độ khung hình giảm trên máy có thông số kỹ thuật thấp do tải nặng kết xuất WebGL, bản thân công cụ vật lý sẽ ít nhiều duy trì 60 khung hình/giây và không cản trở các thao tác điều khiển trò chơi.

FPS

Hình ảnh này cho thấy tốc độ khung hình thu được trên Lenovo G570. Hộp trên hiển thị tốc độ khung hình cho WebGL (kết xuất hình ảnh) và hộp phía dưới hiển thị tốc độ khung hình cho công cụ vật lý. GPU tích hợp Intel HD Graphics 3000, do đó tốc độ khung hình kết xuất hình ảnh không đạt được mức 60 fps như mong đợi. Tuy nhiên, vì công cụ vật lý đã đạt được tốc độ khung hình dự kiến nên lối chơi không khác biệt nhiều so với hiệu suất trên một máy có thông số kỹ thuật cao.

Vì các luồng có Web Worker đang hoạt động không có đối tượng bảng điều khiển, nên dữ liệu phải được gửi đến luồng chính qua postMessage để tạo nhật ký gỡ lỗi. Việc sử dụng console4Worker sẽ tạo ra đối tượng tương đương với đối tượng bảng điều khiển trong Worker, giúp quá trình gỡ lỗi trở nên dễ dàng hơn đáng kể.

Trình chạy dịch vụ

Các phiên bản Chrome gần đây cho phép bạn đặt các điểm ngắt khi chạy Web Workers, điều này cũng hữu ích khi gỡ lỗi. Bạn có thể tìm thấy mục này trong bảng điều khiển "Worker" trong Công cụ cho nhà phát triển.

Hiệu suất

Các giai đoạn có số lượng đa giác cao đôi khi vượt quá 100.000 đa giác, nhưng hiệu suất không bị ảnh hưởng nhiều ngay cả khi chúng được tạo hoàn toàn dưới dạng Physijs.ConcaveMesh (btBvhTriangleMeshShape trong Dấu đầu dòng).

Ban đầu, tốc độ khung hình giảm khi số lượng đối tượng cần phát hiện va chạm tăng lên, nhưng việc loại bỏ quá trình xử lý không cần thiết trong Physijs đã cải thiện hiệu suất. Cải tiến này được thực hiện đối với nhánh của Physijs ban đầu.

Vật thể ma

Các đối tượng có khả năng phát hiện va chạm nhưng không ảnh hưởng đến các đối tượng khác nên không ảnh hưởng đến các đối tượng khác được gọi là "đối tượng ma" trong Bullet. Mặc dù Physijs không chính thức hỗ trợ các đối tượng ma, nhưng bạn vẫn có thể tạo chúng ở đó bằng cách sửa cờ sau khi tạo Physijs.Mesh. World Wide Mê cung sử dụng các vật thể ma để phát hiện xung đột các vật phẩm và điểm mục tiêu.

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

Đối với collision_flags, 1 là CF_STATIC_OBJECT và 4 là CF_NO_CONTACT_RESPONSE. Hãy thử tìm kiếm trên diễn đàn của Dấu đầu dòng, Stack Overflow hoặc tài liệu về Dấu đầu dòng để biết thêm thông tin. Vì Physijs là một trình bao bọc cho Ammo.js và Ammo.js về cơ bản giống với Bullet, nên hầu hết mọi việc có thể thực hiện trong Bullet cũng có thể được thực hiện trong Physijs.

Sự cố Firefox 18

Bản cập nhật Firefox từ phiên bản 17 lên 18 đã thay đổi cách Web Workers trao đổi dữ liệu và kết quả là Physijs ngừng hoạt động. Vấn đề này được báo cáo trên GitHub và được giải quyết sau vài ngày. Mặc dù tính hiệu quả của nguồn mở này gây ấn tượng với tôi, nhưng sự cố cũng nhắc nhở tôi cách World Wide Mê cung bao gồm một số khung nguồn mở khác nhau. Tôi viết bài này với hy vọng có thể cung cấp cho bạn một số loại phản hồi.

asm.js

Mặc dù điều này không liên quan trực tiếp đến World Wide Mê cung, nhưng Ammo.js đã hỗ trợ asm.js mà Mozilla thông báo gần đây (không có gì đáng ngạc nhiên vì về cơ bản, asm.js được tạo để tăng tốc JavaScript do Emscripten tạo và người tạo ra Emscripten cũng là người tạo ra Ammo.js). Nếu Chrome cũng hỗ trợ asm.js, thì tải trên tính toán của công cụ vật lý sẽ giảm đáng kể. Tốc độ nhanh hơn rõ rệt khi được thử nghiệm với Firefox Nightly. Có lẽ tốt nhất nên viết các phần yêu cầu tốc độ cao hơn trong C/C++ rồi chuyển chúng sang JavaScript bằng Emscripten?

WebGL

Để triển khai WebGL, tôi đã sử dụng thư viện được phát triển tích cực nhất, three.js (r53). Mặc dù bản sửa đổi 57 đã được phát hành trong giai đoạn sau của quá trình phát triển, nhưng những thay đổi lớn đã được thực hiện đối với API, vì vậy, tôi bị mắc kẹt với bản sửa đổi gốc để phát hành.

Hiệu ứng toả sáng

Hiệu ứng toả sáng được thêm vào lõi của quả bóng và các vật phẩm được triển khai bằng cách sử dụng phiên bản đơn giản của "Phương pháp Kawase MGF". Tuy nhiên, trong khi Phương pháp Kawase làm cho tất cả các vùng sáng đều nở hoa, thì World Wide Mê cung tạo ra các mục tiêu kết xuất riêng biệt cho các vùng cần phát sáng. Điều này là do bạn phải sử dụng ảnh chụp màn hình trang web cho hoạ tiết sân khấu và chỉ cần trích xuất tất cả các vùng sáng sẽ làm toàn bộ trang web phát sáng, chẳng hạn như ảnh có nền trắng. Tôi cũng đã cân nhắc xử lý mọi thứ trong HDR, nhưng lần này tôi quyết định không xử lý vì việc triển khai có thể đã khá phức tạp.

Glow

Ở trên cùng bên trái cho thấy lượt truyền đầu tiên, trong đó các khu vực phát sáng được kết xuất riêng biệt và sau đó được áp dụng hiệu ứng làm mờ. Dưới cùng bên phải là lượt truyền thứ hai, trong đó kích thước hình ảnh giảm 50% và sau đó được áp dụng hiệu ứng làm mờ. Ở trên cùng bên phải cho thấy lượt truyền thứ ba, trong đó hình ảnh lại được giảm 50% rồi được làm mờ. Sau đó, ba hình ảnh này được phủ lên để tạo thành hình ảnh kết hợp cuối cùng hiển thị ở dưới cùng bên trái. Đối với hiệu ứng làm mờ, tôi đã sử dụng VerticalBlurShaderHorizontalBlurShader, có trong Three.js, vì vậy vẫn còn khả năng tối ưu hoá thêm.

Quả cầu phản chiếu

Sự phản chiếu trên bóng dựa trên mẫu từ Three.js. Tất cả các hướng được kết xuất từ vị trí của quả bóng và được sử dụng làm bản đồ môi trường. Bản đồ môi trường cần được cập nhật mỗi khi bóng di chuyển, nhưng vì việc cập nhật ở tốc độ 60 khung hình/giây rất tốn kém, nên bản đồ môi trường sẽ được cập nhật mỗi ba khung hình. Kết quả không được mượt mà như việc cập nhật mọi khung hình, nhưng sự khác biệt hầu như không thể nhận thấy trừ khi được chỉ ra.

Chương trình đổ bóng, chương trình đổ bóng, chương trình đổ bóng...

WebGL yêu cầu có chương trình đổ bóng (trình đổ bóng đỉnh, chương trình đổ bóng mảnh) cho tất cả quá trình kết xuất. Mặc dù các chương trình đổ bóng được bao gồm trong Three.js đã cho phép nhiều hiệu ứng, nhưng việc tự viết chương trình đổ bóng là không thể tránh khỏi để tối ưu hoá và đổ bóng phức tạp hơn. Vì World Wide Mê cung khiến CPU bận rộn với công cụ vật lý, nên tôi đã cố gắng sử dụng GPU bằng cách viết càng nhiều càng tốt bằng ngôn ngữ tô bóng (GLSL), ngay cả khi việc xử lý CPU (qua JavaScript) sẽ dễ dàng hơn. Đương nhiên là hiệu ứng sóng biển phụ thuộc vào hiệu ứng đổ bóng, cũng như pháo hoa tại điểm ghi điểm và hiệu ứng lưới khi quả bóng xuất hiện.

Bóng đổ

Thông tin trên là từ các phép kiểm thử về hiệu ứng lưới được dùng khi bóng xuất hiện. Hình bên trái là hình được sử dụng trong trò chơi, gồm 320 đa giác. Hình ở giữa sử dụng khoảng 5.000 đa giác, và hình bên phải sử dụng khoảng 300.000 đa giác. Ngay cả với nhiều hình đa giác như vậy, việc xử lý bằng chương trình đổ bóng có thể giữ được tốc độ khung hình ổn định là 30 khung hình/giây.

Lưới đổ bóng

Các mục nhỏ nằm rải rác trong vùng hiển thị đều được tích hợp vào một lưới và chuyển động riêng lẻ dựa vào các chương trình đổ bóng di chuyển từng mẹo đa giác. Đây là kết quả kiểm thử để xem hiệu suất có bị ảnh hưởng khi có một số lượng lớn các đối tượng hay không. Khoảng 5.000 đối tượng được sắp xếp ở đây, bao gồm khoảng 20.000 đa giác. Hiệu suất không bị ảnh hưởng chút nào.

poly2tri

Các giai đoạn được tạo dựa trên thông tin đường viền nhận được từ máy chủ, sau đó được tạo đa giác bằng JavaScript. Tam giác, một phần quan trọng của quá trình này, được ba.js triển khai kém và thường không thành công. Do đó, tôi quyết định tự tích hợp một thư viện tam giác khác có tên là poly2tri. Kết quả là Three.js đã cố gắng làm điều tương tự trong quá khứ, vì vậy tôi đã làm cho nó hoạt động chỉ bằng cách nhận xét một phần của nó. Do đó, lỗi đã giảm đáng kể, cho phép có nhiều giai đoạn có thể chơi hơn. Lỗi thỉnh thoảng vẫn xảy ra và vì lý do nào đó poly2tri xử lý lỗi bằng cách đưa ra cảnh báo, vì vậy tôi đã sửa đổi để gửi ngoại lệ.

poly2tri

Hình trên cho thấy cách đường viền màu xanh dương được tạo thành hình tam giác và đa giác màu đỏ được tạo.

Bộ lọc không đẳng hướng

Do bản đồ MIP đẳng hướng tiêu chuẩn giảm kích thước hình ảnh trên cả trục ngang và dọc, nên việc xem đa giác từ các góc xiên làm cho các hoạ tiết ở phía xa của các giai đoạn World Wide mê cung trông giống như hoạ tiết kéo dài theo chiều ngang, có độ phân giải thấp. Hình ảnh trên cùng bên phải trên trang Wikipedia này là một ví dụ điển hình cho trường hợp này. Trong thực tế, cần nhiều độ phân giải theo chiều ngang hơn mà WebGL (OpenGL) sẽ phân giải bằng cách sử dụng phương pháp có tên là lọc không đẳng hướng. Trong Three.js, việc đặt giá trị lớn hơn 1 cho THREE.Texture.anisotropy sẽ bật bộ lọc không đẳng hướng. Tuy nhiên, tính năng này là một tiện ích và có thể không hỗ trợ một số GPU.

Tối ưu hoá

Vì bài viết các phương pháp hay nhất về WebP cũng đề cập đến, cách quan trọng nhất để cải thiện hiệu suất của WebGL (OpenGL) là giảm thiểu lệnh gọi vẽ. Trong quá trình phát triển ban đầu của Mê cung World Wide Web, tất cả các hòn đảo, cây cầu và rào chắn trong trò chơi đều là những vật thể riêng biệt. Điều này đôi khi dẫn đến hơn 2.000 hàm gọi vẽ, khiến các giai đoạn phức tạp trở nên khó sử dụng. Tuy nhiên, sau khi tôi đóng gói tất cả các loại đối tượng giống nhau vào một lưới, hàm gọi vẽ giảm xuống còn 50 hoặc hơn, nhờ đó cải thiện đáng kể hiệu suất.

Tôi đã sử dụng tính năng theo dõi của Chrome để tối ưu hoá hơn nữa. Các trình phân tích tài nguyên có trong Công cụ cho nhà phát triển của Chrome có thể xác định tổng thời gian xử lý phương thức ở một mức độ nào đó, nhưng việc theo dõi có thể cho bạn biết chính xác thời gian của mỗi phần, giảm tới 1/1000 giây. Vui lòng xem bài viết này để biết thông tin chi tiết về cách sử dụng tính năng theo dõi.

Tối ưu hoá

Trên đây là kết quả theo dõi của quá trình tạo bản đồ môi trường cho bóng phản chiếu. Khi chèn console.timeconsole.timeEnd vào các vị trí có vẻ phù hợp trong Three.js, chúng ta sẽ thấy một biểu đồ có dạng như sau. Thời gian di chuyển từ trái sang phải và mỗi lớp giống như một ngăn xếp lệnh gọi. Việc lồng console.time trong một console.time sẽ cho phép đo lường thêm. Biểu đồ trên cùng là tính năng tối ưu hoá trước và biểu đồ dưới cùng là tính năng sau khi tối ưu hoá. Như biểu đồ trên cùng cho thấy, updateMatrix (mặc dù từ bị cắt ngắn) được gọi cho mỗi lượt hiển thị 0-5 trong quá trình trước tối ưu hoá. Tôi đã sửa đổi để nó chỉ được gọi một lần, vì quy trình này chỉ bắt buộc khi các đối tượng thay đổi vị trí hoặc hướng.

Tự nhiên quá trình theo dõi sẽ tốn tài nguyên. Vì vậy, việc chèn console.time quá mức có thể gây ra sai lệch đáng kể so với hiệu suất thực tế, gây khó khăn cho việc xác định các khu vực để tối ưu hoá.

Công cụ điều chỉnh hiệu suất

Do bản chất của Internet, trò chơi có thể sẽ được chơi trên các hệ thống có thông số kỹ thuật khác nhau. Find Your Way to Oz (Phát hành vào đầu tháng 2) sử dụng một lớp có tên là IFLAutomaticPerformanceAdjust để điều chỉnh tỷ lệ hiệu ứng giảm dần theo sự biến động về tốc độ khung hình, giúp đảm bảo việc phát nội dung mượt mà. World Wide Mê cung được xây dựng dựa trên cùng một lớp IFLAutomaticPerformanceAdjust và điều chỉnh các hiệu ứng theo thứ tự sau đây để giúp lối chơi mượt mà nhất có thể:

  1. Nếu tốc độ khung hình giảm xuống dưới 45 khung hình/giây, thì bản đồ môi trường sẽ ngừng cập nhật.
  2. Nếu tốc độ vẫn dưới 40 khung hình/giây, độ phân giải kết xuất sẽ giảm xuống còn 70% (50% tỷ lệ khung hình).
  3. Nếu tốc độ vẫn giảm xuống dưới 40 khung hình/giây, FXAA (chống răng cưa) sẽ bị loại bỏ.
  4. Nếu tốc độ video vẫn giảm xuống dưới 30 khung hình/giây thì hiệu ứng toả sáng sẽ bị loại bỏ.

Rò rỉ bộ nhớ

Việc loại bỏ các đối tượng một cách gọn gàng là một vấn đề rắc rối với Three.js. Nhưng nếu để chúng ở một mình thì rõ ràng là sẽ dẫn đến rò rỉ bộ nhớ, vì vậy tôi đã nghĩ ra phương pháp dưới đây. @renderer đề cập đến THREE.WebGLRenderer. (Bản sửa đổi mới nhất của Three.js sử dụng phương thức phân bổ hơi khác, vì vậy phương pháp này có thể sẽ không hoạt động với nó.)

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

Cá nhân, tôi nghĩ điều tuyệt vời nhất của ứng dụng WebGL là khả năng thiết kế bố cục trang bằng HTML. Việc xây dựng giao diện 2D như điểm số hoặc hiển thị văn bản trong Flash hoặc openFrameworks (OpenGL) khá khó khăn. Flash ít nhất có một IDE, nhưng openFrameworks rất khó nếu bạn không quen với nó (sử dụng một cái gì đó như Cocos2D có thể giúp việc này dễ dàng hơn). Mặt khác, HTML cho phép kiểm soát chính xác tất cả các khía cạnh thiết kế giao diện người dùng bằng CSS, giống như khi xây dựng trang web. Mặc dù không thể tạo hiệu ứng phức tạp như các hạt ngưng tụ thành biểu trưng, nhưng vẫn có thể có một số hiệu ứng 3D trong chức năng của Chuyển đổi CSS. Hiệu ứng văn bản "GOAL" và "TIME IS UP" của World Wide Mê cung là hiệu ứng động sử dụng tỷ lệ trong Chuyển đổi CSS (được triển khai bằng Phương tiện công cộng). (Rõ ràng là việc phân cấp nền sử dụng WebGL.)

Mỗi trang trong trò chơi (tiêu đề, KẾT QUẢ, BẢNG XẾP HẠNG, v.v.) có tệp HTML riêng và sau khi các trang này được tải dưới dạng mẫu, $(document.body).append() sẽ được gọi với các giá trị thích hợp tại thời điểm thích hợp. Một vấn đề xảy ra là không thể thiết lập sự kiện chuột và bàn phím trước khi thêm. Vì vậy, việc thử el.click (e) -> console.log(e) trước khi thêm sẽ không hoạt động.

Quốc tế hoá (i18n)

Làm việc với HTML cũng thuận tiện cho việc tạo phiên bản tiếng Anh. Tôi chọn sử dụng i18next, một thư viện web i18n, để phục vụ nhu cầu quốc tế hoá của mình. Tôi có thể dùng thư viện này mà không cần sửa đổi gì thêm.

Việc chỉnh sửa và dịch văn bản trong trò chơi đã được thực hiện trên Bảng tính Google Tài liệu. Vì i18next yêu cầu tệp JSON, nên tôi đã xuất bảng tính sang TSV rồi chuyển đổi chúng bằng một trình chuyển đổi tuỳ chỉnh. Tôi đã thực hiện rất nhiều cập nhật ngay trước khi phát hành, vì vậy, việc tự động hoá quy trình xuất từ Bảng tính Google Tài liệu sẽ giúp mọi việc trở nên dễ dàng hơn nhiều.

Tính năng dịch tự động của Chrome cũng hoạt động bình thường vì các trang được xây dựng bằng HTML. Tuy nhiên, đôi khi tính năng này không phát hiện chính xác ngôn ngữ, thay vì nhầm lẫn ngôn ngữ đó với một ngôn ngữ hoàn toàn khác (ví dụ: tiếng Việt), do đó tính năng này hiện đang bị tắt. (Có thể tắt tính năng này bằng thẻ meta.)

RequireJS

Tôi đã chọn RequireJS làm hệ thống mô-đun JavaScript của mình. 10.000 dòng mã nguồn của trò chơi được chia thành khoảng 60 lớp (= tệp cà phê) và được biên dịch thành các tệp js riêng lẻ. RequestJS tải các tệp riêng lẻ này theo thứ tự thích hợp dựa trên phần phụ thuộc.

define ->
  class Hoge
    hogeMethod: ->

Bạn có thể sử dụng lớp được xác định ở trên (hoge.Tomatoes) như sau:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

Để hoạt động, hoge.js phải được tải trước moge.js và vì "hoge" được chỉ định làm đối số đầu tiên của "define" nên hoge.js luôn được tải trước (được gọi lại sau khi hoge.js tải xong). Cơ chế này được gọi là AMD và bạn có thể sử dụng bất kỳ thư viện bên thứ ba nào cho cùng loại lệnh gọi lại, miễn là cơ chế đó hỗ trợ AMD. Ngay cả những công cụ không hoạt động (ví dụ: Three.js) sẽ hoạt động tương tự, miễn là các quyền uỷ quyền được chỉ định trước.

Việc này cũng tương tự như việc nhập AS3, vì vậy, sẽ không có gì lạ. Nếu bạn phải có nhiều tệp phụ thuộc hơn, thì đây là một giải pháp khả thi.

r.js

Yêu cầu JS bao gồm một trình tối ưu hoá có tên là r.js. Thao tác này sẽ gói js chính với tất cả các tệp js phụ thuộc thành một, sau đó giảm kích thước bằng UglifyJS (hoặc wrap Compiler). Điều này giúp giảm số lượng tệp và tổng lượng dữ liệu mà trình duyệt cần tải. Tổng kích thước tệp JavaScript cho World Wide Mê cung là khoảng 2 MB và có thể giảm xuống còn khoảng 1 MB với tính năng tối ưu hoá r.js. Nếu trò chơi có thể được phân phối bằng gzip, kích thước sẽ được giảm xuống còn 250 KB. (GAE gặp sự cố khiến cho các tệp gzip trở lên không cho phép truyền các tệp gzip trở lên, vì vậy trò chơi hiện được phân phối không nén dưới dạng 1 MB văn bản thuần tuý.)

Trình tạo giai đoạn

Dữ liệu giai đoạn được tạo như sau, được thực hiện hoàn toàn trên máy chủ GCE ở Hoa Kỳ:

  1. URL của trang web cần chuyển đổi thành giai đoạn sẽ được gửi qua WebSocket.
  2. PhantomJS chụp ảnh màn hình, sau đó truy xuất vị trí thẻ div và img và xuất ở định dạng JSON.
  3. Dựa trên ảnh chụp màn hình ở bước 2 và dữ liệu định vị của các phần tử HTML, chương trình C++ (OpenCV, Boost) tuỳ chỉnh sẽ xoá các khu vực không cần thiết, tạo đảo, kết nối các đảo với cầu nối, tính toán lan can bảo vệ và vị trí vật phẩm, thiết lập điểm mục tiêu, v.v. Kết quả xuất ra ở định dạng JSON và được trả về trình duyệt.

PhantomJS

PhantomJS là một trình duyệt không yêu cầu màn hình. Công cụ này có thể tải trang web mà không cần mở cửa sổ, vì vậy, nó có thể được sử dụng trong kiểm thử tự động hoặc để chụp ảnh màn hình phía máy chủ. Công cụ trình duyệt của trình duyệt này là WebKit, cũng là công cụ được Chrome và Safari sử dụng, vì vậy bố cục và kết quả thực thi JavaScript của trình duyệt cũng gần giống với kết quả của các trình duyệt chuẩn.

Với PhantomJS, JavaScript hoặc CoffeeScript được dùng để viết các quy trình bạn muốn thực thi. Chụp ảnh màn hình rất dễ dàng, như minh hoạ trong mẫu này. Tôi đang làm việc trên máy chủ Linux (CentOS), vì vậy tôi cần cài đặt phông chữ để hiển thị tiếng Nhật (M+ fontS). Ngay cả khi đó, việc hiển thị phông chữ được xử lý khác với trong Windows hoặc Mac OS, vì vậy, cùng một phông chữ có thể hiển thị khác trên các máy khác (tuy nhiên, sự khác biệt là rất nhỏ).

Việc truy xuất vị trí thẻ img và div về cơ bản được xử lý giống như trên các trang tiêu chuẩn. jQuery cũng có thể được sử dụng mà không gặp bất kỳ vấn đề nào.

stage_builder

Ban đầu, tôi cân nhắc sử dụng phương pháp dựa trên DOM để tạo các giai đoạn (tương tự như Trình kiểm tra 3D trong Firefox) và thử phân tích DOM trong PhantomJS. Mặc dù vậy, cuối cùng, tôi đã quyết định xử lý hình ảnh. Để đạt được mục tiêu này, tôi đã viết một chương trình C++ sử dụng OpenCV và Boost có tên là "stage_builder". URL này thực hiện những việc sau:

  1. Tải ảnh chụp màn hình và(các) tệp JSON.
  2. Chuyển đổi hình ảnh và văn bản thành "các hòn đảo".
  3. Tạo dựng những cây cầu để kết nối các hòn đảo.
  4. Loại bỏ những cây cầu không cần thiết để tạo ra mê cung.
  5. Đặt mục có kích thước lớn.
  6. Đặt các mục nhỏ.
  7. Lan can địa điểm.
  8. Xuất dữ liệu định vị ở định dạng JSON.

Dưới đây là chi tiết về từng bước.

Đang tải ảnh chụp màn hình và(các) tệp JSON

cv::imread thông thường được dùng để tải ảnh chụp màn hình. Tôi đã thử nghiệm một số thư viện cho các tệp JSON, nhưng picojson có vẻ là cách dễ sử dụng nhất.

Chuyển đổi hình ảnh và văn bản thành "đảo"

Xây dựng giai đoạn

Ở trên là ảnh chụp màn hình phần Tin tức của aid-dcc.com (nhấp để xem kích thước thực tế). Các thành phần hình ảnh và văn bản phải được chuyển đổi thành các hòn đảo. Để tách riêng những phần này, chúng ta nên xoá màu nền trắng – nói cách khác là màu phổ biến nhất trong ảnh chụp màn hình. Sau khi hoàn tất thao tác này, bạn sẽ thấy giao diện như sau:

Xây dựng giai đoạn

Phần màu trắng là các hòn đảo tiềm năng.

Văn bản quá nhỏ và sắc nét, vì vậy, chúng ta sẽ làm dày văn bản bằng cv::dilate, cv::GaussianBlurcv::threshold. Nội dung hình ảnh cũng bị thiếu, vì vậy chúng tôi sẽ tô trắng các vùng đó, dựa trên dữ liệu đầu ra từ thẻ img (hình ảnh) từ PhantomJS. Hình ảnh thu được sẽ có dạng như sau:

Xây dựng giai đoạn

Văn bản lúc này tạo thành các nhóm phù hợp và mỗi hình ảnh là một đảo thích hợp.

Tạo cầu nối để kết nối các hòn đảo

Khi đã sẵn sàng, các hòn đảo sẽ được kết nối với nhau bằng cầu. Mỗi đảo sẽ tìm các đảo liền kề bên trái, phải, trên và dưới, sau đó nối một cây cầu với điểm gần nhất của đảo gần nhất, dẫn đến kết quả như sau:

Xây dựng giai đoạn

Loại bỏ những cây cầu không cần thiết để tạo ra mê cung

Việc giữ lại tất cả các cây cầu sẽ khiến cho sân khấu dễ dàng đi lại, vì vậy phải loại bỏ một số cầu để tạo ra mê cung. Một đảo (ví dụ: đảo ở trên cùng bên trái) được chọn làm điểm bắt đầu và chỉ có một cây cầu (được chọn ngẫu nhiên) kết nối với đảo đó sẽ bị xoá. Sau đó, thực hiện tương tự đối với đảo tiếp theo được nối bằng cây cầu còn lại. Khi đường dẫn đến ngõ cụt hoặc dẫn trở lại hòn đảo đã ghé thăm trước đó, con đường sẽ quay ngược lại tới điểm cho phép ra vào hòn đảo mới. Mê cung sẽ được hoàn thành sau khi tất cả các hòn đảo được xử lý theo cách này.

Xây dựng giai đoạn

Đặt các mục lớn

Một hoặc nhiều vật phẩm lớn được đặt trên mỗi đảo tuỳ thuộc vào kích thước, bằng cách chọn trong số các điểm ở xa nhất tính từ mép của đảo. Mặc dù không rõ ràng lắm, những điểm này được hiển thị bằng màu đỏ bên dưới:

Xây dựng giai đoạn

Từ tất cả các điểm có thể có này, điểm ở trên cùng bên trái được đặt làm điểm bắt đầu (vòng tròn màu đỏ), điểm ở dưới cùng bên phải được đặt làm mục tiêu (vòng tròn màu xanh lục) và tối đa sáu điểm còn lại được chọn cho vị trí mục lớn (vòng tròn màu tím).

Đặt các mục nhỏ

Xây dựng giai đoạn

Số lượng các mục nhỏ phù hợp được đặt dọc theo các dòng với khoảng cách đã đặt tính từ các cạnh đảo. Hình ảnh trên (không phải từ aid-dcc.com) cho thấy các đường vị trí dự kiến có màu xám, bù trừ và được đặt đều đặn từ các cạnh của đảo. Các chấm màu đỏ cho biết vị trí đặt các mục nhỏ. Vì hình ảnh này là từ phiên bản đang phát triển, nên các mục được bố trí theo đường thẳng, nhưng phiên bản cuối cùng phân tán các mục không đều hơn một chút ở một trong hai bên của đường màu xám.

Đặt lan can

Về cơ bản, hàng rào bảo vệ được đặt dọc theo ranh giới bên ngoài của quần đảo nhưng phải cắt bỏ ở những cây cầu để cho phép ra vào. Thư viện Hình học Boost tỏ ra hữu ích cho việc này, giúp đơn giản hoá các phép tính hình học, chẳng hạn như xác định vị trí dữ liệu ranh giới đảo giao nhau với các đường ở một trong hai bên của cầu.

Xây dựng giai đoạn

Các đường màu xanh lục bao quanh các hòn đảo là rào chắn. Có thể khó nhìn thấy trong hình ảnh này, nhưng không có đường màu xanh lục ở nơi cầu. Đây là hình ảnh cuối cùng dùng để gỡ lỗi, trong đó tất cả các đối tượng cần được xuất vào JSON đều được đưa vào. Chấm màu xanh dương nhạt là các mục nhỏ và các chấm màu xám được đề xuất là điểm khởi động lại. Khi quả bóng rơi xuống biển, trận đấu sẽ được tiếp tục từ điểm xuất phát lại gần nhất. Các điểm khởi động lại được sắp xếp nhiều hơn hoặc ít hơn theo cách giống với các thiết bị nhỏ, theo khoảng cách đều đặn tính từ mép của đảo.

Xuất dữ liệu định vị ở định dạng JSON

Tôi cũng dùng picojson cho dữ liệu đầu ra. API này ghi dữ liệu vào đầu ra chuẩn, sau đó được nhận bởi phương thức gọi (Node.js).

Tạo chương trình C++ trên máy Mac để chạy trong Linux

Trò chơi này được phát triển trên hệ điều hành Mac và triển khai trên hệ điều hành Linux. Tuy nhiên, vì OpenCV và Boost đã có mặt trên cả hai hệ điều hành nên việc phát triển sẽ không hề khó khăn sau khi thiết lập môi trường biên dịch. Tôi đã sử dụng Công cụ dòng lệnh trong Xcode để gỡ lỗi bản dựng trên máy Mac, sau đó tạo tệp định cấu hình bằng cách sử dụng automake/autoconf để có thể biên dịch bản dựng trong Linux. Sau đó, tôi chỉ cần sử dụng lệnh "định cấu hình && tạo" trong Linux để tạo tệp thực thi. Tôi gặp một số lỗi dành riêng cho Linux do sự khác biệt về phiên bản trình biên dịch nhưng có thể giải quyết tương đối dễ dàng bằng gdb.

Kết luận

Bạn có thể tạo một trò chơi như thế này bằng Flash hoặc Unity, mang lại nhiều lợi thế. Tuy nhiên, phiên bản này không yêu cầu plugin và các tính năng bố cục của HTML5 + CSS3 đã chứng minh là cực kỳ mạnh mẽ. Chắc chắn là phải có các công cụ phù hợp cho mỗi tác vụ. Cá nhân tôi rất ngạc nhiên về kết quả của trò chơi đối với một trò chơi được tạo hoàn toàn bằng HTML5, và mặc dù nó vẫn còn thiếu ở nhiều khía cạnh, nhưng tôi rất mong được thấy trò chơi này phát triển như thế nào trong tương lai.