So erzeugen Sie schockierend gute 2D-Blitzeffekte in Unity (C#)
() translation by (you can also view the original English article)
Es gibt viele Einsatzmöglichkeiten für Blitzeffekte in Spielen, von der Hintergrundatmosphäre während eines Sturms bis hin zu den verheerenden Blitzangriffen eines Zauberers. In diesem Tutorial erkläre ich, wie Sie programmgesteuert fantastische 2D-Blitzeffekte erzeugen: Schrauben, Zweige und sogar Text.
Dieses Tutorial wurde speziell für Unity geschrieben, mit allen Codeausschnitten in C#. Das gleiche Tutorial ist auch mit JavaScript-Code verfügbar. Wenn Sie Unity nicht verwenden, sehen Sie sich diese plattformunabhängige Version desselben Tutorials an. Es wurde für XNA geschrieben, aber Sie sollten in der Lage sein, dieselben Techniken und Konzepte in jeder Gamedev-Engine und -Plattform zu verwenden.
Demo
Sehen Sie sich die Demo unten an:
Klicken Sie auf das Unity-Objekt und verwenden Sie dann die Zifferntasten, um zwischen den Demos zu wechseln. Bei einigen Demos müssen Sie auf eine oder zwei Stellen klicken, um sie zu aktivieren.
Grundeinstellung
Um zu beginnen, müssen Sie ein neues 2D-Projekt in Unity erstellen. Benennen Sie es, wie Sie möchten. Erstellen Sie in Unity vier Ordner: Materials
, Prefabs
, Skripts
und Sprites
.



Klicken Sie anschließend auf die Hauptkamera und stellen Sie sicher, dass die Projektion auf Orthographic
eingestellt ist. Stellen Sie die Größe der Kamera auf 10
ein.
Klicken Sie mit der rechten Maustaste auf den Ordner Materials und wählen Sie Erstellen > Material. Benennen Sie es in Additive
um. Wählen Sie dieses Material aus und ändern Sie seinen Shader in Partikel > Additiv. Dies wird später dazu beitragen, dass Ihr Blitz "knallt".
Schritt 1: Zeichnen Sie eine leuchtende Linie
Der Grundbaustein, aus dem wir Blitze machen müssen, ist ein Liniensegment. Öffnen Sie zunächst Ihre bevorzugte Bildbearbeitungssoftware und zeichnen Sie eine gerade Blitzlinie mit Glüheffekt. So sieht meiner aus:

Wir möchten Linien unterschiedlicher Länge zeichnen, also schneiden wir das Liniensegment wie unten gezeigt in drei Teile (schneiden Sie Ihr Bild nach Bedarf zu). Dies ermöglicht es uns, das mittlere Segment auf jede gewünschte Länge zu dehnen. Da wir das mittlere Segment strecken werden, können wir es als nur ein einzelnes Pixel dick speichern. Da das linke und das rechte Stück Spiegelbilder voneinander sind, müssen wir nur eines davon speichern; Wir können es im Code umdrehen.

Ziehen Sie Ihre Bilddateien in den Sprites-Ordner im Projektfenster. Dadurch werden die Bilddateien in das Unity-Projekt importiert. Klicken Sie auf die Sprites, um sie im Inspektorfenster anzuzeigen. Stellen Sie sicher, dass der Texturtyp auf Sprite (2D \ uGUI)
eingestellt ist, und stellen Sie das Pack-Tag auf Line
ein.
Das Packing Tag hilft Unity beim Zeichnen unseres Blitzes beim Zeichnen von Draw-Aufrufen zu sparen. Stellen Sie also sicher, dass Sie beiden Sprites das gleiche Packing Tag geben, sonst wird die Leistung nicht verbessert.



Jetzt deklarieren wir eine neue Klasse für das Zeichnen von Liniensegmenten:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class Line : MonoBehaviour |
5 |
{
|
6 |
//Start
|
7 |
public Vector2 A; |
8 |
|
9 |
//End
|
10 |
public Vector2 B; |
11 |
|
12 |
//Thickness of line
|
13 |
public float Thickness; |
14 |
|
15 |
//Children that contain the pieces that make up the line
|
16 |
public GameObject StartCapChild, LineChild, EndCapChild; |
17 |
|
18 |
//Create a new line
|
19 |
public Line(Vector2 a, Vector2 b, float thickness) |
20 |
{
|
21 |
A = a; |
22 |
B = b; |
23 |
Thickness = thickness; |
24 |
}
|
25 |
|
26 |
//Used to set the color of the line
|
27 |
public void SetColor(Color color) |
28 |
{
|
29 |
StartCapChild.GetComponent<SpriteRenderer>().color = color; |
30 |
LineChild.GetComponent<SpriteRenderer>().color = color; |
31 |
EndCapChild.GetComponent<SpriteRenderer>().color = color; |
32 |
}
|
33 |
|
34 |
//...
|
35 |
}
|
A und B sind die Endpunkte der Linie. Durch Skalieren und Drehen der Teile der Linie können wir eine Linie beliebiger Dicke, Länge und Ausrichtung zeichnen.
Fügen Sie die folgende Draw()
-Methode am Ende der Line
-Klasse hinzu:
1 |
//Will actually draw the line
|
2 |
public void Draw() |
3 |
{
|
4 |
Vector2 difference = B - A; |
5 |
float rotation = Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg; |
6 |
|
7 |
//Set the scale of the line to reflect length and thickness
|
8 |
LineChild.transform.localScale = new Vector3(100 * (difference.magnitude / LineChild.GetComponent<SpriteRenderer>().sprite.rect.width), |
9 |
Thickness, |
10 |
LineChild.transform.localScale.z); |
11 |
|
12 |
StartCapChild.transform.localScale = new Vector3(StartCapChild.transform.localScale.x, |
13 |
Thickness, |
14 |
StartCapChild.transform.localScale.z); |
15 |
|
16 |
EndCapChild.transform.localScale = new Vector3(EndCapChild.transform.localScale.x, |
17 |
Thickness, |
18 |
EndCapChild.transform.localScale.z); |
19 |
|
20 |
//Rotate the line so that it is facing the right direction
|
21 |
LineChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation)); |
22 |
StartCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation)); |
23 |
EndCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation + 180)); |
24 |
|
25 |
//Move the line to be centered on the starting point
|
26 |
LineChild.transform.position = new Vector3 (A.x, A.y, LineChild.transform.position.z); |
27 |
StartCapChild.transform.position = new Vector3 (A.x, A.y, StartCapChild.transform.position.z); |
28 |
EndCapChild.transform.position = new Vector3 (A.x, A.y, EndCapChild.transform.position.z); |
29 |
|
30 |
//Need to convert rotation to radians at this point for Cos/Sin
|
31 |
rotation *= Mathf.Deg2Rad; |
32 |
|
33 |
//Store these so we only have to access once
|
34 |
float lineChildWorldAdjust = LineChild.transform.localScale.x * LineChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f; |
35 |
float startCapChildWorldAdjust = StartCapChild.transform.localScale.x * StartCapChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f; |
36 |
float endCapChildWorldAdjust = EndCapChild.transform.localScale.x * EndCapChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f; |
37 |
|
38 |
//Adjust the middle segment to the appropriate position
|
39 |
LineChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust, |
40 |
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust, |
41 |
0); |
42 |
|
43 |
//Adjust the start cap to the appropriate position
|
44 |
StartCapChild.transform.position -= new Vector3 (.01f * Mathf.Cos(rotation) * startCapChildWorldAdjust, |
45 |
.01f * Mathf.Sin(rotation) * startCapChildWorldAdjust, |
46 |
0); |
47 |
|
48 |
//Adjust the end cap to the appropriate position
|
49 |
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust * 2, |
50 |
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust * 2, |
51 |
0); |
52 |
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * endCapChildWorldAdjust, |
53 |
.01f * Mathf.Sin(rotation) * endCapChildWorldAdjust, |
54 |
0); |
55 |
}
|
Die Art und Weise, wie wir das mittlere Segment und die Kappen positionieren, werden sie beim Zeichnen nahtlos verbinden. Die Startkappe wird an Punkt A positioniert, das mittlere Segment wird auf die gewünschte Breite gestreckt und die Endkappe wird um 180° gedreht und an Punkt B gezeichnet.
Jetzt müssen wir ein Prefab erstellen, mit dem unsere Line-Klasse arbeiten kann. Wählen Sie in Unity im Menü GameObject > Create Empty. Das Objekt wird in Ihrem Hierarchiebereich angezeigt. Benennen Sie es in Line
um und ziehen Sie Ihr Line
-Skript darauf. Es sollte in etwa wie das Bild unten aussehen.



