Como gerar efeitos incríveis de relâmpagos 2D com Unity (C#)
() translation by (you can also view the original English article)
Há muitos usos para efeitos de relâmpagos em jogos, de ambiente de fundo durante uma tempestade até ataques devastadores de um feiticeiro. Neste tutorial, vou explicar como programaticamente gerar efeitos de relâmpago incríveis em 2D: raios, ramos e até mesmo texto.
Este tutorial é escrito especificamente para Unity, com todos os trechos de código em C#. O mesmo tutorial também está disponível com o código JavaScript. Se você não usa Unity, dê uma olhada nesta versão de plataforma agnóstica do mesmo tutorial; é escrito para XNA, mas você deve ser capaz de usar as mesmas técnicas e conceitos em qualquer plataforma e motor de desenvolvimento.
Demonstração
Confira a demonstração abaixo:
Clique no objeto Unity, e em seguida, use as teclas para alternar entre as demonstrações. Algumas demonstrações exigem que você clique em um ou dois locais para ativá-las.
Configuração básica
Para começar, você precisará criar um novo projeto 2D na Unity. O nome que você quiser. Na Unity, crie quatro pastas: Materials
, Prefabs
, Scripts
, e Sprites
.



Em seguida, clique na Câmera Principal e certifique-se de que a Projeção está definida como Orthographic
. Defina o tamanho da câmera para 10
.
Clique com o botão direito do mouse na pasta Materials e selecione Create > Material. Renomeie para Additive
. Selecione este material e altere o seu Shader para Particles > Additive. Mais tarde, isso ajudará no "pop" do relâmpago.
Passo 1: Desenhe uma linha brilhante
O bloco de construção básico que precisamos para ter um relâmpago é um segmento de linha. Comece abrindo o seu software de edição de imagem favorito e desenhe uma linha reta de relâmpago com um efeito de brilho. A minha ficou assim:

Queremos desenhar linhas de comprimentos diferentes, então vamos cortar o segmento em três partes como mostrado abaixo (corte a sua imagem, se necessário). Isso nos permitirá esticar o segmento do meio para obter qualquer tamanho. Como nós vamos esticar o segmento do meio, podemos salvá-lo com apenas um único pixel de espessura. Além disso, como os pedaços da direita e da esquerda são imagens espelhadas, só precisamos salvar um deles. Podemos virar o pedaço com o seguinte código.

Arraste seus arquivos de imagem para a pasta Sprites no painel do projeto. Isto irá importar os arquivos para o projeto Unity. Clique sobre os sprites para visualizá-las no painel Inspector. Certifique-se que o tipo de textura está definido como Sprite(2D \ uGUI)
e defina a tag como Line
.
A tag ajudará a Unity a economizar chamadas para desenhar o nosso relâmpago, então certifique-se de dar a mesma tag aos dois sprites, ou a performance será reduzida.



Agora, vamos declarar uma nova classe para lidar com o desenho do segmento da linha:
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 e B são os pontos de extremidade da linha. Ao dimensionar e girar os pedaços, podemos traçar uma linha de qualquer espessura, comprimento e orientação.
Adicione o seguinte método Draw()
no fim da classe 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 |
}
|
A maneira que posicionamos o pedaço do meio e as pontas fará que eles unam-se perfeitamente quando forem desenhados. A primeira ponta é posicionada no ponto A, o meio é esticado para a largura desejada e a ponta final é rotacionada em 180° para desenhar o ponto B.
Agora precisamos criar um prefab para nossa classe Line trabalhar com ele. Na Unity, no menu, selecione GameObject > Create Empty. O objeto aparecerá no seu painel Hierarchy. Renomeie-o para Line
e arraste seu script Line
para ele. Isso deve ficar conforme a imagem abaixo.



Nós vamos usar esse objeto como um container para as peças de nosso segmento de linha.
Agora precisamos criar objetos para as partes do nosso segmento. Crie três Sprites selecionando GameObject > Create Other > Sprite no menu. Renomeie-os para StartCap
, MiddleSegment
e EndCap
. Arraste-os para nosso objeto Line, para que eles se tornem seus filhos — isso deve ficar como na imagem abaixo.



