I denne serien av opplæringsprogrammer, viser jeg deg hvordan du lager en neon tvillingpinne shooter, som Geometry Wars, i XNA. Målet med disse opplæringsprogrammene er ikke å forlate deg med en nøyaktig kopi av Geometry Wars, men heller å gå over de nødvendige elementene som vil tillate deg å lage din egen variant av høy kvalitet.
I serien så langt har vi satt opp det grunnleggende gameplayet for vår neon twin stick shooter, Shape Blaster. I denne opplæringen vil vi skape signatur neon utseende ved å legge til et blomst postbehandlingsfilter.
Advarsel: Høyt!Enkle effekter som dette eller partikkeleffekter kan gjøre et spill betydelig mer tiltalende uten at det kreves noen endringer i spillet. Effektiv bruk av visuelle effekter er et viktig hensyn i alle spill. Etter å ha lagt til blomstfilteret, vil vi også legge til svarte hull i spillet.
Bloom beskriver effekten du ser når du ser på et objekt med et sterkt lys bak det, og lyset ser ut til å blø over objektet. I Shape Blaster vil blomseffekten gjøre de lyse linjene av skipene og partiklene som lyse, glødende, neonlys.
Sollys blomstrer gjennom trærneFor å bruke blomstring i spillet vårt, må vi gjøre scenen vår til et gjengemål, og deretter bruke blomstfilteret vårt til det gjengitte målet.
Bloom jobber i tre trinn:
Hver av disse trinnene krever en shader - i hovedsak et kort program som kjører på grafikkortet ditt. Shaders i XNA er skrevet på et spesielt språk som heter High Level Shader Language (HLSL). Eksempelbildene nedenfor viser resultatet av hvert trinn.
Første bilde De lyse områdene hentet fra bildet De lyse områdene etter sløring Det endelige resultatet etter rekombinering med det opprinnelige bildetFor vårt blomstfilter vil vi bruke XNA Bloom Postprocess Sample.
Det er enkelt å integrere blomstertesten med prosjektet vårt. Finn først de to kodefilene fra prøven, BloomComponent.cs
og BloomSettings.cs
, og legg dem til ShapeBlaster prosjekt. Legg også til BloomCombine.fx
, BloomExtract.fx
, og GaussianBlur.fx
til innholdsrørledningsprosjektet.
I GameRoot
, Legg til en ved hjelp av
uttalelse for BloomPostprocess
namespace og legg til en BloomComponent
medlem variabel.
BloomComponent blomstre;
I GameRoot
konstruktør, legg til følgende linjer.
blomstre = ny BloomComponent (dette); Components.Add (bloom); bloom.Settings = nye BloomSettings (null, 0.25f, 4, 2, 1, 1.5f, 1);
Til slutt, i begynnelsen av GameRoot.Draw ()
, legg til følgende linje.
bloom.BeginDraw ();
Det er det. Hvis du kjører spillet nå, bør du se blomsten i kraft.
Når du ringer bloom.BeginDraw ()
, den omdirigerer etterfølgende tegneanrop til et gjengemål som blomsten skal brukes på. Når du ringer base.Draw ()
på slutten av GameRoot.Draw ()
metode, den BloomComponent
's Tegne()
Metoden kalles. Her er blomsten påført og scenen trekkes til bakbufferen. Derfor må alt som trengs, blomstres, trekkes mellom samtalene til bloom.BeginDraw ()
og base.Draw ()
.
Tips: Hvis du vil tegne noe uten blomst (for eksempel brukergrensesnittet), tegner du det etter anropet til base.Draw ()
.
Du kan justere blomstinnstillingene til din smak. Jeg har valgt følgende verdier:
0.25
for blomstertærskelen. Dette betyr at deler av bildet som er mindre enn en fjerdedel av full lysstyrke, ikke vil bidra til å blomstre.4
for uklarheten. For den matematiske tilbøyeligheten er dette standardavviket for Gaussens uskarphet. Større verdier vil uklare lyset blomstre mer. Vær imidlertid oppmerksom på at uskarphetsklareren er innstilt for å bruke et fast antall prøver, uavhengig av uklarheten. Hvis du angir denne verdien for høy, vil uklarheten strekke seg utover radiusen fra hvilken shaderprøver og gjenstander vises. Ideelt sett bør denne verdien ikke være mer enn en tredjedel av prøvetakningsradiusen din for å sikre at feilen er ubetydelig.2
for blomstens intensitet, som bestemmer hvor sterkt blomsten påvirker sluttresultatet.1
for basisintensiteten, som bestemmer hvor sterkt det opprinnelige bildet påvirker sluttresultatet.1.5
for blomstermetningen. Dette fører til at gløden rundt lyse gjenstander har mer mettede farger enn objektene selv. En høy verdi ble valgt for å simulere utseendet på neonlys. Hvis du ser på midten av et klart neonlys, ser det nesten hvitt ut, mens lyset rundt det er sterkere farget.1
for basismetningen. Denne verdien påvirker metningen av basisbildet.Blomstfilteret er implementert i BloomComponent
klasse. Blomsterkomponenten starter ved å opprette og laste inn nødvendige ressurser i sin LoadContent ()
metode. Her laster den de tre shaders det krever, og skaper tre gjengivne mål.
Det første gjengemålet, sceneRenderTarget
, er for å holde scenen som blomsten skal brukes på. De to andre, renderTarget1
og renderTarget2
, brukes til midlertidig å holde mellomliggende resultater mellom hvert gjengivelsespass. Disse gjengene målene blir gjort halvparten av spillets oppløsning for å redusere ytelseskostnaden. Dette reduserer ikke den endelige kvaliteten på blomsten, fordi vi vil uklare blomstrende bilder uansett.
Bloom krever fire gjengivelsespass, som vist i dette diagrammet:
I XNA, den Effekt
klassen innkapsler en skygge. Du skriver koden for shader i separat fil, som du legger til innholdsrørledningen. Dette er filene med .fx
forlengelse vi la til tidligere. Du laster inn skyggeren i en Effekt
objekt ved å ringe Content.Load
metode i LoadContent ()
. Den enkleste måten å bruke en shader på i et 2D-spill er å passere Effekt
objekt som en parameter til SpriteBatch.Begin ()
.
Det finnes flere typer shaders, men for blomstfilteret vil vi bare bruke pixel shaders (noen ganger kalt fragment shaders). En pikselskygger er et lite program som kjører en gang for hver piksel du tegner og bestemmer fargene på pikselet. Vi går over hver av de brukte shaders.
BloomExtract
ShaderDe BloomExtract
shader er den enkleste av de tre shaders. Dens jobb er å trekke ut områdene av bildet som er lysere enn noen terskel, og deretter rescale fargevennene for å bruke hele fargevalg. Eventuelle verdier under terskelen blir svarte.
Den fulde skyggekoden er vist nedenfor.
sampler TextureSampler: register (s0); float BloomThreshold; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Se opp den opprinnelige bildefarge. float4 c = tex2D (TextureSampler, texCoord); // Juster det for å beholde kun verdier lysere enn den angitte grensen. returner mettet ((c - BloomThreshold) / (1 - BloomThreshold)); teknikk BloomExtract pass Pass1 PixelShader = kompilere ps_2_0 PixelShaderFunction ();
Ikke bekymre deg hvis du ikke er kjent med HLSL. La oss undersøke hvordan dette fungerer.
sampler TextureSampler: register (s0);
Denne første delen erklærer en tekstur sampler kalt TextureSampler
. SpriteBatch
vil binde en tekstur til denne sampler når den trekker med denne skyggeren. Angi hvilket register som skal bindes til, er valgfritt. Vi bruker sampler til å se opp piksler fra den bundne tekstur.
float BloomThreshold;
BloomThreshold
er en parameter som vi kan angi fra vår C # kode.
float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0
Dette er vår pixel shader-funksjonen deklarasjon som tar tekstur koordinater som input og returnerer en farge. Fargen returneres som en float4
. Dette er en samling av fire flyter, mye som a Vector4
i XNA. De lagrer rød, grønn, blå og alfakomponenter av fargen som verdier mellom null og en.
TEXCOORD0
og COLOR0
er kalt semantikk, og de viser til kompilatoren hvordan texCoord
parameter og returverdi brukes. For hver pikselutgang, texCoord
vil inneholde koordinatene til det tilsvarende punktet i inngangsteksten, med (0, 0)
å være øverste venstre hjørne og (1, 1)
å være nederst til høyre.
// Se opp den opprinnelige bildefarge. float4 c = tex2D (TextureSampler, texCoord); // Juster det for å beholde kun verdier lysere enn den angitte grensen. returner mettet ((c - BloomThreshold) / (1 - BloomThreshold));
Her er alt det virkelige arbeidet gjort. Den henter pikselfarge fra tekstur, trekker seg ned BloomThreshold
fra hver fargekomponent, og skalerer den deretter tilbake, slik at den maksimale verdien er en. De mette ()
funksjonen klemmer fargens komponenter mellom null og en.
Du kan legge merke til det c
og BloomThreshold
er ikke den samme typen, som c
er en float4
og BloomThreshold
er en flyte
. HLSL lar deg gjøre operasjoner med disse forskjellige typene ved å hovedsakelig snu flyte
inn i en float4
med alle komponenter det samme. (c - BloomThreshold)
blir effektivt:
c - float4 (BloomThreshold, BloomThreshold, BloomThreshold, BloomThreshold)
Resten av skyggeren skaper bare en teknikk som bruker piksel shader-funksjonen, kompilert for shader modell 2.0.
GaussianBlur
ShaderEn gaussisk uskarphet forvirrer et bilde ved hjelp av en Gaussisk funksjon. For hver piksel i utgangsbildet oppsummerer vi pikslene i inngangsbildet vektet av avstanden fra målpikselet. Nærliggende piksler bidrar sterkt til den endelige fargen, mens fjerne piksler bidrar svært lite.
Fordi fjerne piksler gjør ubetydelige bidrag, og fordi teksturoppslag er kostbare, prøver vi bare piksler i en kort radius i stedet for å prøve hele tekstur. Denne shader vil prøve punkter innen 14 piksler av den nåværende piksel.
En naiv implementering kan prøve alle punkter i en firkant rundt den nåværende pixel. Dette kan imidlertid være kostbart. I vårt eksempel må vi prøve punkter i en 29x29 square (14 poeng på hver side av midten piksel, pluss midtpunktet). Det er totalt 841 prøver for hver piksel i bildet vårt. Heldigvis er det en raskere metode. Det viser seg at det å gjøre en 2D Gauss-slør er lik den første som slør bildet horisontalt, og deretter slør det igjen vertikalt. Hver av disse endimensjonale blursene krever bare 29 prøver, og reduserer totalt 58 prøver per piksel.
Et nytt triks brukes til å ytterligere øke effektiviteten til uskarpheten. Når du forteller GPUen å prøve mellom to piksler, vil den returnere en blanding av de to piksler uten ekstra ytelseskost. Siden vårt uskarphet er i ferd med å blande piksler, tillater dette oss å prøve to piksler om gangen. Dette kutter antall nødvendige prøver nesten i halvparten.
Nedenfor er de relevante delene av GaussianBlur
shader.
sampler TextureSampler: register (s0); #define SAMPLE_COUNT 15 float2 SampleOffsets [SAMPLE_COUNT]; float SampleWeights [SAMPLE_COUNT]; float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 float4 c = 0; // Kombiner en rekke vektede bildefilterkraner. for (int i = 0; i < SAMPLE_COUNT; i++) c += tex2D(TextureSampler, texCoord + SampleOffsets[i]) * SampleWeights[i]; return c;
Skyggen er faktisk ganske enkel; det tar bare en rekke offsets og en tilsvarende rekkevidde og beregner vektet sum. Alt det komplekse matte er faktisk i C # -koden som fyller offset- og vektarrayene. Dette gjøres i SetBlurEffectParameters ()
og ComputeGaussian ()
metoder for BloomComponent
klasse. Når du utfører horisontal sløret pass, SampleOffsets
vil bli befolket med bare horisontale forskyvninger (y-komponentene er alle null), og selvfølgelig er reversen sant for det vertikale passet.
BloomCombine
ShaderDe BloomCombine
shader gjør noen ting på en gang. Den kombinerer blomstrestrukturen med den opprinnelige teksturen samtidig som du justerer intensiteten og metningen av hver tekstur.
Shader begynner med å deklarere to tekstur samplers og fire float parametere.
sampler BloomSampler: register (s0); sampler BaseSampler: register (s1); float BloomIntensity; float BaseIntensity; float BloomSaturation; float BaseSaturation;
En ting å merke seg er det SpriteBatch
vil automatisk binde tekstur du passerer den når du ringer SpriteBatch.Draw ()
til første sampler, men det vil ikke automatisk binde noe til den andre sampleren. Den andre sampler er satt manuelt i BloomComponent.Draw ()
med følgende linje.
GraphicsDevice.Textures [1] = sceneRenderTarget;
Deretter har vi en hjelperfunksjon som justerer fargemetningen.
float4 AdjustSaturation (float4 farge, flytmetning) // Konstantene 0.3, 0.59 og 0.11 er valgt fordi // menneskeøyet er mer følsomt for grønt lys og mindre til blått. flyte grå = prikk (farge, float3 (0,3, 0,59, 0,11)); retur lerp (grå, farge, metning);
Denne funksjonen tar en farge og en metningsverdi og returnerer en ny farge. Passerer en metning av 1
etterlater fargen uendret. Passering 0
vil returnere grå, og passerende verdier større enn en vil returnere en farge med økt metning. Å overføre negative verdier er virkelig utenfor den tilsiktede bruken, men vil invertere fargen hvis du gjør det.
Funksjonen fungerer ved å først finne fargens lysstyrke ved å ta en vektet sum basert på øynets følsomhet for rødt, grønt og blått lys. Den interpolerer deretter lineært mellom grått og den opprinnelige fargen med den angitte mengdemetning. Denne funksjonen kalles av pixel shader-funksjonen.
float4 PixelShaderFunction (float2 texCoord: TEXCOORD0): COLOR0 // Se opp blomstene og originale fotobilder. float4 bloom = tex2D (BloomSampler, texCoord); float4 base = tex2D (BaseSampler, texCoord); // Juster fargemetning og intensitet. blomstre = AdjustSaturation (blomst, BloomSaturation) * BloomIntensity; base = AdjustSaturation (base, BaseSaturation) * BaseIntensity; // Mørk ned basebildet i områder der det er mye blomstring, / / for å hindre at ting ser ut til å være utbrent. base * = (1 - mettet (blomst)); // Kombiner de to bildene. returbase + blomst;
Igjen, denne skyggen er ganske enkel. Hvis du lurer på hvorfor basebildet må bli mørkt i områder med klar blomst, husk at å legge til to farger sammen øker lysstyrken og eventuelle fargekomponenter som legger til en verdi som er større enn en (full lysstyrke) vil bli klippet til en . Siden blomstrende bildet ligner på basebildet, vil dette føre til at mye av bildet som har over 50% lysstyrke, blir maksimal. Mørk basebildet kartlegger alle fargene tilbake til det fargespekter vi kan vise riktig.
En av de mest interessante fiender i Geometry Wars er det svarte hullet. La oss undersøke hvordan vi kan gjøre noe lignende i Shape Blaster. Vi vil lage grunnleggende funksjonalitet nå, og vi vil revidere fienden i neste veiledning for å legge til partikkeleffekter og partikkel-interaksjoner.
Et svart hull med omkretsende partiklerDe svarte hullene trekker inn spillerens skip, nærliggende fiender og (etter neste opplæring) partikler, men vil avvise kuler.
Det er mange mulige funksjoner vi kan bruke for tiltrekning eller frastøt. Det enkleste er å bruke konstant kraft, slik at det svarte hullet trekker med samme styrke uavhengig av objektets avstand. Et annet alternativ er å få kraften til å øke lineært fra null ved en viss avstand, til full styrke for objekter direkte på toppen av det svarte hullet.
Hvis vi ønsker å modellere tyngdekraften mer realistisk, kan vi bruke det inverse firkantet av avstanden, noe som betyr at tyngdekraften er proporsjonal med \ (1 / avstand ^ 2 \). Vi vil faktisk bruke hver av disse tre funksjonene til å håndtere forskjellige objekter. Kulene vil bli avstøtet med konstant kraft, fiender og spillerens skip vil bli tiltrukket med en lineær kraft, og partiklene vil bruke en invers firkantfunksjon.
Vi lager en ny klasse for svarte hull. La oss starte med grunnleggende funksjonalitet.
klasse BlackHole: Entitet privat statisk Tilfeldig rand = ny Tilfeldig (); private int hitpoeng = 10; offentlig BlackHole (Vector2 posisjon) image = Art.BlackHole; Posisjon = posisjon; Radius = image.Width / 2f; offentlig ugyldig WasShot () hitpoints--; hvis (hitpoints <= 0) IsExpired = true; public void Kill() hitpoints = 0; WasShot(); public override void Draw(SpriteBatch spriteBatch) // make the size of the black hole pulsate float scale = 1 + 0.1f * (float)Math.Sin(10 * GameRoot.GameTime.TotalGameTime.TotalSeconds); spriteBatch.Draw(image, Position, null, color, Orientation, Size / 2f, scale, 0, 0);
De svarte hullene tar ti skudd for å drepe. Vi justerer skalaen til spritet litt for å gjøre det pulserende. Hvis du bestemmer deg for at ødeleggelse av svarte hull bør også gi poeng, må du gjøre lignende tilpasninger til Svart hull
klasse som vi gjorde med fiendens klasse.
Neste gjør vi at de svarte hullene faktisk bruker en kraft på andre enheter. Vi trenger en liten hjelpemetode fra vår EntityManager
.
offentlige statiske IEnumerable GetNearbyEntities (Vector2 posisjon, floatradius) return entities.Where (x => Vector2.DistanceSquared (posisjon, x.Position) < radius * radius);
Denne metoden kan gjøres mer effektiv ved å bruke en mer komplisert romlig partisjonering, men for antall enheter vi vil ha, er det greit som det er. Nå kan vi få de svarte hullene til å bruke kraft i deres Oppdater()
metode.
offentlig overstyring ugyldig oppdatering () var entities = EntityManager.GetNearbyEntities (posisjon, 250); foreach (var enhet i enheter) hvis (enhet er Enemy &&! (enhet som Enemy) .IsActive) fortsette; // kuler er avstøt av svarte hull og alt annet er tiltrukket hvis (enhet er Bullet) entity.Velocity + = (entity.Position - Position) .ScaleTo (0.3f); ellers var dPos = Posisjon - entity.Position; var lengde = dPos.Length (); entity.Velocity + = dPos.ScaleTo (MathHelper.Lerp (2, 0, lengde / 250f));
Svarte hull påvirker bare enheter innenfor en valgt radius (250 piksler). Kuler innenfor denne radiusen har en konstant frastøtende kraft påført, mens alt annet har en lineær tiltrekkende kraft påført.
Vi må legge til kollisionshåndtering for sorte hull til EntityManager
. Legg til en List <>
for svarte hull som vi gjorde for de andre typer enheter, og legg til følgende kode i EntityManager.HandleCollisions ()
.
// håndter kollisjoner med svarte hull for (int i = 0; i < blackHoles.Count; i++) for (int j = 0; j < enemies.Count; j++) if (enemies[j].IsActive && IsColliding(blackHoles[i], enemies[j])) enemies[j].WasShot(); for (int j = 0; j < bullets.Count; j++) if (IsColliding(blackHoles[i], bullets[j])) bullets[j].IsExpired = true; blackHoles[i].WasShot(); if (IsColliding(PlayerShip.Instance, blackHoles[i])) KillPlayer(); break;
Til slutt åpner du EnemySpawner
klasse og få det til å lage noen svarte hull. Jeg begrenset det maksimale antall svarte hull til to, og ga en 1 til 600 sjanse for at et svart hull skulle gyte hver ramme.
hvis (EntityManager.BlackHoleCount < 2 && rand.Next((int)inverseBlackHoleChance) == 0) EntityManager.Add(new BlackHole(GetSpawnPosition()));
Vi har lagt til blomst ved hjelp av ulike shaders og svarte hull ved hjelp av ulike kraftformler. Shape Blaster begynner å se ganske bra ut. I neste del legger vi til noen galte, over de beste partikkeleffekter.