Giriş
Racer, Active Theory tarafından geliştirilmiş web tabanlı bir mobil Chrome denemesi. 5'e kadar arkadaş, telefonlarını veya tabletlerini bağlayarak her ekranda yarışabilir. Google Creative Lab'den aldığımız konsept, tasarım ve prototipi ve Plan8'den aldığımız sesleri kullanarak I/O 2013'te lansmana kadar 8 hafta boyunca yapıları tekrar tekrar denedik. Oyun birkaç haftadır kullanımda olduğundan geliştirici topluluğundan gelen ve oyunun işleyişiyle ilgili bazı soruları yanıtlama fırsatı bulduk. Aşağıda, temel özelliklerin dökümü ve en sık sorulan soruların yanıtları verilmiştir.
Parça
Karşılaştığımız en belirgin zorluk, çeşitli cihazlarda iyi çalışan web tabanlı bir mobil oyun oluşturmaktı. Oyuncuların farklı telefon ve tabletlerle yarış oluşturabilmesi gerekiyordu. Nexus 4 kullanan bir oyuncu, iPad kullanan arkadaşıyla yarış yapmak isteyebilir. Her yarış için ortak bir pist boyutu belirlemenin bir yolunu bulmamız gerekiyordu. Çözüm, yarışa katılan her cihazın teknik özelliklerine bağlı olarak farklı boyutlarda parçalar kullanılmasını gerektiriyordu.
Parça Boyutlarını Hesaplama
Her oyuncu katıldıkça cihazıyla ilgili bilgiler sunucuya gönderilir ve diğer oyuncularla paylaşılır. Parça oluşturulurken bu veriler, parçanın 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. Genişlik ise tüm ekranların toplam genişliğidir. Bu nedenle, aşağıdaki örnekte parçanın genişliği 1.152 piksel, yüksekliği ise 519 piksel olur.

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;
}
Parçayı çizme
Paper.js, HTML5 Canvas'ın üzerinde çalışan açık kaynaklı bir vektör grafik komut dosyası çerçevesidir. Paper.js'in, parçalar için vektör şekilleri oluşturmak üzere mükemmel bir araç olduğunu tespit ettik. Bu nedenle, Adobe Illustrator'da oluşturulan SVG parçaları bir <canvas>
öğesinde oluşturmak için Paper.js'in özelliklerinden yararlandık. TrackModel
sınıfı, parçayı oluşturmak için SVG kodunu DOM'a ekler ve parçayı bir kanvas üzerine çizecek TrackPathView
sınıfına aktarılacak orijinal boyutlar ve konumlandırma hakkında bilgi toplar.
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;
Parça çizildikten sonra her cihaz, cihaz serisindeki sırasına göre x ofsetini bulur ve parçayı buna göre konumlandırır.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

