Ta spillets fordybelse til neste nivå med Responsive Game Music

Musikk som er i stand til å endre dynamisk og sømløst for å gjenspeile hva som skjer på skjermen, kan legge til et helt nytt nivå av nedsenking til et spill. I denne opplæringen tar vi en titt på en av de enkleste måtene å legge til lydig musikk til et spill.

Merk: Selv om denne opplæringen er skrevet ved hjelp av JavaScript og Web Audio API, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Demo

Her er en live responsiv musikk-JavaScript-demo for deg å spille med (med nedlastbar kildekode). Du kan se en innspilt versjon av demoen i følgende video hvis nettleseren din ikke kan kjøre live-demoen:


Viktig notat: Når du skriver denne veiledningen, er W3C Web Audio API (brukt av JS demo) en eksperimentell teknologi og er bare tilgjengelig i Google Chrome nettleseren.


Introduksjon

Journey, et spill utviklet av thatgamecompany, er et godt utgangspunkt for denne opplæringen. Spillets grafikk og musikk smelter sammen for å skape en fantastisk og følelsesmessig interaktiv opplevelse, men det er noe spesielt med musikken i spillet som gjør opplevelsen så kraftig som den er - den strømmer sømløst gjennom hele spillet og utvikler seg dynamisk som spilleren utvikler seg og utløser visse in-game hendelser. Journey bruker "responsiv" musikk for å forbedre følelsene spilleren opplever mens han spiller spillet.

For å være rettferdig, bruker mange moderne spill lydhør musikk på en eller annen måte - Tomb Raider og Bioshock Infinite er to eksempler som kommer til å tenke - men hvert spill kan ha nytte av lyd som er lydhør.

Så hvordan kan du faktisk legge til lydig lyd til spillene dine? Vel, det er mange måter å oppnå dette på; Noen måter er mye mer sofistikerte enn andre, og krever at flere lydkanaler blir streamet fra en lokal lagringsenhet, men det er faktisk ganske enkelt å legge til litt grunnleggende lydhør i et spill, hvis du har tilgang til en lavnivå lyd API.

Vi skal ta en titt på en løsning som er enkel nok og lett nok til å bli brukt i dag i online spill - inkludert JavaScript-baserte spill.


I et nøtteskall

Den enkleste måten å oppnå lydhør i et online spill, er å laste inn en enkelt lydfil i minnet ved kjøring, og deretter programmere looping bestemte deler av lydfilen. Dette krever en koordinert innsats fra spillprogrammerne, lydingeniører og designere.

Det første vi må vurdere er den faktiske strukturen av musikken.


Musikkstruktur

Den lydløse musikkløsningen vi ser på her krever musikken skal struktureres på en måte som gjør at deler av det musikalske arrangementet kan løses sømløst - disse loopable delene av musikken vil bli kalt "soner" gjennom hele denne opplæringen.

I tillegg til å ha soner, musikken kan bestå av ikke-loopbare deler som brukes som overganger mellom ulike soner - disse vil bli kalt "fyller" gjennom resten av denne opplæringen.

Følgende bilde visualiserer en veldig enkel musikkstruktur bestående av to soner og to fyllinger:


Hvis du er programmerer som har brukt lavt nivå lyd-APIer før, har du kanskje allerede utarbeidet hvor vi skal med dette: Hvis musikken er strukturert på en slik måte at det tillater at deler av arrangementet løses sømløst, musikk kan programmeres etter hvert - alt vi trenger å vite er hvor sonene og fyllingene ligger i musikken. Det er der en descriptor filen kommer i brukbar.

Merk: Det må ikke være noe stille i begynnelsen av musikken; det må begynne med det samme. Hvis det er et tilfeldig stykke av stillhet i begynnelsen av musikken, vil sonene og fyller inn musikken ikke justeres til barer (betydningen av dette vil bli dekket senere i denne opplæringen).


Musikkbeskrivelse

Hvis vi ønsker å kunne programmere spille og løse bestemte deler av en musikkfil, må vi vite hvor musiksonene og fyllingene er plassert i musikken. Den mest åpenbare løsningen er en beskrivelsesfil som kan lastes sammen med musikken, og for å holde det enkelt, skal vi bruke en JSON-fil fordi de fleste programmeringsspråk er i stand til å dekode og kode JSON-data i disse dager.

Følgende er en JSON-fil som beskriver den enkle musikkstrukturen i det forrige bildet:

 "bpm": 120, "bpb": 4, "struktur": ["type": 0, "størrelse": 2, "navn": "Avslappet", "type": 0, "størrelse" : 2, "navn": "jakket", "type": 1, "størrelse": 1, "navn": "A", "type": 1, "størrelse": 1, "navn" : "B"]
  • De bpm feltet er tempoet for musikken, i slag per minutt.
  • De BPB feltet er signaturen til musikken, i beats per bar.
  • De struktur feltet er et bestilt utvalg av objekter som beskriver hver sone og fyller inn musikken.
  • De type feltet forteller oss om objektet er en sone eller en fylling (null og en henholdsvis).
  • De størrelse feltet er lengden eller sonen eller fylle, i stolper.
  • De Navn feltet er en identifikator for sonen eller fylle.

Musikk Timing

Informasjonen i musikkbeskrivelsen gjør at vi kan beregne ulike tidsrelaterte verdier som trengs for å spille musikk nøyaktig gjennom en lav lyd-API.

