Cómo generar asombrosamente buenos efectos de rayos 2D en Unity (C #)
() translation by (you can also view the original English article)
Hay muchos usos para los efectos del rayo en los juegos, desde el ambiente de fondo durante una tormenta hasta los devastadores rayos de un hechicero. En este tutorial, explicaré cómo generar mediante programación efectos asombrosos de rayos 2D: pernos, ramas e incluso texto.
Este tutorial está escrito específicamente para Unity, con todos los fragmentos de código en C #. El mismo tutorial también está disponible con código JavaScript. Si no usa Unity, eche un vistazo a esta versión agnóstica de plataforma del mismo tutorial; está escrito para XNA, pero debería poder utilizar las mismas técnicas y conceptos en cualquier motor y plataforma de gamedev.
Demostración
Mira la demostración a continuación:
Haga clic en el objeto Unity, luego use las teclas numéricas para alternar entre demostraciones. Algunas demostraciones requieren que haga clic en una o dos ubicaciones para activarlas.
Configuración básica
Para comenzar, deberá crear un nuevo proyecto en 2D en Unity. Llámalo como quieras En Unity, cree cuatro carpetas: Materiales
, Prefabricados
, Scripts
y Sprites
.



A continuación, haga clic en la cámara principal y asegúrese de que su proyección esté configurada en ortográfica
. Establezca el Tamaño de la cámara en 10
.
Haga clic derecho en la carpeta Materiales y seleccione Crear> Material. Cambiarle el nombre a Aditivo
. Seleccione este material y cambie su Sombreador a Partículas> Aditivo. Esto ayudará a que tu rayo "explote" más adelante.
Paso 1: Dibuja una línea brillante
El bloque de construcción básico del que necesitamos hacer un rayo es un segmento de línea. Comience abriendo su software favorito de edición de imágenes y dibujando una línea recta de rayos con un efecto de brillo. Así es como se ve el mío:

Queremos dibujar líneas de diferentes longitudes, por lo que cortaremos el segmento de línea en tres partes como se muestra a continuación (recorte su imagen según sea necesario). Esto nos permitirá estirar el segmento medio a cualquier longitud que deseemos. Dado que vamos a estirar el segmento medio, podemos guardarlo como un solo espesor de píxel. Además, como las piezas izquierda y derecha son imágenes especulares entre sí, solo necesitamos guardar una de ellas; podemos voltearlo en el código.

Arrastra tus archivos de imagen a la carpeta Sprites en el panel Proyecto. Esto importará los archivos de imagen en el proyecto de Unity. Haga clic en los sprites para verlos en el panel Inspector. Asegúrese de que el Tipo de textura esté configurado en Sprite (2D \ uGUI)
y configure la Etiqueta de embalaje en Línea
.
La etiqueta de embalaje ayudará a Unity a ahorrar en las llamadas al sorteo al dibujar nuestro rayo, así que asegúrese de darle a ambos sprites la misma etiqueta de embalaje o de lo contrario no mejorará el rendimiento.



Ahora, declaremos una nueva clase para manejar segmentos de líneas de dibujo:
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 y B son los puntos finales de la línea. Al escalar y rotar las piezas de la línea, podemos dibujar una línea de cualquier espesor, longitud y orientación.
Agregue el siguiente método Draw ()
al final de la clase 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 |
}
|
La forma en que posicionamos el segmento medio y las mayúsculas hará que se unan sin problemas cuando las dibujemos. La tapa de inicio se coloca en el punto A, el segmento medio se estira al ancho deseado, y la tapa final se gira 180 ° y se dibuja en el punto B.
Ahora tenemos que crear un prefabricado para que trabaje nuestra clase Line. En Unity, en el menú, selecciona GameObject> Create Empty. El objeto aparecerá en su panel de Jerarquía. Cambie el nombre a Line
y arrastre su script Line
en él. Debería verse algo así como la imagen de abajo.



Usaremos este objeto como un contenedor para las piezas de nuestro segmento de línea.
Ahora necesitamos crear objetos para las piezas de nuestro segmento de línea. Crea tres Sprites seleccionando GameObject> Create Other> Sprite en el menú. Cambie el nombre a StartCap
, MiddleSegment
y EndCap
. Arrástrelos a nuestro objeto Línea para que se conviertan en sus hijos, esto debería parecerse a la imagen siguiente.



