Bỏ qua tới nội dung
~nguyen-huu-trung
← Quay lại dự án

Dự án cá nhân · Chơi được ngay trên web

Lặn Biển Săn Cá — game 3D nhiều người chơi trong trình duyệt

Server Go giữ toàn bộ luật chơi ở nhịp 20Hz, client Babylon.js, một schema protobuf sinh code cho cả hai phía — và voice chat đi qua chính kết nối WebSocket đó thay vì WebRTC.

Vai trò
Tác giả duy nhất — server, client, giao thức, hạ tầng
Thời gian
2026
Công nghệ
GoWebSocketTypeScriptBabylon.jsVitePostgreSQLPWADocker
  • 20Hz

    nhịp mô phỏng do server quyết định

  • 100

    người chơi mỗi phòng, tick dưới 50ms

  • ~270KB

    gzip cho phần 3D, sảnh chỉ 9KB

  • 64kbps

    voice qua WebSocket, không WebRTC

  • 1

    schema protobuf sinh cả Go lẫn TS

  • 90m

    bán kính chỉ gửi thứ nhìn thấy

  • 133

    loài sinh vật chia 5 bậc hiếm

  • 1

    VPS chạy toàn bộ, HTTPS tự động

Bài toán

Tôi muốn tự kiểm chứng một câu hỏi: một game nhiều người chơi thời gian thực, 3D, có thể chạy thẳng trong trình duyệt điện thoại — không cài đặt, không store, không WebRTC hay TURN — và ở được trên một VPS nhỏ hay không.

Hai ràng buộc kéo ngược nhau. Server phải giữ quyền quyết định để mọi người trong phòng thấy cùng một thế giới và không ai sửa được kết quả từ phía mình; nhưng cùng lúc đó, mọi thứ phải mượt trên 4G và trên điện thoại tầm trung, nơi băng thông lẫn GPU đều không dư dả.

Và một bài toán không thuộc về kỹ thuật: game nhiều người vào lúc chưa có ai là một mặt biển trống. Người mới mở lên, không thấy ai, rồi đóng tab — trước khi kịp biết game có gì.

