Studi Kasus - Pembalap

Pengantar

Racer adalah Eksperimen Chrome seluler berbasis web yang dikembangkan oleh Active Theory. Hingga 5 teman dapat menghubungkan ponsel atau tablet mereka untuk berlomba di setiap layar. Dengan konsep, desain, dan prototipe dari Google Creative Lab serta suara dari Plan8, kami melakukan iterasi pada build selama 8 minggu menjelang peluncuran di I/O 2013. Setelah game ini ditayangkan selama beberapa minggu, kami berkesempatan untuk menjawab beberapa pertanyaan dari komunitas developer tentang cara kerjanya. Berikut adalah perincian fitur utama dan jawaban atas pertanyaan yang paling sering diajukan kepada kami.

Jalur

Tantangan yang cukup jelas yang kami hadapi adalah cara membuat game seluler berbasis web yang berfungsi dengan baik di berbagai perangkat. Pemain harus dapat membuat perlombaan dengan ponsel dan tablet yang berbeda. Satu pemain dapat memiliki Nexus 4 dan ingin berlomba dengan temannya yang memiliki iPad. Kami perlu menemukan cara untuk menentukan ukuran trek yang sama untuk setiap perlombaan. Solusi ini harus melibatkan penggunaan jalur berukuran berbeda, bergantung pada spesifikasi untuk setiap perangkat yang disertakan dalam perlombaan.

Menghitung Dimensi Jalur

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

Area merah menunjukkan total lebar dan tinggi jalur untuk contoh ini.
Area merah menunjukkan total lebar dan tinggi jalur 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 Jalur

Paper.js adalah framework skrip grafik vektor open source yang berjalan di atas Kanvas HTML5. Kami menemukan bahwa Paper.js adalah alat yang sempurna untuk membuat bentuk vektor untuk jalur, jadi kami menggunakan kemampuannya untuk merender jalur SVG yang dibuat di Adobe Illustrator pada elemen <canvas>. Untuk membuat jalur, class TrackModel menambahkan kode SVG ke DOM dan mengumpulkan informasi tentang dimensi dan pemosisian asli 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 akan menemukan offset x-nya berdasarkan posisinya dalam urutan susunan perangkat, dan memosisikan trek dengan sesuai.

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 menampilkan bagian trek yang sesuai.
Offset x kemudian dapat digunakan untuk menampilkan bagian trek yang sesuai

Animasi CSS

Paper.js menggunakan banyak pemrosesan CPU untuk menggambar jalur trek dan proses ini akan memerlukan waktu lebih lama atau lebih singkat di perangkat yang berbeda. Untuk menangani hal ini, kita 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 thread UI terpisah, sehingga kita dapat menganimasikan kilau di seluruh teks "BUILDING TRACK" dengan lancar.

.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 berguna untuk efek dalam game. Perangkat seluler, dengan daya yang terbatas, terus disibukkan dengan menganimasikan kereta yang berjalan di sepanjang rel. Jadi, untuk menambah keseruan, kami menggunakan sprite sebagai cara untuk menerapkan animasi yang telah dirender sebelumnya ke dalam game. Dalam sprite CSS, transisi menerapkan animasi berbasis langkah yang mengubah properti background-position, sehingga menciptakan 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 sheet sprite yang disusun di satu baris. Untuk melakukan loop melalui 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 balap mobil lainnya, kami tahu pentingnya memberikan perasaan akselerasi dan penanganan kepada pengguna. Menerapkan jumlah traksi yang berbeda sangat penting untuk penyeimbangan game dan faktor menyenangkan, sehingga setelah pemain merasakan fisika, mereka akan mendapatkan rasa pencapaian dan menjadi pembalap yang lebih baik.

Sekali lagi, kita memanggil Paper.js yang dilengkapi dengan serangkaian utilitas matematika yang luas. Kami menggunakan beberapa metodenya untuk menggerakkan mobil di sepanjang jalur, sambil menyesuaikan posisi dan rotasi mobil dengan lancar di setiap frame.

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 hal 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 Tetap Sinkron

Bagian terpenting (dan tersulit) dari pengembangan adalah memastikan game disinkronkan di seluruh perangkat. Kami pikir pengguna dapat memaafkan jika mobil terkadang melewatkan beberapa frame karena koneksi lambat, tetapi tidak akan menyenangkan jika mobil Anda melompat-lompat, muncul di beberapa layar sekaligus. Untuk mengatasinya, kami harus melakukan banyak percobaan, tetapi akhirnya kami menemukan beberapa trik yang berhasil.

Menghitung Latensi

Titik awal untuk menyinkronkan perangkat adalah mengetahui berapa lama waktu yang diperlukan untuk menerima pesan dari relay Compute Engine. Bagian yang sulit adalah jam di setiap perangkat tidak akan pernah sepenuhnya disinkronkan. Untuk mengatasi hal ini, kita perlu menemukan perbedaan waktu antara perangkat dan server.

Untuk menemukan offset waktu antara perangkat dan server utama, kita mengirim pesan dengan stempel waktu perangkat saat ini. Server kemudian akan membalas dengan stempel waktu asli beserta stempel waktu server. Kita 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;

Melakukannya sekali saja tidak cukup, karena perjalanan bolak-balik ke server tidak selalu simetris, yang berarti respons mungkin memerlukan waktu lebih lama untuk sampai ke server daripada waktu yang diperlukan server untuk menampilkannya. Untuk mengatasi hal ini, kami melakukan polling server beberapa kali, dengan mengambil hasil median. Hal ini membuat kita berada dalam perbedaan 10 md dari perbedaan sebenarnya antara perangkat dan server.

Akselerasi/Deselerasi

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

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

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

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

Pada contoh di atas, jika Pemain 1 memiliki mobil di layarnya dan waktu yang diperlukan untuk menerima pesan kurang dari 75 md, mobil akan menyesuaikan kecepatannya, mempercepatnya untuk menutupi perbedaan tersebut. Jika perangkat tidak ada di layar atau pesan memerlukan waktu terlalu lama, perangkat akan menjalankan fungsi render dan benar-benar membuat mobil melompat ke tempat yang seharusnya.

Menjaga Mobil Tetap Sinkron

Meskipun setelah memperhitungkan latensi dalam akselerasi, mobil masih dapat tidak sinkron dan muncul di beberapa layar sekaligus; terutama saat bertransisi dari satu perangkat ke perangkat berikutnya. Untuk mencegah hal ini, peristiwa pembaruan sering dikirim agar mobil tetap berada di posisi yang sama di jalur di semua layar.

Logikanya adalah, setiap 4 frame, jika mobil terlihat di layar, perangkat tersebut akan mengirimkan nilainya ke setiap perangkat lainnya. Jika mobil tidak terlihat, aplikasi akan memperbarui nilai dengan nilai yang diterima, lalu memindahkan mobil ke depan berdasarkan waktu yang diperlukan untuk mendapatkan peristiwa pembaruan.

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

Segera setelah mendengar konsep Racer, kami tahu bahwa game ini berpotensi menjadi project yang sangat spesial. Kami dengan cepat membuat prototipe yang memberi kami gambaran kasar tentang cara mengatasi latensi dan performa jaringan. Ini adalah project yang menantang yang membuat kami sibuk pada larut malam dan akhir pekan yang panjang, tetapi kami merasa senang saat game mulai terbentuk. Pada akhirnya, kami sangat puas dengan hasil akhirnya. Konsep Google Creative Lab mendorong batas teknologi browser dengan cara yang menyenangkan, dan sebagai developer, kami tidak bisa meminta yang lebih baik.