Vá em cada filho e ajuste seu Material no Sprite Renderer para o material Additive que criamos anteriormente. Atribua a cada um o sprite apropriado. (As duas pontas devem pegar o sprite de ponta e o segmento do meio deve ter o sprite da linha).
Clique sobre o objeto Line para que você possa ver o script no painel Inspector. Coloque os filhos nos encaixes apropriados e arraste o objeto Line para a pasta Prefabs para criar um objeto pré-fabricado. Agora, você pode excluir o objeto Line do painel Hierarchy.
Passo 2: Criar linhas irregulares
Relâmpagos tendem a formar linhas irregulares, então vamos precisar de um algoritmo para gerar estas. Nós faremos isto escolhendo pontos aleatoriamente ao longo de uma linha e deslocando-os com uma distância aleatória.
Usar um deslocamento completamente aleatório tende a fazer a linha muito irregular, então nós vamos suavizar os resultados limitando quão longe os pontos vão ser deslocados de seus vizinhos — veja a diferença entre a segunda e terceira linha na figura abaixo.

Podemos suavizar a linha colocando pontos em um deslocamento similar ao ponto anterior. Isso permitirá que a linha como um todo vague para cima e para baixo, enquanto prevê que qualquer parte fique muito irregular.
Vamos criar uma classe LightningBolt
para manipular a criação das nossas linhas irregulares.
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 |
}
|
O código pode ser um pouco intimidante, mas não é tão ruim depois que você entender a lógica. Antes de continuarmos, entenda que nós escolhemos juntar nossos segmentos no raio (já que instanciar e destruir objetos constantemente pode pesado para a Unity).
- A função
Initialize()
será chamada uma vez em cada relâmpago e determinará quantos segmentos de linha que cada raio poderá usar. - A função
activateLine()
irá ativar um segmento usando os dados de determinada posição. - A função
DeactivateSegments()
irá desativar quaisquer segmentos de linha ativa em nosso raio. - A função
ActivateBolt()
irá lidar com criação de nossas linhas irregulares e irá chamar a funçãoactivateLine()
para ativar nossos segmentos nas posições adequadas.
Para criar as nossas linhas irregulares, começamos calculando o declive entre nossos dois pontos, bem como o vetor normal a essa inclinação. Em seguida, escolhemos um número de posições aleatórias ao longo da linha e armazenamos em nossa lista de posições. Nós escalamos essas posições entre 0
e 1
, tal que 0
representa o início da linha e 1
representa o ponto final. Em seguida classificamos essas posições para permitir que sejam facilmente adicionados segmentos entre eles.
O loop atravessa os pontos escolhidos aleatoriamente e desloca-os ao longo do normal por um valor aleatório. O fator de escala
está lá para evitar ângulos muito agudos, e o envelope
garante que o relâmpago realmente vai para o ponto de destino, limitando o deslocamento quando estamos perto do fim. O spread
é para ajudar no controle de quanto os segmentos afastam-se da inclinação da nossa linha; um spread
de 0
essencialmente lhe dará uma linha reta.



