Lightning har många användningsområden i spel, från bakgrundsmiljö under en storm till en trollkarls förödande blixtattacker. I denna handledning kommer jag att förklara hur man programmerat genererar fantastiska 2D-ljuseffekter: bultar, grenar och jämn text.
Notera: Även om denna handledning skrivs med C # och XNA, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.
Det grundläggande byggstenen som vi behöver göra blixt är ett linjesegment. Börja med att öppna din favoritbildredigeringsprogram och dra en rak linje av blixten. Här ser min ut som:
Vi vill rita linjer av olika längder, så vi ska skära linjesegmentet i tre stycken som visas nedan. Detta gör det möjligt för oss att sträcka mellansegmentet till vilken längd vi vill. Eftersom vi kommer att sträcka mittsegmentet kan vi spara det som en enda pixel tjock. Eftersom de vänstra och högra bitarna är spegelbilder av varandra behöver vi bara spara en av dem. Vi kan vända det i koden.
Låt oss nu förklara en ny klass för att hantera ritningslinjesegment:
offentlig klasslinje public Vector2 A; allmän vektor2b; offentliga float Tjocklek; allmän linje () allmän linje (Vector2a, Vector2b, float thickness = 1) A = a; B = b; Tjocklek = tjocklek;
A och B är linjens slutpunkter. Genom att skala och rotera bitarna av linjen kan vi rita en linje av vilken tjocklek, längd och orientering som helst. Lägg till följande Dra()
metod till Linje
klass:
public void Draw (SpriteBatch spriteBatch, Färgfärg) Vector2 tangent = B - A; float rotation = (float) Math.Atan2 (tangent.Y, tangent.X); const float ImageThickness = 8; float thicknessScale = Tjocklek / ImageThickness; Vector2 capOrigin = ny Vector2 (Art.HalfCircle.Width, Art.HalfCircle.Height / 2f); Vector2 middleOrigin = ny Vector2 (0, Art.LightningSegment.Height / 2f); Vector2 middleScale = ny Vector2 (tangent.Length (), thicknessScale); spriteBatch.Draw (Art.LightningSegment, A, null, färg, rotation, middleOrigin, middleScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, A, null, färg, rotation, capOrigin, thicknessScale, SpriteEffects.None, 0f); spriteBatch.Draw (Art.HalfCircle, B, null, färg, rotation + MathHelper.Pi, capOrigin, thicknessScale, SpriteEffects.None, 0f);
Här, Art.LightningSegment
och Art.HalfCircle
är statiska Texture2D
variabler som håller bilderna av bitarna i linjesegmentet. ImageThickness
är inställd på linjets tjocklek utan glöd. I min bild är det 8 pixlar. Vi bestämmer ursprungsbeteckningen för locket till höger och uppkomsten av mitt segmentet till vänster sida. Detta kommer att göra dem förenade sömlöst när vi ritar dem båda vid punkt A. Mellansegmentet sträcker sig till önskad bredd och en annan keps dras vid punkt B, roteras 180 °.
XNA s SpriteBatch
klassen tillåter dig att skicka den a SpriteSortMode
i sin konstruktör, vilket indikerar den ordning i vilken den borde dra spritesna. När du ritar linjen, se till att passera den a SpriteBatch
med dess SpriteSortMode
satt till SpriteSortMode.Texture
. Detta är för att förbättra prestanda.
Grafikkort är bra på att dra samma textur många gånger. Men varje gång de byter texturer, finns det overhead. Om vi ritar en massa linjer utan att sortera, skulle vi rita våra texturer i följande ordning:
LightningSegment, HalfCircle, HalfCircle, LightningSegment, HalfCircle, HalfCircle, ...
Det betyder att vi skulle byta texturer två gånger för varje linje vi ritar. SpriteSortMode.Texture
berättar SpriteBatch
att sortera Dra()
samtal av textur så att alla LightningSegments
kommer att dras tillsammans och alla HalfCircles
kommer att dras tillsammans. Dessutom, när vi använder dessa linjer för att göra blixtbultar, skulle vi vilja använda tillsatsblandning för att göra ljuset från överlappande blixtar lägga till tillsammans.
SpriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); // rita linjer SpriteBatch.End ();
Blixten tenderar att bilda skarpa linjer, så vi behöver en algoritm för att generera dessa. Vi gör det genom att plocka poäng slumpmässigt längs en linje och förskjuta dem ett slumpmässigt avstånd från linjen. Att använda en helt slumpmässig förskjutning tenderar att göra linjen för spetsad, så vi släpper ut resultaten genom att begränsa hur långt från varandra kan angränsande punkter förskjutas.
Linjen slätas genom att placera punkter på en liknande förskjutning till föregående punkt; Detta gör det möjligt för linjen som helhet att vandra upp och ner, samtidigt som någon del av det hindras från att vara för avtagna. Här är koden:
skyddad statisk listaCreateBolt (Vector2-källa, Vector2 dest, flottörtjocklek) var results = new List (); Vector2 tangent = dest-source; Vector2 normal = Vector2.Normalize (new Vector2 (tangent.Y, -tangent.X)); floatlängd = tangent.Length (); Lista positioner = ny lista (); positions.Add (0); för (int i = 0; i < length / 4; i++) positions.Add(Rand(0, 1)); positions.Sort(); const float Sway = 80; const float Jaggedness = 1 / Sway; Vector2 prevPoint = source; float prevDisplacement = 0; for (int i = 1; i < positions.Count; i++) float pos = positions[i]; // used to prevent sharp angles by ensuring very close positions also have small perpendicular variation. float scale = (length * Jaggedness) * (pos - positions[i - 1]); // defines an envelope. Points near the middle of the bolt can be further from the central line. float envelope = pos > 0.95f? 20 * (1 - pos): 1; float displacement = Rand (-Sway, Sway); förskjutning - = (förskjutning - prevDisplacement) * (1-skala); förskjutning * = kuvert; Vector2 punkt = källa + pos * tangent + förskjutning * normal; results.Add (ny linje (prevPoint, punkt, tjocklek)); prevPoint = punkt; prevDisplacement = displacement; results.Add (ny linje (prevPoint, dest, tjocklek)); returnera resultat;
Koden kan se lite skrämmande ut, men det är inte så dåligt när du förstår logiken. Vi börjar med att beräkna linjens normala och tangentvektorer tillsammans med längden. Då väljer vi slumpmässigt ett antal positioner längs linjen och lagrar dem i vår positionslista. Positionerna är skalade mellan 0
och 1
Så att 0
representerar början på linjen och 1
representerar slutpunkten. Dessa positioner sorteras sedan för att vi enkelt kan lägga till linjesegment mellan dem.
Slingan går genom de slumpmässigt valda punkterna och förskjuter dem längs normalen med en slumpmässig mängd. Skalfaktorn finns för att undvika alltför skarpa vinklar och kuvertet säkerställer att blixten faktiskt går till destinationspunkten genom att begränsa förskjutningen när vi är nära slutet.
Blixten ska blinka starkt och sedan blekna ut. För att hantera detta, låt oss skapa en Blixt
klass.
klass LightningBolt public listSegment = Ny lista (); offentlig float Alpha get; uppsättning; public float FadeOutRate get; uppsättning; offentlig färgton get; uppsättning; public bool IsComplete get return Alpha <= 0; public LightningBolt(Vector2 source, Vector2 dest) : this(source, dest, new Color(0.9f, 0.8f, 1f)) public LightningBolt(Vector2 source, Vector2 dest, Color color) Segments = CreateBolt(source, dest, 2); Tint = color; Alpha = 1f; FadeOutRate = 0.03f; public void Draw(SpriteBatch spriteBatch) if (Alpha <= 0) return; foreach (var segment in Segments) segment.Draw(spriteBatch, Tint * (Alpha * 0.6f)); public virtual void Update() Alpha -= FadeOutRate; protected static List CreateBolt (Vector2 source, Vector2 dest, float thickness) // ... // ...
För att använda detta, skapa helt enkelt en ny Blixt
och ringa Uppdatering()
och Dra()
varje ram. Kallelse Uppdatering()
gör det blekna. Är komplett
kommer att berätta när bulten är helt bleknad.
Nu kan du rita dina bultar genom att använda följande kod i din spelklass:
LightningBolt bolt; MouseState mouseState, lastMouseState; skyddad åsidosätt annullering Uppdatering (GameTime gameTime) lastMouseState = mouseState; mouseState = Mouse.GetState (); var screenSize = ny Vector2 (GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height); var mousePosition = ny vektor2 (mouseState.X, mouseState.Y); om (MouseWasClicked ()) bolt = ny LightningBolt (screenSize / 2, mousePosition); om (bult! = null) bult. Uppdatera (); privat bool MouseWasClicked () return mouseState.LeftButton == ButtonState.Pressed && lastMouseState.LeftButton == ButtonState.Released; skyddad åsidosätt rubbning (GameTime gameTime) GraphicsDevice.Clear (Color.Black); spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); om (bult! = null) bult.Traw (spriteBatch); spriteBatch.End ();
Du kan använda Blixt
klass som ett byggstenar för att skapa mer intressanta ljuseffekter. Till exempel kan du göra bultarna grenen ut som visas nedan:
För att göra blixtgrenen väljer vi slumpmässiga punkter längs blixtbulten och lägger till nya bultar som grenar ut från dessa punkter. I koden nedan skapar vi mellan tre och sex grenar som skiljer sig från huvudbulten vid 30 ° vinklar.
klass BranchLightning Listbultar = ny lista (); offentlig bool IsComplete get return bolts.Count == 0; allmän vektor2 slut get; privat uppsättning Privat Vector2-riktning; statisk Random rand = ny slumpmässig (); offentlig BranchLightning (Vector2 start, Vector2 slutet) End = end; direction = Vector2.Normalize (end-start); Skapa (start, slut); public void Update () bolts = bolts.Where (x =>! x.IsComplete) .ToList (); foreach (var bolt i bultar) bolt.Update (); public void Draw (SpriteBatch spriteBatch) foreach (var bolt i bultar) bolt.Draw (spriteBatch); privat tomt Skapa (Vector2 start, Vector2 slut) var mainBolt = ny LightningBolt (start, slutet); bolts.Add (mainBolt); int numBranches = rand.Next (3, 6); Vector2 diff = endstart; // plocka ett gäng slumpmässiga punkter mellan 0 och 1 och sortera dem float [] branchPoints = Enumerable.Range (0, numBranches) .Välj (x => Rand (0, 1f)) .OrderBy (x => x). toArray (); för (int i = 0; i < branchPoints.Length; i++) // Bolt.GetPoint() gets the position of the lightning bolt at specified fraction (0 = start of bolt, 1 = end) Vector2 boltStart = mainBolt.GetPoint(branchPoints[i]); // rotate 30 degrees. Alternate between rotating left and right. Quaternion rot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathHelper.ToRadians(30 * ((i & 1) == 0 ? 1 : -1))); Vector2 boltEnd = Vector2.Transform(diff * (1 - branchPoints[i]), rot) + boltStart; bolts.Add(new LightningBolt(boltStart, boltEnd)); static float Rand(float min, float max) return (float)rand.NextDouble() * (max - min) + min;
Nedan är en video av en annan effekt som du kan få ut av blixtbultarna:
Först måste vi få pixlarna i texten som vi skulle vilja rita. Vi gör detta genom att dra vår text till en RenderTarget2D
och läser tillbaka pixeldata med RenderTarget2D.GetData
. Om du vill läsa mer om hur du gör textpartikeleffekter har jag en mer detaljerad handledning här.
Vi lagrar koordinaterna för pixlarna i texten som en Lista
. Sedan väljer vi varje ram vi slumpmässigt par av dessa punkter och skapar en blixtbult mellan dem. Vi vill utforma det så att de närmare två punkterna är till varandra, ju större är chansen att vi skapar en bult mellan dem. Det finns en enkel teknik som vi kan använda för att uppnå detta: vi väljer slumpmässigt den första punkten och sedan väljer vi ett fast antal andra punkter slumpmässigt och väljer närmaste.
Antalet kandidatpoäng som vi testar kommer att påverka bländningstextens utseende. Om du kontrollerar ett större antal poäng kan vi hitta mycket nära punkter för att dra bultar mellan, vilket gör texten mycket snygg och läsbar men med färre långa blixtar mellan bokstäver. Mindre siffror gör att blixten blir mer galen men mindre läsbar.
public void Update () foreach (var partikel i textParticles) float x = particle.X / 500f; om (rand.Nästa (50) == 0) Vector2 nearestParticle = Vector2.Zero; float nearestDist = float.MaxValue; för (int i = 0; i < 50; i++) var other = textParticles[rand.Next(textParticles.Count)]; var dist = Vector2.DistanceSquared(particle, other); if (dist < nearestDist && dist > 10 * 10) nearestDist = dist; närmastePartikel = annat; om (närmasteDist < 200 * 200 && nearestDist > 10 * 10) bultar. Lägg till (ny LightningBolt (partikel, närmaste partikel, Färg.White)); för (int i = bolts.Count - 1; i> = 0; i--) bultar [i]. Uppdatera (); om (bultar [i] .IsComplete) bultar.RemoveAt (i);
Blixtens text, som visas ovan, kan fungera smidigt om du har en topp på linjedatorn, men det är verkligen mycket skattande. Varje bult varar över 30 ramar, och vi skapar dussintals nya bultar varje ram. Eftersom varje blixtbult kan ha upp till ett par hundra linjesegment, och varje linjesegment har tre stycken, hamnar vi på en hel del sprites. Min demo drar till exempel över 25 000 bilder varje ram med optimeringar avstängd. Vi kan göra bättre.
I stället för att dra varje bult tills den tappar ut kan vi rita varje ny bult till ett beläggningsmål och blekna utvändigt målet varje ram. Det betyder att vi, i stället för att dra varje bult för 30 eller fler ramar, bara ritar det en gång. Det betyder också att det inte finns några extra prestandakostnader för att göra våra blixtbultar blekna långsammare och varar längre.
Först ändrar vi LightningText
klass för att bara dra varje bult för en ram. I din Spel
klass, deklarera två RenderTarget2D
variabler: current
och lastFrame
. I LoadContent ()
, initiera dem så här:
lastFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None); currentFrame = new RenderTarget2D (GraphicsDevice, screenSize.X, screenSize.Y, false, SurfaceFormat.HdrBlendable, DepthFormat.None);
Observera att ytformatet är inställt på HdrBlendable
. HDR står för High Dynamic Range, och det indikerar att vår HDR-yta kan representera ett större antal färger. Detta krävs eftersom det gör det möjligt att göra målmålet att ha färger som är ljusare än vita. När flera blixtbultar överlappar vi behöver återställningsmålet för att lagra hela summan av sina färger, vilket kan lägga till över det vanliga färgområdet. Medan dessa ljusare än vita färger fortfarande visas som vita på skärmen är det viktigt att lagra sin fulla ljusstyrka för att få dem att blekna ut korrekt.
Varje ram ritar vi först innehållet i den sista ramen till den aktuella ramen, men lite mörkret. Vi lägger sedan till några nyskapade bultar till den aktuella ramen. Slutligen gör vi vår nuvarande ram till skärmen, och byter sedan de två gör målen så för vår nästa ram, lastFrame
kommer att referera till ramen vi just gjordes.
void DrawLightningText () GraphicsDevice.SetRenderTarget (currentFrame); GraphicsDevice.Clear (Color.Black); // rita den sista ramen vid 96% ljusstyrka spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (lastFrame, Vector2.Zero, Color.White * 0.96f); spriteBatch.End (); // rita nya bultar med tillsatsblandning spriteBatch.Begin (SpriteSortMode.Texture, BlendState.Additive); lightningText.Draw (); spriteBatch.End (); // dra hela grejen till backbuffer GraphicsDevice.SetRenderTarget (null); spriteBatch.Begin (0, BlendState.Opaque, SamplerState.PointClamp, null, null); spriteBatch.Draw (currentFrame, Vector2.Zero, Color.White); spriteBatch.End (); Byt (ref currentFrame, ref lastFrame); tomrumsbyte(ref T a, ref T b) T temp = a; a = b; b = temp;
Vi har diskuterat att göra takljus och blixtljus, men det är säkert inte de enda effekterna du kan göra. Låt oss titta på några andra variationer i blixten du kan använda dig av.
Ofta kanske du vill göra en rörlig blixtljus. Du kan göra detta genom att lägga till en ny kortbult varje ram vid slutet av den föregående ramens bult.
Vector2 lightningEnd = ny vektor2 (100, 100); Vector2 lightningVelocity = ny Vector2 (50, 0); void Update (GameTime gameTime) Bolts.Add (ny LightningBolt (lightningEnd, lightningEnd + lightningVelocity)); lightningEnd + = lightningVelocity; // ...
Du kanske har märkt att blixten lyser ljusare på lederna. Detta beror på tillsatsblandningen. Du kanske vill ha en jämnare, jämnare look för din blixt. Detta kan uppnås genom att ändra blandningsstatusfunktionen för att välja maxvärdet för käll- och destinationsfärgerna, som visas nedan.
privat statisk readonly BlendState maxBlend = ny BlendState () AlphaBlendFunction = BlendFunction.Max, ColorBlendFunction = BlendFunction.Max, AlphaDestinationBlend = Blend.One, AlphaSourceBlend = Blend.One, ColorDestinationBlend = Blend.One, ColorSourceBlend = Blend.One;
Därefter, i din Dra()
funktion, samtal SpriteBatch.Begin ()
med maxBlend
som den BlendState
istället för BlendState.Additive
. Bilderna nedan visar skillnaden mellan tillsatsblandning och maximal blandning på en blixtbult.
Naturligtvis tillåter maximal blandning inte ljuset från flera bultar eller från bakgrunden för att lägga upp snyggt. Om du vill att bulten ska se smidig ut, men också att blanda tillsats med andra bultar, kan du först göra bulten till ett beläggningsmål med maximal blandning och dra sedan in målmålet på skärmen med tillsatsblandning. Var försiktig så att du inte använder för många stora gör mål eftersom detta kommer att skada prestanda.
Ett annat alternativ, som kommer att fungera bättre för ett stort antal bultar, är att eliminera glödet inbyggt i linjesegmentbilderna och lägga till det med en efterbehandlad glödseffekt. Detaljerna om hur du använder shaders och ger glödseffekter ligger utanför ramen för denna handledning, men du kan använda XNA Bloom Sample för att komma igång. Denna teknik kommer inte att kräva mer göra mål eftersom du lägger till fler bultar.
Blixt är en bra speciell effekt för sprucing upp dina spel. Effekterna som beskrivs i denna handledning är en bra utgångspunkt, men det är verkligen inte allt du kan göra med blixtnedslag. Med lite fantasi kan du göra alla slags ångerinspirerande blixtkänslor! Ladda ner källkoden och experimentera med din egen.
Om du haft den här artikeln, ta en titt på min handledning om 2D-vatteneffekter också.