Örnek Olay - Yarışçı Kazanma

Active Theory
Active Theory

Giriş

Racer, Active Theory tarafından geliştirilen web tabanlı bir mobil Chrome Denemesidir. En fazla 5 arkadaşınız, telefon veya tabletlerini bağlayarak her ekranda yarışabilir. Google Creative Lab'in konsepti, tasarımı ve prototipi ve Plan8'in sesi ile I/O 2013'teki lansmana kadar 8 hafta boyunca derlemeler üzerinde yineleme yaptık. Oyun birkaç haftadır yayında olduğuna göre, geliştirici topluluğundan oyunun işleyişiyle ilgili bazı soruları yanıtlama fırsatımız oldu. Temel özelliklerin ve en sık sorulan soruların yanıtlarının ayrıntıları aşağıda verilmiştir.

Parça

Karşılaştığımız en bariz zorluklardan biri, çok çeşitli cihazlarda iyi çalışan web tabanlı bir mobil oyunun nasıl geliştirileceğiydi. Oyuncuların farklı telefonlar ve tabletlerle bir yarış oluşturabilmesi gerekiyordu. Oyunculardan birinin Nexus 4 cihazı olup iPad'i olan arkadaşına karşı yarışmak isteyebilir. Her yarış için ortak bir pist büyüklüğü belirlemenin yolunu bulmamız gerekiyordu. Çözüm, yarışa dahil olan her cihazın özelliklerine bağlı olarak farklı boyutlarda parçaların kullanılmasını gerektiriyordu.

Parkur Boyutlarını Hesaplama

Her oyuncu katıldığında cihazıyla ilgili bilgiler sunucuya gönderilir ve diğer oyuncularla paylaşılır. Kanal oluşturulurken bu veriler, parkurun yüksekliğini ve genişliğini hesaplamak için kullanılır. Yüksekliği, en küçük ekranın yüksekliğini bularak hesaplarız ve genişlik, tüm ekranların toplam genişliğidir. Dolayısıyla, aşağıdaki örnekte parçanın genişliği 1152 piksel, yüksekliği ise 519 piksel olur.

Kırmızı alan, bu örnek için kanalın toplam genişliğini ve yüksekliğini gösterir.
Kırmızı alan, bu örnek için kanalın toplam genişliğini ve yüksekliğini gösterir.
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;
}

Track Çizme

Paper.js, HTML5 Tuvali üzerinde çalışan açık kaynaklı bir vektör grafik komut dosyası çerçevesidir. Paper.js'nin, hatlar için vektör şekilleri oluşturmak için mükemmel bir araç olduğunu gördük. Bu yüzden, Adobe Illustrator'da oluşturulan SVG parçalarını bir <canvas> öğesi üzerinde oluşturmak için bu aracın yeteneklerinden yararlandık. TrackModel sınıfı, kanalı oluşturmak için SVG kodunu DOM'ye ekler ve TrackPathView'ye iletilecek orijinal boyutlar ve konumlandırma hakkında bilgi toplar. Bu bilgiler, parkuru bir zemine çizer.

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;

Parkur çizildikten sonra, her cihaz, cihaz dizilimi sıralamasındaki konumuna göre x ofsetini bulur ve parkuru buna uygun şekilde konumlandırır.

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
Daha sonra x ofseti, parkurun uygun kısmını göstermek için kullanılabilir.
x ofseti daha sonra parçanın uygun kısmını göstermek için kullanılabilir

CSS Animasyonları

Paper.js, parkur şeritlerini çizmek için çok fazla CPU işlemi kullanır ve bu işlem, farklı cihazlarda daha fazla veya daha az zaman alır. Bunu yapabilmek için, parkuru işlemeyi tüm cihazlarda tamamlayana kadar döngü oluşturacak bir yükleyiciye ihtiyacımız vardı. Sorun, JavaScript tabanlı animasyonların, Paper.js'nin CPU gereksinimleri nedeniyle kareleri atlamasıydı. Ayrı bir kullanıcı arayüzü iş parçacığında çalışan CSS animasyonlarını girin. Bu animasyonlar, "BUILDING PARÇA" metni boyunca parlaklığı düzgün bir şekilde canlandırmamıza olanak tanır.

.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 İmgeleri

CSS, oyun içi efektler için de kullanışlıdır. Sınırlı güçle mobil cihazlar, hatlar boyunca çalışan arabaları canlandırarak meşgul oluyor. Daha fazla heyecan yaratmak için oyuna önceden oluşturulmuş animasyonları uygulamanın bir yolu olarak sprite görseller kullandık. Bir CSS imgesinde geçişler, background-position özelliğini değiştirerek araba patlaması oluşturan adıma dayalı bir animasyon uygular.

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

Bu teknikteki sorun, yalnızca tek bir satırda düzenlenmiş model sayfalarını kullanabilmenizdir. Birden çok satır arasında geçiş yapmak için animasyonun birden çok animasyon karesi bildirimlerinden zincirlenmesi gerekir.

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

Arabalar oluşturuluyor

Tüm araba yarışı oyunlarında olduğu gibi, kullanıcıya hızlanma ve yol tutuş hissi vermenin önemli olduğunu biliyorduk. Oyun dengesinde ve eğlence faktöründe farklı miktarda çekim uygulamak önemliydi. Böylece bir oyuncu fiziğe aşina olduğunda başarı hissi ve daha iyi bir yarışçı haline gelirdi.