Então, como fizemos com a nossa classe Line
, vamos fazer um prefab. No menu, selecione GameObject > Create Empty. O objeto aparecerá no seu painel Hierarchy. Renomeie-o para Bolt
e arraste uma cópia do script LightningBolt
para ele. Finalmente, clique no objeto Bolt e atribua o prefab Line, da pasta Prefabs, para o encaixe adequado do script LightningBolt. Quando terminar, simplesmente arraste o objeto Bolt para a pasta Prefabs para criar uma prefab.
Passo 3: Adicionar animação
O relâmpago deve brilhar forte e depois desaparecer. É para isso que temos nossa funções Update()
e Draw()
no LightningBolt
. Chamar o Update()
fará com que o relâmpago desapareça. Chamar o Draw()
irá atualizar a cor do raio na tela. IsComplete
lhe dirá quando o relâmpago desapareceu totalmente.
Passo 4: Criar um relâmpago
Agora que temos nossa classe LightningBolt
, vamos de fato usá-la e montar uma cena de demonstração rápida.
Vamos usar um objeto pool para esta demonstração, então vamos criar um objeto vazio para manter nossos raios ativos e inativos (simplesmente para fins organizacionais). Na Unity, no menu, selecione GameObject > Create Empty. O objeto aparecerá no seu painel Hierarchy. Renomeie-o para LightningPoolHolder
.
Clique com o botão direito na pasta Scripts e selecione Create > C# Script nomeie seu script para DemoScript
e abra-o. Aqui está um código rápido para você começar:
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 |
}
|
Tudo o que este código faz é nos dar uma maneira de criar os raios usando o objetos pool. Há outras maneiras que você pode configurar isso, mas essa é a que vamos usar! Uma vez criado, tudo o que você vai ter que fazer é clicar duas vezes para criar um raio na tela: uma vez para a posição inicial e outra vez para a posição final.
Vamos precisar de um objeto para colocar nosso DemoScript
. No menu, selecione GameObject > Create Empty. O objeto aparecerá no seu painel Hierarchy. Renomeie-o para DemoScript
e arraste seu script DemoScript para ele. Clique no objeto DemoScript para podermos vê-lo no painel Inspector. Atribua o prefab Bolt, da pasta Prefabs, ao encaixe correspondente no DemoScript.



Isso deve ser o suficiente para seguir! Execute a cena na Unity unidade e experimente!
Passo 5: Criar os ramos do relâmpago
Você pode usar a classe LightningBolt
como um bloco de construção para criar efeitos de relâmpago mais interessantes. Por exemplo, você pode fazer o ramo do relâmpago conforme mostrado abaixo:

Para fazer o ramo do relâmpago, escolhemos pontos aleatórios ao longo dele e adicionamos novos relâmpagos que se ramificam para fora desses pontos. No código abaixo, criamos entre três e seis ramos que separam o relâmpago principal em ângulos de 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 |
}
|
Esse código funciona de forma muito semelhante a nossa classe LightningBolt
com a exceção de que ele não usa objetos pool. Chamar Initialize()
é tudo o que você precisará fazer para criar uma ramificação. Depois disso, você só precisará chamar Update()
e Draw()
. Eu vou te mostrar exatamente como fazer isso no nosso DemoScript
mais adiante no tutorial.
Você deve ter notado a referência a uma função GetPoint()
na classe LightningBolt
. Nós na verdade ainda não implementamos essa função ainda, então vamos tratar disso agora.
Adicione a seguinte função no final da classe 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 |
}
|
Passo 6: Criar o texto de relâmpago
Abaixo está um vídeo de outro efeito que você pode fazer com os relâmpagos:
Nós precisamos fazer mais algumas instalações para este. Primeiro, no painel Project, selecione Create > RenderTexture. Renomeie-o para RenderText
e defina seu tamanho para 256x256px
. (Não necessariamente precisa ser esse tamanho exato, mas quanto menor for, mais rápido o programa será executado).
No menu, selecione Edit > Project Setting > Tags and Layers. Em seguida, no painel Inspector, expanda Layers para adicionar o Text
na opção User Layer 8.



Então precisaremos criar uma segunda câmera. No menu, selecione GameObject > Create Other > Camera. Renomeie para TextCamera
e defina a projeção para Orthographic
e Clear Flags para Solid Color
. Defina o seu Background para (R: 0, G: 0, B: 0, A: 0)
e a Culling Mask para apenas ser Text
(a camada que acabamos de criar). Finalmente, defina o Target Texture como RenderText
(o RenderTexture que criamos anteriormente). Você provavelmente precisará brincar com tamanho da câmera mais tarde, a fim de que tudo caiba na tela.