Den viktigste delen av informasjonen vi trenger er lengden på en enkelt musikklinje, i prøver. De musikalske sonene og fyllene er alle justert til stolper, og når vi trenger overgang fra en del av musikken til en annen, må overgangen skje i starten av en bar - vi vil ikke at musikken skal hoppe fra en tilfeldig posisjon innenfor en bar fordi det ville høres veldig forferdelig.

Følgende pseudokode beregner prøve lengden på en enkelt musikklinje:

 bpm = 120 // slag per minutt bpb = 4 // slag per bar srt = 44100 // prøvefrekvens bar_length = srt * (60 / (bpm / bpb))

Med bar_length beregnet kan vi nå utarbeide prøveposisjonen og lengden på sonene og fyller i musikken. I den følgende pseudokoden går vi bare gjennom beskrivelsens struktur array og legg til to nye verdier til sonen og fyll objekter:

 i = 0 n = descriptor.structure.length // antall soner og fyller s = 0 mens (i < n )  o = descriptor.structure[i++] o.start = s o.length = o.size * bar_length s += o.length 

For denne opplæringen er det all informasjon vi trenger for vår responsive musikkløsning - vi kjenner nå prøveposisjonen og lengden på hver sone og fyller inn musikken, og det betyr at nå kan spilles sonene og fylles i hvilken som helst rekkefølge vi liker. I hovedsak kan vi nå programmatisk sekvensere et uendelig langt musikkspor ved kjøring med svært lite overhead.


Musikkavspilling

Nå som vi har all den informasjonen vi trenger for å spille musikken, er programmatisk spillesoner og fyllinger fra musikken en relativt enkel oppgave, og vi kan håndtere dette med to funksjoner.

Den første funksjonen omhandler oppgaven med å trekke prøver fra vår musikkfil og skyve dem til lavt nivå lyd-API. Igjen skal jeg demonstrere dette ved hjelp av pseudokode fordi forskjellige programmeringsspråk har forskjellige APIer for å gjøre denne typen ting, men teorien er konsekvent i alle programmeringsspråk.

 input // buffer som inneholder prøvene fra vår musikkutgang // lavt nivå lyd-API-utgangsbuffer avspillingshode = 0 // plassering av spillingshode i musikkfilen, i eksempler start = 0 // startposisjon for den aktive sonen eller fylling, i prøve lengde = 0 // lengden på den aktive sonen eller fylle, i prøver neste = null // neste sone eller fyll (objekt) som må spilles // påkrevd når lavnivå lyd API krever mer sample data funksjon oppdatering () i = 0 n = output.length // sample lengde på output buffer end = lengde - start mens (i < n )  // is the playhead at the end of the active zone or fill if( playhead == end )  // is another zone or fill waiting to be played if( next != null )  start = next.start length = next.length next = null  // reset the playhead playhead = start  // pull samples from the input and push them to the output output[i++] = input[playhead++]  

Den andre funksjonen brukes til å kjøre neste sone eller fyll som må spilles:

 // param 'navn' er navnet på sonen eller fylle (definert i beskrivelsen) funksjon setNext (navn) i = 0 n = descriptor.structure.length // antall soner og fyller mens (jeg < n )  o = descriptor.structure[i++] if( o.name == name )  // set the 'next' value and return from the function next = o return   // the requested zone or fill could not be found throw new Exception() 

For å spille "Relaxed" sone av musikk, ville vi ringe setNext ( "avslappet"), og sonen vil være i kø og deretter spilt på neste mulige mulighet.

Følgende bilde visualiserer avspillingen av "Avslappet" sone:


For å spille "Hunted" sone av musikk, ville vi ringe setNext ( "Jaget"):


Tror det eller ikke, vi har nå nok til å jobbe med for å legge til enkel lydhør musikk til ethvert spill som har tilgang til et lavt nivå lyd-API, men det er ingen grunn til at denne løsningen må forbli enkel - vi kan spille ulike deler av musikken i hvilken som helst rekkefølge vi liker, og som åpner døren til mer komplekse lydspor.

En av tingene vi kan gjøre er å gruppere ulike deler av musikken for å lage sekvenser, og disse sekvensene kan brukes som komplekse overganger mellom de forskjellige sonene i musikken.


Musikk sekvensering

Gruppering av ulike deler av musikken for å lage sekvenser vil bli dekket i en fremtidig opplæring, men i mellomtiden vurdere hva som skjer i følgende bilde:


I stedet for å overføre direkte fra en veldig høy musikkdel til en veldig stille del av musikk, kan vi kaste ting ned gradvis ved hjelp av en sekvens - det vil si en jevn overgang.


Konklusjon

Vi har sett på en mulig løsning for responsiv spillemusikk i denne opplæringen, ved hjelp av en musikkstruktur og en musikkbeskrivelse, og kjernekoden som kreves for å håndtere musikkavspillingen.

Responsive musikk kan legge til et helt nytt nivå av nedsenking til et spill, og det er definitivt noe som spillutviklere burde vurdere å utnytte når man starter utviklingen av et nytt spill. Spillutviklere bør ikke gjøre feilen om å forlate denne typen ting til de siste stadiene av utviklingen, skjønt; det krever en koordinert innsats fra spillprogrammerne, lydingeniørene og designerne.