Cara Menghasilkan Efek Petir 2D yang Mengejutkan dan Menyenangkan dalam Unity (C#)
() translation by (you can also view the original English article)
Ada banyak kegunaan untuk efek petir dalam permainan, dari latar belakang suasana saat badai hingga serangan petir seorang penyihir yang dahsyat. Dalam tutorial ini, saya akan menjelaskan cara pemrograman menghasilkan efek petir 2D yang mengagumkan: petir, cabang, dan bahkan teks.
Tutorial ini ditulis khusus untuk Unity, dengan semua cuplikan kode di C#. Tutorial yang sama juga tersedia dengan kode JavaScript. Jika Anda tidak menggunakan Unity, lihatlah versi platform-agnostic dari tutorial yang sama; ini ditulis untuk XNA, tetapi Anda harus dapat menggunakan teknik dan konsep yang sama dalam mesin dan platform gamedev.
Demo
Lihat demo di bawah ini:
Klik objek Unity, lalu gunakan tombol angka untuk beralih antar demo. Beberapa demo mengharuskan Anda mengklik satu atau dua lokasi untuk mengaktifkannya.
Pengaturan Dasar
Untuk memulai, Anda harus membuat proyek 2D baru di Unity. Beri nama apa pun yang Anda suka. Di Unity, buat empat folder: Materials
, Prefabs
, Scripts
, dan Sprites
.



Selanjutnya, klik Main Camera dan pastikan Projection-nya diatur ke Orthographic
. Atur Size kamera menjadi 10
.
Klik kanan pada folder Materials dan pilih Create > Material. Ganti namanya menjadi Additive
. Pilih material ini dan ubah Shader-nya menjadi Particles > Additive. Ini akan membantu Anda petir "pop" nantinya.
Langkah 1: Menggambar Garis Bersinar
Blok bangunan dasar yang kita butuhkan untuk membuat petir adalah segmen garis. Mulailah dengan membuka perangkat lunak pengedit gambar favorit Anda dan menggambar garis lurus petir dengan efek cahaya. Inilah yang seperti milik saya:

Kami ingin menggambar garis dengan panjang yang berbeda, jadi kami akan memotong segmen garis menjadi tiga bagian seperti yang ditunjukkan di bawah ini (potong gambar Anda seperlunya). Ini akan memungkinkan kita untuk meregangkan segmen menengah ke setiap panjang yang kita suka. Karena kita akan meregangkan segmen menengah, kita dapat menyimpannya hanya dengan ketebalan satu piksel. Juga, karena potongan kiri dan kanan adalah bayangan cermin satu sama lain, kita hanya perlu menyimpan salah satunya; kita bisa membaliknya di kode.

Seret file gambar Anda ke folder Sprites di panel Project. Ini akan mengimpor file gambar ke dalam proyek Unity. Klik sprite untuk melihatnya di panel Inspector. Pastikan Texture Type diatur ke Sprite(2D \ uGUI)
, dan atur Packing Tag ke Line
.
Packing Tag akan membantu Unity menghemat panggilan menggambar saat menggambar petir kami, jadi pastikan Anda memberi kedua sprite-nya Packing Tag yang sama atau yang lain itu tidak akan meningkatkan kinerjanya.



Sekarang, mari kita deklarasikan kelas baru untuk menangani menggambar segmen garis:
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 dan B adalah titik akhir garis. Dengan skala dan memutar potongan garis, kita bisa menggambar ketebalan garis, panjang, dan orientasi apa pun.
Tambahkan metode Draw()
berikut ke bagian bawah kelas Line
:
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 |
}
|
Cara kita memposisikan segmen tengah dan penutup akan membuat mereka bergabung dengan mulus ketika kita menggambarnya. Awal penutup diposisikan pada titik A, segmen tengah direntangkan ke lebar yang diinginkan, dan akhir penutup diputar 180° dan digambar pada titik B.
Sekarang kita perlu membuat prefab agar kelas Line kita dapat berfungsi. Di Unity, dari menu, pilih GameObject > Create Empty. Objek akan muncul di panel Hierarchy Anda. Ganti namanya menjadi Line
dan seret skrip Line
Anda ke atasnya. Seharusnya terlihat seperti gambar di bawah ini.



Kita akan menggunakan objek ini sebagai wadah untuk potongan segmen garis kita.
Sekarang kita perlu membuat objek untuk potongan-potongan segmen garis kita. Buat tiga Sprite dengan memilih GameObject > Create Other> Sprite dari menu. Ubah nama mereka menjadi StartCap
, MiddleSegment
, dan EndCap
. Seret mereka ke objek Line kita sehingga mereka menjadi anak-anaknya—ini akan terlihat seperti gambar di bawah ini.



