Giriş
Racer, çok oyunculu ve çok cihazlı bir Chrome denemesidir. Ekranlar arasında oynanan retro tarzı bir slot araba oyunu. Android veya iOS işletim sistemli telefonlarda ya da tabletlerde Herkes katılabilir. Uygulama yok. İndirmeye gerek yok. Yalnızca mobil web'de.
Plan8, 14islands'taki arkadaşlarımızla birlikte Giorgio Moroder'in orijinal bestesine dayalı dinamik bir müzik ve ses deneyimi oluşturdu. Racer'da duyarlı motor sesleri ve yarış ses efektleri bulunur. Ancak en önemlisi, yarışçılar katıldıkça kendini çeşitli cihazlara dağıtan dinamik bir müzik karışımıdır. Akıllı telefonlardan oluşan çok hoparlörlü bir kurulumdur.
Birden fazla cihazı birbirine bağlama fikri üzerinde bir süredir çalışıyorduk. Sesin farklı cihazlara bölündüğü veya cihazlar arasında zıpladığı müzik denemeleri yapmıştık. Bu fikirleri Racer'a uygulamak için sabırsızlanıyorduk.
Daha ayrıntılı olarak belirtmek gerekirse, oyuna daha fazla kullanıcı katıldıkça davul ve bas ile başlayıp gitar ve sentezleyiciler ekleyerek cihazlar arasında müzik parçasını oluşturup oluşturamayacağımızı test etmek istedik. Bazı müzik denemeleri yaptık ve kodlamaya daldık. Çok hoparlörlü efekt gerçekten çok faydalıydı. Bu noktada senkronizasyon tam olarak doğru değildi ancak cihazlara yayılan ses katmanlarını duyduğumuzda iyi bir şeye imza attığımızı anladık.
Sesleri oluşturma
Google Creative Lab, ses ve müzik için yaratıcı bir yön belirledi. Gerçek sesleri kaydetmek veya ses kitaplıklarına başvurmak yerine, ses efektlerini oluşturmak için analog sentezleyiciler kullanmak istedik. Çıkış hoparlörü çoğu durumda küçük bir telefon veya tablet hoparlörü olacağından, hoparlörlerin bozulmasını önlemek için seslerin frekans spektrumunda sınırlandırılması gerektiğini de biliyorduk. Bu, oldukça zor bir süreç oldu. Giorgio'dan ilk müzik taslaklarını aldığımızda rahatladık. Çünkü bestesi, oluşturduğumuz seslerle mükemmel bir uyum sağladı.
Motor sesi
Sesleri programlarken en büyük zorluk, en iyi motor sesini bulmak ve davranışını şekillendirmekti. Yarış pisti bir F1 veya Nascar pistine benziyordu. Bu nedenle, arabaların hızlı ve patlayıcı hissi vermesi gerekiyordu. Aynı zamanda arabalar çok küçük olduğu için büyük bir motor sesi, sesle görselleri birbirine bağlamıyordu. Mobil hoparlörde güçlü bir motorun gürlemesini çalamayacaktık. Bu yüzden başka bir çözüm bulmak zorunda kaldık.
İlham almak için arkadaşımız Jon Ekstrand'ın modüler sentezleyici koleksiyonundan bazılarını bağladık ve denemeye başladık. Bu geri bildirim bizi mutlu etti. İki osilatörü, bazı güzel filtreleri ve LFO'yu kullanarak böyle bir ses elde ettim.
Web Audio API daha önce analog ekipmanların yeniden tasarlanmasında büyük başarılar elde etmişti. Bu nedenle, Web Audio'da basit bir sentezleyici oluşturmaya başladık. Oluşturulan ses en duyarlı seçenektir ancak cihazın işlem gücünü zorlar. Görsellerin sorunsuz bir şekilde çalışması için mümkün olan tüm kaynakları kullanmak üzere son derece sade bir yaklaşım benimsememiz gerekiyordu. Bu nedenle, ses örnekleri çalmak için tekniği değiştirdik.

