Örnek Olay - Web Sesi İçeren Bir HTML5 Oyununun Öyküsü

Saha koşucuları

Fieldrunners ekran görüntüsü
Fieldrunners ekran görüntüsü

Fieldrunners, ilk olarak 2008'de iPhone için kullanıma sunulan ödüllü bir kule savunması tarzı oyundur. O zamandan beri bu özellik birçok farklı platforma aktarıldı. En yeni platformlardan biri, Ekim 2011'de Chrome tarayıcı oldu. Fieldrunner'ları bir HTML5 platformuna taşımanın zorluklarından biri, sesin nasıl çalınacağıydı.

Saha koşucuları ses efektlerini karmaşık bir şekilde kullanmıyor ancak bu efektlerin ses efektleriyle nasıl etkileşim kurabileceği konusunda bazı beklentiler içeriyor. Oyunda 88 ses efekti var. Bu efektlere aynı anda çok sayıda ses efekti oynanabilir. Bu seslerin çoğu çok kısadır ve grafik sunumla herhangi bir kopukluk olmaması için mümkün olduğunca zamanında çalınmalıdır.

Bazı Zorluklar Karşılaşıldı

Fieldrunners'ı HTML5'e taşırken Ses etiketiyle ses çalmayla ilgili sorunlarla karşılaştık ve erkenden bunun yerine Web Audio API'sına odaklanmaya karar verdik. WebAudio kullanmak, Fieldrunners'ın gerektirdiği çok sayıda eşzamanlı efekti sunma gibi sorunları çözmemize yardımcı oldu. Yine de Fieldrunners HTML5 için bir ses sistemi geliştirirken diğer geliştiricilerin bilmek isteyebileceği birkaç küçük sorunla karşılaştık.

AudioBufferSourceNodes Yapısı

