Giới thiệu
Racer là một Thử nghiệm Chrome dành cho thiết bị di động dựa trên web do Active Theory phát triển. Tối đa 5 người bạn có thể kết nối điện thoại hoặc máy tính bảng để đua trên mọi màn hình. Với ý tưởng, thiết kế và nguyên mẫu của Google Creative Lab cùng âm thanh của Plan8, chúng tôi đã lặp lại các bản dựng trong 8 tuần trước khi ra mắt tại I/O 2013. Sau khi trò chơi ra mắt được vài tuần, chúng tôi đã có cơ hội giải đáp một số câu hỏi của cộng đồng nhà phát triển về cách hoạt động của trò chơi. Dưới đây là thông tin chi tiết về các tính năng chính và câu trả lời cho những câu hỏi thường gặp nhất.
Bản nhạc
Một thách thức khá rõ ràng mà chúng tôi gặp phải là làm cách nào để tạo một trò chơi dành cho thiết bị di động dựa trên web hoạt động tốt trên nhiều thiết bị. Người chơi cần có thể tạo một cuộc đua bằng nhiều loại điện thoại và máy tính bảng. Một người chơi có thể dùng Nexus 4 và muốn đua với bạn bè dùng iPad. Chúng tôi cần tìm ra cách xác định kích thước đường đua chung cho mỗi cuộc đua. Giải pháp này phải sử dụng các kênh có kích thước khác nhau tuỳ thuộc vào thông số kỹ thuật của từng thiết bị tham gia cuộc đua.
Tính toán phương diện của bản nhạc
Khi mỗi người chơi tham gia, thông tin về thiết bị của họ sẽ được gửi đến máy chủ và chia sẻ với những người chơi khác. Khi đường đua đang được tạo, dữ liệu này được dùng để tính chiều cao và chiều rộng của đường đua. Chúng ta tính chiều cao bằng cách tìm chiều cao của màn hình nhỏ nhất và chiều rộng là tổng chiều rộng của tất cả màn hình. Vì vậy, trong ví dụ bên dưới, kênh sẽ có chiều rộng 1152 pixel và chiều cao 519 pixel.