Pek çok farklı matematik yardımcı programını içeren Paper.js'yi bir kez daha tanıttık. Arabayı yol boyunca hareket ettirmek için bu sistemin bazı yöntemlerini kullandık. Ayrıca, her karede arabanın konumunu ve dönüşünü yumuşak bir şekilde ayarladık.

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

Otomobil oluşturmayı optimize ederken ilginç bir noktaya ulaştık. iOS'te en iyi performans, arabaya translate3d dönüşümü uygulanarak elde edilmiştir:

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

Android için Chrome'da, en iyi performans, matris değerleri hesaplanarak ve bir matris dönüşümü uygulanarak elde edilmiştir:

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

Cihazları Senkronize Etme

Geliştirme sürecinin en önemli (ve zor) yanı, oyunun cihazlar arasında senkronize edildiğinden emin olmaktı. Bir arabanın yavaş bağlantı nedeniyle ara sıra birkaç kareyi atlaması kullanıcıların affedebileceğini düşündük, ancak arabanız etrafta zıplayıp aynı anda birden çok ekranda göründüğünde bu pek de eğlenceli olmazdı. Bu sorunu çözmek için çok fazla deneme ve yanılma gerekse de sonunda işe yarayan birkaç hileye karar verdik.

Gecikme Hesaplama

Cihazları senkronize etmek için başlangıç noktası, mesajların Compute Engine geçişinden alınmasının ne kadar sürdüğünü bilmektir. İşin zor kısmı, her cihazdaki saatlerin hiçbir zaman tam olarak senkronize olmayacak olmasıdır. Bu sorunu aşmak için cihazla sunucu arasındaki zaman farkını bulmamız gerekiyordu.

Cihaz ile ana sunucu arasındaki zaman farkını bulmak için geçerli cihaz zaman damgasını içeren bir mesaj göndeririz. Ardından sunucu, sunucunun zaman damgasıyla birlikte orijinal zaman damgasıyla yanıt verir. Bu yanıtı, gerçek zaman farkını hesaplamak için kullanırız.

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

Sunucuya gidiş dönüş her zaman simetrik olmadığından bunu bir kez yapmak yeterli değildir. Diğer bir deyişle, yanıtın sunucuya ulaşması, sunucunun yanıtı döndürmesinden daha uzun sürebilir. Bunu aşmak için, ortanca değeri alarak sunucuyu birkaç kez sorgularız. Bu işlem bize, cihaz ve sunucu arasındaki asıl farkın 10 ms içinde gösterilmesini sağlar.

Hızlandırma/Yavaşlama

Oyuncu 1 ekrana bastığında veya ekranı serbest bıraktığında hızlandırma etkinliği sunucuya gönderilir. Alındığında, sunucu geçerli zaman damgasını ekler ve bu verileri diğer tüm oynatıcılara iletir.

Bir cihaz "accelerate on" (hızlanma zamanı) veya "accelerate off" (hızlanma kapalı) etkinliği alındığında bu iletinin alınmasının ne kadar sürdüğünü öğrenmek için sunucu uzaklığını (yukarıda hesaplanmıştır) kullanabiliriz. Bu yararlı bir özelliktir, çünkü Oyuncu 1 mesajı 20 ms içinde alabilir, ancak Oyuncu 2'nin alması 50 ms sürebilir. Bu durumda, 1. cihaz hızlanmayı daha erken başlatacağından araba iki farklı yerde olur.

Etkinliği almak için gereken zamanı alıp karelere dönüştürebiliriz. 60 fps'de her kare 16,67 ms'dir.Böylece, kaçırdığı kareleri hesaba katmak için arabaya daha fazla hız (ivme) veya sürtünme (yavaşlama) ekleyebiliriz.

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

Yukarıdaki örnekte, Oyuncu 1'in ekranında araba varsa ve mesajı alması 75 ms'den azsa, arabanın hızını ayarlayarak farkı telafi etmek için arabayı hızlandırır. Cihaz ekranda değilse veya mesaj çok uzun sürdüyse oluşturma işlevini çalıştırır ve arabanın olması gereken yere atlamasını sağlar.

Arabaları Senkronize Etme

Hızlanmadaki gecikme hesaba katıldıktan sonra bile araba senkronizasyonu bozularak aynı anda birden fazla ekranda görünebilir (özellikle bir cihazdan diğerine geçiş yaparken). Bunu önlemek için, arabaların yol üzerinde tüm ekranlarda aynı konumda kalmasını sağlamak için sık sık güncelleme etkinlikleri gönderilir.

Mantık, araba ekranda görünürse her 4 karede bir cihazın değerlerini diğer cihazların her birine göndermesidir. Araba görünür halde değilse uygulama, değerleri alınan değerlerle günceller ve daha sonra, güncelleme etkinliğini almak için geçen süreye göre arabayı ileri hareket ettirir.

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

Sonuç

Racer konseptini öğrenir öğrenmez, çok özel bir proje olma potansiyeline sahip olduğunu anladık. Gecikmenin ve ağ performansının nasıl üstesinden geleceğimize dair bize kabaca bir fikir veren hızlı bir prototip oluşturduk. Bu, gece geç saatlerde ve uzun hafta sonları bizi meşgul eden zor bir projeydi. Oyunun şekillenmeye başlaması bizi çok mutlu etti. Son olarak, sonuçtan çok memnunuz. Google Creative Lab'in konsepti, tarayıcı teknolojisinin sınırlarını eğlenceli bir şekilde zorladı ve geliştiriciler olarak daha fazlasını isteyemiyorduk.