AudioBufferSourceNodes, WebAudio ile ses çalmanın birincil yönteminizdir. Bunların tek kullanımlık bir nesne olduğunun anlaşılması çok önemlidir. Bir AudioBufferSourceNode oluşturur, ona bir arabellek atar, grafiğe bağlayın ve noteOn veya noteGrainOn ile oynatırsınız. Ardından, çalmayı durdurmak için noteOff işlevini çağırabilirsiniz, ancak noteOn veya noteGrainOn yöntemini çağırarak kaynağı tekrar oynatamazsınız. Başka bir AudioBufferSourceNode oluşturmanız gerekir. Bununla birlikte, aynı alttaki AudioBuffer nesnesini yeniden kullanabilirsiniz (bu da önemlidir) (aslında, aynı AudioBuffer örneğine işaret eden birden çok etkin AudioBufferSourceNode'unuz bile olabilir!). Give Me a Beat'te Fieldrunners'dan bir oynatma snippet'i bulabilirsiniz.

Önbelleğe alınmayan içerik

Yayınlanma tarihinde Fieldrunners HTML5 sunucusu, müzik dosyaları için çok sayıda istek gösterdi. Bu sonuç, Chrome 15 sürümünde dosyayı parçalar halinde indirip önbelleğe almama sonucunda ortaya çıkmıştır. O zamana karşılık olarak, diğer ses dosyalarımız gibi müzik dosyalarını da yüklemeye karar verdik. Bunu yapmak ideal değildir ancak diğer tarayıcıların bazı sürümleri bunu yapmaya devam eder.

Odak dışındayken sessize alma

Daha önce oyununuzun sekmesinin odak dışına çıktığını algılamak zordu. Fieldrunners, Chrome 13'ten önce taşımaya başladı. Bu sürümle birlikte, sekme bulanıklaştırmayı algılamak için karmaşık koda olan ihtiyacın yerini Sayfa Görünürlüğü API'si aldı. Her oyun, tüm oyunu duraklatmazsa, sesini kapatmak veya duraklatmak için küçük bir snippet yazmak üzere Görünürlük API'sini kullanmalıdır. Fieldrunners requestAnimationFrame API'sini kullandığından, oyun duraklatma dolaylı olarak işlendi, ancak ses duraklatma söz konusu değildi.

Sesler duraklatılıyor

İşin ilginç tarafı, bu makaleye ilişkin geri bildirim aldığımızda, sesleri duraklatmak için kullandığımız tekniğin uygun olmadığı bildirildi. Seslerin çalınmasını duraklatmak için Web Audio'nun mevcut uygulamasında bir hata kullanıyorduk. Bu sorun ileride düzeltileceğinden, oynatmayı durdurmak için bir düğümün veya alt grafiğin bağlantısını keserek sesi duraklatamazsınız.

Basit Web Ses Düğümü Mimarisi

Fieldrunners'ın çok basit bir ses modeli var. Bu model aşağıdaki özellik grubunu destekleyebilir:

  • Ses efektlerinin ses düzeyini kontrol etme.
  • Arka plan müzik parçasının ses düzeyini denetleyin.
  • Tüm sesleri kapatın.
  • Oyun duraklatıldığında ses çalmayı kapatın.
  • Oyun devam ettirildiğinde aynı sesleri tekrar açın.
  • Oyunun sekmesinin odağı kaybolduğunda tüm sesleri kapatır.
  • Gerektiğinde bir ses çalındıktan sonra oynatmayı yeniden başlatın.

Yukarıdaki özelliklere Web Audio ile ulaşmak için, sağlanan olası düğümlerden 3'ünü kullandı: DestinationNode, GainNode, AudioBufferSourceNode. AudioBufferSourceNodes sesleri çalar. GainNodes, AudioBufferSourceNodes'u birbirine bağlar. Hedef olarak adlandırılan, Web Audio bağlamı tarafından oluşturulan DestinationNode, oynatıcı için sesler çalar. Web Audio'nun daha pek çok düğüm türü vardır ancak yalnızca bunlarla bir oyundaki sesler için çok basit bir grafik oluşturabiliriz.

Düğüm Grafiği Grafiği

Bir Web Sesi düğüm grafiği, yaprak düğümlerinden hedef düğüme doğru uzanıyor. Fieldrunners 6 kalıcı kazanç düğümü kullandı ancak 3 tanesi hacim üzerinde kolay kontrol sağlamak ve arabellek oynatacak daha fazla sayıda geçici düğüm bağlamak için yeterli. Öncelikle, her alt düğümü hedefe bağlayan bir ana kazanç düğümü. Ana kazanç düğümüne doğrudan, biri müzik kanalı ve diğeri tüm ses efektlerini bağlamak için kullanılan iki kazanç düğümü vardır.

Fieldrunners, bir hatanın özellik olarak yanlış kullanımı nedeniyle fazladan 3 kazanç düğümüne sahipti. Bu düğümleri, ilerlemelerini durduracak şekilde grafikteki çalan sesleri kesip çıkarmak için kullandık. Bu işlemi sesleri duraklatmak için yaptık. Bu doğru olmadığından, artık yukarıda açıklandığı gibi yalnızca 3 toplam kazanç düğümü kullanabiliriz. Aşağıdaki snippet'lerin çoğu hatalı düğümlerimizi içerecek, ne yaptığımızı ve kısa vadede bunu nasıl düzelteceğimizi gösterecektir. Ancak uzun vadede, coreEffectsGain düğümümüzden sonra düğümlerimizi kullanmak istemezsiniz.

function AudioManager() {
  // map for loaded sounds
  this.sounds = {};

  // create our permanent nodes
  this.nodes = {
    destination: this.audioContext.destination,
    masterGain: this.audioContext.createGain(),

    backgroundMusicGain: this.audioContext.createGain(),

    coreEffectsGain: this.audioContext.createGain(),
    effectsGain: this.audioContext.createGain(),
    pausedEffectsGain: this.audioContext.createGain()
  };

  // and setup the graph
  this.nodes.masterGain.connect( this.nodes.destination );

  this.nodes.backgroundMusicGain.connect( this.nodes.masterGain );

  this.nodes.coreEffectsGain.connect( this.nodes.masterGain );
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
  this.nodes.pausedEffectsGain.connect( this.nodes.coreEffectsGain );
}

Çoğu oyunda, ses efektleri ve müzik ayrı ayrı kontrol edilebilir. Yukarıdaki grafiğimizi kullanarak bunu kolayca başarabilirsiniz. Her kazanç düğümü, 0 ile 1 arasında herhangi bir ondalık değere ayarlanabilen bir "kazanç" özelliğine sahiptir. Bu özellik, temelde hacmi kontrol etmek için kullanılabilir. Müzik ve ses efekti kanallarının ses seviyelerini ayrı ayrı kontrol etmek istediğimizden her biri için ses seviyelerini kontrol edebileceğimiz bir kazanç düğümümüz var.

function setArbitraryVolume() {
  var musicGainNode = this.nodes.backgroundMusicGain;

  // set music volume to 50%
  musicGainNode.gain.value = 0.5;
}

Ses efektlerinin ve müziğin her şeyin ses düzeyini kontrol etmek için de bu özelliği kullanabiliriz. Ana düğümün kazancını ayarlamak oyundaki tüm sesleri etkiler. Kazanç değerini 0 olarak ayarlarsanız sesi ve müziği kapatırsınız. AudioBufferSourceNodes bir kazanç parametresine de sahiptir. Çalınan tüm seslerin listesini izleyebilir ve toplam ses düzeyi için kazanç değerlerini tek tek ayarlayabilirsiniz. Ses etiketlerini kullanarak ses efektleri yapıyor olsaydınız bunu yapmanız gerekirdi. Bunun yerine, Web Audio'nun düğüm grafiği, sayısız sesin ses düzeyini değiştirmeyi çok daha kolay hale getirir. Ses seviyesini bu şekilde kontrol ederek karmaşa yaşamadan fazladan güç elde edersiniz. Müzik çalmak ve kendi kazancını kontrol etmek için bir AudioBufferSourceNode'u doğrudan ana düğüme ekleyebiliriz. Ancak, müzik çalmak amacıyla her AudioBufferSourceNode oluşturduğunuzda bu değeri ayarlamanız gerekir. Bunun yerine, bir düğüm sadece bir oynatıcı müzik ses düzeyini değiştirdiğinde ve başlangıçta değiştirilir. Artık başka bir şey yapmamız için tampon kaynaklarında değer kazanmış durumdayız. Müzikler için yaygın bir kullanım, bir parçadan diğerine geçişin çapraz geçişinin oluşturulması olabilir. Web Audio, bunu kolayca gerçekleştirmek için iyi bir yöntem sağlar.

function arbitraryCrossfade( track1, track2 ) {
  track1.gain.linearRampToValueAtTime( 0, 1 );
  track2.gain.linearRampToValueAtTime( 1, 1 );
}

Saha koşucuları özel olarak çapraz geçişleri kullanmadılar. Ses sistemini ilk kez aktarırken WebAudio'nun değer ayarlama işlevini bilmemiz gerekecekti.

Sesler Duraklatılıyor

Oyuncu oyunu duraklattığında bazı seslerin çalmaya devam etmesini bekleyebilir. Ses, oyun menülerindeki kullanıcı arayüzü öğelerine genel olarak basılmasıyla ilgili geri bildirimin mükemmel bir parçasıdır. Fieldrunners'ın oyun duraklatılmışken kullanıcının etkileşimde bulunabileceği çeşitli arayüzler olduğundan, bunları oynamaya devam etmelerini istiyoruz. Ancak uzun veya döngüsel seslerin çalmaya devam etmesini istemiyoruz. Web Audio ile bu sesleri durdurmak oldukça kolay ya da en azından biz öyle düşündük.

AudioManager.prototype.pauseEffects = function() {
  this.nodes.effectsGain.disconnect();
}

Duraklatılan efektler düğümü hâlâ bağlı. Oyunun duraklatılmış durumunu yoksaymasına izin verilen tüm sesler bu modda çalmaya devam eder. Oyun devam ettirildiğinde bu düğümleri yeniden bağlayabilir ve tüm sesin anında tekrar çalmasını sağlayabiliriz.

AudioManager.prototype.resumeEffects = function() {
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
}

Fieldrunners'ı gönderdikten sonra, tek başına bir düğümün veya alt grafiğin bağlantısının kesilmesinin AudioBufferSourceNodes'un çalınmasını duraklatmayacağını fark ettik. WebAudio'da, şu anda grafikteki Hedef düğüme bağlı olmayan düğümlerin oynatılmasını engelleyen bir hatadan yararlandık. Gelecekte bu düzeltmeye hazır olduğumuzdan emin olmak için aşağıdaki gibi bir koda ihtiyacımız var:

AudioManager.prototype.pauseEffects = function() {
  this.nodes.effectsGain.disconnect();

  var now = Date.now();
  for ( var name in this.sounds ) {
    var sound = this.sounds[ name ];

    if ( !sound.ignorePause && ( now - sound.source.noteOnAt < sound.buffer.duration * 1000 ) ) {
      sound.pausedAt = now - sound.source.noteOnAt;
      sound.source.noteOff();
    }
  }
}

AudioManager.prototype.resumeEffects = function() {
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );

  var now = Date.now();
  for ( var name in this.sounds ) {
    if ( sound.pausedAt ) {
      this.play( sound.name );
      delete sound.pausedAt;
    }
  }
};