Agora precisaremos criar o texto que vai ser desenhado com o nosso raio. No menu selecione GameObject > Create Other > GUI Text. Selecione o objeto de texto GUI no painel Hierarchy e escreva no campo Text LIGHTNING
, defina Anchor como middle center
, e Alignment como center
. Em seguida, defina o Layer da camada Text
que criamos anteriormente. Você provavelmente vai ter que brincar com a propriedade Font Size para ajustar o texto na tela.



Agora selecione a câmera principal e defina o Culling Mask para ver tudo, menos nossa camada de texto. Isso fará com que nosso texto GUI aparentemente desapareça da tela, mas ele deve ser desenhado no RenderTexture que criamos anteriormente: selecione RenderText do painel Project e você deve ser capaz de ver a palavra Lightning no preview na parte inferior do painel.
Se não consegue ver a palavra LIGHTNING, você precisará brincar com seu posicionamento, tamanho da fonte e tamanho da câmera. Para ajudá-lo a posicionar seu texto, clique em TextCamera no painel Hierarchy e defina o Target Texture para None
. Agora você será capaz de ver seu texto GUI se centralizá-lo sobre o TextCamera. Depois de ter tudo posicionado, defina o Target Texture da TextCamera de volta para RenderText
.
Agora para o código! Nós necessitaremos obter os pixels do texto que estamos desenhando. Podemos fazer isso desenhando o nosso texto para um RenderTarget
e voltando a leitura dos dados do pixel em um Texture2D
com Texture2D.ReadPixels()
. Então, podemos armazenar as coordenadas dos pixels do texto como um List<Vector2>
.
Aqui está o código para fazer isso:
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 |
}
|
Nota: nós teremos que executar essa função como um Coroutine no início do nosso programa para que ele execute corretamente.
Depois disso, em cada quadro, podemos escolher aleatoriamente pares destes pontos e criar um relâmpago entre eles. Nós queremos que quanto mais perto estiverem os dois pontos, maior seja a chance de nós criarmos um relâmpago entre eles.
Há uma técnica simples que podemos usar para realizar essa tarefa: vamos escolher aleatoriamente o primeiro ponto, e então vamos pegar um número fixo de outros pontos aleatoriamente e escolher o mais próximo.
Aqui está o código para isso (nós vamos adicioná-lo ao nosso DemoScript
mais tarde):
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 |
---------------------------------------------*/
|
O número de pontos candidatos que testamos afetará a aparência do texto relâmpago; a verificação de um maior número de pontos nos permitirá encontrar pontos muito próximos para desenhar relâmpagos entre eles, o que tornará o texto muito limpo e legível, mas com relâmpagos mais curtos entre as letras. Números menores farão o texto do relâmpago parecer selvagem mas menos legível.
Passo 7: Tentar outras variações
Já discutimos sobre ramificar relâmpagos e adicionar relâmpago em textos, mas certamente não são os únicos efeitos que você pode fazer. Vamos olhar algumas outras variações de relâmpagos que você pode querer usar.
Movendo um relâmpago
Você muitas vezes pode querer fazer um relâmpago em movimento. Você pode fazer isso adicionando um novo relâmpago curto a cada quadro no ponto final do relâmpago do quadro anterior.
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 |
}
|
Raio de explosão
Esta variação oferece um efeito dramático que dispara relâmpagos em um círculo a partir de um ponto:
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 |
}
|
Passo 8: Juntar tudo no DemoScript
Você vai querer experimentar todos estes efeitos extravagantes que criamos até agora, então vamos colocar todos eles no DemoScript
que fizemos anteriormente. Você será capaz de alternar entre os efeitos usando as teclas numéricas do teclado para selecionar o efeito, e então clicando duas vezes, como fizemos com os anteriores.
Aqui está o código completo:
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 |
}
|
Conclusão
O relâmpago é um grande efeito especial para enfeitar seus jogos. Os efeitos descritos neste tutorial são um bom ponto de partida, mas certamente não é tudo o que você pode fazer com eles. Com um pouco de imaginação, você pode fazer todos os tipos de efeitos de relâmpago que imaginar! Baixe o código fonte e experimente por conta própria.