Wir verwenden dieses Objekt als Container für die Teile unseres Liniensegments.
Jetzt müssen wir Objekte für die Teile unseres Liniensegments erstellen. Erstellen Sie drei Sprites, indem Sie GameObject > Create Other > Sprite aus dem Menü auswählen. Benennen Sie sie in StartCap
, MiddleSegment
und EndCap
um. Ziehen Sie sie auf unser Line-Objekt, sodass sie zu seinen Kindern werden – dies sollte ungefähr wie in der Abbildung unten aussehen.



Gehen Sie jedes Kind durch und stellen Sie sein Material im Sprite-Renderer auf das zuvor erstellte Additiv-Material ein. Weisen Sie jedem Kind das entsprechende Sprite zu. (Die beiden Caps sollten das Cap-Sprite erhalten und das mittlere Segment sollte das Line-Sprite erhalten.)
Klicken Sie auf das Linienobjekt, damit Sie das Skript im Inspektorfenster sehen können. Weisen Sie den Kindern die entsprechenden Slots zu und ziehen Sie dann das Line-Objekt in den Prefabs-Ordner, um ein Prefab dafür zu erstellen. Sie können jetzt das Linienobjekt aus dem Hierarchiebedienfeld löschen.
Schritt 2: Erstellen Sie gezackte Linien
Blitze neigen dazu, gezackte Linien zu bilden, daher benötigen wir einen Algorithmus, um diese zu generieren. Wir werden dies tun, indem wir zufällig Punkte entlang einer Linie auswählen und sie um einen zufälligen Abstand von der Linie verschieben.
Die Verwendung einer völlig zufälligen Verschiebung führt dazu, dass die Linie zu gezackt wird. Daher glätten wir die Ergebnisse, indem wir einschränken, wie weit benachbarte Punkte voneinander verschoben werden können – siehe den Unterschied zwischen der zweiten und dritten Linie in der Abbildung unten.

Wir glätten die Linie, indem wir Punkte mit einem ähnlichen Versatz zum vorherigen Punkt platzieren; Dadurch kann die Linie als Ganzes auf und ab wandern und gleichzeitig verhindern, dass ein Teil davon zu gezackt wird.
Lassen Sie uns eine LightningBolt
-Klasse erstellen, um das Erstellen unserer gezackten Linien zu verarbeiten.
1 |
using UnityEngine; |
2 |
using System.Collections.Generic; |
3 |
|
4 |
class LightningBolt : MonoBehaviour |
5 |
{
|
6 |
//List of all of our active/inactive lines
|
7 |
public List<GameObject> ActiveLineObj; |
8 |
public List<GameObject> InactiveLineObj; |
9 |
|
10 |
//Prefab for a line
|
11 |
public GameObject LinePrefab; |
12 |
|
13 |
//Transparency
|
14 |
public float Alpha { get; set; } |
15 |
|
16 |
//The speed at which our bolts will fade out
|
17 |
public float FadeOutRate { get; set; } |
18 |
|
19 |
//The color of our bolts
|
20 |
public Color Tint { get; set; } |
21 |
|
22 |
//The position where our bolt started
|
23 |
public Vector2 Start { get { return ActiveLineObj[0].GetComponent<Line>().A; } } |
24 |
|
25 |
//The position where our bolt ended
|
26 |
public Vector2 End { get { return ActiveLineObj[ActiveLineObj.Count-1].GetComponent<Line>().B; } } |
27 |
|
28 |
//True if the bolt has completely faded out
|
29 |
public bool IsComplete { get { return Alpha <= 0; } } |
30 |
|
31 |
public void Initialize(int maxSegments) |
32 |
{
|
33 |
//Initialize lists for pooling
|
34 |
ActiveLineObj = new List<GameObject>(); |
35 |
InactiveLineObj = new List<GameObject>(); |
36 |
|
37 |
for(int i = 0; i < maxSegments; i++) |
38 |
{
|
39 |
//instantiate from our Line Prefab
|
40 |
GameObject line = (GameObject)GameObject.Instantiate(LinePrefab); |
41 |
|
42 |
//parent it to our bolt object
|
43 |
line.transform.parent = transform; |
44 |
|
45 |
//set it inactive
|
46 |
line.SetActive(false); |
47 |
|
48 |
//add it to our list
|
49 |
InactiveLineObj.Add(line); |
50 |
}
|
51 |
}
|
52 |
|
53 |
public void ActivateBolt(Vector2 source, Vector2 dest, Color color, float thickness) |
54 |
{
|
55 |
//Store tint
|
56 |
Tint = color; |
57 |
|
58 |
//Store alpha
|
59 |
Alpha = 1.5f; |
60 |
|
61 |
//Store fade out rate
|
62 |
FadeOutRate = 0.03f; |
63 |
|
64 |
//actually create the bolt
|
65 |
//Prevent from getting a 0 magnitude
|
66 |
if(Vector2.Distance(dest, source) <= 0) |
67 |
{
|
68 |
Vector2 adjust = Random.insideUnitCircle; |
69 |
if(adjust.magnitude <= 0) adjust.x += .1f; |
70 |
dest += adjust; |
71 |
}
|
72 |
|
73 |
//difference from source to destination
|
74 |
Vector2 slope = dest - source; |
75 |
Vector2 normal = (new Vector2(slope.y, -slope.x)).normalized; |
76 |
|
77 |
//distance between source and destination
|
78 |
float distance = slope.magnitude; |
79 |
|
80 |
List<float> positions = new List<float>(); |
81 |
positions.Add(0); |
82 |
|
83 |
for (int i = 0; i < distance / 4; i++) |
84 |
{
|
85 |
//Generate random positions between 0 and 1 to break up the bolt
|
86 |
//positions.Add (Random.Range(0f, 1f));
|
87 |
positions.Add (Random.Range(.25f, .75f)); |
88 |
}
|
89 |
|
90 |
positions.Sort(); |
91 |
|
92 |
const float Sway = 80; |
93 |
const float Jaggedness = 1 / Sway; |
94 |
|
95 |
//Affects how wide the bolt is allowed to spread
|
96 |
float spread = 1f; |
97 |
|
98 |
//Start at the source
|
99 |
Vector2 prevPoint = source; |
100 |
|
101 |
//No previous displacement, so just 0
|
102 |
float prevDisplacement = 0; |
103 |
|
104 |
for (int i = 1; i < positions.Count; i++) |
105 |
{
|
106 |
//don't allow more than we have in the pool
|
107 |
int inactiveCount = InactiveLineObj.Count; |
108 |
if(inactiveCount <= 0) break; |
109 |
|
110 |
float pos = positions[i]; |
111 |
|
112 |
//used to prevent sharp angles by ensuring very close positions also have small perpendicular variation.
|
113 |
float scale = (distance * Jaggedness) * (pos - positions[i - 1]); |
114 |
|
115 |
//defines an envelope. Points near the middle of the bolt can be further from the central line.
|
116 |
float envelope = pos > 0.95f ? 20 * (1 - pos) : spread; |
117 |
|
118 |
float displacement = Random.Range(-Sway, Sway); |
119 |
displacement -= (displacement - prevDisplacement) * (1 - scale); |
120 |
displacement *= envelope; |
121 |
|
122 |
//Calculate the end point
|
123 |
Vector2 point = source + (pos * slope) + (displacement * normal); |
124 |
|
125 |
activateLine(prevPoint, point, thickness); |
126 |
prevPoint = point; |
127 |
prevDisplacement = displacement; |
128 |
}
|
129 |
|
130 |
activateLine(prevPoint, dest, thickness); |
131 |
}
|
132 |
|
133 |
public void DeactivateSegments() |
134 |
{
|
135 |
for(int i = ActiveLineObj.Count - 1; i >= 0; i--) |
136 |
{
|
137 |
GameObject line = ActiveLineObj[i]; |
138 |
line.SetActive(false); |
139 |
ActiveLineObj.RemoveAt(i); |
140 |
InactiveLineObj.Add(line); |
141 |
}
|
142 |
}
|
143 |
|
144 |
void activateLine(Vector2 A, Vector2 B, float thickness) |
145 |
{
|
146 |
//get the inactive count
|
147 |
int inactiveCount = InactiveLineObj.Count; |
148 |
|
149 |
//only activate if we can pull from inactive
|
150 |
if(inactiveCount <= 0) return; |
151 |
|
152 |
//pull the GameObject
|
153 |
GameObject line = InactiveLineObj[inactiveCount - 1]; |
154 |
|
155 |
//set it active
|
156 |
line.SetActive(true); |
157 |
|
158 |
//get the Line component
|
159 |
Line lineComponent = line.GetComponent<Line>(); |
160 |
lineComponent.SetColor(Color.white); |
161 |
lineComponent.A = A; |
162 |
lineComponent.B = B; |
163 |
lineComponent.Thickness = thickness; |
164 |
InactiveLineObj.RemoveAt(inactiveCount - 1); |
165 |
ActiveLineObj.Add(line); |
166 |
}
|
167 |
|
168 |
public void Draw() |
169 |
{
|
170 |
//if the bolt has faded out, no need to draw
|
171 |
if (Alpha <= 0) return; |
172 |
|
173 |
foreach (GameObject obj in ActiveLineObj) |
174 |
{
|
175 |
Line lineComponent = obj.GetComponent<Line>(); |
176 |
lineComponent.SetColor(Tint * (Alpha * 0.6f)); |
177 |
lineComponent.Draw(); |
178 |
}
|
179 |
}
|
180 |
|
181 |
public void UpdateBolt() |
182 |
{
|
183 |
Alpha -= FadeOutRate; |
184 |
}
|
185 |
|
186 |
//...
|
187 |
}
|
Der Code sieht vielleicht etwas einschüchternd aus, ist aber nicht so schlimm, wenn Sie die Logik verstanden haben. Bevor wir fortfahren, sollten Sie verstehen, dass wir uns entschieden haben, unsere Liniensegmente in den Bolzen zu bündeln (da das ständige Instanziieren und Zerstören von Objekten in Unity kostspielig sein kann).
- Die Funktion
Initialize()
wird einmal für jeden Blitz aufgerufen und bestimmt, wie viele Liniensegmente jeder Blitz verwenden darf. - Die Funktion
activateLine()
aktiviert ein Liniensegment mit den gegebenen Positionsdaten. - Die Funktion
DeactivateSegments()
deaktiviert alle aktiven Liniensegmente in unserer Schraube. - Die Funktion
ActivateBolt()
übernimmt die Erstellung unserer gezackten Linien und ruft die FunktionactivateLine()
auf, um unsere Liniensegmente an den entsprechenden Positionen zu aktivieren.
Um unsere gezackten Linien zu erstellen, berechnen wir zunächst die Steigung zwischen unseren beiden Punkten sowie den Normalenvektor zu dieser Steigung. Wir wählen dann eine Reihe von zufälligen Positionen entlang der Linie und speichern sie in unserer Positionsliste. Wir skalieren diese Positionen zwischen 0
und 1
, sodass 0
den Anfang der Linie und 1
den Endpunkt darstellt. Dann sortieren wir diese Positionen, damit wir einfach Liniensegmente dazwischen einfügen können.
Die Schleife durchläuft die zufällig ausgewählten Punkte und verschiebt sie um einen zufälligen Betrag entlang der Normalen. Der Skalierungsfaktor
ist dazu da, zu scharfe Winkel zu vermeiden, und die envelope
stellt sicher, dass der Blitz tatsächlich zum Zielpunkt geht, indem sie die Verschiebung begrenzt, wenn wir uns dem Ende nähern. Die spread
soll dabei helfen, zu kontrollieren, wie weit die Segmente von der Steigung unserer Linie abweichen; ein spread
von 0
ergibt im Wesentlichen eine gerade Linie.