Bunu daha önce, bir hatayı kötüye kullandığımızı bilseydik ses kodumuzun yapısı çok farklı olurdu. Bu nedenle, bu makalenin birçok bölümü bu durumdan etkilendi. Bu yöntemin doğrudan etkisi vardır. Aynı zamanda, Losing Focus and Give Me a Beat'teki kod snippet'lerimizde de etkisi vardır. Bunun gerçekte nasıl çalıştığını bilmek, hem Fieldrunners düğüm grafiğinde (oynatma işlemini kısaltmak için düğümler oluşturduğumuz için) hem de Web Audio'nun kendi başına yapmadığı duraklatılmış durumları kaydedip sağlayacak ek kodda değişiklik yapılmasını gerektirir.

Odağı Kaybediyor

Bu özellik için ana düğümümüz devreye girer. Tarayıcı kullanıcısı başka bir sekmeye geçtiğinde oyun artık gösterilmez. Görüntüden, gönülden ve ses de gitmiş olmalı. Bir oyun sayfasına ilişkin belirli görünürlük durumlarını belirlemek için yapılabilecek bazı püf noktaları vardır, ancak Görünürlük API'sı ile bu işlem çok daha kolay hale geldi.

Güncelleme döngüsünü çağırmak için requestAnimationFrame kullanıldığından, saha koşucuları yalnızca etkin sekme olarak oynarlar. Ancak Web Audio bağlamı, kullanıcı başka bir sekmedeyken döngüsel efektleri ve arka plan parçalarını oynatmaya devam eder. Ancak bunu, çok küçük bir Görünürlük API'sine duyarlı snippet ile durdurabiliriz.