Pergi melalui setiap anak dan mengatur Material-nya di Sprite Renderer ke materi Additive yang kami buat sebelumnya. Tetapkan setiap anak dengan sprite yang sesuai. (Dua penutup harus mendapat sprite penutup dan segmen tengah harus mendapatkan sprite garis.)
Klik pada objek Line sehingga Anda dapat melihat skrip di panel Inspector. Tetapkan anak-anak ke slot yang sesuai dan kemudian tarik objek Line ke folder Prefabs untuk membuat prefab untuknya. Anda sekarang dapat menghapus objek Line dari panel Hierarchy.
Langkah 2: Membuat Garis Bergerigi
Petir cenderung membentuk garis-garis bergerigi, jadi kita membutuhkan algoritma untuk menghasilkannya. Kita akan melakukan ini dengan mengambil poin secara acak di sepanjang garis, dan memindahkannya dengan jarak acak dari garis.
Menggunakan pemindahan yang sepenuhnya acak cenderung membuat garis terlalu bergerigi, jadi kami akan menghaluskan hasilnya dengan membatasi seberapa jauh dari setiap titik tetangga lainnya dapat dipindahkan—lihat perbedaan antara garis kedua dan ketiga pada gambar di bawah.

Kami menghaluskan garis dengan menempatkan titik pada offset yang sama dengan titik sebelumnya; ini memungkinkan garis secara keseluruhan untuk mengembara ke atas dan ke bawah, sambil mencegah bagian mana pun dari menjadi terlalu bergerigi.
Mari kita buat kelas LightningBolt
untuk menangani pembuatan garis bergerigi.
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 |
}
|
Kode mungkin terlihat sedikit mengintimidasi, tetapi tidak terlalu buruk setelah Anda memahami logikanya. Sebelum kita melanjutkan, pahamilah bahwa kita telah memilih untuk menyatukan segmen-segmen garis kita dalam petir (karena secara konstan instansiasi dan menghancurkan objek dapat menjadi mahal dalam Unity).
- Fungsi
Initialize()
akan dipanggil satu kali pada setiap petir dan akan menentukan berapa banyak segmen garis setiap petir yang diizinkan untuk digunakan. - Fungsi
activateLine()
akan mengaktifkan segmen garis menggunakan data posisi yang diberikan. - Fungsi
DeactivateSegments()
akan menonaktifkan semua segmen garis aktif di petir kita. - Fungsi
ActivateBolt()
akan menangani pembuatan garis bergerigi dan akan memanggil fungsiactivateLine()
untuk mengaktifkan segmen garis kita di posisi yang sesuai.
Untuk membuat garis bergerigi, kita mulai dengan menghitung kemiringan antara dua titik kita, serta vektor normal ke kemiringan itu. Kita kemudian memilih sejumlah posisi acak sepanjang garis dan menyimpannya dalam daftar posisi kita. Kita menskala posisi ini antara 0
dan 1
, sehingga 0
mewakili awal baris dan 1
mewakili titik akhir., dan kemudian mengurutkan posisi ini agar memungkinkan kita untuk dengan mudah menambahkan segmen garis di antara mereka.
Perulangan melewati titik-titik yang dipilih secara acak dan menggesernya sepanjang normal dengan jumlah acak. Faktor scale
ada untuk menghindari sudut yang terlalu tajam, dan envelope
memastikan petir benar-benar pergi ke titik tujuan dengan membatasi perpindahan ketika kita mendekati akhir. Spread
ini untuk membantu mengendalikan seberapa jauh segmen menyimpang dari kemiringan garis kita; spread
0
pada dasarnya akan memberi Anda garis lurus.