Also, wie wir es mit unserer Line
-Klasse gemacht haben, machen wir daraus ein Fertighaus. Wählen Sie im Menü GameObject > Leer erstellen. Das Objekt wird in Ihrem Hierarchiebereich angezeigt. Benennen Sie es in Bolt
um und ziehen Sie eine Kopie des LightningBolt
-Skripts darauf. Klicken Sie abschließend auf das Bolt-Objekt und weisen Sie das Line-Prefab aus dem Prefabs-Ordner dem entsprechenden Slot im LightningBolt-Skript zu. Wenn Sie damit fertig sind, ziehen Sie einfach das Bolt-Objekt in den Prefabs-Ordner, um ein Prefab zu erstellen.
Schritt 3: Animation hinzufügen
Blitze sollten hell aufblitzen und dann ausblenden. Dafür sind unsere Funktionen Update()
und Draw()
in LightningBolt
gedacht. Durch Aufrufen von Update()
wird die Schraube ausgeblendet. Der Aufruf von Draw()
aktualisiert die Farbe der Schraube auf dem Bildschirm. IsComplete
zeigt Ihnen an, wenn der Bolzen vollständig ausgeblendet ist.
Schritt 4: Erstellen Sie eine Schraube
Nun, da wir unsere LightningBolt
-Klasse haben, wollen wir sie tatsächlich sinnvoll nutzen und eine schnelle Demo-Szene einrichten.
Wir werden für diese Demo einen Objektpool verwenden, also möchten wir ein leeres Objekt erstellen, um unsere aktiven und inaktiven Schrauben aufzunehmen (einfach aus organisatorischen Gründen). Wählen Sie in Unity im Menü GameObject > Create Empty. Das Objekt wird in Ihrem Hierarchiefenster angezeigt. Benennen Sie es in LightningPoolHolder
um.
Klicken Sie mit der rechten Maustaste auf den Ordner Skripte und wählen Sie Erstellen > C# Skript. Benennen Sie Ihr Skript DemoScript
und öffnen Sie es. Hier ist ein kurzer Code, um Ihnen den Einstieg zu erleichtern:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
using System.Collections.Generic; |
4 |
|
5 |
public class DemoScript : MonoBehaviour |
6 |
{
|
7 |
//Prefabs to be assigned in Editor
|
8 |
public GameObject BoltPrefab; |
9 |
|
10 |
//For pooling
|
11 |
List<GameObject> activeBoltsObj; |
12 |
List<GameObject> inactiveBoltsObj; |
13 |
int maxBolts = 1000; |
14 |
|
15 |
//For handling mouse clicks
|
16 |
int clicks = 0; |
17 |
Vector2 pos1, pos2; |
18 |
|
19 |
void Start() |
20 |
{
|
21 |
//Initialize lists
|
22 |
activeBoltsObj = new List<GameObject>(); |
23 |
inactiveBoltsObj = new List<GameObject>(); |
24 |
|
25 |
//Grab the parent we'll be assigning to our bolt pool
|
26 |
GameObject p = GameObject.Find("LightningPoolHolder"); |
27 |
|
28 |
//For however many bolts we've specified
|
29 |
for(int i = 0; i < maxBolts; i++) |
30 |
{
|
31 |
//create from our prefab
|
32 |
GameObject bolt = (GameObject)Instantiate(BoltPrefab); |
33 |
|
34 |
//Assign parent
|
35 |
bolt.transform.parent = p.transform; |
36 |
|
37 |
//Initialize our lightning with a preset number of max sexments
|
38 |
bolt.GetComponent<LightningBolt>().Initialize(25); |
39 |
|
40 |
//Set inactive to start
|
41 |
bolt.SetActive(false); |
42 |
|
43 |
//Store in our inactive list
|
44 |
inactiveBoltsObj.Add(bolt); |
45 |
}
|
46 |
}
|
47 |
|
48 |
void Update() |
49 |
{
|
50 |
//Declare variables for use later
|
51 |
GameObject boltObj; |
52 |
LightningBolt boltComponent; |
53 |
|
54 |
//store off the count for effeciency
|
55 |
int activeLineCount = activeBoltsObj.Count; |
56 |
|
57 |
//loop through active lines (backwards because we'll be removing from the list)
|
58 |
for (int i = activeLineCount - 1; i >= 0; i--) |
59 |
{
|
60 |
//pull GameObject
|
61 |
boltObj = activeBoltsObj[i]; |
62 |
|
63 |
//get the LightningBolt component
|
64 |
boltComponent = boltObj.GetComponent<LightningBolt>(); |
65 |
|
66 |
//if the bolt has faded out
|
67 |
if(boltComponent.IsComplete) |
68 |
{
|
69 |
//deactive the segments it contains
|
70 |
boltComponent.DeactivateSegments(); |
71 |
|
72 |
//set it inactive
|
73 |
boltObj.SetActive(false); |
74 |
|
75 |
//move it to the inactive list
|
76 |
activeBoltsObj.RemoveAt(i); |
77 |
inactiveBoltsObj.Add(boltObj); |
78 |
}
|
79 |
}
|
80 |
|
81 |
//If left mouse button pressed
|
82 |
if(Input.GetMouseButtonDown(0)) |
83 |
{
|
84 |
//if first click
|
85 |
if(clicks == 0) |
86 |
{
|
87 |
//store starting position
|
88 |
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
89 |
pos1 = new Vector2(temp.x, temp.y); |
90 |
}
|
91 |
else if(clicks == 1) //second click |
92 |
{
|
93 |
//store end position
|
94 |
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
95 |
pos2 = new Vector2(temp.x, temp.y); |
96 |
|
97 |
//create a (pooled) bolt from pos1 to pos2
|
98 |
CreatePooledBolt(pos1,pos2, Color.white, 1f); |
99 |
}
|
100 |
|
101 |
//increment our tick count
|
102 |
clicks++; |
103 |
|
104 |
//restart the count after 2 clicks
|
105 |
if(clicks > 1) clicks = 0; |
106 |
}
|
107 |
|
108 |
//update and draw active bolts
|
109 |
for(int i = 0; i < activeBoltsObj.Count; i++) |
110 |
{
|
111 |
activeBoltsObj[i].GetComponent<LightningBolt>().UpdateBolt(); |
112 |
activeBoltsObj[i].GetComponent<LightningBolt>().Draw(); |
113 |
}
|
114 |
}
|
115 |
|
116 |
void CreatePooledBolt(Vector2 source, Vector2 dest, Color color, float thickness) |
117 |
{
|
118 |
//if there is an inactive bolt to pull from the pool
|
119 |
if(inactiveBoltsObj.Count > 0) |
120 |
{
|
121 |
//pull the GameObject
|
122 |
GameObject boltObj = inactiveBoltsObj[inactiveBoltsObj.Count - 1]; |
123 |
|
124 |
//set it active
|
125 |
boltObj.SetActive(true); |
126 |
|
127 |
//move it to the active list
|
128 |
activeBoltsObj.Add(boltObj); |
129 |
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1); |
130 |
|
131 |
//get the bolt component
|
132 |
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>(); |
133 |
|
134 |
//activate the bolt using the given position data
|
135 |
boltComponent.ActivateBolt(source, dest, color, thickness); |
136 |
}
|
137 |
}
|
138 |
}
|
Dieser Code bietet uns lediglich die Möglichkeit, Schrauben mithilfe von Objektpooling zu erstellen. Es gibt andere Möglichkeiten, dies einzurichten, aber hiermit gehen wir vor! Sobald wir es eingerichtet haben, müssen Sie nur noch zweimal klicken, um eine Schraube auf dem Bildschirm zu erstellen: einmal für die Startposition und einmal für die Endposition.
Wir benötigen ein Objekt, um unser DemoScript
anzulegen. Wählen Sie im Menü GameObject > Leer erstellen. Das Objekt wird in Ihrem Hierarchiebereich angezeigt. Benennen Sie es in DemoScript
um und ziehen Sie Ihr DemoScript-Skript darauf. Klicken Sie auf das DemoScript-Objekt, damit wir es im Inspektorfenster anzeigen können. Weisen Sie das Bolt Prefab aus dem Prefabs-Ordner dem passenden Slot im DemoScript zu.