CSS Animasyonları
Paper.js, yol şeritlerini çizmek için çok fazla CPU işleme kullanır ve bu işlem farklı cihazlarda daha az veya daha fazla zaman alır. Bu sorunu çözmek için tüm cihazlar parçayı işleyene kadar döngüde çalışan bir yükleyiciye ihtiyacımız vardı. Sorun, Paper.js'in CPU gereksinimleri nedeniyle JavaScript tabanlı animasyonların kareleri atlamasıydı. Ayrı bir kullanıcı arayüzü iş parçacığında çalışan CSS animasyonları sayesinde "BUILDING TRACK" metninde parıltıyı sorunsuz bir şekilde canlandırabiliriz.
.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 görseller
CSS, oyun içi efektler için de kullanışlı oldu. Sınırlı güçleri olan mobil cihazlar, pistlerde giden arabaları animasyonlu olarak göstermekle meşguldür. Bu nedenle, daha fazla heyecan katmak için önceden oluşturulmuş animasyonları oyuna uygulamak amacıyla sprite'ları kullandık. CSS sprite'inde geçişler, background-position
mülkünü değiştirerek araba patlamasını 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 tekniğin sorunu, yalnızca tek bir satırda düzenlenmiş sprite sayfalarını kullanabilmenizdir. Birden fazla satırda döngü oluşturmak için animasyon, birden fazla animasyon karesi beyanı aracılığıyla zincirlenmelidir.
#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şturma
Her araba yarışı oyununda olduğu gibi, kullanıcıya hızlanma ve kontrol hissi vermenin önemli olduğunu biliyorduk. Farklı miktarlarda çekiş gücü uygulamak, oyun dengesi ve eğlence faktörü açısından önemliydi. Böylece oyuncular, fiziği anladıktan sonra başarı hissi yaşayıp daha iyi yarışçı olabilirdi.
Geniş bir matematik yardımcı programı grubuna sahip olan Paper.js'i bir kez daha kullandık. Aracın konumunu ve dönüşünü her karede sorunsuz bir şekilde ayarlayarak aracı yolda hareket ettirmek için bu yöntemlerden bazılarını kullandı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);
}
Araba oluşturma işlemini optimize ederken ilginç bir noktaya rastladık. iOS'te en iyi performans, araca translate3d
dönüşümü uygulanarak elde edildi:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
Android için Chrome'da en iyi performans, matris değerlerinin hesaplanması ve matris dönüşümü uygulanmasıyla elde edildi:
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 tutma
Geliştirmenin en önemli (ve en zor) kısmı, oyunun cihazlar arasında senkronize edilmesini sağlamaktı. Yavaş bağlantı nedeniyle bir aracın zaman zaman birkaç kare atlamasını kullanıcıların affedebileceğini düşündük. Ancak aracınızın birden fazla ekranda aynı anda görünmesi ve sıçraması pek eğlenceli olmaz. Bu sorunu çözmek için çok sayıda deneme yanılma yöntemi denedik ancak sonunda işe yarayan birkaç yola karar verdik.
Gecikmeyi hesaplama
Cihazları senkronize etmenin başlangıç noktası, mesajların Compute Engine aktarıcısından alınmasının ne kadar sürdüğünü bilmektir. Zor olan kısım, her cihazdaki saatlerin hiçbir zaman tamamen senkronize olmamasıdır. Bu sorunun üstesinden gelmek için cihaz ile sunucu arasındaki zaman farkını bulmamız gerekiyordu.
Cihaz ile ana sunucu arasındaki zaman farkının bulunması için mevcut cihaz zaman damgasını içeren bir mesaj göndeririz. Sunucu, orijinal zaman damgasını ve sunucunun zaman damgasını içeren bir yanıt gönderir. Gerçek zaman farkını hesaplamak için yanıtı 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, yanıtın sunucuya ulaşması sunucunun yanıtı döndürmesinden daha uzun sürebilir. Bu nedenle, bunu bir kez yapmak yeterli değildir. Bu sorunun üstesinden gelmek için sunucuyu birden çok kez yoklayarak ortalama sonucu alırız. Bu sayede cihaz ile sunucu arasındaki gerçek farkın 10 ms'den daha az olduğu sonucuna varırız.
İvmelenme/Yavaşlama
1. Oyuncu ekrana bastığında veya ekrandan elini çektiğinde ivmelenme etkinliği sunucuya gönderilir. Sunucu, bu verileri aldıktan sonra mevcut zaman damgasını ekler ve ardından bu verileri diğer tüm oyunculara iletir.
Bir cihaz "hızlandırmayı etkinleştir" veya "hızlandırmayı devre dışı bırak" etkinliği aldığında, bu mesajın alınmasının ne kadar sürdüğünü öğrenmek için sunucu ofsetini (yukarıda hesaplanmıştır) kullanabiliriz. Bu, 1. oyuncunun mesajı 20 ms içinde alabilmesine rağmen 2. oyuncunun mesajı 50 ms içinde alabilmesi nedeniyle kullanışlıdır. Bu durumda, 1. cihaz hızlandırmayı daha erken başlatacağından araba iki farklı yerde olur.
Etkinliğin alınması için geçen süreyi karelere dönüştürebiliriz. 60 fps'de her kare 16,67 ms olduğundan, kaçırdığı kareleri hesaba katmak için arabaya daha fazla hız (ivme) veya sürtünme (yavaşlama) ekleyebilirsiniz.
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, 1. Oyuncu'nun ekranında araba varsa ve mesajı alması 75 ms'den kısaysa arabanın hızını ayarlayarak farkı telafi etmek için hızlandırır. Cihaz ekranda değilse veya mesaj çok uzun sürdüyse oluşturma işlevi çalıştırılır ve araç olması gereken yere atlar.
Araçları senkronize tutma
Hızlanmadaki gecikmeler hesaba katıldığında bile araba senkronizasyonunu kaybedebilir ve özellikle bir cihazdan diğerine geçiş yaparken aynı anda birden fazla ekranda görünebilir. Bunu önlemek için, arabaların tüm ekranlarda pistte aynı konumda kalması amacıyla sık sık güncelleme etkinlikleri gönderilir.
Mantık şudur: Araba ekranda görünüyorsa her 4 karede bir bu cihaz, değerlerini diğer cihazların her birine gönderir. Araba görünmüyorsa uygulama, değerleri alınan değerlerle günceller ve ardından güncelleme etkinliğinin alınması için geçen süreye göre arabayı ileri doğru 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'ın konseptini duyduğumuz anda çok özel bir proje olabileceğini anladık. Hızlıca bir prototip oluşturduk. Bu prototip, gecikme süresi ve ağ performansının nasıl aşılacağı konusunda bize kabaca bir fikir verdi. Gece geç saatlerde ve uzun hafta sonlarında bizi meşgul eden zorlu bir projeydi ancak oyun şekillenmeye başladığında harika bir duyguydu. Sonuçta, elde ettiğimiz sonuca çok memnunuz. Google Creative Lab'in konsepti, tarayıcı teknolojisinin sınırlarını eğlenceli bir şekilde zorladı. Geliştirici olarak bundan daha fazlasını isteyemezdik.