Sana Özel bölümündeki seslerden motor sesi oluşturmak için kullanılabilecek birkaç teknik vardır. Konsol oyunlarında en yaygın yaklaşım, farklı devir sayılarında (yük altında) motorun birden fazla sesinden (ne kadar çok olursa o kadar iyi) oluşan bir katman oluşturmak ve ardından bunlar arasında geçiş yapmak ve ses perdesini değiştirmek olacaktır. Ardından, aynı devirde (yük olmadan) çalışan motorun birden fazla sesini ekleyin ve iki ses arasında geçiş yapın. Vites değiştirirken bu katmanlar arasında doğru şekilde yapılan geçişler çok gerçekçi bir ses verir ancak bunun için çok sayıda ses dosyanız olması gerekir. Çapraz perde çok geniş olamaz. Aksi takdirde çok yapay bir ses çıkar. Uzun yükleme sürelerinden kaçınmamız gerektiği için bu seçenek bizim için uygun değildi. Her katman için beş veya altı ses dosyası kullanmayı denedik ancak sesler hayal kırıklığına uğratacak kadar kötüydü. Daha az dosya içeren bir yöntem bulmak zorunda kaldık.
En etkili çözüm şu şekilde oldu:
- Arabanın görsel hızlanma hızıyla senkronize edilmiş hızlanma ve vites değiştirme seslerinin yer aldığı bir ses dosyası. Dosya, en yüksek perde / devir sayısıyla programlanmış bir döngüde sona erer. Web Audio API, döngü oluşturma konusunda çok başarılıdır. Bu sayede, bu işlemi herhangi bir kesinti veya ses patlaması olmadan yapabildik.
- Yavaşlama / motor devrinin düşmesi içeren bir ses dosyası.
- Son olarak, hareketsiz / bekleme sesini döngüde çalan bir ses dosyası.
Şu şekilde görünür