Repase a cada niño y establezca su Material en el Renderizador de Sprite en el material Aditivo que creamos anteriormente. Asigne a cada niño el sprite apropiado. (Las dos tapas deben obtener el sprite de la tapa y el segmento del medio debe obtener la línea del sprite).
Haga clic en el objeto Línea para que pueda ver la secuencia de comandos en el panel Inspector. Asigne a los niños a sus ranuras apropiadas y luego arrastre el objeto Line a la carpeta Prefab para crear un prefab. Ahora puede eliminar el objeto Línea del panel Jerarquía.
Paso 2: crea líneas dentadas
Los rayos tienden a formar líneas dentadas, por lo que necesitaremos un algoritmo para generarlos. Haremos esto seleccionando puntos al azar a lo largo de una línea y desplazándolos a una distancia aleatoria de la línea.
Usar un desplazamiento completamente aleatorio tiende a hacer que la línea quede demasiado dentada, por lo que suavizaremos los resultados al limitar cuán lejos se pueden desplazar los puntos vecinos: vea la diferencia entre la segunda y la tercera líneas en la figura siguiente.

Suavizamos la línea colocando puntos con un desplazamiento similar al punto anterior; esto permite que la línea como un todo vague hacia arriba y hacia abajo, a la vez que evita que una parte de ella quede demasiado dentada.
Vamos a crear una clase LightningBolt
para manejar la creación de nuestras líneas dentadas.
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 |
}
|
El código puede parecer un poco intimidante, pero no es tan malo una vez que comprendes la lógica. Antes de continuar, comprendan que hemos elegido agrupar nuestros segmentos de línea en los pernos (ya que crear instancias y destruir constantemente objetos puede ser costoso en Unity).
- La función
Initialize ()
se llamará una vez en cada rayo y determinará cuántos segmentos de línea puede usar cada perno. - La función
activateLine ()
activará un segmento de línea usando los datos de posición dados. - La función
DeactivateSegments ()
desactivará cualquier segmento de línea activo en nuestro perno. - La función
ActivateBolt ()
se encargará de crear nuestras líneas dentadas y llamará a la funciónactivateLine ()
para activar nuestros segmentos de línea en las posiciones apropiadas.
Para crear nuestras líneas irregulares, comenzamos por calcular la pendiente entre nuestros dos puntos, así como el vector normal a esa pendiente. Luego elegimos una cantidad de posiciones aleatorias a lo largo de la línea y las almacenamos en nuestra lista de posiciones. Escalamos estas posiciones entre 0
y 1
, de modo que 0
representa el inicio de la línea y 1
representa el punto final. Luego, clasifique estas posiciones para permitirnos agregar fácilmente segmentos de línea entre ellas.
El ciclo pasa por los puntos elegidos al azar y los desplaza a lo largo de la normal en una cantidad aleatoria. El factor de escala
está ahí para evitar ángulos demasiado agudos, y la envolvente
asegura que el rayo llegue al punto de destino al limitar el desplazamiento cuando estamos cerca del final. La extensión
es para ayudar a controlar qué tan lejos se desvían los segmentos de la pendiente de nuestra línea; una extensión
de 0
esencialmente te dará una línea recta.



