Studi Kasus - Pembalap

Pengantar

Racer adalah Eksperimen Chrome seluler berbasis web yang dikembangkan oleh Active Theory. Maksimal 5 teman dapat menghubungkan ponsel atau tablet mereka untuk balapan di setiap layar. Berbekal konsep, desain, dan purwarupa dari Google Creative Lab serta suara dari Plan8, kami melakukan iterasi pada build selama 8 minggu menjelang peluncuran di I/O 2013. Sekarang setelah game ini ditayangkan selama beberapa minggu, kami sempat mengajukan beberapa pertanyaan dari komunitas developer tentang cara kerjanya. Di bawah ini adalah perincian fitur utama dan jawaban atas pertanyaan yang paling sering diajukan.

Lagu

Tantangan yang cukup jelas yang kami hadapi adalah bagaimana membuat game seluler berbasis web yang berfungsi dengan baik di berbagai perangkat. Pemain harus dapat membuat perlombaan dengan ponsel dan tablet yang berbeda. Seorang pemain bisa memiliki Nexus 4 dan ingin berlomba dengan temannya yang memiliki iPad. Kami perlu menemukan cara untuk menentukan ukuran lintasan yang umum untuk setiap perlombaan. Solusi ini harus melibatkan penggunaan trek dengan ukuran berbeda tergantung spesifikasi untuk setiap perangkat yang disertakan dalam perlombaan.

Menghitung Dimensi Trek

Saat setiap pemain bergabung, informasi tentang perangkat mereka akan dikirim ke server dan dibagikan kepada pemain lain. Saat trek sedang dibuat, data ini digunakan untuk menghitung tinggi dan lebar trek. Kami menghitung tinggi dengan mencari tinggi layar terkecil, dan lebar adalah total lebar semua layar. Jadi dalam contoh di bawah ini, trek akan memiliki lebar 1152 piksel dan tinggi 519 piksel.

Area merah menunjukkan lebar dan tinggi total trek untuk contoh ini.
Area merah menunjukkan total lebar dan tinggi trek untuk contoh ini.
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;
}

Menggambar Trek

Paper.js adalah framework pembuatan skrip grafik vektor open source yang berjalan di atas Kanvas HTML5. Kami mendapati bahwa Paper.js adalah alat yang tepat untuk membuat bentuk vektor untuk trek, jadi kami menggunakan kemampuannya untuk merender trek SVG yang dibuat di Adobe Illustrator pada elemen <canvas>. Untuk membuat jalur, class TrackModel menambahkan kode SVG ke DOM dan mengumpulkan informasi tentang dimensi asli dan pemosisian yang akan diteruskan ke TrackPathView yang akan menggambar jalur ke kanvas.

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;

Setelah trek digambar, setiap perangkat menemukan offset x berdasarkan posisinya dalam urutan line-up perangkat, dan memosisikan trek sebagaimana mestinya.

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
Offset x kemudian dapat digunakan untuk menunjukkan bagian trek yang sesuai.
Offset x dapat digunakan untuk menampilkan bagian trek yang sesuai

Animasi CSS

Paper.js menggunakan banyak pemrosesan CPU untuk menggambar jalur trek dan proses ini akan memakan waktu lebih banyak atau lebih sedikit di perangkat yang berbeda. Untuk menangani hal ini, kami memerlukan loader untuk melakukan loop hingga semua perangkat selesai memproses jalur. Masalahnya adalah animasi berbasis JavaScript akan melewati frame karena persyaratan CPU Paper.js. Masukkan animasi CSS, yang berjalan di UI thread terpisah, sehingga kita dapat menganimasikan kemiringan di seluruh teks "MENCIPTAKAN LACAK".

.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 juga sangat berguna untuk efek dalam game. Perangkat seluler, dengan kekuatannya yang terbatas, terus sibuk menganimasikan mobil yang melintasi lintasan. Jadi, untuk meningkatkan antusiasme, kami menggunakan sprite sebagai cara untuk menerapkan animasi yang sudah dipra-render ke dalam game. Pada sprite CSS, transisi menerapkan animasi berbasis langkah yang mengubah properti background-position, sehingga menghasilkan ledakan mobil.

#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;
  }
}

Masalah dengan teknik ini adalah Anda hanya dapat menggunakan sprite sheet yang ditata pada satu baris. Agar dapat melakukan loop pada beberapa baris, animasi harus dirantai melalui beberapa deklarasi keyframe.

#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;
  }
}

Merender Mobil

Seperti game balapan mobil lainnya, kami tahu bahwa penting untuk memberikan kesan akselerasi dan penanganan kepada pengguna. Menerapkan jumlah daya tarik yang berbeda penting untuk penyeimbangan game dan faktor kesenangan, sehingga setelah pemain merasakan fisiknya, mereka akan merasakan pencapaian dan menjadi pembalap yang lebih baik.

Sekali lagi, kami meminta Paper.js yang dilengkapi dengan banyak utilitas matematika. Kami menggunakan beberapa metodenya untuk menggerakkan mobil di sepanjang jalan, sambil menyesuaikan posisi mobil dan rotasi setiap bingkai dengan mulus.

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