function AudioManager() {
  // map and node setup
  // ...

  // disable all sound when on other tabs
  var self = this;
  window.addEventListener( 'webkitvisibilitychange', function( e ) {
    if ( document.webkitHidden ) {
      self.nodes.masterGain.disconnect();

      // As noted in Pausing Sounds disconnecting isn't enough.
      // For Fieldrunners calling our new pauseEffects method would be
      // enough to accomplish that, though we may still need some logic
      // to not resume if already paused.
      self.pauseEffects();
    } else {
      self.nodes.masterGain.connect( this.nodes.destination );
      self.resumeEffects();
    }
  });
}

Bu makaleyi yazmadan önce, ana makinenin bağlantısını kesmenin, ana sesi kapatmak yerine tüm sesi duraklatmak için yeterli olacağını düşünmüştük. Aynı zamanda düğümün bağlantısını keserek bu düğümü ve alt öğelerini işlemeyi ve oynamayı durdurduk. Tekrar bağlandığında, oyun kaldığı yerden devam edeceği gibi tüm sesler ve müzik kaldığı yerden devam ediyordu. Ancak bu beklenmedik bir davranıştır. Oynatmayı durdurmak için bağlantıyı kesmek yeterli değildir.

Sayfa Görünürlüğü API'si, sekmenizin artık odakta olmadığını kolayca görmenizi sağlar. Sesleri duraklatmak için kullanabileceğiniz etkili bir kodunuz varsa oyunlar sekmesi gizlendiğinde ses duraklatma için sadece birkaç satır yazmanız yeterlidir.

Bir Ritim Verin

Şu anda ayarlanmış birkaç şeyimiz var. Düğüm grafiğimiz var. Oyuncu oyunu duraklattığında sesleri duraklatabilir ve oyun menüleri gibi öğeler için yeni sesler çalabiliriz. Kullanıcı yeni bir sekmeye geçtiğinde tüm sesi ve müziği duraklatabiliriz. Şimdi gerçekten bir ses çalmamız gerekiyor.

Fieldrunners, karakterin ölmesi gibi bir oyun varlığının birden fazla örneği için sesin birden fazla kopyasını çalmak yerine oyun süresince yalnızca bir ses çalar. Çalınması bittikten sonra ses gerekiyorsa cihaz yeniden başlatılabilir, ancak zaten çalınırken çalınmaz. Bu, hızlı bir şekilde çalınması istenen seslere sahip olduğu ve yeniden başlatılmasına izin verilirse veya birden fazla örneği oynatmasına izin verildiği takdirde hoş olmayan bir kakofoni oluşturduğu için Fieldrunners'ın ses tasarımı için bir karardır. AudioBufferSourceNodes'un tek çekim olarak kullanılması beklenir. Bir düğüm oluşturun, bir arabellek ekleyin, gerekirse döngü boole değerini ayarlayın, grafik üzerinde hedefe yönlendirme yapacak düğüme bağlanın, noteOn veya noteGrainOn öğesini çağırın ve isteğe bağlı olarak noteOff yöntemini çağırın.

Fieldrunners için bu görünüm şuna benzer:

AudioManager.prototype.play = function( options ) {
  var now = Date.now(),
    // pull from a map of loaded audio buffers
    sound = this.sounds[ options.name ],
    channel,
    source,
    resumeSource;

  if ( !sound ) {
    return;
  }

  if ( sound.source ) {
    var source = sound.source;
    if ( !options.loop && now - source.noteOnAt > sound.buffer.duration * 1000 ) {
      // discard the previous source node
      source.stop( 0 );
      source.disconnect();
    } else {
      return;
    }
  }

  source = this.audioContext.createBufferSource();
  sound.source = source;
  // track when the source is started to know if it should still be playing
  source.noteOnAt = now;

  // help with pausing
  sound.ignorePause = !!options.ignorePause;

  if ( options.ignorePause ) {
    channel = this.nodes.pausedEffectsGain;
  } else {
    channel = this.nodes.effectsGain;
  }

  source.buffer = sound.buffer;
  source.connect( channel );
  source.loop = options.loop || false;

  // Fieldrunners' current code doesn't consider sound.pausedAt.
  // This is an added section to assist the new pausing code.
  if ( sound.pausedAt ) {
    source.start( ( sound.buffer.duration * 1000 - sound.pausedAt ) / 1000 );
    source.noteOnAt = now + sound.buffer.duration * 1000 - sound.pausedAt;

    // if you needed to precisely stop sounds, you'd want to store this
    resumeSource = this.audioContext.createBufferSource();
    resumeSource.buffer = sound.buffer;
    resumeSource.connect( channel );
    resumeSource.start(
      0,
      sound.pausedAt,
      sound.buffer.duration - sound.pausedAt / 1000
    );
  } else {
    // start play immediately with a value of 0 or less
    source.start( 0 );
  }
}

Akış Çok Fazla

Fieldrunners ilk olarak bir ses etiketiyle çalınan arka plan müziğiyle yayınlandı. Lansman sırasında müzik dosyalarının, diğer oyun içeriklerinin isteklerinden orantısız sayıda talep edildiğini fark ettik. Biraz araştırmadan sonra, Chrome tarayıcısının müzik dosyalarının akış şeklindeki parçalarını önbelleğe almadığını keşfettik. Bu da tarayıcının, biterken çalan parçayı birkaç dakikada bir istemesine neden oldu. Daha yeni testlerde, Chrome tarafından önbelleğe alınan kanallar akış gerçekleştirildi ancak diğer tarayıcılar henüz bunu yapmıyor olabilir. Müzik çalma gibi işlevler için Ses etiketine sahip büyük ses dosyalarını akışla yayınlamak en uygun seçenektir ancak bazı tarayıcı sürümlerinde müziğinizi, ses efektlerini yüklediğiniz şekilde yüklemek isteyebilirsiniz.

Tüm ses efektleri Web Audio üzerinden oynatıldığından, arka plan müziğini de Web Audio'ya taşıdık. Bu, kanalları tüm efektleri XMLHttpRequests ve dizi arabelleği yanıt türüyle yüklediğimiz şekilde yükleyeceğimiz anlamına geliyordu.

AudioManager.prototype.load = function( options ) {
  var xhr,
      // pull from a map of name, object pairs
      sound = this.sounds[ options.name ];

  if ( sound ) {
    // this is a great spot to add success methods to a list or use promises
    // for handling the load event or call success if already loaded
    if ( sound.buffer && options.success ) {
      options.success( options.name );
    } else if ( options.success ) {
      sound.success.push( options.success );
    }

    // one buffer is enough so shortcut here
    return;
  }

  sound = {
    name: options.name,
    buffer: null,
    source: null,
    success: ( options.success ? [ options.success ] : [] )
  };
  this.sounds[ options.name ] = sound;

  xhr = new XMLHttpRequest();
  xhr.open( 'GET', options.path, true );
  xhr.responseType = 'arraybuffer';
  xhr.onload = function( e ) {
    sound.buffer = self._context.createBuffer( xhr.response, false );

    // call all waiting handlers
    sound.success.forEach( function( success ) {
      success( sound.name );
    });
    delete sound.success;
  };
  xhr.onerror = function( e ) {

    // failures are uncommon but you want to do deal with them

  };
  xhr.send();
}

Özet

Fieldrunners, Chrome ve HTML5'te harikalar yarattı. Binlerce C++ satırını JavaScript'e dahil eden kendi çalışmalarının dışında, HTML5'e özgü bazı ilginç ikilemler ve kararlar can sıkıcı hale gelmiştir. Bunlardan birini tekrarlamak gerekirse, AudioBufferSourceNodes tek seferlik kullanım nesneleridir. Bunları oluşturun, bir Ses Arabelleği ekleyin, Web Audio grafiğine bağlayın ve noteOn veya noteGrainOn ile çalın. O sesi tekrar çalmanız mı gerekiyor? Ardından başka bir AudioBufferSourceNode oluşturun.