Entonces, como hicimos con nuestra clase Line
, hagamos esto en un prefabricado. En el menú, selecciona GameObject> Create Empty. El objeto aparecerá en su panel de Jerarquía. Cambie el nombre a Bolt
y arrastre una copia del script LightningBolt
. Finalmente, haga clic en el objeto Bolt y asigne Line prefab, de la carpeta Prefabs, a la ranura apropiada en el script LightningBolt. Una vez que haya terminado con eso, simplemente arrastre el objeto Bolt a la carpeta Prefab para crear un prefabricado.
Paso 3: agregar animación
Los relámpagos deberían brillar intensamente y luego desaparecer. Esto es para lo que son nuestras funciones Update ()
y Draw ()
en LightningBolt
. Llamando Update ()
hará que el perno se desvanezca. Llamar a Draw ()
actualizará el color del cerrojo en la pantalla. IsComplete
le dirá cuándo el perno se ha desvanecido por completo.
Paso 4: Crear un rayo
Ahora que tenemos nuestra clase LightningBolt
, aprovechemos de verdad y creemos una escena de demostración rápida.
Vamos a utilizar un conjunto de objetos para esta demostración, por lo que querremos crear un objeto vacío para mantener nuestros pernos activos e inactivos (simplemente para fines de organización). En Unity, en el menú, selecciona GameObject> Create Empty. El objeto aparecerá en su panel de Jerarquía. Cambiar el nombre a LightningPoolHolder
.
Haga clic con el botón derecho en la carpeta Scripts y seleccione Crear> C # Script. Denomine su script DemoScript
y ábralo. Aquí hay un código rápido para comenzar:
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 |
}
|
Todo lo que hace este código es darnos una forma de crear pernos utilizando la agrupación de objetos. Hay otras maneras en que puede configurar esto, ¡pero esto es con lo que estamos yendo! Una vez que lo hayamos configurado, todo lo que tendrá que hacer es hacer clic dos veces para crear un perno en la pantalla: una para la posición de inicio y otra para la posición final.
Necesitaremos un objeto para poner nuestro DemoScript
. En el menú, selecciona GameObject> Create Empty. El objeto aparecerá en su panel de Jerarquía. Cambie el nombre a DemoScript
y arrastre su script de DemoScript en it.n. Haga clic en el objeto DemoScript para que podamos verlo en el panel Inspector. Asigne el prefabricado de rayos, desde la carpeta Prefabricados, a la ranura correspondiente en DemoScript.



¡Eso debería ser suficiente para ponerte en marcha! ¡Ejecuta la escena en Unity y pruébalo!
Paso 5: Crear Branch Lightning
Puede usar la clase LightningBolt
como un bloque de construcción para crear efectos de iluminación más interesantes. Por ejemplo, puede hacer que los pernos se ramifiquen como se muestra a continuación:

Para hacer la rama del rayo, elegimos puntos aleatorios a lo largo del rayo y agregamos nuevos pernos que se ramifican desde estos puntos. En el siguiente código, creamos entre tres y seis ramas que se separan del perno principal en á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 |
}
|
Este código
funciona de manera muy similar a nuestra clase LightningBolt
con la
excepción de que no utiliza la agrupación de objetos. Llamar a Initialize ()
es todo lo que tendrá que hacer para crear un perno de bifurcación; después de eso, solo tendrá que llamar a Update ()
y Draw ()
. Le mostraré exactamente cómo hacer esto en nuestro DemoScript
más adelante en el tutorial.
Es posible que haya notado la referencia a una función GetPoint ()
en la clase LightningBolt
. Todavía no hemos implementado esa función, así que ocupémonos de eso ahora.
Agregue la siguiente función en la parte inferior de la clase 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 |
}
|
Paso 6: crea un texto Lightning
A continuación se muestra un video de otro efecto que puede hacer de los rayos:
Tendremos que hacer un poco más de configuración para este. Primero, desde el panel Proyecto, selecciona Crear> RenderTextura. Cambie el nombre a RenderText
y establezca su Tamaño a 256x256 px
. (No necesariamente tiene que ser del mismo tamaño, pero cuanto más pequeño sea, más rápido se ejecutará el programa).
Desde el menú, seleccione Editar> Configuración del proyecto> Etiquetas y Capas. Luego, en el panel Inspector, expanda el menú desplegable Capas y agregue Texto
en la Capa de usuario 8.



Entonces necesitaremos crear una segunda cámara. En el menú, selecciona GameObject> Crear Otro> Cámara. Cambie el nombre a TextCamera
y establezca su proyección en Ortográfica
y sus Clear Flags en Solid Color
. Establezca su color de fondo en (R: 0, G: 0, B: 0, A: 0)
y configure su máscara de exclusión para que solo sea Text
(la capa que acabamos de crear). Finalmente, establezca su textura objetivo en RenderText
(la RenderTexture que creamos anteriormente). Probablemente necesites jugar más tarde con el tamaño de la cámara para que todo encaje en la pantalla.