Cách giải

  1. 01

    Server quyết định, client chỉ gửi ý định

    Một phòng là một vòng lặp Go chạy 20 lần mỗi giây: cá, oxy, va chạm, phát lao trúng hay trượt đều tính ở server. Client gửi ý định — bơi hướng này, bắn lúc này — chứ không gửi kết quả. Điều đó biến hiệu năng thành một con số phải giữ: benchmark một phòng đầy 100 người rải khắp bản đồ phải chạy dưới 50ms mỗi tick, kể cả phần mã hoá và gửi cho từng người, nếu không thì 20Hz không giữ được.

  2. 02

    Chỉ gửi những gì người chơi thật sự nhìn thấy

    Bản đồ 1km² chia thành các chunk 128m; mỗi lượt gửi, server dựng một chỉ mục theo chunk rồi chỉ gói phần nằm trong bán kính 90m, vẽ tối đa 16 người gần nhất. Client cũng chỉ tải chunk quanh camera, còn server chỉ mô phỏng cá ở chunk có người. Phòng không có ai đang xem thì nhịp mô phỏng thưa lại — nhờ vậy các phòng luôn mở sẵn gần như không tốn CPU.

  3. 03

    Một schema cho hai ngôn ngữ

    Giao thức là Protocol Buffers nhị phân đi trên WebSocket, sinh code cho Go và TypeScript từ cùng một file .proto. Không có bước tuần tự hoá nào viết tay hai lần, nên wire format không thể lệch giữa server và client — thêm một trường là thêm ở một chỗ, và cả hai phía cùng biết về nó khi build.

  4. 04

    Thế giới sinh từ seed, cài hai lần, khoá bằng golden test

    Địa hình, đá và xác tàu sinh ra từ một worldSeed, và thuật toán tồn tại hai bản: Go để mô phỏng, TypeScript để dựng hình. Hai bản lệch nhau một chút thôi là người chơi bơi xuyên đá trên máy này nhưng bị chặn ở máy kia. Nên đầu ra được đóng băng thành một file golden dùng chung: sửa thuật toán thì test Go phải khớp lại với file đó, và file đó chính là hợp đồng giữa hai ngôn ngữ.

  5. 05

    Voice chat không cần WebRTC

    Micro đi qua AudioWorklet, hạ về 16kHz, nén IMA ADPCM 4-bit còn khoảng 64kbps rồi gửi trên chính kết nối WebSocket đang có — không TURN, không cổng mới, không phụ thuộc dịch vụ ngoài. Server chỉ giới hạn kích thước, giới hạn tốc độ và chuyển tiếp cho cả phòng. Khi hàng đợi của một người nghe bị nghẽn, khung voice bị bỏ chứ snapshot thì không, và kết nối cũng không bị ngắt: nghe cà giật vẫn tốt hơn là văng khỏi phòng.

  6. 06

    Điện thoại là thiết bị chính, không phải trường hợp phụ

    Sảnh nặng khoảng 9KB và hiện gần như tức thì, trong khi phần 3D (Babylon.js, ~270KB gzip) tải song song lúc người chơi còn đang chọn phòng. Mức đồ hoạ tự chọn theo thiết bị và tự hạ khi FPS tụt dưới 26, mức đã hạ được nhớ lại cho lần sau. Đồ trang trí mỗi chunk gộp lại còn một hai mesh màu theo đỉnh, joystick và các nút cảm ứng là giao diện mặc định khi xoay ngang, và game cài được ra màn hình chính như một PWA.

  7. 07

    Bot để mặt biển không bao giờ trống

    Server luôn giữ sẵn ba phòng công khai và lấp bot cho đủ số người mà phòng đó tự chọn trong khoảng 6–20, đổi lại sau mỗi 12 phút để danh sách phòng có chỗ đông chỗ vắng như thật. Bot săn cá, nổi lên thở, về tàu khi túi đầy và cùng đánh boss; người thật vào thì bot lặng lẽ rút. Bot không có tài khoản và không ghi gì vào database — chúng giải bài toán khởi động nguội mà không làm bẩn dữ liệu hay bảng xếp hạng.

  8. 08

    Vận hành nó như một sản phẩm, không phải một bản demo

    Trang quản trị có số liệu người chơi theo ngày, tỉ lệ quay lại D1/D7, phễu hoàn thành hướng dẫn, quản lý tài khoản và phòng, chỉnh giá cửa hàng hay bật sự kiện — lưu vào database và áp dụng ngay cho mọi phòng. Lỗi JavaScript phía người chơi được gửi về server và ghi log cùng cảnh báo khi một phòng tick chậm. Triển khai là một lệnh: Docker Compose dựng Caddy với HTTPS tự động, server Go và PostgreSQL trên một VPS, kèm sao lưu database hằng ngày.

Kiến trúc

Trình duyệt

Babylon.js client

PWA, cảm ứng, tự hạ đồ hoạ

AudioWorklet

16kHz → ADPCM 4-bit

Giao thức

Protobuf / WebSocket

một schema, sinh Go + TS

Server Go

room loop 20Hz

toàn quyền quyết định

interest index

chunk 128m, bán kính 90m

bot runner

lấp phòng, không lưu dữ liệu

Dữ liệu

PostgreSQL

sqlc, migration tự chạy

shared/data

JSON cân bằng, dùng chung

Vận hành

Caddy

HTTPS tự động

Docker Compose

một VPS, deploy một lệnh

admin + analytics

D1/D7, phễu, cấu hình

Trình duyệt chỉ gửi ý định và vẽ lại thế giới; mọi luật chơi nằm trong vòng lặp phòng của server Go.

Kết quả

  • Game chạy thật và chơi được ngay trên điện thoại qua một đường link — không cài đặt, không store, không plugin.
  • Dựng theo cách tôi vẫn làm: viết spec trước, để AI hỗ trợ hiện thực, rồi siết bằng test, golden file và benchmark.
  • Một phòng chứa tới 100 người trên một VPS nhỏ, nhờ chỉ gửi thứ nhìn thấy được và hạ nhịp mô phỏng ở phòng không ai xem.
  • Voice chat toàn phòng chạy trên đúng kết nối WebSocket của game — không thêm hạ tầng, không thêm chi phí, và bỏ khung tiếng thay vì bỏ người chơi khi mạng nghẽn.
  • Đủ nội dung để chơi chứ không dừng ở demo kỹ thuật: 133 loài chia năm bậc hiếm, chu kỳ ngày/đêm, boss ban đêm, đơn hàng, nhiệm vụ, bảng xếp hạng tuần và nhóm thuyền.