Thêm lối chơi WebRTC vào trải nghiệm của người Hobbit
Để kịp ra mắt bộ phim Hobbit mới "The Hobbit: The Battle of the Five Armies" (Người Hobbit: Cuộc chiến của 5 đạo quân), chúng tôi đã nỗ lực mở rộng Thử nghiệm Chrome năm ngoái, A Journey through Middle-earth (Chuyến phiêu lưu qua Trung địa) bằng một số nội dung mới. Lần này, trọng tâm chính là mở rộng việc sử dụng WebGL vì nhiều trình duyệt và thiết bị hơn có thể xem nội dung và hoạt động với các chức năng WebRTC trong Chrome và Firefox. Chúng tôi đã có ba mục tiêu với thử nghiệm năm này:
- Chơi trò chơi P2P bằng WebRTC và WebGL trên Chrome dành cho Android
- Tạo một trò chơi nhiều người chơi dễ chơi và dựa trên phương thức nhập bằng thao tác chạm
- Lưu trữ trên Google Cloud Platform
Xác định trò chơi
Logic trò chơi được xây dựng dựa trên chế độ thiết lập theo lưới, trong đó quân đội di chuyển trên một bảng trò chơi. Điều này giúp chúng tôi dễ dàng thử nghiệm lối chơi trên giấy khi xác định các quy tắc. Việc sử dụng chế độ thiết lập dựa trên lưới cũng giúp phát hiện va chạm trong trò chơi để duy trì hiệu suất tốt vì bạn chỉ phải kiểm tra xem có va chạm với các đối tượng trong cùng hoặc các thẻ thông tin lân cận hay không. Ngay từ đầu, chúng tôi đã biết rằng mình muốn tập trung vào cuộc chiến giữa 4 lực lượng chính của Trung địa: Con người, Người lùn, Tiên và Orc. Trò chơi cũng phải đủ đơn giản để có thể chơi trong một Thử nghiệm trên Chrome và không có quá nhiều hoạt động tương tác để tìm hiểu. Chúng tôi bắt đầu bằng cách xác định 5 Battlegrounds (Đấu trường) trên bản đồ Trung địa. Đây là các phòng chơi nơi nhiều người chơi có thể cạnh tranh trong một trận chiến ngang hàng. Hiển thị nhiều người chơi trong phòng trên màn hình thiết bị di động và cho phép người dùng chọn người để thách đấu là một thách thức. Để tương tác và tạo cảnh dễ dàng hơn, chúng tôi quyết định chỉ có một nút để thách đấu và chấp nhận, đồng thời chỉ sử dụng phòng để hiển thị các sự kiện và người đang là vua của đồi. Hướng dẫn này cũng giải quyết một số vấn đề về mặt ghép đôi và cho phép chúng tôi kết nối những ứng cử viên phù hợp nhất để tham chiến. Trong thử nghiệm trước đây trên Chrome với trò chơi Cube Slam, chúng tôi nhận thấy rằng cần phải làm rất nhiều việc để xử lý độ trễ trong trò chơi nhiều người chơi nếu kết quả của trò chơi phụ thuộc vào độ trễ đó. Bạn liên tục phải đưa ra giả định về trạng thái của đối thủ, vị trí mà đối thủ cho rằng bạn đang ở và đồng bộ hoá thông tin đó với ảnh động trên các thiết bị. Bài viết này giải thích chi tiết hơn về những thách thức này. Để dễ chơi hơn, chúng tôi đã thiết kế trò chơi này theo lượt.
Logic của trò chơi được xây dựng dựa trên sơ đồ lưới với các đội quân di chuyển trên bảng trò chơi. Điều này giúp chúng tôi dễ dàng thử chơi trò chơi trên giấy khi chúng tôi đặt ra các quy tắc. Việc sử dụng chế độ thiết lập dựa trên lưới cũng giúp phát hiện va chạm trong trò chơi để duy trì hiệu suất tốt vì bạn chỉ phải kiểm tra xung đột với các đối tượng trong cùng hoặc các thẻ thông tin lân cận.
Các phần của trò chơi
Để tạo trò chơi nhiều người chơi này, chúng ta phải xây dựng một số phần chính:
- API quản lý người chơi phía máy chủ xử lý người dùng, tính năng so khớp, phiên và số liệu thống kê trò chơi.
- Máy chủ giúp thiết lập kết nối giữa người chơi.
- Một API để xử lý tín hiệu API Kênh AppEngine dùng để kết nối và giao tiếp với tất cả người chơi trong phòng trò chơi.
- Công cụ phát triển trò chơi JavaScript xử lý việc đồng bộ hoá trạng thái và nhắn tin RTC giữa hai người chơi/đồng cấp.
- Chế độ xem trò chơi WebGL.
Quản lý người chơi
Để hỗ trợ một số lượng lớn người chơi, chúng tôi sử dụng nhiều phòng trò chơi song song cho mỗi Chiến trường. Lý do chính để giới hạn số lượng người chơi trong mỗi phòng chơi là để cho phép người chơi mới có thể vươn lên dẫn đầu bảng xếp hạng trong một khoảng thời gian hợp lý. Giới hạn này cũng liên quan đến kích thước của đối tượng json mô tả phòng trò chơi được gửi thông qua Channel API (API Kênh) có giới hạn là 32 kb. Chúng ta phải lưu trữ người chơi, phòng, điểm số, phiên và mối quan hệ của họ trong trò chơi. Để thực hiện việc này, trước tiên, chúng ta đã sử dụng NDB cho các thực thể và sử dụng giao diện truy vấn để xử lý các mối quan hệ. NDB là một giao diện cho Google Cloud Datastore. Việc sử dụng NDB lúc đầu hoạt động hiệu quả nhưng chúng tôi sớm gặp vấn đề về cách sử dụng NDB. Truy vấn được chạy trên phiên bản "đã cam kết" của cơ sở dữ liệu (NDB Writes được giải thích chi tiết trong bài viết chuyên sâu này) có thể bị trễ vài giây. Tuy nhiên, bản thân các thực thể không bị độ trễ đó vì chúng phản hồi trực tiếp từ bộ nhớ đệm. Có thể sẽ dễ giải thích hơn một chút thông qua một số mã ví dụ:
// example code to explain our issue with eventual consistency
def join_room(player_id, room_id):
room = Room.get_by_id(room_id)
player = Player.get_by_id(player_id)
player.room = room.key
player.put()
// the player Entity is updated directly in the cache
// so calling this will return the room key as expected
player.room // = Key(Room, room_id)
// Fetch all the players with room set to 'room.key'
players_in_room = Player.query(Player.room == room.key).fetch()
// = [] (an empty list of players)
// even though the saved player above may be expected to be in the
// list it may not be there because the query api is being run against the
// "committed" version and may still be empty for a few seconds
return {
room: room,
players: players_in_room,
}
Sau khi thêm các bài kiểm thử đơn vị, chúng ta có thể thấy rõ vấn đề và đã chuyển từ các truy vấn sang giữ các mối quan hệ trong danh sách được phân tách bằng dấu phẩy trong memcache. Cách này có vẻ hơi hack nhưng đã hiệu quả và memcache AppEngine có một hệ thống giống như giao dịch cho các khoá bằng cách sử dụng tính năng "so sánh và đặt" tuyệt vời, vì vậy, các bài kiểm thử đã vượt qua lại.
Rất tiếc, memcache không phải là một công cụ hoàn hảo mà có một vài giới hạn, trong đó đáng chú ý nhất là kích thước giá trị 1 MB (không thể có quá nhiều phòng liên quan đến một chiến trường) và thời gian hết hạn của khoá, hoặc như tài liệu giải thích:
Chúng tôi đã cân nhắc sử dụng một kho khoá-giá trị tuyệt vời khác, Redis. Tuy nhiên, tại thời điểm đó, việc thiết lập một cụm có thể mở rộng khá khó khăn và vì chúng tôi muốn tập trung vào việc xây dựng trải nghiệm hơn là duy trì máy chủ, nên chúng tôi đã không đi theo hướng đó. Mặt khác, Google Cloud Platform gần đây đã phát hành một tính năng Nhấp để triển khai đơn giản, trong đó một trong các tuỳ chọn là Cụm Redis. Đây là một lựa chọn rất thú vị.
Cuối cùng, chúng tôi đã tìm thấy Google Cloud SQL và di chuyển các mối quan hệ vào MySQL. Phải làm rất nhiều việc nhưng cuối cùng thì mọi thứ đã hoạt động tốt, các bản cập nhật hiện đã hoàn toàn nguyên tử và các bài kiểm thử vẫn đạt. Điều này cũng giúp việc triển khai tính năng so khớp và tính điểm trở nên đáng tin cậy hơn rất nhiều.
Theo thời gian, nhiều dữ liệu dần được chuyển từ NDB và memcache sang SQL. Tuy nhiên, nhìn chung, người chơi, các thực thể chiến trường và phòng vẫn được lưu trữ trong NDB, trong khi các phiên hoạt động và mối quan hệ giữa chúng được lưu trữ trong SQL.
Chúng tôi cũng phải theo dõi xem ai đang chơi với ai và ghép nối người chơi với nhau bằng một cơ chế so khớp có tính đến cấp độ kỹ năng và kinh nghiệm của người chơi. Chúng tôi dựa vào thư viện nguồn mở Glicko2 để so khớp.
Vì đây là trò chơi nhiều người chơi, nên chúng ta muốn thông báo cho những người chơi khác trong phòng về các sự kiện như "ai đã tham gia hoặc rời khỏi phòng", "ai đã thắng hoặc thua" và liệu có thử thách nào cần chấp nhận hay không. Để xử lý vấn đề này, chúng tôi đã tích hợp tính năng nhận thông báo vào API Quản lý người chơi.
Thiết lập WebRTC
Khi hai người chơi được so khớp để chiến đấu, một dịch vụ báo hiệu sẽ được dùng để giúp hai người chơi được so khớp trò chuyện với nhau và bắt đầu kết nối ngang hàng.
Bạn có thể sử dụng một số thư viện bên thứ ba cho dịch vụ báo hiệu và điều này cũng giúp đơn giản hoá việc thiết lập WebRTC. Một số tuỳ chọn là PeerJS, SimpleWebRTC và SDK WebRTC của PubNub. PubNub sử dụng giải pháp máy chủ được lưu trữ và đối với dự án này, chúng tôi muốn lưu trữ trên Google Cloud Platform. Hai thư viện còn lại sử dụng máy chủ node.js mà chúng ta có thể đã cài đặt trên Google Compute Engine, nhưng chúng ta cũng phải đảm bảo rằng máy chủ này có thể xử lý hàng nghìn người dùng đồng thời, một điều mà chúng ta đã biết Channel API có thể làm được.
Một trong những lợi thế chính của việc sử dụng Google Cloud Platform trong trường hợp này là khả năng mở rộng quy mô. Bạn có thể dễ dàng mở rộng quy mô tài nguyên cần thiết cho một dự án AppEngine thông qua Google Developers Console và không cần làm gì thêm để mở rộng quy mô dịch vụ báo hiệu khi sử dụng Channels API.
Có một số lo ngại về độ trễ và độ mạnh của API Kênh, nhưng trước đây chúng tôi đã sử dụng API này cho dự án CubeSlam và đã chứng minh được rằng API này hoạt động hiệu quả cho hàng triệu người dùng trong dự án đó, vì vậy, chúng tôi quyết định sử dụng lại API này.
Vì không chọn sử dụng thư viện bên thứ ba để hỗ trợ WebRTC nên chúng tôi phải tự xây dựng thư viện của riêng mình. May mắn là chúng ta có thể sử dụng lại nhiều công việc đã làm cho dự án CubeSlam. Khi cả hai người chơi đã tham gia một phiên, phiên được đặt thành “đang hoạt động” và sau đó cả hai người chơi sẽ sử dụng mã phiên hoạt động đó để bắt đầu kết nối ngang hàng thông qua API kênh. Sau đó, tất cả hoạt động giao tiếp giữa hai người chơi sẽ được xử lý qua RTCDataChannel.
Chúng ta cũng cần máy chủ STUN và TURN để thiết lập kết nối và xử lý NAT và tường lửa. Đọc thêm về cách thiết lập WebRTC trong bài viết WebRTC trong thực tế: STUN, TURN và tín hiệu trên HTML5 Rocks.
Số lượng máy chủ TURN được sử dụng cũng cần có thể mở rộng tuỳ thuộc vào lưu lượng truy cập. Để xử lý vấn đề này, chúng tôi đã kiểm thử Trình quản lý triển khai của Google. Điều này cho phép chúng ta triển khai linh động các tài nguyên trên Google Compute Engine và cài đặt máy chủ TURN bằng mẫu. Tính năng này vẫn đang ở giai đoạn alpha nhưng đã hoạt động hoàn hảo cho mục đích của chúng tôi. Đối với máy chủ TURN, chúng tôi sử dụng coturn. Đây là một phương thức triển khai STUN/TURN rất nhanh, hiệu quả và có vẻ đáng tin cậy.
Channel API
Channel API được dùng để gửi tất cả thông tin liên lạc đến và đi từ phòng trò chơi ở phía máy khách. API Quản lý người chơi của chúng tôi đang sử dụng Channel API cho thông báo về các sự kiện trò chơi.
Quá trình làm việc với API Kênh đã gặp phải một số sự cố nhanh chóng. Ví dụ: vì các thông báo có thể không theo thứ tự, nên chúng ta phải gói tất cả các thông báo vào một đối tượng và sắp xếp các thông báo đó. Dưới đây là một số mã ví dụ về cách hoạt động:
var que = []; // [seq, packet...]
var seq = 0;
var rcv = -1;
function send(message) {
var packet = JSON.stringify({
seq: seq++,
msg: message
});
channel.send(packet);
}
function recv(packet) {
var data = JSON.parse(packet);
if (data.seq <= rcv) {
// ignoring message, older or already received
} else if (data.seq > rcv + 1) {
// message from the future. queue it up.
que.push(data.seq, packet);
} else {
// message in order! update the rcv index and emit the message
rcv = data.seq;
emit('message', data.message);
// and now that we have updated the `rcv` index we
// will check the que for any other we can send
setTimeout(flush, 10);
}
}
function flush() {
for (var i=0; i<que.length; i++) {
var seq = que[i];
var packet = que[i+1];
if (data.seq == rcv + 1) {
recv(packet);
return; // wait for next flush
}
}
}
Chúng tôi cũng muốn giữ cho các API khác nhau của trang web ở dạng mô-đun và tách biệt với việc lưu trữ trang web, đồng thời bắt đầu bằng cách sử dụng các mô-đun tích hợp sẵn trong GAE. Rất tiếc, sau khi làm cho mọi thứ hoạt động trong quá trình phát triển, chúng tôi nhận thấy Channel API không hoạt động với các mô-đun trong bản phát hành chính thức. Thay vào đó, chúng tôi chuyển sang sử dụng các phiên bản GAE riêng biệt và gặp phải các vấn đề về CORS, buộc chúng tôi phải sử dụng cầu postMessage iframe.
Công cụ phát triển trò chơi
Để công cụ phát triển trò chơi linh động nhất có thể, chúng tôi đã xây dựng ứng dụng giao diện người dùng bằng phương pháp hệ thống thành phần thực thể (ECS). Khi bắt đầu phát triển, chúng tôi chưa thiết lập sơ đồ khung và thông số kỹ thuật chức năng. Vì vậy, việc có thể thêm các tính năng và logic trong quá trình phát triển là rất hữu ích. Ví dụ: mẫu đầu tiên sử dụng một hệ thống kết xuất canvas đơn giản để hiển thị các thực thể trong một lưới. Sau đó vài vòng lặp, chúng tôi đã bổ sung hệ thống xung đột và một hệ thống dành cho người chơi do AI điều khiển. Ở giữa dự án, chúng ta có thể chuyển sang hệ thống kết xuất 3D mà không cần thay đổi phần còn lại của mã. Khi các bộ phận kết nối mạng được thiết lập và đang chạy, hệ thống AI có thể được sửa đổi để dùng lệnh từ xa.
Vì vậy, logic cơ bản của chế độ nhiều người chơi là gửi cấu hình của lệnh hành động đến các ứng dụng ngang hàng khác thông qua DataChannels và cho phép mô phỏng hoạt động như thể đó là một người chơi AI. Ngoài ra, có logic để quyết định lượt chơi, nếu người chơi nhấn nút chuyền/tấn công, xếp hàng các lệnh nếu chúng xuất hiện trong khi người chơi vẫn xem ảnh động trước đó, v.v.
Nếu chỉ có hai người dùng chuyển lượt chơi, thì cả hai người chơi có thể chia sẻ trách nhiệm chuyển lượt chơi cho đối thủ khi họ đã hoàn tất, nhưng có một người chơi thứ ba tham gia. Hệ thống AI lại trở nên hữu ích (không chỉ để thử nghiệm) khi chúng tôi cần thêm kẻ thù như nhện và người khổng lồ. Để các hành động này phù hợp với luồng theo lượt, bạn phải tạo và thực thi các hành động này giống hệt nhau ở cả hai bên. Vấn đề này được giải quyết bằng cách cho phép một máy ngang hàng điều khiển hệ thống rẽ và gửi trạng thái hiện tại đến máy ngang hàng từ xa. Sau đó, khi đến lượt của các spider, trình quản lý lượt sẽ cho phép hệ thống AI tạo một lệnh được gửi đến người dùng từ xa. Vì công cụ phát triển trò chơi chỉ hoạt động trên các lệnh và mã nhận dạng thực thể, nên trò chơi sẽ được mô phỏng giống nhau ở cả hai bên. Tất cả các đơn vị cũng có thể có thành phần AI để dễ dàng kiểm thử tự động.
Tốt nhất là nên có một trình kết xuất canvas đơn giản hơn khi bắt đầu phát triển, đồng thời tập trung vào logic trò chơi. Nhưng điều thú vị thực sự bắt đầu khi phiên bản 3D được triển khai và các cảnh trở nên sống động với môi trường và ảnh động. Chúng tôi sử dụng three.js làm công cụ 3D và dễ dàng chuyển sang trạng thái có thể chơi được nhờ kiến trúc.
Vị trí con chuột được gửi thường xuyên hơn cho người dùng từ xa và một gợi ý tinh tế về ánh sáng 3D về vị trí của con trỏ tại thời điểm này.