Nghiên cứu điển hình – Tay đua xây dựng

Active Theory
Active Theory

Giới thiệu

Racer là Thử nghiệm Chrome dành cho thiết bị di động dựa trên nền tảng 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 của mình để đua trên mọi màn hình. Được trang bị ý tưởng, thiết kế và nguyên mẫu từ Google Creative Lab và âm thanh từ 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. Bây giờ, 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 thức hoạt động của trò chơi. Dưới đây là bảng chi tiết các tính năng chính và câu trả lời cho những câu hỏi chúng tô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 phải đối mặt là làm sao để tạo ra một trò chơi dựa trên nền tảng web dành cho thiết bị di động hoạt động tốt trên nhiều loại thiết bị. Người chơi cần xây dựng được 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ể đang sở hữu Nexus 4 và muốn đua với người bạn của mình cũng có 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 phải bao gồm việc 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ị được tham gia cuộc đua.

Đang tính toán kích thước đường ray

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à được chia sẻ với những người chơi khác. Khi đường đi đang được xây dựng, dữ liệu này được sử dụng để tính toán chiều cao và chiều rộng của tuyến đường. 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 chính 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 bản nhạc sẽ có chiều rộng là 1152 pixel và chiều cao là 519 pixel.

Vùng màu đỏ thể hiện tổng chiều rộng và chiều cao của bản nhạc trong ví dụ này.
Vùng màu đỏ hiển thị tổng chiều rộng và chiều cao của bản nhạc trong ví dụ này.
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ẽ đường đi

Paper.js là khung tập lệnh đồ họa vectơ nguồn mở chạy trên Canvas HTML5. 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 đã dùng tính năng 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 bản nhạc, lớp TrackModel sẽ thêm mã SVG vào DOM, đồng thời thu thập thông tin về kích thước và vị trí ban đầu sẽ được truyền đến TrackPathView, nhằm vẽ bản nhạc vào 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 đường đi được vẽ, mỗi thiết bị sẽ tìm độ lệch x của đường đi dựa trên vị trí của đường đi trong thứ tự trong danh sách thiết bị và xác định vị trí của bản nhạc tương ứng.

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
Sau đó, giá trị chênh lệch x có thể được dùng để hiển thị phần phù hợp của bản nhạc.
Sau đó, bạn có thể dùng độ lệch x để hiển thị phần thích hợp của bản nhạc

Ảnh động CSS

Paper.js sử dụng nhiều quá trình xử lý của CPU để vẽ các làn đường và quá trình này sẽ mất nhiều thời gian hơn hoặc ít hơn trên các thiết bị khác nhau. Để xử lý vấn đề này, chúng tôi cần có một trình tải để lặp lại cho đến khi tất cả các thiết bị xử lý xong kênh. Vấn đề là bất kỳ hoạt ảnh nào dựa trên JavaScript đều sẽ bỏ qua các khung do yêu cầu về CPU của Paper.js. Nhập các ảnh động CSS. Các hoạt ảnh này 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 hiệu ứng chuyển động mượt mà trên văn bản "ĐANG XÂY DỰNG".

.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);
  }
}
}

CSS Sprite

CSS cũng hữu ích cho các hiệu ứng trong trò chơi. Các thiết bị di động với năng lượng hạn chế luôn liên tục tạo ảnh động cho những chiếc xe chạy trên đường đua. Vì vậy, để tăng thêm sự hứng thú, chúng tôi đã sử dụng sprite làm cách triển khai ảnh động kết xuất trước vào trò chơi. Trong CSS sprite, hiệu ứng chuyển đổi áp dụng ảnh động theo bước để thay đổi thuộc tính background-position, tạo ra vụ 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 đề của kỹ thuật này là bạn chỉ có thể sử dụng các trang tính sprite được bố trí trên một hàng. Để lặp lại qua nhiều hàng, ảnh động phải được xâu chuỗi 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 ô tô

Giống như bất kỳ trò chơi đua xe nào, chúng tôi biết điều quan trọng là mang lại cho người dùng cảm giác tăng tốc và xử lý. Việc áp dụng các yếu tố khác biệt về vật lý đóng vai trò quan trọng trong việc cân bằng trò chơi và tạo ra yếu tố thú vị. Vì vậy, khi người chơi cảm nhận được yếu tố vật lý, họ sẽ có cảm giác thành công và trở thành một tay đua giỏi hơn.