Das sollte reichen, um dich in Schwung zu bringen! Führen Sie die Szene in Unity aus und probieren Sie es aus!
Schritt 5: Branch Lightning erstellen
Sie können die LightningBolt
-Klasse als Baustein verwenden, um interessantere Blitzeffekte zu erstellen. Sie können die Schrauben beispielsweise wie unten gezeigt verzweigen:

Um den Blitz zu verzweigen, wählen wir zufällige Punkte entlang des Blitzes und fügen neue Bolzen hinzu, die von diesen Punkten abzweigen. Im folgenden Code erstellen wir zwischen drei und sechs Zweige, die sich in einem Winkel von 30° vom Hauptbolzen trennen.
1 |
using UnityEngine; |
2 |
using System.Collections.Generic; |
3 |
|
4 |
class BranchLightning : MonoBehaviour |
5 |
{
|
6 |
//For holding all of our bolts in our branch
|
7 |
List<GameObject> boltsObj = new List<GameObject>(); |
8 |
|
9 |
//If there are no bolts, then the branch is complete (we're not pooling here, but you could if you wanted)
|
10 |
public bool IsComplete { get { return boltsObj.Count == 0; } } |
11 |
|
12 |
//Start position of branch
|
13 |
public Vector2 Start { get; private set; } |
14 |
|
15 |
//End position of branch
|
16 |
public Vector2 End { get; private set; } |
17 |
|
18 |
static Random rand = new Random(); |
19 |
|
20 |
public void Initialize(Vector2 start, Vector2 end, GameObject boltPrefab) |
21 |
{
|
22 |
//store start and end positions
|
23 |
Start = start; |
24 |
End = end; |
25 |
|
26 |
//create the main bolt from our bolt prefab
|
27 |
GameObject mainBoltObj = (GameObject)GameObject.Instantiate(boltPrefab); |
28 |
|
29 |
//get the LightningBolt component
|
30 |
LightningBolt mainBoltComponent = mainBoltObj.GetComponent<LightningBolt>(); |
31 |
|
32 |
//initialize our bolt with a max of 5 segments
|
33 |
mainBoltComponent.Initialize(5); |
34 |
|
35 |
//activate the bolt with our position data
|
36 |
mainBoltComponent.ActivateBolt(start, end, Color.white, 1f); |
37 |
|
38 |
//add it to our list
|
39 |
boltsObj.Add(mainBoltObj); |
40 |
|
41 |
//randomly determine how many sub branches there will be (3-6)
|
42 |
int numBranches = Random.Range(3,6); |
43 |
|
44 |
//calculate the difference between our start and end points
|
45 |
Vector2 diff = end - start; |
46 |
|
47 |
// pick a bunch of random points between 0 and 1 and sort them
|
48 |
List<float> branchPoints = new List<float>(); |
49 |
for(int i = 0; i < numBranches; i++) branchPoints.Add(Random.value); |
50 |
branchPoints.Sort(); |
51 |
|
52 |
//go through those points
|
53 |
for (int i = 0; i < branchPoints.Count; i++) |
54 |
{
|
55 |
// Bolt.GetPoint() gets the position of the lightning bolt based on the percentage passed in (0 = start of bolt, 1 = end)
|
56 |
Vector2 boltStart = mainBoltComponent.GetPoint(branchPoints[i]); |
57 |
|
58 |
//get rotation of 30 degrees. Alternate between rotating left and right. (i & 1 will be true for all odd numbers...yay bitwise operators!)
|
59 |
Quaternion rot = Quaternion.AngleAxis(30 * ((i & 1) == 0 ? 1 : -1), new Vector3(0,0,1)); |
60 |
|
61 |
//calculate how much to adjust for our end position
|
62 |
Vector2 adjust = rot * (Random.Range(.5f, .75f) * diff * (1 - branchPoints[i])); |
63 |
|
64 |
//get the end position
|
65 |
Vector2 boltEnd = adjust + boltStart; |
66 |
|
67 |
//instantiate from our bolt prefab
|
68 |
GameObject boltObj = (GameObject)GameObject.Instantiate(boltPrefab); |
69 |
|
70 |
//get the LightningBolt component
|
71 |
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>(); |
72 |
|
73 |
//initialize our bolt with a max of 5 segments
|
74 |
boltComponent.Initialize(5); |
75 |
|
76 |
//activate the bolt with our position data
|
77 |
boltComponent.ActivateBolt(boltStart, boltEnd, Color.white, 1f); |
78 |
|
79 |
//add it to the list
|
80 |
boltsObj.Add(boltObj); |
81 |
}
|
82 |
}
|
83 |
|
84 |
public void UpdateBranch() |
85 |
{
|
86 |
//go through our active bolts
|
87 |
for (int i = boltsObj.Count - 1; i >= 0; i--) |
88 |
{
|
89 |
//get the GameObject
|
90 |
GameObject boltObj = boltsObj[i]; |
91 |
|
92 |
//get the LightningBolt component
|
93 |
LightningBolt boltComp = boltObj.GetComponent<LightningBolt>(); |
94 |
|
95 |
//update/fade out the bolt
|
96 |
boltComp.UpdateBolt(); |
97 |
|
98 |
//if the bolt has faded
|
99 |
if(boltComp.IsComplete) |
100 |
{
|
101 |
//remove it from our list
|
102 |
boltsObj.RemoveAt(i); |
103 |
|
104 |
//destroy it (would be better to pool but I'll let you figure out how to do that =P)
|
105 |
Destroy(boltObj); |
106 |
}
|
107 |
}
|
108 |
}
|
109 |
|
110 |
//Draw our active bolts on screen
|
111 |
public void Draw() |
112 |
{
|
113 |
foreach (GameObject boltObj in boltsObj) |
114 |
{
|
115 |
boltObj.GetComponent<LightningBolt>().Draw(); |
116 |
}
|
117 |
}
|
118 |
}
|
Dieser Code funktioniert sehr ähnlich wie unsere LightningBolt
-Klasse, mit der Ausnahme, dass er kein Objektpooling verwendet. Der Aufruf von Initialize()
ist alles, was Sie tun müssen, um einen Verzweigungsbolzen zu erstellen; Danach müssen Sie nur noch Update()
und Draw()
aufrufen. Wie das geht, zeige ich Ihnen in unserem DemoScript
später im Tutorial.
Möglicherweise haben Sie den Verweis auf eine GetPoint()
-Funktion in der LightningBolt
-Klasse bemerkt. Wir haben diese Funktion noch nicht wirklich implementiert, also kümmern wir uns jetzt darum.
Fügen Sie die folgende Funktion am Ende der LightningBolt
-Klasse hinzu:
1 |
// Returns the point where the bolt is at a given fraction of the way through the bolt. Passing
|
2 |
// zero will return the start of the bolt, and passing 1 will return the end.
|
3 |
public Vector2 GetPoint(float position) |
4 |
{
|
5 |
Vector2 start = Start; |
6 |
float length = Vector2.Distance(start, End); |
7 |
Vector2 dir = (End - start) / length; |
8 |
position *= length; |
9 |
|
10 |
//find the appropriate line
|
11 |
Line line = ActiveLineObj.Find(x => Vector2.Dot(x.GetComponent<Line>().B - start, dir) >= position).GetComponent<Line>(); |
12 |
float lineStartPos = Vector2.Dot(line.A - start, dir); |
13 |
float lineEndPos = Vector2.Dot(line.B - start, dir); |
14 |
float linePos = (position - lineStartPos) / (lineEndPos - lineStartPos); |
15 |
|
16 |
return Vector2.Lerp(line.A, line.B, linePos); |
17 |
}
|
Schritt 6: Blitztext erstellen
Unten ist ein Video eines anderen Effekts, den Sie aus den Blitzen machen können:
Dafür müssen wir noch etwas mehr einrichten. Wählen Sie zunächst im Projektfenster Erstellen > RenderTexture aus. Benennen Sie es in RenderText
um und setzen Sie seine Größe auf 256x256px
. (Es muss nicht unbedingt genau so groß sein, aber je kleiner es ist, desto schneller wird das Programm ausgeführt.)
Wählen Sie im Menü Bearbeiten > Projekteinstellungen > Tags und Ebenen. Erweitern Sie dann im Informationsfenster das Dropdown-Menü Ebenen und fügen Sie Text
in Benutzerebene 8 hinzu.