this.getDimensions = function () {
var response = {};
response.width = 0;
response.height = _gamePlayers[0].scrn.h; // First screen height
response.screens = [];
for (var i = 0; i < _gamePlayers.length; i++) {
var player = _gamePlayers[i];
response.width += player.scrn.w;
if (player.scrn.h < response.height) {
// Find the smallest screen height
response.height = player.scrn.h;
}
response.screens.push(player.scrn);
}
return response;
}
Vẽ bản nhạc
Paper.js là một khung tập lệnh đồ hoạ vectơ nguồn mở chạy trên HTML5 Canvas. Chúng tôi nhận thấy Paper.js là công cụ hoàn hảo để tạo hình dạng vectơ cho các bản nhạc, vì vậy, chúng tôi đã sử dụng các tính năng của công cụ này để kết xuất các bản nhạc SVG được tạo trong Adobe Illustrator trên phần tử <canvas>
. Để tạo kênh, lớp TrackModel
sẽ thêm mã SVG vào DOM và thu thập thông tin về kích thước và vị trí ban đầu để truyền vào TrackPathView
. Lớp này sẽ vẽ kênh vào một canvas.
paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
var svg = document.getElementById('track');
var layer = new _paper.Layer();
_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;
Sau khi vẽ kênh, mỗi thiết bị sẽ tìm độ dời x dựa trên vị trí của thiết bị trong thứ tự thiết bị và đặt kênh cho phù hợp.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Ảnh động CSS
Paper.js sử dụng nhiều hoạt động xử lý CPU để vẽ các làn đường và quá trình này sẽ mất nhiều hoặc ít thời gian trên các thiết bị khác nhau. Để xử lý vấn đề này, chúng ta cần một trình tải lặp lại cho đến khi tất cả thiết bị xử lý xong bản nhạc. Vấn đề là mọi ảnh động dựa trên JavaScript sẽ bỏ qua các khung hình do các yêu cầu về CPU của Paper.js. Nhập ảnh động CSS chạy trên một luồng giao diện người dùng riêng biệt, cho phép chúng ta tạo ảnh động mượt mà trên văn bản "BUILDING TRACK" (XÂY DỰNG ĐƯỜNG ĐUA).
.glow {
width: 290px;
height: 290px;
background: url('img/track-glow.png') 0 0 no-repeat;
background-size: 100%;
top: 0;
left: -290px;
z-index: 1;
-webkit-animation: wipe 1.3s linear 0s infinite;
}
@-webkit-keyframes wipe {
0% {
-webkit-transform: translate(-300px, 0);
}
25% {
-webkit-transform: translate(-300px, 0);
}
75% {
-webkit-transform: translate(920px, 0);
}
100% {
-webkit-transform: translate(920px, 0);
}
}
}
Sprite CSS
CSS cũng rất hữu ích cho các hiệu ứng trong trò chơi. Các thiết bị di động có nguồn điện hạn chế, phải liên tục tạo ảnh động cho các ô tô chạy trên đường đua. Vì vậy, để tăng thêm sự thú vị, chúng tôi đã sử dụng sprite để triển khai ảnh động kết xuất trước vào trò chơi. Trong sprite CSS, các hiệu ứng chuyển đổi sẽ áp dụng ảnh động dựa trên bước để thay đổi thuộc tính background-position
, tạo hiệu ứng nổ xe.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}
@-webkit-keyframes play-sprite {
0% {
background-position: 0 0;
}
100% {
background-position: -900px 0;
}
}
Vấn đề với kỹ thuật này là bạn chỉ có thể sử dụng các trang sprite được bố trí trên một hàng. Để lặp lại nhiều hàng, ảnh động phải được tạo chuỗi thông qua nhiều nội dung khai báo khung hình chính.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation-name: row1, row2, row3;
-webkit-animation-duration: 0.2s;
-webkit-animation-delay: 0s, 0.2s, 0.4s;
-webkit-animation-timing-function: steps(5), steps(5), steps(5);
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes row1 {
0% {
background-position: 0 0;
}
100% {
background-position: -500px 0;
}
}
@-webkit-keyframes row2 {
0% {
background-position: 0 -100px;
}
100% {
background-position: -500px -100px;
}
}
@-webkit-keyframes row3 {
0% {
background-position: 0 -200px;
}
100% {
background-position: -500px -200px;
}
}
Kết xuất xe
Giống như mọi trò chơi đua xe, chúng tôi biết rằng điều quan trọng là phải mang lại cho người dùng cảm giác tăng tốc và xử lý. Việc áp dụng nhiều mức độ bám đường là rất quan trọng để cân bằng trò chơi và tạo ra yếu tố thú vị. Nhờ đó, khi người chơi cảm nhận được tính chất vật lý, họ sẽ có cảm giác thành tựu và trở thành tay đua giỏi hơn.
Một lần nữa, chúng ta gọi Paper.js đi kèm với một bộ công cụ toán học phong phú. Chúng tôi đã sử dụng một số phương thức của lớp này để di chuyển ô tô dọc theo đường dẫn, đồng thời điều chỉnh vị trí và độ xoay của ô tô một cách mượt mà trong mỗi khung hình.
var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;
// Apply the throttle
_velocity.length += _throttle;
if (!_throttle) {
// Slow down since the throttle is off
_velocity.length *= FRICTION;
}
if (_velocity.length > MAXVELOCITY) {
_velocity.length = MAXVELOCITY;
}
_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;
// Find if a lap has been completed
if (trackOffset < 0) {
while (trackOffset < 0) trackOffset += _path.length;
trackPoint = _path.getPointAt(trackOffset);
console.log('LAP COMPLETE!');
}
if (_velocity.length > 0.1) {
// Render the car if there is actually velocity
renderCar(trackPoint);
}
Trong khi tối ưu hoá quá trình kết xuất xe, chúng tôi đã tìm thấy một điểm thú vị. Trên iOS, hiệu suất tốt nhất đạt được bằng cách áp dụng phép biến đổi translate3d
cho ô tô:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
Trên Chrome cho Android, hiệu suất tốt nhất đạt được bằng cách tính toán các giá trị ma trận và áp dụng phép biến đổi ma trận:
var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + _position.x + ', ' + _position.y + ')';
Duy trì đồng bộ hoá thiết bị
Phần quan trọng nhất (và khó khăn nhất) trong quá trình phát triển là đảm bảo trò chơi đồng bộ hoá trên các thiết bị. Chúng tôi nghĩ rằng người dùng có thể bỏ qua nếu ô tô thỉnh thoảng bỏ qua một vài khung hình do kết nối chậm, nhưng sẽ không thú vị nếu ô tô của bạn nhảy xung quanh, xuất hiện trên nhiều màn hình cùng một lúc. Để giải quyết vấn đề này, chúng tôi đã phải thử nghiệm rất nhiều lần, nhưng cuối cùng cũng tìm ra một số mẹo giúp giải quyết vấn đề.
Tính toán độ trễ
Điểm xuất phát để đồng bộ hoá thiết bị là biết thời gian nhận được thông báo từ relay Compute Engine. Phần khó khăn là đồng hồ trên mỗi thiết bị sẽ không bao giờ đồng bộ hoá hoàn toàn. Để giải quyết vấn đề này, chúng ta cần tìm sự khác biệt về thời gian giữa thiết bị và máy chủ.
Để tìm độ lệch thời gian giữa thiết bị và máy chủ chính, chúng ta gửi một thông báo có dấu thời gian hiện tại của thiết bị. Sau đó, máy chủ sẽ trả lời bằng dấu thời gian ban đầu cùng với dấu thời gian của máy chủ. Chúng ta sử dụng phản hồi này để tính toán chênh lệch thời gian thực tế.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Việc này không đủ, vì hành trình đến máy chủ không phải lúc nào cũng đối xứng, nghĩa là phản hồi có thể mất nhiều thời gian hơn để đến máy chủ so với thời gian máy chủ trả về phản hồi. Để giải quyết vấn đề này, chúng ta thăm dò ý kiến máy chủ nhiều lần, lấy kết quả trung bình. Điều này giúp chúng ta có được sự khác biệt thực tế trong vòng 10 mili giây giữa thiết bị và máy chủ.
Tăng tốc/Giảm tốc
Khi Người chơi 1 nhấn hoặc nhả màn hình, sự kiện tăng tốc sẽ được gửi đến máy chủ. Sau khi nhận được, máy chủ sẽ thêm dấu thời gian hiện tại rồi chuyển dữ liệu đó cho mọi người chơi khác.
Khi một thiết bị nhận được sự kiện "bật chế độ tăng tốc" hoặc "tắt chế độ tăng tốc", chúng ta có thể sử dụng độ lệch máy chủ (được tính toán ở trên) để tìm hiểu thời gian nhận được thông báo đó. Điều này rất hữu ích vì Người chơi 1 có thể nhận được thông báo trong 20 mili giây, nhưng Người chơi 2 có thể mất 50 mili giây để nhận được thông báo đó. Điều này sẽ dẫn đến việc xe ở hai vị trí khác nhau vì thiết bị 1 sẽ bắt đầu tăng tốc sớm hơn.
Chúng ta có thể lấy thời gian nhận sự kiện và chuyển đổi thời gian đó thành khung hình. Ở tốc độ 60 khung hình/giây, mỗi khung hình là 16,67 mili giây.Vì vậy, chúng ta có thể thêm tốc độ (gia tốc) hoặc ma sát (giảm tốc) cho ô tô để tính đến các khung hình bị thiếu.
var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;
for (var i = 0; i < frames; i++) {
if (onScreen) {
_velocity.length += _throttle * Math.round(frames * .215);
} else {
_this.render();
}
}}
Trong ví dụ trên, nếu Người chơi 1 có ô tô trên màn hình và thời gian nhận được thông báo ít hơn 75 mili giây, thì ô tô sẽ điều chỉnh tốc độ, tăng tốc để bù lại sự khác biệt. Nếu thiết bị không hiển thị trên màn hình hoặc thông báo mất quá nhiều thời gian, thì thiết bị sẽ chạy hàm kết xuất và thực sự khiến ô tô chuyển đến vị trí cần thiết.
Duy trì trạng thái đồng bộ hoá của các xe
Ngay cả sau khi tính đến độ trễ trong quá trình tăng tốc, ô tô vẫn có thể không đồng bộ và xuất hiện trên nhiều màn hình cùng một lúc; cụ thể là khi chuyển đổi từ thiết bị này sang thiết bị khác. Để ngăn chặn điều này, các sự kiện cập nhật được gửi thường xuyên để giữ cho các xe ở cùng một vị trí trên đường đua trên tất cả màn hình.
Logic là cứ 4 khung hình, nếu ô tô xuất hiện trên màn hình, thì thiết bị đó sẽ gửi giá trị của nó đến từng thiết bị khác. Nếu không thấy ô tô, ứng dụng sẽ cập nhật các giá trị bằng các giá trị đã nhận được, sau đó di chuyển ô tô về phía trước dựa trên thời gian nhận được sự kiện cập nhật.
this.getValues = function () {
_values.p = _position.clone();
_values.r = _rotation;
_values.e = _elapsed;
_values.v = _velocity.length;
_values.pos = _this.position;
return _values;
}
this.setValues = function (val, time) {
_position.x = val.p.x;
_position.y = val.p.y;
_rotation = val.r;
_elapsed = val.e;
_velocity.length = val.v;
var frames = time / 16.67;
for (var i = 0; i < frames; i++) {
_this.render();
}
}
Kết luận
Ngay khi nghe ý tưởng về Racer, chúng tôi đã biết đây có thể là một dự án rất đặc biệt. Chúng tôi nhanh chóng tạo một nguyên mẫu để có ý tưởng sơ bộ về cách khắc phục độ trễ và hiệu suất mạng. Đây là một dự án đầy thử thách khiến chúng tôi bận rộn vào những đêm khuya và cuối tuần dài, nhưng chúng tôi cảm thấy rất vui khi trò chơi bắt đầu hình thành. Cuối cùng, chúng tôi rất hài lòng với kết quả cuối cùng. Ý tưởng của Google Creative Lab đã đẩy giới hạn của công nghệ trình duyệt theo cách thú vị. Là nhà phát triển, chúng tôi không thể mong muốn gì hơn.