Jadi, seperti yang kami lakukan dengan kelas Line
kita, mari kita buat ini menjadi sebuah prefab. Dari menu, pilih GameObject > Create Empty. Objek akan muncul di panel Hierarchy Anda. Ubah nama ke Bolt
, dan seret salinan skrip LightningBolt
ke atasnya. Terakhir, klik pada objek Bolt dan tetapkan prefab Line, dari folder Prefabs, ke slot yang sesuai dalam skrip LightningBolt. Setelah Anda selesai dengan itu, cukup tarik objek Bolt ke folder Prefabs untuk membuat prefab.
Langkah 3: Menambahkan Animasi
Petir harus berkedip cerah dan kemudian memudar. Inilah kegunaan fungsi Update()
dan Draw()
kita di LightningBolt
. Memanggil Update()
akan membuat petir memudar. Memanggil Draw()
akan memperbarui warna petir di layar. IsComplete
akan memberi tahu Anda kapan petir telah sepenuhnya pudar.
Langkah 4: Membuat Petir
Sekarang setelah kita memiliki kelas LightningBolt
, mari kita benar-benar menggunakannya dengan baik dan mengatur adegan demo cepat.
Kita akan menggunakan pool objek untuk demo ini, jadi kami ingin membuat objek kosong untuk menampung petir aktif dan tidak aktif (hanya untuk tujuan organisasi). Di Unity, dari menu, pilih GameObject > Create Empty. Objek akan muncul di panel Hierarchy Anda. Ubah namanya menjadi LightningPoolHolder
.
Klik kanan pada folder Scripts dan pilih Create> C# Script. Namakan skrip Anda DemoScript
dan buka. Berikut beberapa kode cepat untuk memulai:
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 |
}
|
Semua kode ini adalah memberi kita cara untuk membuat petir menggunakan penggabungan objek. Ada beberapa cara lain yang bisa Anda lakukan, tetapi inilah yang akan kita lakukan! Setelah kita mengaturnya, yang harus Anda lakukan adalah mengklik dua kali untuk membuat petir di layar: satu kali untuk posisi awal dan satu kali untuk posisi akhir.
Kita membutuhkan sebuah objek untuk menyalakan DemoScript
kita. Dari menu, pilih GameObject > Create Empty. Objek akan muncul di panel Hierarchy Anda. Ubah nama ke DemoScript
dan seret skrip DemoScript Anda kepadanya. Klik pada objek DemoScript sehingga kita dapat melihatnya di panel Inspector. Tetapkan prefab Bolt, dari folder Prefabs, ke slot yang cocok di DemoScript.



Itu seharusnya cukup untuk membuat Anda pergi! Jalankan adegan di Unity dan cobalah!
Langkah 5: Membuat Cabang Petir
Anda dapat menggunakan kelas LightningBolt
sebagai blok bangunan untuk menciptakan efek petir yang lebih menarik. Misalnya, Anda dapat membuat cabang petir seperti ditunjukkan di bawah ini:

Untuk membuat cabang petir, kami memilih titik acak sepanjang petir dan menambahkan petir baru yang keluar dari titik-titik ini. Dalam kode di bawah ini, kami membuat antara tiga dan enam cabang yang terpisah dari petir utama pada sudut 30°.
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 |
}
|
Kode ini bekerja sangat mirip dengan kelas LightningBolt
kita dengan pengecualian ia tidak menggunakan penggabungan objek. Memanggil Initialize()
adalah semua yang perlu Anda lakukan untuk membuat percabangan petir; setelah itu, Anda hanya perlu memanggil Update()
dan Draw()
. Saya akan menunjukkan kepada Anda bagaimana melakukan ini di DemoScript
kita nanti di tutorial.
Anda mungkin telah memperhatikan referensi ke fungsi GetPoint()
di kelas LightningBolt
. Kita belum benar-benar mengimplementasikan fungsi itu, jadi mari kita urus itu sekarang.
Tambahkan fungsi berikut di bagian bawah kelas LightningBolt
:
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 |
}
|
Langkah 6: Membuat Teks Petir
Di bawah ini adalah video dari efek lain yang dapat Anda buat dari petir:
Kita perlu melakukan sedikit pengaturan untuk yang satu ini. Pertama, dari panel Project, pilih Create > RenderTexture. Ganti namanya menjadi RenderText
dan atur Size-nya menjadi 256x256px
. (Ini tidak harus menjadi ukuran yang tepat, tetapi semakin kecil itu, semakin cepat program akan berjalan.)
Dari menu, pilih Edit > Project Settings > Tags and Layers. Kemudian, di panel Inspector, rentangkan drop-down Layers dan tambahkan Text
ke User Layer 8.



Selanjutnya kita perlu membuat kamera kedua. Dari menu, pilih GameObject > Create Other > Camera. Ubah nama ke TextCamera
, dan atur Projection-nya menjadi Orthographic
dan Clear Flags-nya menjadi Solid Color
. Atur warna Background-nya ke (R: 0, G: 0, B: 0, A: 0)
dan atur Culling Mask-nya hanya menjadi Text
(layer yang baru kita buat). Akhirnya, atur Target Texture-nya ke RenderText
(RenderTexture yang kita buat sebelumnya). Anda mungkin harus bermain-main dengan Size kamera nanti, agar semuanya muat di layar.



