Khi đại dịch kéo dài, nhóm làm việc từ xa đột nhiên của tôi ngày càng trở nên bóng_bóng_bóng_bóng_bóng_bóng_bóng- ... Đội đầu tiên ghi được 10 bàn thắng sẽ thắng.
Tất nhiên, ý tưởng sử dụng ô tô để chơi bóng đá không phải là duy nhất, nhưng hai ý tưởng chính sẽ giúp Autowuzzler trở nên khác biệt: Tôi muốn tái tạo lại một số hình ảnh và cảm giác khi chơi trên bàn bóng bàn thực tế và tôi muốn đảm bảo rằng việc mời bạn bè hoặc đồng đội tham gia một trò chơi thông thường nhanh chóng trở nên dễ dàng nhất có thể.
Trong bài viết này, tôi sẽ mô tả quy trình tạo ra Autowuzzler, các công cụ và khuôn khổ mà tôi đã chọn, đồng thời chia sẻ một số chi tiết triển khai và bài học mà tôi đã học được.

Vì trò chơi cần hỗ trợ nhiều người chơi theo thời gian thực, nên tôi đã sử dụng Express làm môi giới WebSockets. Đây chính là lúc mọi thứ trở nên khó khăn.
Vì các phép tính vật lý được thực hiện trên máy khách trong trò chơi Phaser, nên tôi đã chọn một logic đơn giản nhưng rõ ràng là có lỗi: Máy khách được kết nối đầu tiên có đặc quyền đáng ngờ là thực hiện các phép tính vật lý cho tất cả các đối tượng trong trò chơi, gửi kết quả đến máy chủ express, sau đó phát lại các vị trí, góc và lực đã cập nhật cho máy khách của người chơi khác. Sau đó, các máy khách khác sẽ áp dụng các thay đổi cho các đối tượng trong trò chơi.
Điều này dẫn đến tình huống mà người chơi đầu tiên được xem các hiện tượng vật lý diễn ra theo thời gian thực (suy cho cùng, chúng diễn ra cục bộ trên trình duyệt của họ), trong khi tất cả những người chơi khác đều chậm hơn ít nhất 30 mili giây (tốc độ phát sóng mà tôi đã chọn) hoặc — nếu kết nối mạng của người chơi đầu tiên chậm — tệ hơn đáng kể.
Nếu điều này nghe có vẻ như là một kiến trúc kém đối với bạn — thì bạn hoàn toàn đúng. Tuy nhiên, tôi chấp nhận sự thật này để nhanh chóng có được thứ gì đó có thể chơi được để tìm ra liệu trò chơi có thực sự vui khi chơi hay không.
Đã đến lúc phải xóa nguyên mẫu và bắt đầu với thiết lập mới và mục tiêu rõ ràng.
Đối với các khối xây dựng chính khác của trò chơi, tôi đã chọn:
Trong ví dụ trên, một lớp mới mở rộng lớp lược đồ do Colyseus cung cấp được tạo ra; trong hàm tạo, tất cả các thuộc tính đều nhận được một giá trị ban đầu. Vị trí và chuyển động của quả bóng được mô tả bằng năm thuộc tính:
Các kiểu thuộc tính có thể là các kiểu nguyên thủy:
Sơ đồ cho người chơi cũng tương tự, nhưng bao gồm thêm một vài thuộc tính để lưu trữ tên người chơi và số hiệu đội, cần được cung cấp khi tạo phiên bản Người chơi:
Cuối cùng, lược đồ cho Autowuzzler
Lưu ý: Định nghĩa của lớp
Quy trình chỉ định người chơi vào phòng chơi mong muốn được gọi là "ghép cặp". Colyseus giúp việc thiết lập trở nên rất dễ dàng bằng cách sử dụng phương thức
Bây giờ, bất kỳ người chơi nào tham gia trò chơi bằng cùng một
Với Matter.js, bạn định nghĩa một thế giới vật lý với một số thuộc tính vật lý nhất định như kích thước và trọng lực. Nó cung cấp một số phương pháp để tạo các đối tượng vật lý nguyên thủy tương tác với nhau bằng cách tuân thủ các định luật vật lý (mô phỏng), bao gồm khối lượng, va chạm, chuyển động có ma sát, v.v. Bạn có thể di chuyển các vật thể xung quanh bằng cách tác dụng lực — giống như bạn làm trong thế giới thực.
"Thế giới" Matter.js nằm ở trung tâm của trò chơi Autowuzzler; nó xác định tốc độ di chuyển của ô tô, độ nảy của quả bóng, vị trí của các mục tiêu và điều gì sẽ xảy ra nếu ai đó ghi bàn.
Mã đơn giản để thêm đối tượng trò chơi “ball” vào sân khấu trong Matter.js.
Sau khi các quy tắc được xác định, Matter.js có thể chạy có hoặc không thực sự hiển thị thứ gì đó lên màn hình. Đối với Autowuzzler, tôi đang sử dụng tính năng này để tái sử dụng mã thế giới vật lý cho cả máy chủ và máy khách — với một số điểm khác biệt chính:
Thế giới vật lý trên máy chủ:

Bây giờ, với tất cả những điều kỳ diệu diễn ra trên máy chủ, máy khách chỉ xử lý dữ liệu đầu vào và vẽ trạng thái mà nó nhận được từ máy chủ lên màn hình. Với một ngoại lệ:
Các đối tượng vật lý độc lập với các đối tượng Colyseus, điều này khiến chúng ta có hai hoán vị của cùng một đối tượng trò chơi (như quả bóng), tức là một đối tượng trong thế giới vật lý và một đối tượng Colyseus có thể được đồng bộ hóa.
Ngay khi đối tượng vật lý thay đổi, các thuộc tính đã cập nhật của nó cần được áp dụng trở lại đối tượng Colyseus. Chúng ta có thể thực hiện điều đó bằng cách lắng nghe sự kiện
Có thêm một bản sao của các đối tượng mà chúng ta cần xử lý: các đối tượng trò chơi trong trò chơi hướng đến người dùng.

Những tính năng chính này khiến tôi chọn SvelteKit thay vì Next.js để triển khai thực tế giao diện trò chơi:

Đây là trường hợp sử dụng tuyệt vời cho điểm cuối phía máy chủ SvelteKits kết hợp với hàm onMount của Sveltes: Điểm cuối

Đối với máy khách JavaScript, chúng tôi nhập hàm
Lưu ý:
Trong SvelteKit, các trang có tham số tuyến động được tạo bằng cách đặt các phần động của tuyến trong dấu ngoặc vuông khi đặt tên tệp trang:
Vì SvelteKit ban đầu hiển thị các trang trên máy chủ, chúng ta cần đảm bảo rằng mã này chỉ chạy trên máy khách sau khi trang tải xong. Một lần nữa, chúng ta sử dụng hàm vòng đời
Bây giờ chúng ta đã kết nối với máy chủ trò chơi Colyseus, chúng ta có thể bắt đầu lắng nghe bất kỳ thay đổi nào đối với các đối tượng trò chơi của mình.
Sau đây là một ví dụ về cách lắng nghe người chơi tham gia phòng (
Trong phương thức
Lưu ý: Hàm này chỉ chạy trên phiên bản máy khách của thế giới vật lý, vì các đối tượng trò chơi chỉ được thao tác gián tiếp thông qua máy chủ Colyseus.
Quy trình tương tự áp dụng cho các đối tượng trò chơi khác (bóng và đội): lắng nghe những thay đổi của chúng và áp dụng các giá trị đã thay đổi vào thế giới vật lý của máy khách.
Cho đến nay, không có đối tượng nào di chuyển vì chúng ta vẫn cần lắng nghe đầu vào bàn phím và gửi đến máy chủ. Thay vì gửi trực tiếp các sự kiện trên mọi sự kiện
Bây giờ chu trình đã hoàn tất: lắng nghe các lần nhấn phím, gửi các lệnh tương ứng đến máy chủ Colyseus để thao tác thế giới vật lý trên máy chủ. Sau đó, máy chủ Colyseus áp dụng các thuộc tính vật lý mới cho tất cả các đối tượng trò chơi và truyền dữ liệu trở lại máy khách để cập nhật phiên bản trò chơi dành cho người dùng.
Điều phức tạp trong thiết lập này là lớp vật lý của trò chơi vì nó đưa ra một bản sao bổ sung của mỗi đối tượng trò chơi liên quan đến vật lý cần được duy trì. Việc lưu trữ mã PIN trò chơi trong Supabase.io từ ứng dụng SvelteKit rất đơn giản. Nhìn lại, tôi có thể chỉ sử dụng cơ sở dữ liệu SQLite để lưu trữ mã PIN trò chơi, nhưng thử những điều mới là một nửa niềm vui khi xây dựng các dự án phụ.
Cuối cùng, việc sử dụng SvelteKit để xây dựng giao diện người dùng của trò chơi cho phép tôi di chuyển nhanh chóng — và thỉnh thoảng nở nụ cười vui vẻ trên khuôn mặt.
Bây giờ, hãy tiếp tục và mời bạn bè của bạn tham gia một vòng Autowuzzler!
Tất nhiên, ý tưởng sử dụng ô tô để chơi bóng đá không phải là duy nhất, nhưng hai ý tưởng chính sẽ giúp Autowuzzler trở nên khác biệt: Tôi muốn tái tạo lại một số hình ảnh và cảm giác khi chơi trên bàn bóng bàn thực tế và tôi muốn đảm bảo rằng việc mời bạn bè hoặc đồng đội tham gia một trò chơi thông thường nhanh chóng trở nên dễ dàng nhất có thể.
Trong bài viết này, tôi sẽ mô tả quy trình tạo ra Autowuzzler, các công cụ và khuôn khổ mà tôi đã chọn, đồng thời chia sẻ một số chi tiết triển khai và bài học mà tôi đã học được.

Nguyên mẫu đầu tiên (tệ hại) đang hoạt động
Nguyên mẫu đầu tiên được xây dựng bằng công cụ trò chơi mã nguồn mở Phaser.js, chủ yếu là vì công cụ vật lý đi kèm và vì tôi đã có một số kinh nghiệm với nó. Giai đoạn trò chơi được nhúng trong ứng dụng Next.js, một lần nữa vì tôi đã hiểu rõ về Next.js và muốn tập trung chủ yếu vào trò chơi.Vì trò chơi cần hỗ trợ nhiều người chơi theo thời gian thực, nên tôi đã sử dụng Express làm môi giới WebSockets. Đây chính là lúc mọi thứ trở nên khó khăn.
Vì các phép tính vật lý được thực hiện trên máy khách trong trò chơi Phaser, nên tôi đã chọn một logic đơn giản nhưng rõ ràng là có lỗi: Máy khách được kết nối đầu tiên có đặc quyền đáng ngờ là thực hiện các phép tính vật lý cho tất cả các đối tượng trong trò chơi, gửi kết quả đến máy chủ express, sau đó phát lại các vị trí, góc và lực đã cập nhật cho máy khách của người chơi khác. Sau đó, các máy khách khác sẽ áp dụng các thay đổi cho các đối tượng trong trò chơi.
Điều này dẫn đến tình huống mà người chơi đầu tiên được xem các hiện tượng vật lý diễn ra theo thời gian thực (suy cho cùng, chúng diễn ra cục bộ trên trình duyệt của họ), trong khi tất cả những người chơi khác đều chậm hơn ít nhất 30 mili giây (tốc độ phát sóng mà tôi đã chọn) hoặc — nếu kết nối mạng của người chơi đầu tiên chậm — tệ hơn đáng kể.
Nếu điều này nghe có vẻ như là một kiến trúc kém đối với bạn — thì bạn hoàn toàn đúng. Tuy nhiên, tôi chấp nhận sự thật này để nhanh chóng có được thứ gì đó có thể chơi được để tìm ra liệu trò chơi có thực sự vui khi chơi hay không.
Xác thực ý tưởng, loại bỏ nguyên mẫu
Mặc dù quá trình triển khai còn nhiều lỗi, nhưng nó đủ khả thi để mời bạn bè tham gia lái thử lần đầu. Phản hồi rất tích cực, với mối quan tâm chính — không có gì ngạc nhiên — là hiệu suất thời gian thực. Các vấn đề cố hữu khác bao gồm tình huống khi người chơi đầu tiên (hãy nhớ rằng, người phụ trách mọi thứ) rời khỏi trò chơi — ai sẽ tiếp quản? Vào thời điểm này chỉ có một phòng trò chơi, vì vậy bất kỳ ai cũng có thể tham gia cùng một trò chơi. Tôi cũng hơi lo ngại về kích thước gói mà thư viện Phaser.js giới thiệu.Đã đến lúc phải xóa nguyên mẫu và bắt đầu với thiết lập mới và mục tiêu rõ ràng.
Thiết lập dự án
Rõ ràng là cách tiếp cận "khách hàng đầu tiên quyết định tất cả" cần được thay thế bằng giải pháp trong đó trạng thái trò chơi nằm trên máy chủ. Trong quá trình nghiên cứu, tôi đã tìm thấy Colyseus, nghe có vẻ như là công cụ hoàn hảo cho công việc này.Đối với các khối xây dựng chính khác của trò chơi, tôi đã chọn:
- Matter.js là một công cụ vật lý thay vì Phaser.js vì nó chạy trong Node và Autowuzzler không yêu cầu một khuôn khổ trò chơi đầy đủ.
- SvelteKit là một khuôn khổ ứng dụng thay vì Next.js vì nó vừa chuyển sang giai đoạn beta công khai vào thời điểm đó. (Ngoài ra: Tôi thích làm việc với Svelte.)
- Supabase.io để lưu trữ mã PIN trò chơi do người dùng tạo.
Trạng thái trò chơi đồng bộ, tập trung với Colyseus
Colyseus là một nền tảng trò chơi nhiều người chơi dựa trên Node.js và Express. Về cơ bản, nó cung cấp:- Đồng bộ hóa trạng thái giữa các máy khách theo cách có thẩm quyền;
- Truyền thông hiệu quả theo thời gian thực bằng WebSockets bằng cách chỉ gửi dữ liệu đã thay đổi;
- Thiết lập nhiều phòng;
- Thư viện máy khách cho JavaScript, Unity, Defold Engine, Haxe, Cocos Creator, Construct3;
- Các móc vòng đời, ví dụ: phòng được tạo, người dùng tham gia, người dùng rời đi, v.v.;
- Gửi tin nhắn, dưới dạng tin nhắn phát sóng đến tất cả người dùng trong phòng hoặc đến một người dùng duy nhất;
- Bảng điều khiển giám sát tích hợp và công cụ kiểm tra tải.
npm init
và kho lưu trữ ví dụ.Tạo lược đồ
Thực thể chính của ứng dụng Colyseus là phòng trò chơi, nơi lưu giữ trạng thái của một phiên bản phòng duy nhất và tất cả các đối tượng trò chơi của phòng đó. Trong trường hợp của Autowuzzler, đó là một phiên chơi với:- hai đội,
- một số lượng người chơi hữu hạn,
- một quả bóng.
Mã:
class Ball extends Schema { constructor() { super(); this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; }}defineTypes(Ball, { x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number"});
x
, y
, angle
, velocityX,
velocityY
. Ngoài ra, chúng ta cần chỉ định các loại của từng thuộc tính. Ví dụ này sử dụng cú pháp JavaScript, nhưng bạn cũng có thể sử dụng cú pháp TypeScript gọn nhẹ hơn một chút.Các kiểu thuộc tính có thể là các kiểu nguyên thủy:
-
string
-
boolean
-
number
(cũng như các kiểu số nguyên và số thực hiệu quả hơn)
-
ArraySchema
(tương tự như Array trong JavaScript) -
MapSchema
(tương tự như Map trong JavaScript) -
SetSchema
(tương tự như Set trong JavaScript) -
CollectionSchema
(tương tự như ArraySchema, nhưng không kiểm soát được các chỉ mục)
Ball
ở trên có năm thuộc tính thuộc kiểu number
: tọa độ (x
, y
), góc
hiện tại của nó và vectơ vận tốc (velocityX
, velocityY
).Sơ đồ cho người chơi cũng tương tự, nhưng bao gồm thêm một vài thuộc tính để lưu trữ tên người chơi và số hiệu đội, cần được cung cấp khi tạo phiên bản Người chơi:
Mã:
class Player extends Schema { constructor(teamNumber) { super(); this.name = ""; this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; this.teamNumber = teamNumber; }}defineTypes(Player, { name: "string", x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number", angularVelocity: "number", teamNumber: "number",});
Room
kết nối các lớp đã xác định trước đó: Một thể hiện của phòng có nhiều đội (được lưu trữ trong ArraySchema). Nó cũng chứa một quả bóng duy nhất, do đó chúng tôi tạo một thể hiện Ball mới trong hàm tạo của RoomSchema. Người chơi được lưu trữ trong MapSchema để truy xuất nhanh bằng ID của họ.
Mã:
class RoomSchema extends Schema { constructor() { super(); this.teams = new ArraySchema(); this.ball = new Ball(); this.players = new MapSchema(); }}defineTypes(RoomSchema, { teams: [Team], // một Mảng Team ball: Ball, // một thể hiện Ball duy nhất players: { map: Player } // một Bản đồ Người chơi});
Team
đã bị bỏ qua.Thiết lập nhiều phòng ("Match-Making")
Bất kỳ ai cũng có thể tham gia trò chơi Autowuzzler nếu họ có mã PIN trò chơi hợp lệ. Máy chủ Colyseus của chúng tôi tạo một phiên bản Phòng mới cho mỗi phiên chơi ngay khi người chơi đầu tiên tham gia và hủy phòng khi người chơi cuối cùng rời khỏi.Quy trình chỉ định người chơi vào phòng chơi mong muốn được gọi là "ghép cặp". Colyseus giúp việc thiết lập trở nên rất dễ dàng bằng cách sử dụng phương thức
filterBy
khi xác định phòng mới:
Mã:
gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);
gamePIN
(chúng ta sẽ xem cách "tham gia" sau) sẽ kết thúc ở cùng một phòng chơi! Bất kỳ cập nhật trạng thái và các thông báo phát sóng khác đều bị giới hạn đối với những người chơi trong cùng một phòng.Vật lý trong ứng dụng Colyseus
Colyseus cung cấp nhiều tính năng sẵn dùng để khởi chạy nhanh chóng với máy chủ trò chơi có thẩm quyền, nhưng để nhà phát triển tự tạo cơ chế trò chơi thực tế — bao gồm cả vật lý. Phaser.js, mà tôi đã sử dụng trong nguyên mẫu, không thể chạy trong môi trường không phải trình duyệt, nhưng công cụ vật lý tích hợp của Phaser.js là Matter.js có thể chạy trên Node.js.Với Matter.js, bạn định nghĩa một thế giới vật lý với một số thuộc tính vật lý nhất định như kích thước và trọng lực. Nó cung cấp một số phương pháp để tạo các đối tượng vật lý nguyên thủy tương tác với nhau bằng cách tuân thủ các định luật vật lý (mô phỏng), bao gồm khối lượng, va chạm, chuyển động có ma sát, v.v. Bạn có thể di chuyển các vật thể xung quanh bằng cách tác dụng lực — giống như bạn làm trong thế giới thực.
"Thế giới" Matter.js nằm ở trung tâm của trò chơi Autowuzzler; nó xác định tốc độ di chuyển của ô tô, độ nảy của quả bóng, vị trí của các mục tiêu và điều gì sẽ xảy ra nếu ai đó ghi bàn.
Mã:
let ball = Bodies.circle( ballInitialXPosition, ballInitialYPosition, radius, { render: { sprite: { texture: '/assets/ball.png', } }, friction: 0.002, restitution: 0.8 });World.add(this.engine.world, [ball]);
Sau khi các quy tắc được xác định, Matter.js có thể chạy có hoặc không thực sự hiển thị thứ gì đó lên màn hình. Đối với Autowuzzler, tôi đang sử dụng tính năng này để tái sử dụng mã thế giới vật lý cho cả máy chủ và máy khách — với một số điểm khác biệt chính:
Thế giới vật lý trên máy chủ:
- nhận dữ liệu đầu vào của người dùng (sự kiện bàn phím để lái xe) thông qua Colyseus và tác dụng lực thích hợp lên đối tượng trò chơi (xe của người dùng);
- thực hiện tất cả các phép tính vật lý cho tất cả các đối tượng (người chơi và quả bóng), bao gồm cả phát hiện va chạm;
- truyền đạt trạng thái đã cập nhật cho từng đối tượng trò chơi trở lại Colyseus, sau đó Colyseus sẽ phát trạng thái đó đến máy khách;
- được cập nhật sau mỗi 16,6 mili giây (= 60 khung hình mỗi giây), được kích hoạt bởi máy chủ Colyseus của chúng tôi.
- không thao tác với các đối tượng trò chơi trực tiếp;
- nhận trạng thái cập nhật cho từng đối tượng trò chơi từ Colyseus;
- áp dụng các thay đổi về vị trí, vận tốc và góc sau khi nhận được trạng thái cập nhật;
- gửi dữ liệu đầu vào của người dùng (sự kiện bàn phím để lái xe) đến Colyseus;
- tải các sprite trò chơi và sử dụng trình kết xuất để vẽ thế giới vật lý lên một phần tử canvas;
- bỏ qua phát hiện va chạm (sử dụng tùy chọn
isSensor
cho các đối tượng); - cập nhật bằng requestAnimationFrame, lý tưởng nhất là ở tốc độ 60 fps.

Bây giờ, với tất cả những điều kỳ diệu diễn ra trên máy chủ, máy khách chỉ xử lý dữ liệu đầu vào và vẽ trạng thái mà nó nhận được từ máy chủ lên màn hình. Với một ngoại lệ:
Nội suy trên máy khách
Vì chúng ta đang sử dụng lại cùng một thế giới vật lý Matter.js trên máy khách, chúng ta có thể cải thiện hiệu suất trải nghiệm bằng một mẹo đơn giản. Thay vì chỉ cập nhật vị trí của đối tượng trò chơi, chúng ta cũng đồng bộ hóa vận tốc của đối tượng. Theo cách này, đối tượng tiếp tục di chuyển theo quỹ đạo của nó ngay cả khi bản cập nhật tiếp theo từ máy chủ mất nhiều thời gian hơn bình thường. Vì vậy, thay vì di chuyển các đối tượng theo các bước riêng biệt từ vị trí A đến vị trí B, chúng ta thay đổi vị trí của chúng và khiến chúng di chuyển theo một hướng nhất định.Vòng đời
Lớp AutowuzzlerRoom
là nơi xử lý logic liên quan đến các giai đoạn khác nhau của phòng Colyseus. Colyseus cung cấp một số phương thức vòng đời:-
onCreate
: khi một phòng mới được tạo (thường là khi máy khách đầu tiên kết nối); -
onAuth
: như một móc xác thực để cho phép hoặc từ chối vào phòng; -
onJoin
: khi một máy khách kết nối với phòng; -
onLeave
: khi một máy khách ngắt kết nối khỏi phòng; -
onDispose
: khi phòng bị hủy.
onCreate
) và thêm người chơi vào thế giới khi máy khách kết nối (onJoin
). Sau đó, nó cập nhật thế giới vật lý 60 lần một giây (mỗi 16,6 mili giây) bằng phương thức setSimulationInterval
(vòng lặp trò chơi chính của chúng tôi):
Mã:
// deltaTime xấp xỉ 16,6 mili giâythis.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));
Ngay khi đối tượng vật lý thay đổi, các thuộc tính đã cập nhật của nó cần được áp dụng trở lại đối tượng Colyseus. Chúng ta có thể thực hiện điều đó bằng cách lắng nghe sự kiện
afterUpdate
của Matter.js và thiết lập các giá trị từ đó:
Mã:
Events.on(this.engine, "afterUpdate", () => { // áp dụng vị trí x của đối tượng bóng vật lý trở lại đối tượng bóng colyseus this.state.ball.x = this.physicsWorld.ball.position.x; // ... tất cả các thuộc tính bóng khác // lặp qua tất cả các cầu thủ vật lý và áp dụng các thuộc tính của họ trở lại đối tượng cầu thủ colyseus})

Ứng dụng phía máy khách
Bây giờ chúng ta đã có ứng dụng trên máy chủ xử lý việc đồng bộ hóa trạng thái trò chơi cho nhiều phòng cũng như tính toán vật lý, hãy tập trung vào xây dựng trang web và giao diện trò chơi thực tế. Giao diện Autowuzzler có các trách nhiệm sau:- cho phép người dùng tạo và chia sẻ mã PIN trò chơi để truy cập vào từng phòng;
- gửi mã PIN trò chơi đã tạo đến cơ sở dữ liệu Supabase để lưu trữ;
- cung cấp trang "Tham gia trò chơi" tùy chọn để người chơi nhập mã PIN trò chơi;
- xác thực mã PIN trò chơi khi người chơi tham gia trò chơi;
- lưu trữ và hiển thị trò chơi thực tế trên URL có thể chia sẻ (tức là duy nhất);
- kết nối với máy chủ Colyseus và xử lý các bản cập nhật trạng thái;
- cung cấp trang đích ("tiếp thị").
Tại sao lại chọn SvelteKit?
Tôi đã muốn phát triển một ứng dụng khác bằng Svelte kể từ khi tôi xây dựng neolightsout. Khi SvelteKit (khung ứng dụng chính thức cho Svelte) chuyển sang bản beta công khai, tôi quyết định xây dựng Autowuzzler bằng nó và chấp nhận mọi rắc rối phát sinh khi sử dụng bản beta mới — niềm vui khi sử dụng Svelte rõ ràng đã bù đắp cho điều đó.Những tính năng chính này khiến tôi chọn SvelteKit thay vì Next.js để triển khai thực tế giao diện trò chơi:
- Svelte là một khung UI và là một trình biên dịch và do đó cung cấp mã tối thiểu mà không có thời gian chạy của máy khách;
- Svelte có ngôn ngữ mẫu biểu cảm và hệ thống thành phần (sở thích cá nhân);
- Svelte bao gồm các kho lưu trữ toàn cầu, chuyển tiếp và hoạt ảnh ngay khi cài đặt, nghĩa là: không mệt mỏi khi quyết định chọn bộ công cụ quản lý trạng thái toàn cầu và thư viện hoạt ảnh;
- Svelte hỗ trợ CSS có phạm vi trong các thành phần tệp đơn;
- SvelteKit hỗ trợ SSR, định tuyến dựa trên tệp đơn giản nhưng linh hoạt và các tuyến phía máy chủ để xây dựng API;
- SvelteKit cho phép mỗi trang chạy mã trên máy chủ, ví dụ: để lấy dữ liệu được sử dụng để hiển thị trang;
- Các bố cục được chia sẻ trên các tuyến đường;
- SvelteKit có thể chạy trong môi trường không có máy chủ.
Tạo và lưu trữ mã PIN trò chơi
Trước khi người dùng có thể bắt đầu chơi trò chơi, trước tiên họ cần tạo mã PIN trò chơi. Bằng cách chia sẻ mã PIN với những người khác, tất cả họ đều có thể truy cập vào cùng một phòng trò chơi.
Đây là trường hợp sử dụng tuyệt vời cho điểm cuối phía máy chủ SvelteKits kết hợp với hàm onMount của Sveltes: Điểm cuối
/api/createcode
tạo mã PIN trò chơi, lưu trữ trong cơ sở dữ liệu Supabase.io và đưa ra mã PIN trò chơi dưới dạng phản hồi. Phản hồi này được lấy ngay khi thành phần trang của trang “create” được gắn kết:
Lưu trữ mã PIN trò chơi bằng Supabase.io
Supabase.io là một giải pháp thay thế nguồn mở cho Firebase. Supabase giúp bạn dễ dàng tạo cơ sở dữ liệu PostgreSQL và truy cập cơ sở dữ liệu đó thông qua một trong các thư viện máy khách hoặc thông qua REST.Đối với máy khách JavaScript, chúng tôi nhập hàm
createClient
và thực thi hàm đó bằng các tham số supabase_url
và supabase_key
mà chúng tôi nhận được khi tạo cơ sở dữ liệu. Để lưu mã PIN trò chơi được tạo trên mỗi lệnh gọi đến điểm cuối createcode
, tất cả những gì chúng tôi cần làm là chạy truy vấn insert
đơn giản này:
Mã:
import { createClient } from '@supabase/supabase-js'const database = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY);const { data, error } = await database .from("games") .insert([{ code: 123456 }]);
supabase_url
và supabase_key
được lưu trữ trong tệp .env. Do Vite — công cụ xây dựng cốt lõi của SvelteKit — nên cần phải thêm tiền tố VITE_ vào các biến môi trường để có thể truy cập được trong SvelteKit.Truy cập trò chơi
Tôi muốn việc tham gia trò chơi Autowuzzler trở nên dễ dàng như việc nhấp vào liên kết. Do đó, mỗi phòng trò chơi cần có URL riêng dựa trên mã PIN trò chơi đã tạo trước đó, ví dụ: https://autowuzzler.com/play/12345.Trong SvelteKit, các trang có tham số tuyến động được tạo bằng cách đặt các phần động của tuyến trong dấu ngoặc vuông khi đặt tên tệp trang:
client/src/routes/play/[gamePIN].svelte
. Sau đó, giá trị của tham số gamePIN
sẽ khả dụng trong thành phần trang (xem tài liệu SvelteKit để biết chi tiết). Trong tuyến đường play
, chúng ta cần kết nối với máy chủ Colyseus, khởi tạo thế giới vật lý để hiển thị trên màn hình, xử lý các bản cập nhật cho các đối tượng trong trò chơi, lắng nghe dữ liệu nhập từ bàn phím và hiển thị các giao diện người dùng khác như điểm số, v.v.Kết nối với Colyseus và cập nhật trạng thái
Thư viện máy khách Colyseus cho phép chúng ta kết nối máy khách với máy chủ Colyseus. Trước tiên, hãy tạo mộtColyseus.Client
mới bằng cách trỏ nó đến máy chủ Colyseus (ws://localhost:2567
đang trong quá trình phát triển). Sau đó, hãy tham gia phòng bằng tên chúng ta đã chọn trước đó (autowuzzler
) và gamePIN
từ tham số tuyến đường. Tham số gamePIN
đảm bảo người dùng tham gia đúng phiên bản phòng (xem “match-making” ở trên).
Mã:
let client = new Colyseus.Client("ws://localhost:2567");this.room = await client.joinOrCreate("autowuzzler", { gamePIN });
onMount
cho trường hợp sử dụng đó. (Nếu bạn quen thuộc với React, onMount
tương tự như hook useEffect
với một mảng phụ thuộc rỗng.)
Mã:
onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN });})
Sau đây là một ví dụ về cách lắng nghe người chơi tham gia phòng (
onAdd
) và nhận các bản cập nhật trạng thái liên tiếp cho người chơi này:
Mã:
this.room.state.players.onAdd = (player, key) => { console.log(`Người chơi đã được thêm với sessionId: ${key}`); // thêm thực thể người chơi vào thế giới trò chơi this.world.createPlayer(key, player.teamNumber); // lắng nghe những thay đổi đối với người chơi này player.onChange = (changes) => { changes.forEach(({ field, value }) => { this.world.updatePlayer(key, field, value); // xem bên dưới }); };};
updatePlayer
của thế giới vật lý, chúng ta cập nhật từng thuộc tính một vì onChange
của Colyseus cung cấp một tập hợp tất cả các thuộc tính đã thay đổi.Lưu ý: Hàm này chỉ chạy trên phiên bản máy khách của thế giới vật lý, vì các đối tượng trò chơi chỉ được thao tác gián tiếp thông qua máy chủ Colyseus.
Mã:
updatePlayer(sessionId, field, value) { // lấy đối tượng vật lý của người chơi theo sessionId của nó let player = this.world.players.get(sessionId); // thoát nếu không tìm thấy if (!player) return; // áp dụng các thay đổi cho các thuộc tính switch (field) { case "angle": Body.setAngle(player, value); break; case "x": Body.setPosition(player, { x: value, y: player.position.y }); break; case "y": Body.setPosition(player, { x: player.position.x, y: value }); break; // set velocityX, velocityY, angularVelocity ... }}
Cho đến nay, không có đối tượng nào di chuyển vì chúng ta vẫn cần lắng nghe đầu vào bàn phím và gửi đến máy chủ. Thay vì gửi trực tiếp các sự kiện trên mọi sự kiện
keydown
, chúng ta duy trì một bản đồ các phím hiện đang được nhấn và gửi các sự kiện đến máy chủ Colyseus trong một vòng lặp 50ms. Theo cách này, chúng ta có thể hỗ trợ nhấn nhiều phím cùng lúc và giảm thiểu tình trạng tạm dừng xảy ra sau sự kiện keydown
đầu tiên và liên tiếp khi phím vẫn được nhấn:
Mã:
let keys = {};const keyDown = e => { keys[e.key] = true;};const keyUp = e => { keys[e.key] = false;};document.addEventListener('keydown', keyDown);document.addEventListener('keyup', keyUp);let loop = () => { if (keys["ArrowLeft"]) { this.room.send("di chuyển", { hướng: "trái" }); } else if (keys["ArrowRight"]) { this.room.send("di chuyển", { hướng: "phải" }); } if (keys["ArrowUp"]) { this.room.send("di chuyển", { hướng: "lên" }); } else if (keys["ArrowDown"]) { this.room.send("di chuyển", { hướng: "xuống" }); } // lần lặp tiếp theo requestAnimationFrame(() => { setTimeout(loop, 50); });}// bắt đầu vòng lặpsetTimeout(loop, 50);
Những phiền toái nhỏ
Nhìn lại, hai điều thuộc danh mục nobody-told-me-but-someone-should-have hiện ra trong đầu tôi:- Hiểu rõ về cách thức hoạt động của các engine vật lý là có lợi. Tôi đã dành khá nhiều thời gian để tinh chỉnh các thuộc tính và ràng buộc vật lý. Mặc dù trước đây tôi đã xây dựng một trò chơi nhỏ bằng Phaser.js và Matter.js, nhưng vẫn có rất nhiều lần thử và sai để khiến các vật thể di chuyển theo cách tôi hình dung.
- Thời gian thực rất khó — đặc biệt là trong các trò chơi dựa trên vật lý. Những sự chậm trễ nhỏ làm giảm đáng kể trải nghiệm và mặc dù đồng bộ hóa trạng thái giữa các máy khách với Colyseus hoạt động tốt, nhưng nó không thể loại bỏ sự chậm trễ trong tính toán và truyền tải.
Những điều cần lưu ý và cảnh báo với SvelteKit
Vì tôi đã sử dụng SvelteKit khi nó mới ra khỏi lò beta, nên có một số điều cần lưu ý và cảnh báo mà tôi muốn chỉ ra:- Phải mất một thời gian để tìm ra rằng các biến môi trường cần được thêm tiền tố VITE_ để sử dụng chúng trong SvelteKit. Điều này hiện đã được ghi lại đầy đủ trong Câu hỏi thường gặp.
- Để sử dụng Supabase, tôi đã phải thêm Supabase vào cả danh sách
dependencies
vàdevDependencies
của package.json. Tôi tin rằng điều này không còn đúng nữa. - Hàm
load
của SvelteKit chạy trên cả máy chủ và máy khách! - Để bật chức năng thay thế mô-đun nóng hoàn toàn (bao gồm cả việc giữ nguyên trạng thái), bạn phải tự tay thêm một dòng chú thích
vào các thành phần trang của mình. Xem Câu hỏi thường gặp để biết thêm chi tiết.
Triển khai và lưu trữ
Ban đầu, tôi lưu trữ máy chủ Colyseus (Node) trên một phiên bản Heroku và lãng phí rất nhiều thời gian để làm cho WebSockets và CORS hoạt động. Hóa ra, hiệu suất của một máy thử nghiệm Heroku nhỏ (miễn phí) không đủ cho trường hợp sử dụng thời gian thực. Sau đó, tôi đã di chuyển ứng dụng Colyseus sang một máy chủ nhỏ tại Linode. Ứng dụng phía máy khách được triển khai và lưu trữ trên Netlify thông qua SvelteKits adapter-netlify. Không có gì ngạc nhiên ở đây: Netlify hoạt động rất tốt!Kết luận
Bắt đầu với một nguyên mẫu thực sự đơn giản để xác thực ý tưởng đã giúp tôi rất nhiều trong việc tìm ra liệu dự án có đáng để theo dõi hay không và những thách thức kỹ thuật của trò chơi nằm ở đâu. Trong lần triển khai cuối cùng, Colyseus đã xử lý tất cả các công việc nặng nhọc của việc đồng bộ hóa trạng thái theo thời gian thực trên nhiều máy khách, phân phối trong nhiều phòng. Thật ấn tượng khi thấy một ứng dụng đa người dùng theo thời gian thực có thể được xây dựng nhanh như thế nào với Colyseus — một khi bạn tìm ra cách mô tả lược đồ chính xác. Bảng điều khiển giám sát tích hợp của Colyseus giúp khắc phục mọi sự cố đồng bộ hóa.Điều phức tạp trong thiết lập này là lớp vật lý của trò chơi vì nó đưa ra một bản sao bổ sung của mỗi đối tượng trò chơi liên quan đến vật lý cần được duy trì. Việc lưu trữ mã PIN trò chơi trong Supabase.io từ ứng dụng SvelteKit rất đơn giản. Nhìn lại, tôi có thể chỉ sử dụng cơ sở dữ liệu SQLite để lưu trữ mã PIN trò chơi, nhưng thử những điều mới là một nửa niềm vui khi xây dựng các dự án phụ.
Cuối cùng, việc sử dụng SvelteKit để xây dựng giao diện người dùng của trò chơi cho phép tôi di chuyển nhanh chóng — và thỉnh thoảng nở nụ cười vui vẻ trên khuôn mặt.
Bây giờ, hãy tiếp tục và mời bạn bè của bạn tham gia một vòng Autowuzzler!
Đọc thêm
- “Bắt đầu với React bằng cách xây dựng trò chơi Whac-A-Mole,” Jhey Tompkins
- “Cách xây dựng trò chơi thực tế ảo nhiều người chơi theo thời gian thực,” Alvin Wan
- “Viết công cụ phiêu lưu văn bản nhiều người chơi bằng Node.js,” Fernando Doglio
- “Tương lai của thiết kế web di động: Thiết kế trò chơi điện tử và kể chuyện,” Suzanne Scacca