Wir müssen dann eine zweite Kamera erstellen. Wählen Sie im Menü GameObject > Andere erstellen > Kamera. Benennen Sie es in TextCamera
um und stellen Sie seine Projektion auf Orthographic
und seine Clear Flags auf Solid Color
. Stellen Sie die Hintergrundfarbe auf (R: 0, G: 0, B: 0, A: 0)
und die Culling-Maske auf nur Text
(die Ebene, die wir gerade erstellt haben). Setzen Sie schließlich die Zieltextur auf RenderText
(die zuvor erstellte RenderTexture). Wahrscheinlich müssen Sie später mit der Größe der Kamera herumspielen, damit alles auf den Bildschirm passt.



Jetzt müssen wir den eigentlichen Text erstellen, den wir mit unserem Blitz zeichnen werden. Wählen Sie im Menü GameObject > Andere erstellen > GUI-Text. Wählen Sie das GUI-Textobjekt aus dem Hierarchiebedienfeld aus und stellen Sie seinen Text auf LIGHTNING
, seinen Anker auf die middle center
und seine Ausrichtung auf die center
ein. Setzen Sie dann seine Ebene auf die Textebene
, die wir zuvor erstellt haben. Sie müssen wahrscheinlich mit der Schriftgröße herumspielen, damit der Text auf den Bildschirm passt.