Một lần nữa, chúng tôi yêu cầu Paper.js cung cấp một bộ tiện ích toán học phong phú. Chúng tôi đã sử dụng một số phương pháp của hàm này để di chuyển ô tô dọc theo đường, đồng thời điều chỉnh vị trí của ô tô và xoay trơn tru từng khung.

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á tính năng kết xuất ô tô, 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 dành cho Android, hiệu suất tốt nhất đạt được bằng cách tính toán giá trị ma trận và áp dụng 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 + ')';

Luôn đồng bộ hoá các 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 được đồng bộ hoá trên các thiết bị. Chúng tôi nghĩ người dùng có thể bỏ qua nếu thỉnh thoảng ô tô bỏ qua một vài khung hình do kết nối chậm nhưng sẽ không thú vị lắm 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 lúc. Giải quyết vấn đề này cần rất nhiều thử và sai, nhưng cuối cùng chúng tôi cũng đã nắm được một vài thủ thuật để giải quyết vấn đề.

Tính độ trễ

Điểm bắt đầu để đồng bộ hoá thiết bị là biết cần bao lâu để nhận được thông báo từ dịch vụ chuyển tiếp Compute Engine. Điều khó khăn là các đồng hồ trên mỗi thiết bị sẽ không bao giờ đồng bộ hoàn toàn. Để giải quyết vấn đề này, chúng tôi cần tìm ra sự khác biệt về thời gian giữa thiết bị và máy chủ.

Để tìm khoảng thời gian chênh lệch giữa thiết bị và máy chủ chính, chúng ta sẽ gửi một thông báo kèm theo 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 tôi sử dụng phản hồi để tính toán mức chênh lệch thực tế về thời gian.

var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;

Thực hiện việc này một lần là không đủ, vì lượt khứ hồi đến máy chủ không phải lúc nào cũng đối xứng, nghĩa là có thể mất nhiều thời gian hơn để phản hồi đến được máy chủ so với máy chủ để trả về nó. Để giải quyết vấn đề này, chúng tôi thăm dò máy chủ nhiều lần, lấy kết quả trung bình. Việc này giúp chúng tôi giải quyết được sự khác biệt thực tế giữa thiết bị và máy chủ trong vòng 10 mili giây.

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 một sự kiện "tăng tốc bật" hoặc "giảm tốc độ" (tăng tốc tắt), chúng tôi có thể sử dụng mức bù trừ máy chủ (được tính ở 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ể dành thời gian cần thiết để nhận được sự kiện và chuyển đổi sự kiện đó 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ể tăng thêm vận tốc (gia tốc) hoặc ma sát (giảm tốc) trên xe để tính đến các khung hình mà nó bỏ lỡ.

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 nhìn thấy ô tô trên màn hình và thời gian nhận được tin nhắn dưới 75 mili giây, thì người chơi sẽ điều chỉnh tốc độ của ô tô, tăng tốc để tạo ra sự chênh lệch. Nếu thiết bị không hiện trên màn hình hoặc thông báo mất quá nhiều thời gian, thiết bị sẽ chạy chức năng kết xuất và thực sự khiến ô tô chuyển đến vị trí cần đến.

Luôn đồng bộ hoá các loại ô tô

Ngay cả sau khi tính đến độ trễ khi tăng tốc, xe vẫn có thể không đồng bộ hoá và xuất hiện trên nhiều màn hình cùng lúc; đặc biệt là khi chuyển đổi từ thiết bị này sang thiết bị khác. Để tránh tình trạng này, các sự kiện cập nhật sẽ được gửi thường xuyên để đảm bảo các ô tô ở cùng một vị trí trên đường đua trên mọi màn hình.

Logic là cứ 4 khung hình, nếu ô tô hiển thị 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 ô tô không xuất hiện, ứng dụng sẽ cập nhật các giá trị đã nhận được rồi di chuyển ô tô về phía trước dựa trên thời gian cần thiết để 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 biết đến ý tưởng về Racer, chúng tôi đã biết rằng nó có tiềm năng trở thành một dự án rất đặc biệt. Chúng tôi nhanh chóng xây dựng một nguyên mẫu cung cấp ý tưởng sơ bộ về cách vượt qua độ trễ và hiệu suất mạng. Đó là một dự án đầy thách thức 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 đó là một cảm giác tuyệt vời khi trò chơi bắt đầu thành hì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 đã vượt qua các giới hạn của công nghệ trình duyệt theo cách thú vị và với tư cách là nhà phát triển, chúng tôi không thể đòi hỏi nhiều hơn nữa.