Sekarang kita harus membuat teks yang sebenarnya akan kita gambar dengan petir kita. Dari menu, pilih GameObject > Create Other > GUI Text. Pilih objek GUI Text dari panel Hierarchy dan atur Text ke LIGHTNING
, Anchor-nya ke middle center
, dan Alignment-nya ke center
. Kemudian, atur Layer ke layer Text
yang kita buat sebelumnya. Anda mungkin harus bermain-main dengan Font Size agar sesuai dengan teks di layar.



Sekarang pilih Main Camera dan atur Culling Mask-nya menjadi segalanya kecuali layer Teks kita. Ini akan menyebabkan GUI Text kita tampaknya menghilang dari layar, tetapi seharusnya digambar pada RenderTexture yang kita buat sebelumnya: pilih RenderText dari panel Project dan Anda seharusnya dapat melihat kata LIGHTNING pada pratinjau di bagian bawah panel.
Jika Anda tidak dapat melihat kata LIGHTNING, Anda harus bermain-main dengan penempatan, ukuran font, dan ukuran kamera (teks) Anda. Untuk membantu Anda memposisikan teks Anda, klik TextCamera di panel Hierarchy, dan atur Target Texture ke None
. Sekarang Anda akan dapat melihat GUI Text jika Anda memusatkannya pada TextCamera. Setelah Anda memiliki posisi semuanya, atur Target Texture TextCamera kembali ke RenderText
.
Sekarang untuk kodenya! Kita harus mendapatkan piksel dari teks yang kita gambar. Kita dapat melakukan ini dengan menggambar teks kita ke RenderTarget
dan membaca kembali data piksel ke dalam Texture2D
dengan Texture2D.ReadPixels()
. Kemudian, kita dapat menyimpan koordinat piksel dari teks sebagai List<Vector2>
.
Berikut kode untuk melakukan itu:
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 |
}
|
Catatan: Kita harus menjalankan fungsi ini sebagai Coroutine di awal program kita agar berjalan dengan benar.
Setelah itu, setiap frame, kita dapat secara acak memilih pasangan dari titik-titik ini dan membuat petir di antara mereka. Kami ingin mendesainnya agar dua titik yang lebih dekat satu sama lain, semakin besar peluangnya adalah kami menciptakan sebuah petir di antara mereka.
Ada teknik sederhana yang dapat kita gunakan untuk mencapai hal ini: kita akan memilih titik pertama secara acak, dan kemudian kita akan memilih sejumlah titik lain secara acak dan memilih yang terdekat.
Berikut kode untuk itu (kita akan menambahkannya ke DemoScript
kita nanti):
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 |
---------------------------------------------*/
|
Jumlah poin kandidat yang kami uji akan mempengaruhi tampilan teks petir; memeriksa jumlah poin yang lebih banyak akan memungkinkan kita untuk menemukan titik-titik yang sangat dekat untuk menggambar petir diantaranya, yang akan membuat teks sangat rapi dan dapat dibaca, tetapi dengan lebih sedikit petir antar huruf. Angka yang lebih kecil akan membuat teks petir terlihat lebih liar tetapi kurang dapat dibaca.
Langkah 7: Mencoba Variasi Lain
Kami telah membahas membuat cabang petir dan teks petir, tetapi itu tentu bukan satu-satunya efek yang dapat Anda buat. Mari kita lihat beberapa variasi lain pada petir yang mungkin ingin Anda gunakan.
Petir Bergerak
Anda mungkin sering ingin membuat petir yang bergerak. Anda dapat melakukan ini dengan menambahkan petir pendek baru setiap frame pada titik akhir dari frame petir sebelumnya.
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 |
}
|
Semburan Petir
Variasi ini menawarkan efek dramatis yang menembakkan petir dalam lingkaran dari titik pusat:
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 |
}
|
Langkah 8: Letakkan Ini Semua Bersama di DemoScript
Anda akan ingin dapat mencoba semua efek mewah yang telah kami buat sejauh ini, jadi mari kita masukkan semua ke dalam DemoScript
yang kita buat sebelumnya. Anda akan dapat beralih antara efek dengan menekan tombol angka pada keyboard Anda untuk memilih efek, dan kemudian hanya mengklik dua kali seperti yang kami lakukan dengan petir kami sebelumnya.
Berikut kode lengkapnya:
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 |
}
|
Kesimpulan
Petir adalah efek spesial yang bagus untuk mendandani game Anda. Efek yang dijelaskan dalam tutorial ini adalah titik awal yang bagus, tetapi tentu saja tidak semua yang dapat Anda lakukan dengan petir. Dengan sedikit imajinasi Anda dapat membuat semua jenis efek petir yang menakjubkan! Unduh kode sumber dan bereksperimen sendiri.