Ahora necesitaremos crear el texto real que dibujaremos con nuestro rayo. En el menú, seleccione GameObject> Crear otro> Texto GUI. Seleccione el objeto Texto GUI en el panel Jerarquía y establezca su Texto en RAYO
, su Ancla en centro medio
y su Alineamiento en centro
. Luego, configure su Capa a la capa de Texto
que creamos anteriormente. Probablemente tendrá que jugar con el Tamaño de fuente para ajustar el texto en la pantalla.



Ahora seleccione la cámara principal y configure su máscara de exclusión para que sea todo menos nuestra capa de texto. Esto hará que nuestro Texto GUI aparentemente desaparezca de la pantalla, pero debería dibujarse en RenderTexture que creamos anteriormente: seleccione RenderText en el panel Proyecto y podrá ver la palabra RELÁMPAGO en la vista previa en la parte inferior del panel .
Si no puede ver la palabra RELÁMPAGO, tendrá que jugar con su posición, tamaño de fuente y tamaño de la cámara (de texto). Para ayudarlo a ubicar su texto, haga clic en TextCamera en el panel Jerarquía y establezca su textura objetivo en None
. Ahora podrá ver su Texto GUI si lo centra en la TextCamera. Una vez que tenga todo posicionado, vuelva a establecer TextCamera Target Texture en RenderText
.
¡Ahora para el código! Tendremos que obtener los píxeles del texto que estamos dibujando. Podemos hacer esto dibujando nuestro texto en un RenderTarget
y leyendo los datos de píxeles en un Texture2D
con Texture2D.ReadPixels ()
. Entonces, podemos almacenar las coordenadas de los píxeles del texto como List<Vector2>
Aquí está el código para hacer eso:
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: Tendremos que ejecutar esta función como una Corutina al inicio de nuestro programa para que se ejecute correctamente.
Después de eso, en cada cuadro, podemos elegir aleatoriamente pares de estos puntos y crear un rayo entre ellos. Queremos diseñarlo para que los dos puntos más cercanos estén entre sí, mayor es la posibilidad de que creemos un cerrojo entre ellos.
Hay una técnica simple que podemos usar para lograr esto: elegiremos el primer punto al azar, y luego seleccionaremos un número fijo de otros puntos al azar y elegiremos el más cercano.
Aquí está el código para eso (lo agregaremos a nuestro DemoScript
más adelante):
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 |
---------------------------------------------*/
|
La cantidad de puntos candidatos que evaluamos afectará la apariencia del texto del rayo; comprobar un mayor número de puntos nos permitirá encontrar puntos muy cercanos para dibujar los tornillos entre ellos, lo que hará que el texto sea muy prolijo y legible, pero con menos rayos largos entre las letras. Los números más pequeños harán que el texto del rayo se vea más salvaje pero menos legible.
Paso 7: prueba otras variaciones
Hemos discutido la creación de rayo de rama y texto de rayo, pero esos no son los únicos efectos que puedes hacer. Veamos algunas otras variaciones en los rayos que puede usar.
Movimiento de relámpagos
A menudo querrás hacer un rayo de luz en movimiento. Puede hacer esto agregando un nuevo perno corto cada cuadro en el punto final del perno del marco 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 |
}
|
Explosión relámpago
Esta variación ofrece un efecto dramático que dispara relámpagos en un círculo desde el punto central:
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 |
}
|
Paso 8: Ponlo todo junto en DemoScript
Querrá poder probar todos estos efectos sofisticados que hemos creado hasta ahora, así que vamos a ponerlos todos en el DemoScript
que hicimos anteriormente. Podrá alternar entre los efectos al presionar las teclas numéricas de su teclado para seleccionar el efecto, y luego simplemente hacer clic dos veces como lo hicimos con nuestros pernos antes.
Aquí está el 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 |
}
|
Conclusión
Lightning es un gran efecto especial para mejorar tus juegos. Los efectos descritos en este tutorial son un buen punto de partida, pero ciertamente no es todo lo que puedes hacer con un rayo. ¡Con un poco de imaginación puedes hacer todo tipo de impresionantes efectos de rayos! Descargue el código fuente y experimente por su cuenta.