Musik som kan förändras dynamiskt och sömlöst för att reflektera vad som händer på skärmen kan lägga till en helt ny nivå av nedsänkning av ett spel. I denna handledning tar vi en titt på ett av de enklare sätten att lägga till lyhörd musik i ett spel.
Notera: Även om denna handledning skrivs med JavaScript och Web Audio API, bör du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst..
Här är en live-responsiv musik-JavaScript-demo som du kan spela med (med nedladdningsbar källkod). Du kan titta på en inspelad version av demoen i följande video om din webbläsare inte kan köra live demo:
Viktig notering: Vid skrivning av denna handledning är W3C Web Audio API (används av JS demo) en experimentell teknik och är endast tillgänglig i Google Chrome webbläsare.
Journey, ett spel utvecklat av thatgamecompany, är en bra utgångspunkt för denna handledning. Spelets grafik och musik smälter samman för att skapa en fantastisk och känslomässig interaktiv upplevelse, men det finns något speciellt om musiken i spelet som gör upplevelsen så kraftfull som den är - den strömmar sömlöst genom hela spelet och utvecklas dynamiskt som spelaren fortskrider och utlöser vissa spelhändelser. Journey använder "responsiv" musik för att förbättra de känslor spelaren upplever när han spelar spelet.
För att vara rättvis, använder många moderna spel på ett eller annat sätt lyhörd musik - Tomb Raider och Bioshock Infinite är två exempel som kommer att komma i åtanke - men varje spel kan dra nytta av responsiv musik.
Så hur kan du faktiskt lägga till lyhörd musik till dina spel? Tja, det finns många sätt att uppnå detta. vissa sätt är mycket mer sofistikerade än andra och kräver att flera ljudkanaler ska strömma från en lokal lagringsenhet, men det är faktiskt ganska enkelt att lägga till lite grundläggande ljud i ett spel om du har tillgång till ett ljud med låg ljudnivå.
Vi ska ta en titt på en lösning som är enkel nog och lätt nog att användas idag i onlinespel - inklusive JavaScript-baserade spel.
Det enklaste sättet att uppnå lyhörd musik i ett onlinespel är att ladda en enda ljudfil i minnet vid körning och sedan programmässigt loopa specifika delar av den ljudfilen. Detta kräver en samordnad insats från spelprogrammerare, ljudingenjörer och designers.
Det första vi behöver tänka på är musikens faktiska struktur.
Den responsiva musiklösningen som vi tittar på här kräver musiken ska struktureras på ett sätt som gör att delar av det musikaliska arrangemanget kan slås sömlöst - dessa loopbara delar av musiken kommer att kallas "zoner" under hela denna handledning.
Förutom att ha zoner, musiken kan består av icke-loopbara delar som används som övergångar mellan olika zoner - dessa kallas fyllningar under resten av denna handledning.
Följande bild visualiserar en mycket enkel musikstruktur bestående av två zoner och två fyllningar:
Om du är en programmerare som tidigare har använt lågnivå ljud API: er, kanske du redan har utarbetat var vi går med det här: Om musiken är strukturerad så att delar av arrangemanget kan slås sömlöst, så musik kan programmeras sequentially - allt vi behöver veta är där zoner och fyllningar ligger inom musiken. Det är där en deskriptor filen kommer till nytta.
Notera: Det får inte finnas någon tystnad i början av musiken; Det måste börja omedelbart. Om det finns en slumpmässig bit av tystnad i början av musiken kommer zonerna och fyller i musiken inte att anpassas till staplarna (betydelsen av detta kommer att täckas senare i denna handledning).
Om vi vill kunna programmera och spela specifika delar av en musikfil, behöver vi veta var musikzonerna och fyllningarna finns i musiken. Den mest uppenbara lösningen är en beskrivningsfil som kan laddas tillsammans med musiken och för att hålla det enkelt kommer vi att använda en JSON-fil eftersom de flesta programmeringsspråk kan avkoda och koda JSON-data dessa dagar.
Följande är en JSON-fil som beskriver den enkla musikstrukturen i föregående bild:
"bpm": 120, "bpb": 4, "struktur": ["typ": 0, "storlek": 2, "namn": "Avslappnad", "typ": 0, "storlek" : 2, "namn": "Hunted", "typ": 1, "storlek": 1, "namn": "A", "typ": 1, "storlek": 1, "namn" : "B"]
bpm
fältet är tempot för musiken, i slag per minut.BPB
Fältet är signaturen för musiken, i slag per bar.strukturera
fältet är en ordnad serie objekt som beskriver varje zon och fyller i musiken.typ
fältet berättar om objektet är en zon eller en fyllning (noll respektive en).storlek
Fältet är längden eller zonen eller fyllningen, i staplarna.namn
Fältet är en identifierare för zonen eller fyllningen.Informationen i musikdeskriptorn tillåter oss att beräkna olika tidsrelaterade värden som behövs för att noggrant spela upp musiken genom ett lågt ljudljud API.
Den viktigaste biten av information vi behöver är längden på en enda musikbana, i prover. Musikaliska zoner och fyllningar är alla inriktade på staplar, och när vi behöver övergå från en del av musiken till en annan måste övergången ske i början av en stapel - vi vill inte att musiken ska hoppa från en slumpmässig position inom en stapel eftersom det skulle låta verkligen förvirrande.
Följande pseudokod beräknar provlängden för en enda musikbana:
bpm = 120 // slag per minut bpb = 4 // slag per bar srt = 44100 // provhastighet bar_length = srt * (60 / (bpm / bpb))
Med bar_length
beräknat kan vi nu utarbeta provpositionen och längden på zonerna och fyller i musiken. I följande pseudokod slår vi helt enkelt igenom beskrivarens strukturera
array och lägg till två nya värden till zonen och fyll i objekt:
i = 0 n = descriptor.structure.length // antal zoner och fyller s = 0 medan (i < n ) o = descriptor.structure[i++] o.start = s o.length = o.size * bar_length s += o.length
För denna handledning är det all information vi behöver för vår responsiva musiklösning - vi känner nu till provpositionen och längden för varje zon och fyller i musiken, och det betyder att nu kan spelas zoner och fyller i vilken ordning som helst vi gillar. I huvudsak kan vi nu programmera en oändligt lång musikspår vid körning med mycket lite overhead.
Nu när vi har all den information vi behöver för att spela musiken är programmatiskt spelande zoner och fyllningar från musiken en relativt enkel uppgift, och vi kan hantera detta med två funktioner.
Den första funktionen behandlar uppgiften att dra prover från vår musikfil och driva dem till lågnivå ljud API. Återigen demonstrerar jag detta med hjälp av pseudokod eftersom olika programmeringsspråk har olika API för att göra den här typen av saker, men teorin är konsekvent i alla programmeringsspråk.
ingång // buffert innehållande proverna från vår musikproduktion // lågnivå ljud API utgång buffertspelhuvud = 0 // läget för spelhuvudet i musikfilen, i prover start = 0 // startposition för aktiv zon eller fyllning, i provlängden = 0 // längden på den aktiva zonen eller fyllningen, i proverna nästa = null // nästa zon eller fyllning (objekt) som behöver spelas // åberopas när API: n med låg nivå kräver mer samplingsdatafunktion uppdatering () i = 0 n = output.length // provlängd för utgångsbuffertänden = längd - start medan (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 andra funktionen används för att köa nästa zon eller fyllning som behöver spelas:
// param 'namn' är namnet på zonen eller fyllningen (definierad i beskrivaren) funktionen setNext (namn) i = 0 n = descriptor.structure.length // antal zoner och fyller under (i < 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()
För att spela den "avslappnade" musikzonen, skulle vi ringa setNext ( "avslappnad")
, och zonen skulle köas och spelas sedan vid nästa möjliga tillfälle.
Följande bild visualiserar uppspelningen av den "avslappnade" zonen:
För att spela musikområdet "Hunted" skulle vi ringa setNext ( "Hunted")
:
Tro det eller inte, vi har nu nog att jobba med för att lägga till enkel responsiv musik till något spel som har tillgång till ett lågt ljud API, men det finns ingen anledning till att denna lösning behöver förbli enkel - vi kan spela olika delar av musiken i vilken ordning vi helst, och det öppnar dörren till mer komplexa ljudspår.
En av de saker vi kan göra är att gruppera olika delar av musiken för att skapa sekvenser, och dessa sekvenser kan användas som komplexa övergångar mellan olika zoner i musiken.
Gruppering av olika delar av musiken för att skapa sekvenser kommer att behandlas i en framtida handledning, men under tiden undersöka vad som händer i följande bild:
Istället för att övergå direkt från en mycket hög musikdel till ett mycket tyst avsnitt av musik kunde vi lugna ner saker och ting gradvis med hjälp av en sekvens - det vill säga en smidig övergång.
Vi har tittat på en möjlig lösning för responsiv spelmusik i denna handledning, med hjälp av en musikstruktur och en musikbeskrivare och kärnkoden som krävs för att hantera musikuppspelningen.
Responsiv musik kan lägga till en helt ny nivå av nedsänkning till ett spel och det är definitivt något som spelutvecklare borde överväga att utnyttja när man börjar utveckla ett nytt spel. Spelutvecklare borde inte göra misstaget att lämna den här typen av saker till de sista utvecklingsstadierna. det kräver en samordnad insats från spelprogrammerare, ljudingenjörer och designers.