İlk dokunma etkinliği / gaz verme için ilk dosyayı baştan oynatırız. Oynatıcı gaz pedalını bırakırsa ses dosyasında nerede olduğumuzu hesaplarız. Böylece, gaz pedalı tekrar açıldığında ikinci (devir düşürme) dosya oynatıldıktan sonra gaz verme dosyasında doğru yere atlanır.
function throttleOn(throttle) {
//Calculate the start position depending
//on the current amount of throttle.
//By multiplying throttle we get a start position
//between 0 and 3 seconds.
var startPosition = throttle * 3;
var audio = context.createBufferSource();
audio.buffer = loadedBuffers["accelerate_and_loop"];
//Sets the loop positions for the buffer source.
audio.loopStart = 5;
audio.loopEnd = 9;
//Starts the buffer source at the current time
//with the calculated offset.
audio.start(context.currentTime, startPosition);
}
Deneyin
Motoru çalıştırın ve "Gaz" düğmesine basın.
<input type="button" id="playstop" value = "Start/Stop Engine" onclick='playStop()'>
<input type="button" id="throttle" value = "Throttle" onmousedown='throttleOn()' onmouseup='throttleOff()'>
Bu nedenle, yalnızca üç küçük ses dosyası ve iyi ses veren bir motorla bir sonraki zorluğa geçmeye karar verdik.
Senkronizasyonu alma
14islands'tan David Lindkvist ile birlikte, cihazların mükemmel bir senkronizasyonla çalma sorununu daha ayrıntılı bir şekilde incelemeye başladık. Temel teori basittir. Cihaz, sunucuya saatini sorar, ağ gecikmesini hesaba katar ve ardından yerel saat kaymasını hesaplar.
syncOffset = localTime - serverTime - networkLatency
Bu ofset sayesinde, bağlı her cihaz aynı zaman kavramını paylaşır. Kolay, değil mi? (Yine de teoride.)
Ağ gecikmesini hesaplama
Gecikmenin, sunucudan istek gönderip yanıt almanın yarısı olduğunu varsayabiliriz:
networkLatency = (receivedTime - sentTime) × 0.5
Bu varsayımdaki sorun, sunucuya gidip gelme işleminin her zaman simetrik olmamasıdır. Yani istek, yanıttan daha uzun sürebilir veya bunun tam tersi olabilir. Ağ gecikmesi ne kadar yüksek olursa bu asimetrinin etkisi de o kadar büyük olur. Bu da seslerin gecikmesine ve diğer cihazlarla senkronize olmadan çalınmasına neden olur.
Neyse ki beynimiz, seslerin biraz gecikmesini fark etmeyecek şekilde tasarlanmıştır. Yapılan çalışmalar, beynimizin sesleri ayrı olarak algılaması için 20 ila 30 milisaniye (msn) gecikme yaşandığını gösteriyor. Ancak yaklaşık 12-15 ms'de, gecikmeli bir sinyali tam olarak "algılayamazsanız" bile etkilerini "hissetmeye" başlarsınız. Birkaç yerleşik zaman senkronizasyon protokolünü ve daha basit alternatifleri inceledik ve bunlardan bazılarını uygulamaya çalıştık. Sonuç olarak, Google'ın düşük gecikmeli altyapısı sayesinde bir istek grubunu örnekleyip en düşük gecikmeye sahip örneği referans olarak kullanabildik.
Saat kaymasını önleme
İşe yaradı. 5'ten fazla cihazda mükemmel senkronizasyonla bir nabız ritmi çalıyorduk ancak bu durum yalnızca bir süreliğine sürdü. Sesi yüksek hassasiyetli Web Audio API bağlam zamanını kullanarak planlamış olsak bile, birkaç dakika oynatıldıktan sonra cihazlar birbirinden uzaklaşıyordu. Gecikme, her seferinde yalnızca birkaç milisaniye yavaş yavaş birikiyordu ve ilk başta fark edilemiyordu. Ancak daha uzun süre oynatıldıktan sonra müzik katmanlarının tamamen senkronize olmamasına neden oluyordu. Merhaba, saat kayması.
Çözüm, birkaç saniyede bir yeniden senkronize etmek, yeni bir saat kaydırma payı hesaplamak ve bunu ses planlayıcıya sorunsuz bir şekilde beslemekti. Ağ gecikmesi nedeniyle müzikte belirgin değişiklikler yaşanma riskini azaltmak için son senkronizasyon ofsetlerinin geçmişini saklayıp ortalamasını hesaplayarak değişikliği yumuşatmaya karar verdik.
Şarkı planlama ve düzenlemeleri değiştirme
Etkileşimli bir ses deneyimi oluşturmak, mevcut durumu değiştirmek için kullanıcı işlemlerine bağlı olduğunuz için artık şarkının hangi bölümlerinin ne zaman çalınacağını kontrol edemeyeceğiniz anlamına gelir. Şarkıdaki düzenlemeler arasında zamanında geçiş yapabilmemiz gerekiyordu. Yani planlayıcımızın, bir sonraki düzenlemeye geçmeden önce şu anda çalınan çubukta ne kadar süre kaldığını hesaplayabilmesi gerekiyordu. Algoritmamız şu şekilde göründü:
Client(1)
şarkıyı başlatır.Client(n)
, ilk istemciden şarkının ne zaman başlatıldığını sorar.Client(n)
, syncOffset değerini ve ses bağlamı oluşturulduğundan bu yana geçen süreyi dikkate alarak şarkının Web Audio bağlamını kullanarak başlatıldığı referans noktasını hesaplar.playDelta = Date.now() - syncOffset - songStartTime - context.currentTime
Client(n)
, playDelta değerini kullanarak şarkının ne kadar süredir oynatıldığını hesaplar. Şarkı planlayıcı, mevcut düzenlemede hangi çubuğun çalınacağını öğrenmek için bunu kullanır.playTime = playDelta + context.currentTime nextBar = Math.ceil((playTime % loopDuration) ÷ barDuration) % numberOfBars
Düzenlemelerimizin her zaman sekiz ölçü uzunluğunda ve aynı tempoda (dakikada vuruş) olması için düzenlemelerimizi sınırladık.
Önünüze bakın
JavaScript'te setTimeout
veya setInterval
kullanırken her zaman önceden planlama yapmanız önemlidir. Bunun nedeni, JavaScript saatinin çok hassas olmaması ve planlanmış geri çağırmaların düzen, oluşturma, çöp toplama ve XMLHTTPRequest'ler nedeniyle kolayca onlarca milisaniye veya daha fazla saptırılabilmesidir. Bizim durumumuzda, tüm istemcilerin ağ üzerinden aynı etkinliği almasının ne kadar sürdüğünü de hesaba katmamız gerekiyordu.
Ses sprite'ları
Sesleri tek bir dosyada birleştirmek, hem HTML Audio hem de Web Audio API için HTTP isteklerini azaltmanın mükemmel bir yoludur. Ayrıca, oynatmadan önce yeni bir ses nesnesi yüklemesi gerekmediğinden, ses nesnesi kullanılarak sesleri duyarlı bir şekilde oynatmanın en iyi yoludur. Başlangıç noktası olarak kullandığımız bazı iyi uygulamalar mevcuttur. Sprite'ımızı hem iOS hem de Android'de güvenilir bir şekilde çalışacak ve cihazların uykuya geçtiği bazı garip durumları ele alacak şekilde genişlettik.
Android'de, cihazı uyku moduna geçirseniz bile ses öğeleri çalmaya devam eder. Uyku modunda, JavaScript yürütme işlemi pil tasarrufu yapmak için sınırlandırılır ve geri çağırma işlevini başlatmak için requestAnimationFrame
, setInterval
veya setTimeout
'den yararlanamazsınız. Ses sprite'ları, oynatmanın durdurulup durdurulmayacağını kontrol etmek için JavaScript'i kullandığından bu bir sorundur. Daha da kötüsü, bazı durumlarda ses oynatılmaya devam etse bile Ses öğesinin currentTime
değeri güncellenmez.
Chrome Racer'da Web Audio dışında bir yedek olarak kullandığımız AudioSprite uygulamasına göz atın.
Ses öğesi
Racer üzerinde çalışmaya başladığımızda Android için Chrome henüz Web Audio API'yi desteklemiyordu. Bazı cihazlar için HTML Audio, diğerleri için Web Audio API kullanma mantığı, ulaşmak istediğimiz gelişmiş ses çıkışıyla birlikte bazı ilginç zorluklar oluşturdu. Neyse ki artık bu sorunla karşılaşmayacaksınız. Web Audio API, Android M28 beta sürümünde uygulanır.
- Gecikmeler/zamanlama sorunları. Ses öğesi her zaman tam olarak çalmasını istediğiniz zaman çalmaz. JavaScript tek iş parçacıklı olduğundan tarayıcı meşgul olabilir ve bu da iki saniyeye varan oynatma gecikmelerine neden olabilir.
- Oynatma gecikmeleri, sorunsuz bir şekilde döngü oynatmanın her zaman mümkün olmadığı anlamına gelir. Masaüstünde, çift arabelleğe alma özelliğini kullanarak neredeyse kesintisiz döngüler elde edebilirsiniz. Ancak mobil cihazlarda bu seçenek kullanılamaz. Bunun nedeni:
- Mobil cihazların çoğu aynı anda birden fazla ses öğesi çalamaz.
- Sabit ses seviyesi. Android veya iOS, bir Audio nesnesinin ses düzeyini değiştirmenize izin vermez.
- Önceden yükleme yapılamaz. Mobil cihazlarda, oynatma bir
touchStart
işleyicisinde başlatılmadığı sürece Ses öğesi kaynağını yüklemeye başlamaz. - Sorunları arayın. Sunucunuz HTTP Byte-Range'ı desteklemediği sürece
duration
değerini alma veyacurrentTime
değerini ayarlama işlemi başarısız olur. Bizim yaptığımız gibi bir ses sprite'i oluşturuyorsanız bu noktaya dikkat edin. - MP3'te temel yetkilendirme başarısız olur. Bazı cihazlar, hangi tarayıcıyı kullandığınızdan bağımsız olarak Temel Kimlik Doğrulama ile korunan MP3 dosyalarını yükleyemez.
Sonuçlar
Web'de sesle ilgili en iyi seçenek olarak sessize alma düğmesine bastığımızdan bu yana çok yol kat ettik. Ancak bu sadece başlangıç. Web'de sesler çok yakında çok daha iyi olacak. Birden fazla cihazın senkronizasyonu söz konusu olduğunda neler yapılabileceğine dair yalnızca yüzeysel bir inceleme yaptık. Telefon ve tabletlerde sinyal işleme ve efektlere (yankı gibi) girmek için yeterli işlem gücümüz yoktu ancak cihaz performansı arttıkça web tabanlı oyunlar da bu özelliklerden yararlanacak. Sesle ilgili yenilikleri geliştirmeye devam etmek için heyecan verici bir dönemdeyiz.