Wählen Sie nun die Hauptkamera aus und stellen Sie ihre Culling-Maske so ein, dass sie alles außer unserer Textebene ist. Dies führt dazu, dass unser GUI-Text scheinbar vom Bildschirm verschwindet, aber er sollte auf der zuvor erstellten RenderTexture gezeichnet werden: Wählen Sie RenderText aus dem Projektfenster und Sie sollten das Wort LIGHTNING in der Vorschau unten im Fenster sehen können.
Wenn Sie das Wort LIGHTNING nicht sehen können, müssen Sie mit Ihrer Position, Schriftgröße und (Text-) Kameragröße herumspielen. Um Ihnen bei der Positionierung Ihres Textes zu helfen, klicken Sie im Hierarchie-Bedienfeld auf TextCamera und setzen Sie die Zieltextur auf None
. Sie können Ihren GUI-Text jetzt sehen, wenn Sie ihn auf der TextCamera zentrieren. Sobald Sie alles positioniert haben, setzen Sie die Zieltextur der TextCamera wieder auf RenderText
.
Nun zum Code! Wir müssen die Pixel aus dem Text abrufen, den wir zeichnen. Wir können dies tun, indem wir unseren Text auf ein RenderTarget
zeichnen und die Pixeldaten mit Texture2D.ReadPixels()
in ein Texture2D
zurücklesen. Dann können wir die Koordinaten der Pixel aus dem Text als List<Vector2>
speichern.
Hier ist der Code, um das zu tun:
1 |
//Capture the important points of our text for later
|
2 |
IEnumerator TextCapture() |
3 |
{
|
4 |
//must wait until end of frame so something is actually drawn or else it will error
|
5 |
yield return new WaitForEndOfFrame(); |
6 |
|
7 |
//get the camera that draws our text
|
8 |
Camera cam = GameObject.Find("TextCamera").GetComponent<Camera>(); |
9 |
|
10 |
//make sure it has an assigned RenderTexture
|
11 |
if(cam.targetTexture != null) |
12 |
{
|
13 |
//pull the active RenderTexture
|
14 |
RenderTexture.active = cam.targetTexture; |
15 |
|
16 |
//capture the image into a Texture2D
|
17 |
Texture2D image = new Texture2D(cam.targetTexture.width, cam.targetTexture.height); |
18 |
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0); |
19 |
image.Apply(); |
20 |
|
21 |
//calculate how the text will be scaled when it is displayed as lightning on the screen
|
22 |
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x); |
23 |
|
24 |
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
|
25 |
positionText.x -= image.width * scaleText * .5f; |
26 |
positionText.y -= image.height * scaleText * .5f; |
27 |
|
28 |
//basically determines how many pixels we skip/check
|
29 |
const int interval = 2; |
30 |
|
31 |
//loop through pixels
|
32 |
for(int y = 0; y < image.height; y += interval) |
33 |
{
|
34 |
for(int x = 0; x < image.width; x += interval) |
35 |
{
|
36 |
//get the color of the pixel
|
37 |
Color color = image.GetPixel(x,y); |
38 |
|
39 |
//if the color has any r (red) value
|
40 |
if(color.r > 0) |
41 |
{
|
42 |
//add it to our points for drawing
|
43 |
textPoints.Add(new Vector2(x,y)); |
44 |
}
|
45 |
}
|
46 |
}
|
47 |
}
|
48 |
}
|
Hinweis: Wir müssen diese Funktion beim Start unseres Programms als Coroutine ausführen, damit sie korrekt ausgeführt wird.
Danach können wir in jedem Frame zufällig Paare dieser Punkte auswählen und einen Blitz zwischen ihnen erstellen. Wir wollen es so gestalten, dass je näher zwei Punkte beieinander liegen, desto größer ist die Chance, dass wir einen Bolzen zwischen ihnen erzeugen.
Es gibt eine einfache Technik, die wir verwenden können, um dies zu erreichen: Wir wählen den ersten Punkt nach dem Zufallsprinzip aus, und dann wählen wir eine feste Anzahl anderer Punkte nach dem Zufallsprinzip und wählen den nächsten.
Hier ist der Code dafür (wir werden ihn später zu unserem DemoScript
hinzufügen):
1 |
//go through the points we capture earlier
|
2 |
foreach (Vector2 point in textPoints) |
3 |
{
|
4 |
//randomly ignore certain points
|
5 |
if(Random.Range(0,75) != 0) continue; |
6 |
|
7 |
//placeholder values
|
8 |
Vector2 nearestParticle = Vector2.zero; |
9 |
float nearestDistSquared = float.MaxValue; |
10 |
|
11 |
for (int i = 0; i < 50; i++) |
12 |
{
|
13 |
//select a random point
|
14 |
Vector2 other = textPoints[Random.Range(0, textPoints.Count)]; |
15 |
|
16 |
//calculate the distance (squared for performance benefits) between the two points
|
17 |
float distSquared = DistanceSquared(point, other); |
18 |
|
19 |
//If this point is the nearest point (but not too near!)
|
20 |
if (distSquared < nearestDistSquared && distSquared > 3 * 3) |
21 |
{
|
22 |
//store off the data
|
23 |
nearestDistSquared = distSquared; |
24 |
nearestParticle = other; |
25 |
}
|
26 |
}
|
27 |
|
28 |
//if the point we found isn't too near/far
|
29 |
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3) |
30 |
{
|
31 |
//create a (pooled) bolt at the corresponding screen position
|
32 |
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f); |
33 |
}
|
34 |
}
|
35 |
|
36 |
/* The code above uses the following function
|
37 |
* It'll need to be placed appropriately
|
38 |
---------------------------------------------
|
39 |
//calculate distance squared (no square root = performance boost)
|
40 |
public float DistanceSquared(Vector2 a, Vector2 b)
|
41 |
{
|
42 |
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
|
43 |
}
|
44 |
---------------------------------------------*/
|
Die Anzahl der getesteten Kandidatenpunkte beeinflusst das Aussehen des Blitztextes. Wenn wir eine größere Anzahl von Punkten überprüfen, können wir sehr nahe Punkte finden, zwischen denen Bolzen gezogen werden können, wodurch der Text sehr sauber und lesbar wird, jedoch mit weniger langen Blitzen zwischen den Buchstaben. Kleinere Zahlen lassen den Blitztext wilder aussehen, aber weniger lesbar.
Schritt 7: Probieren Sie andere Variationen aus
Wir haben darüber gesprochen, Zweigblitze und Blitztexte zu erstellen, aber dies sind sicherlich nicht die einzigen Effekte, die Sie erzielen können. Schauen wir uns ein paar andere Variationen von Blitzen an, die Sie möglicherweise verwenden möchten.
Beweglicher Blitz
Vielleicht möchten Sie oft einen beweglichen Blitz machen. Sie können dies tun, indem Sie jedem Rahmen eine neue kurze Schraube am Endpunkt der Schraube des vorherigen Rahmens hinzufügen.
1 |
//Will contain all of the pieces for the moving bolt
|
2 |
List<GameObject> movingBolt = new List<GameObject>(); |
3 |
|
4 |
//used for actually moving the moving bolt
|
5 |
Vector2 lightningEnd = new Vector2(100, 100); |
6 |
Vector2 lightningVelocity = new Vector2(1, 0); |
7 |
|
8 |
void Update() |
9 |
{
|
10 |
//loop through all of our bolts that make up the moving bolt
|
11 |
for(int i = movingBolt.Count - 1; i >= 0; i--) |
12 |
{
|
13 |
//get the bolt component
|
14 |
boltComponent = movingBolt[i].GetComponent<LightningBolt>(); |
15 |
|
16 |
//if the bolt has faded out
|
17 |
if(boltComponent.IsComplete) |
18 |
{
|
19 |
//destroy it
|
20 |
Destroy(movingBolt[i]); |
21 |
|
22 |
//remove it from our list
|
23 |
movingBolt.RemoveAt(i); |
24 |
|
25 |
//on to the next one, on on to the next one
|
26 |
continue; |
27 |
}
|
28 |
|
29 |
//update and draw bolt
|
30 |
boltComponent.UpdateBolt(); |
31 |
boltComponent.Draw(); |
32 |
}
|
33 |
|
34 |
//if our moving bolt is active
|
35 |
if(movingBolt.Count > 0) |
36 |
{
|
37 |
//calculate where it currently ends
|
38 |
lightningEnd = movingBolt[movingBolt.Count-1].GetComponent<LightningBolt>().End; |
39 |
|
40 |
//if the end of the bolt is within 25 units of the camera
|
41 |
if(Vector2.Distance(lightningEnd,(Vector2)Camera.main.transform.position) < 25) |
42 |
{
|
43 |
//instantiate from our bolt prefab
|
44 |
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab); |
45 |
|
46 |
//get the bolt component
|
47 |
boltComponent = boltObj.GetComponent<LightningBolt>(); |
48 |
|
49 |
//initialize it with a maximum of 5 segments
|
50 |
boltComponent.Initialize(5); |
51 |
|
52 |
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
|
53 |
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f); |
54 |
|
55 |
//add it to our list
|
56 |
movingBolt.Add(boltObj); |
57 |
|
58 |
//update and draw our new bolt
|
59 |
boltComponent.UpdateBolt(); |
60 |
boltComponent.Draw(); |
61 |
}
|
62 |
}
|
63 |
}
|
Burst Lightning
Diese Variante bietet einen dramatischen Effekt, der Blitze in einem Kreis aus dem Mittelpunkt schießt:
1 |
Vector2 diff = pos2 - pos1; |
2 |
|
3 |
void Update() |
4 |
{
|
5 |
//define how many bolts we want in our circle
|
6 |
int boltsInBurst = 10; |
7 |
|
8 |
for(int i = 0; i < boltsInBurst; i++) |
9 |
{
|
10 |
//rotate around the z axis to the appropriate angle
|
11 |
Quaternion rot = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1)); |
12 |
|
13 |
//Calculate the end position for the bolt
|
14 |
Vector2 boltEnd = (Vector2)(rot * diff) + pos1; |
15 |
|
16 |
//create a (pooled) bolt from pos1 to boltEnd
|
17 |
CreatePooledBolt(pos1, boltEnd, Color.white, 1f); |
18 |
}
|
19 |
}
|
Schritt 8: Fügen Sie alles in DemoScript zusammen
Sie möchten all diese ausgefallenen Effekte, die wir bisher erstellt haben, ausprobieren können, also fügen wir sie alle in das DemoScript
ein, das wir zuvor erstellt haben. Sie können zwischen den Effekten wechseln, indem Sie die Zifferntasten auf Ihrer Tastatur drücken, um den Effekt auszuwählen, und dann einfach zweimal klicken, wie wir es mit unseren Schrauben zuvor getan haben.
Hier ist der vollständige Code:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
using System.Collections.Generic; |
4 |
|
5 |
public class DemoScript : MonoBehaviour |
6 |
{
|
7 |
//Prefabs to be assigned in Editor
|
8 |
public GameObject BoltPrefab; |
9 |
public GameObject BranchPrefab; |
10 |
|
11 |
//For pooling
|
12 |
List<GameObject> activeBoltsObj; |
13 |
List<GameObject> inactiveBoltsObj; |
14 |
int maxBolts = 1000; |
15 |
|
16 |
float scaleText; |
17 |
Vector2 positionText; |
18 |
|
19 |
//Different modes for the demo
|
20 |
enum Mode : byte |
21 |
{
|
22 |
bolt, |
23 |
branch, |
24 |
moving, |
25 |
text, |
26 |
nodes, |
27 |
burst
|
28 |
}
|
29 |
|
30 |
//The current mode the demo is in
|
31 |
Mode currentMode = Mode.bolt; |
32 |
|
33 |
//Will contain all of the pieces for the moving bolt
|
34 |
List<GameObject> movingBolt = new List<GameObject>(); |
35 |
|
36 |
//used for actually moving the moving bolt
|
37 |
Vector2 lightningEnd = new Vector2(100, 100); |
38 |
Vector2 lightningVelocity = new Vector2(1, 0); |
39 |
|
40 |
//Will contain all of the pieces for the branches
|
41 |
List<GameObject> branchesObj; |
42 |
|
43 |
//For handling mouse clicks
|
44 |
int clicks = 0; |
45 |
Vector2 pos1, pos2; |
46 |
|
47 |
//For storing all of the pixels that need to be drawn by the bolts
|
48 |
List<Vector2> textPoints = new List<Vector2>(); |
49 |
|
50 |
//true in text mode
|
51 |
bool shouldText = false; |
52 |
|
53 |
void Start() |
54 |
{
|
55 |
//Initialize lists
|
56 |
activeBoltsObj = new List<GameObject>(); |
57 |
inactiveBoltsObj = new List<GameObject>(); |
58 |
branchesObj = new List<GameObject>(); |
59 |
|
60 |
//Grab the parent we'll be assigning to our bolt pool
|
61 |
GameObject p = GameObject.Find("LightningPoolHolder"); |
62 |
|
63 |
//For however many bolts we've specified
|
64 |
for(int i = 0; i < maxBolts; i++) |
65 |
{
|
66 |
//create from our prefab
|
67 |
GameObject bolt = (GameObject)Instantiate(BoltPrefab); |
68 |
|
69 |
//Assign parent
|
70 |
bolt.transform.parent = p.transform; |
71 |
|
72 |
//Initialize our lightning with a preset number of max sexments
|
73 |
bolt.GetComponent<LightningBolt>().Initialize(25); |
74 |
|
75 |
//Set inactive to start
|
76 |
bolt.SetActive(false); |
77 |
|
78 |
//Store in our inactive list
|
79 |
inactiveBoltsObj.Add(bolt); |
80 |
}
|
81 |
|
82 |
//Start up a coroutine to capture the pixels we'll be drawing from our text (need the coroutine or error)
|
83 |
StartCoroutine(TextCapture()); |
84 |
}
|
85 |
|
86 |
void Update() |
87 |
{
|
88 |
//Declare variables for use later
|
89 |
GameObject boltObj; |
90 |
LightningBolt boltComponent; |
91 |
|
92 |
//store off the count for effeciency
|
93 |
int activeLineCount = activeBoltsObj.Count; |
94 |
|
95 |
//loop through active lines (backwards because we'll be removing from the list)
|
96 |
for (int i = activeLineCount - 1; i >= 0; i--) |
97 |
{
|
98 |
//pull GameObject
|
99 |
boltObj = activeBoltsObj[i]; |
100 |
|
101 |
//get the LightningBolt component
|
102 |
boltComponent = boltObj.GetComponent<LightningBolt>(); |
103 |
|
104 |
//if the bolt has faded out
|
105 |
if(boltComponent.IsComplete) |
106 |
{
|
107 |
//deactive the segments it contains
|
108 |
boltComponent.DeactivateSegments(); |
109 |
|
110 |
//set it inactive
|
111 |
boltObj.SetActive(false); |
112 |
|
113 |
//move it to the inactive list
|
114 |
activeBoltsObj.RemoveAt(i); |
115 |
inactiveBoltsObj.Add(boltObj); |
116 |
}
|
117 |
}
|
118 |
|
119 |
//check for key press and set mode accordingly
|
120 |
if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) |
121 |
{
|
122 |
shouldText = false; |
123 |
currentMode = Mode.bolt; |
124 |
}
|
125 |
else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) |
126 |
{
|
127 |
shouldText = false; |
128 |
currentMode = Mode.branch; |
129 |
}
|
130 |
else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) |
131 |
{
|
132 |
shouldText = false; |
133 |
currentMode = Mode.moving; |
134 |
}
|
135 |
else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) |
136 |
{
|
137 |
shouldText = true; |
138 |
currentMode = Mode.text; |
139 |
}
|
140 |
else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) |
141 |
{
|
142 |
shouldText = false; |
143 |
currentMode = Mode.nodes; |
144 |
}
|
145 |
else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6)) |
146 |
{
|
147 |
shouldText = false; |
148 |
currentMode = Mode.burst; |
149 |
}
|
150 |
|
151 |
//If left mouse button pressed
|
152 |
if(Input.GetMouseButtonDown(0)) |
153 |
{
|
154 |
//if first click
|
155 |
if(clicks == 0) |
156 |
{
|
157 |
//store starting position
|
158 |
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
159 |
pos1 = new Vector2(temp.x, temp.y); |
160 |
}
|
161 |
else if(clicks == 1) //second click |
162 |
{
|
163 |
//store end position
|
164 |
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
165 |
pos2 = new Vector2(temp.x, temp.y); |
166 |
|
167 |
//Handle the current mode appropriately
|
168 |
switch (currentMode) |
169 |
{
|
170 |
case Mode.bolt: |
171 |
//create a (pooled) bolt from pos1 to pos2
|
172 |
CreatePooledBolt(pos1,pos2, Color.white, 1f); |
173 |
break; |
174 |
|
175 |
case Mode.branch: |
176 |
//instantiate from our branch prefab
|
177 |
GameObject branchObj = (GameObject)GameObject.Instantiate(BranchPrefab); |
178 |
|
179 |
//get the branch component
|
180 |
BranchLightning branchComponent = branchObj.GetComponent<BranchLightning>(); |
181 |
|
182 |
//initialize the branch component using our position data
|
183 |
branchComponent.Initialize(pos1, pos2, BoltPrefab); |
184 |
|
185 |
//add it to the list of active branches
|
186 |
branchesObj.Add(branchObj); |
187 |
break; |
188 |
|
189 |
case Mode.moving: |
190 |
//Prevent from getting a 0 magnitude (0 causes errors
|
191 |
if(Vector2.Distance(pos1, pos2) <= 0) |
192 |
{
|
193 |
//Try a random position
|
194 |
Vector2 adjust = Random.insideUnitCircle; |
195 |
|
196 |
//failsafe
|
197 |
if(adjust.magnitude <= 0) adjust.x += .1f; |
198 |
|
199 |
//Adjust the end position
|
200 |
pos2 += adjust; |
201 |
}
|
202 |
|
203 |
//Clear out any old moving bolt (this is designed for one moving bolt at a time)
|
204 |
for(int i = movingBolt.Count - 1; i >= 0; i--) |
205 |
{
|
206 |
Destroy(movingBolt[i]); |
207 |
movingBolt.RemoveAt(i); |
208 |
}
|
209 |
|
210 |
//get the "velocity" so we know what direction to send the bolt in after initial creation
|
211 |
lightningVelocity = (pos2 - pos1).normalized; |
212 |
|
213 |
//instantiate from our bolt prefab
|
214 |
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab); |
215 |
|
216 |
//get the bolt component
|
217 |
boltComponent = boltObj.GetComponent<LightningBolt>(); |
218 |
|
219 |
//initialize it with 5 max segments
|
220 |
boltComponent.Initialize(5); |
221 |
|
222 |
//activate the bolt using our position data
|
223 |
boltComponent.ActivateBolt(pos1, pos2, Color.white, 1f); |
224 |
|
225 |
//add it to our list
|
226 |
movingBolt.Add(boltObj); |
227 |
break; |
228 |
|
229 |
case Mode.burst: |
230 |
//get the difference between our two positions (destination - source = vector from source to destination)
|
231 |
Vector2 diff = pos2 - pos1; |
232 |
|
233 |
//define how many bolts we want in our circle
|
234 |
int boltsInBurst = 10; |
235 |
|
236 |
for(int i = 0; i < boltsInBurst; i++) |
237 |
{
|
238 |
//rotate around the z axis to the appropriate angle
|
239 |
Quaternion rot = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1)); |
240 |
|
241 |
//Calculate the end position for the bolt
|
242 |
Vector2 boltEnd = (Vector2)(rot * diff) + pos1; |
243 |
|
244 |
//create a (pooled) bolt from pos1 to boltEnd
|
245 |
CreatePooledBolt(pos1, boltEnd, Color.white, 1f); |
246 |
}
|
247 |
|
248 |
break; |
249 |
}
|
250 |
}
|
251 |
|
252 |
//increment our tick count
|
253 |
clicks++; |
254 |
|
255 |
//restart the count after 2 clicks
|
256 |
if(clicks > 1) clicks = 0; |
257 |
}
|
258 |
|
259 |
//if in node mode
|
260 |
if(currentMode == Mode.nodes) |
261 |
{
|
262 |
//constantly create a (pooled) bolt between the two assigned positions
|
263 |
CreatePooledBolt(pos1, pos2, Color.white, 1f); |
264 |
}
|
265 |
|
266 |
//loop through any active branches
|
267 |
for(int i = branchesObj.Count - 1; i >= 0; i--) |
268 |
{
|
269 |
//pull the branch lightning component
|
270 |
BranchLightning branchComponent = branchesObj[i].GetComponent<BranchLightning>(); |
271 |
|
272 |
//If it's faded out already
|
273 |
if(branchComponent.IsComplete) |
274 |
{
|
275 |
//destroy it
|
276 |
Destroy(branchesObj[i]); |
277 |
|
278 |
//take it out of our list
|
279 |
branchesObj.RemoveAt(i); |
280 |
|
281 |
//move on to the next branch
|
282 |
continue; |
283 |
}
|
284 |
|
285 |
//draw and update the branch
|
286 |
branchComponent.UpdateBranch(); |
287 |
branchComponent.Draw(); |
288 |
}
|
289 |
|
290 |
//loop through all of our bolts that make up the moving bolt
|
291 |
for(int i = movingBolt.Count - 1; i >= 0; i--) |
292 |
{
|
293 |
//get the bolt component
|
294 |
boltComponent = movingBolt[i].GetComponent<LightningBolt>(); |
295 |
|
296 |
//if the bolt has faded out
|
297 |
if(boltComponent.IsComplete) |
298 |
{
|
299 |
//destroy it
|
300 |
Destroy(movingBolt[i]); |
301 |
|
302 |
//remove it from our list
|
303 |
movingBolt.RemoveAt(i); |
304 |
|
305 |
//on to the next one, on on to the next one
|
306 |
continue; |
307 |
}
|
308 |
|
309 |
//update and draw bolt
|
310 |
boltComponent.UpdateBolt(); |
311 |
boltComponent.Draw(); |
312 |
}
|
313 |
|
314 |
//if our moving bolt is active
|
315 |
if(movingBolt.Count > 0) |
316 |
{
|
317 |
//calculate where it currently ends
|
318 |
lightningEnd = movingBolt[movingBolt.Count-1].GetComponent<LightningBolt>().End; |
319 |
|
320 |
//if the end of the bolt is within 25 units of the camera
|
321 |
if(Vector2.Distance(lightningEnd,(Vector2)Camera.main.transform.position) < 25) |
322 |
{
|
323 |
//instantiate from our bolt prefab
|
324 |
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab); |
325 |
|
326 |
//get the bolt component
|
327 |
boltComponent = boltObj.GetComponent<LightningBolt>(); |
328 |
|
329 |
//initialize it with a maximum of 5 segments
|
330 |
boltComponent.Initialize(5); |
331 |
|
332 |
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
|
333 |
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f); |
334 |
|
335 |
//add it to our list
|
336 |
movingBolt.Add(boltObj); |
337 |
|
338 |
//update and draw our new bolt
|
339 |
boltComponent.UpdateBolt(); |
340 |
boltComponent.Draw(); |
341 |
}
|
342 |
}
|
343 |
|
344 |
//if in text mode
|
345 |
if(shouldText) |
346 |
{
|
347 |
//go through the points we capture earlier
|
348 |
foreach (Vector2 point in textPoints) |
349 |
{
|
350 |
//randomly ignore certain points
|
351 |
if(Random.Range(0,75) != 0) continue; |
352 |
|
353 |
//placeholder values
|
354 |
Vector2 nearestParticle = Vector2.zero; |
355 |
float nearestDistSquared = float.MaxValue; |
356 |
|
357 |
for (int i = 0; i < 50; i++) |
358 |
{
|
359 |
//select a random point
|
360 |
Vector2 other = textPoints[Random.Range(0, textPoints.Count)]; |
361 |
|
362 |
//calculate the distance (squared for performance benefits) between the two points
|
363 |
float distSquared = DistanceSquared(point, other); |
364 |
|
365 |
//If this point is the nearest point (but not too near!)
|
366 |
if (distSquared < nearestDistSquared && distSquared > 3 * 3) |
367 |
{
|
368 |
//store off the data
|
369 |
nearestDistSquared = distSquared; |
370 |
nearestParticle = other; |
371 |
}
|
372 |
}
|
373 |
|
374 |
//if the point we found isn't too near/far
|
375 |
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3) |
376 |
{
|
377 |
//create a (pooled) bolt at the corresponding screen position
|
378 |
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f); |
379 |
}
|
380 |
}
|
381 |
}
|
382 |
|
383 |
//update and draw active bolts
|
384 |
for(int i = 0; i < activeBoltsObj.Count; i++) |
385 |
{
|
386 |
activeBoltsObj[i].GetComponent<LightningBolt>().UpdateBolt(); |
387 |
activeBoltsObj[i].GetComponent<LightningBolt>().Draw(); |
388 |
}
|
389 |
}
|
390 |
|
391 |
//calculate distance squared (no square root = performance boost)
|
392 |
public float DistanceSquared(Vector2 a, Vector2 b) |
393 |
{
|
394 |
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); |
395 |
}
|
396 |
|
397 |
void CreatePooledBolt(Vector2 source, Vector2 dest, Color color, float thickness) |
398 |
{
|
399 |
//if there is an inactive bolt to pull from the pool
|
400 |
if(inactiveBoltsObj.Count > 0) |
401 |
{
|
402 |
//pull the GameObject
|
403 |
GameObject boltObj = inactiveBoltsObj[inactiveBoltsObj.Count - 1]; |
404 |
|
405 |
//set it active
|
406 |
boltObj.SetActive(true); |
407 |
|
408 |
//move it to the active list
|
409 |
activeBoltsObj.Add(boltObj); |
410 |
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1); |
411 |
|
412 |
//get the bolt component
|
413 |
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>(); |
414 |
|
415 |
//activate the bolt using the given position data
|
416 |
boltComponent.ActivateBolt(source, dest, color, thickness); |
417 |
}
|
418 |
}
|
419 |
|
420 |
//Capture the important points of our text for later
|
421 |
IEnumerator TextCapture() |
422 |
{
|
423 |
//must wait until end of frame so something is actually drawn or else it will error
|
424 |
yield return new WaitForEndOfFrame(); |
425 |
|
426 |
//get the camera that draws our text
|
427 |
Camera cam = GameObject.Find("TextCamera").GetComponent<Camera>(); |
428 |
|
429 |
//make sure it has an assigned RenderTexture
|
430 |
if(cam.targetTexture != null) |
431 |
{
|
432 |
//pull the active RenderTexture
|
433 |
RenderTexture.active = cam.targetTexture; |
434 |
|
435 |
//capture the image into a Texture2D
|
436 |
Texture2D image = new Texture2D(cam.targetTexture.width, cam.targetTexture.height); |
437 |
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0); |
438 |
image.Apply(); |
439 |
|
440 |
//calculate how the text will be scaled when it is displayed as lightning on the screen
|
441 |
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x); |
442 |
|
443 |
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
|
444 |
positionText.x -= image.width * scaleText * .5f; |
445 |
positionText.y -= image.height * scaleText * .5f; |
446 |
|
447 |
//basically determines how many pixels we skip/check
|
448 |
const int interval = 2; |
449 |
|
450 |
//loop through pixels
|
451 |
for(int y = 0; y < image.height; y += interval) |
452 |
{
|
453 |
for(int x = 0; x < image.width; x += interval) |
454 |
{
|
455 |
//get the color of the pixel
|
456 |
Color color = image.GetPixel(x,y); |
457 |
|
458 |
//if the color has any r (red) value
|
459 |
if(color.r > 0) |
460 |
{
|
461 |
//add it to our points for drawing
|
462 |
textPoints.Add(new Vector2(x,y)); |
463 |
}
|
464 |
}
|
465 |
}
|
466 |
}
|
467 |
}
|
468 |
}
|
Abschluss
Blitz ist ein großartiger Spezialeffekt, um Ihre Spiele aufzuwerten. Die in diesem Tutorial beschriebenen Effekte sind ein guter Ausgangspunkt, aber es ist sicherlich nicht alles, was Sie mit Blitzen tun können. Mit etwas Fantasie können Sie alle Arten von beeindruckenden Blitzeffekten erstellen! Laden Sie den Quellcode herunter und experimentieren Sie selbst.