Saat mengoptimalkan rendering mobil, kami menemukan poin yang menarik. Di iOS, performa terbaik dicapai dengan menerapkan transformasi translate3d ke mobil:

_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';

Di Chrome untuk Android, performa terbaik dicapai dengan menghitung nilai matriks dan menerapkan transformasi matriks:

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 + ')';

Menjaga Perangkat Disinkronkan

Bagian pengembangan terpenting (dan yang sulit) adalah memastikan game disinkronkan di seluruh perangkat. Kami pikir pengguna dapat memaklumi jika mobil sesekali melewati beberapa frame karena koneksi yang lambat, tetapi tidak akan menyenangkan jika mobil Anda melompat-lompat, muncul di beberapa layar sekaligus. Menyelesaikan masalah ini membutuhkan banyak coba-coba, tetapi akhirnya kami memutuskan beberapa trik yang membuatnya berhasil.

Menghitung Latensi

Titik awal untuk menyinkronkan perangkat adalah mengetahui berapa lama waktu yang dibutuhkan pesan untuk diterima dari relai Compute Engine. Bagian yang rumit adalah bahwa jam di setiap perangkat tidak akan pernah sepenuhnya disinkronkan. Untuk mengatasi hal ini, kami perlu menemukan perbedaan waktu antara perangkat dan server.

Untuk menemukan selisih waktu antara perangkat dan server utama, kita akan mengirim pesan dengan stempel waktu perangkat saat ini. Selanjutnya server akan membalas dengan stempel waktu asli beserta stempel waktu server. Kami menggunakan respons untuk menghitung perbedaan waktu yang sebenarnya.

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

Melakukan ini sekali saja tidak cukup, karena perjalanan pulang pergi ke server tidak selalu simetris, artinya mungkin perlu waktu lebih lama bagi respons untuk sampai ke server daripada yang dilakukan server untuk mengembalikannya. Untuk mengatasi hal ini, kami melakukan polling pada server beberapa kali, dengan mengambil hasil median. Hal ini memberi kita waktu 10 md dari perbedaan sebenarnya antara perangkat dan server.

Akselerasi/Perlambatan

Saat Pemain 1 menekan atau melepaskan layar, peristiwa akselerasi dikirim ke server. Setelah diterima, server menambahkan stempel waktu saat ini, lalu meneruskan data tersebut ke setiap pemutar lain.

Ketika peristiwa "accelerate on" atau "accelerate off" diterima oleh perangkat, kami dapat menggunakan offset server (dihitung di atas) untuk mengetahui waktu yang dibutuhkan hingga pesan tersebut diterima. Hal ini berguna, karena Pemain 1 mungkin menerima pesan dalam 20 md, tetapi Pemain 2 mungkin butuh waktu 50 md untuk menerimanya. Hal ini akan menyebabkan mobil berada di dua tempat yang berbeda karena perangkat 1 akan memulai akselerasi lebih cepat.

Kita dapat menggunakan waktu yang diperlukan untuk menerima peristiwa dan mengubahnya menjadi bingkai. Pada 60 fps, setiap frame adalah 16,67 md—sehingga kita dapat menambahkan lebih banyak kecepatan (akselerasi) atau gesekan (perlambatan) pada mobil untuk memperhitungkan frame yang terlewatkan.

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

Dalam contoh di atas, jika Pemain 1 meletakkan mobil di layarnya dan waktu yang diperlukan untuk menerima pesan kurang dari 75 md, maka akan menyesuaikan kecepatan mobil, mempercepatnya untuk mengompensasi perbedaannya. Jika perangkat tidak ada di layar atau pesan memakan waktu terlalu lama, perangkat akan menjalankan fungsi render dan benar-benar membuat mobil melompat ke tempat yang seharusnya.

Menjaga Mobil Disinkronkan

Bahkan setelah memperhitungkan latensi dalam akselerasi, mobil masih bisa tidak sinkron dan muncul di beberapa layar sekaligus; khususnya ketika beralih dari satu perangkat ke perangkat berikutnya. Untuk mencegah hal ini, peristiwa pembaruan sering dikirim untuk menjaga mobil tetap berada pada posisi yang sama di lintasan di semua layar.

Logikanya adalah, setiap 4 frame, jika mobil terlihat di layar, perangkat tersebut akan mengirimkan nilainya ke masing-masing perangkat lain. Jika mobil tidak terlihat, aplikasi memperbarui nilai dengan nilai yang diterima, lalu menggerakkan mobil berdasarkan waktu yang diperlukan untuk mendapatkan peristiwa update.

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

Kesimpulan

Begitu kami mendengar konsep Racer, kami menyadari bahwa proyek ini berpotensi menjadi proyek yang sangat istimewa. Kami dengan cepat membangun prototipe yang memberi kami gambaran kasar tentang cara mengatasi latensi dan kinerja jaringan. Itu adalah proyek menantang yang membuat kami sibuk selama larut malam dan akhir pekan yang panjang, tetapi kami senang ketika game mulai terbentuk. Pada akhirnya, kami sangat puas dengan hasil akhirnya. Konsep Google Creative Lab mendorong batas-batas teknologi browser dengan cara yang menyenangkan, dan sebagai pengembang kami tidak dapat meminta lebih banyak lagi.