Advertisement
  1. Game Development
  2. Game Art Effects

So erzeugen Sie schockierend gute 2D-Blitzeffekte in Unity (C#)

Scroll to top
Read Time: 39 min

() 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 Funktion activateLine() 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.