Cómo generar efectos de rayos 2D sorprendentemente buenos en Unity (JavaScript)
() translation by (you can also view the original English article)
Hay muchos usos para los efectos de rayos en los juegos, desde el ambiente de fondo durante una tormenta hasta los devastadores ataques de rayos de un hechicero. En este tutorial, explicaré cómo generar mediante programación asombrosos efectos de relámpagos 2D: rayos, ramificaciones e incluso texto.
Este tutorial está escrito específicamente para Unity, con todos los fragmentos de código en JavaScript. El mismo tutorial también está disponible en código C#. Si no usas Unity, echa un vistazo a esta versión independiente de la plataforma del mismo tutorial; está escrito para XNA, pero deberías poder utilizar las mismas técnicas y conceptos en cualquier motor y plataforma de gamedev.
Demo
Echa un vistazo a la demostración a continuación:
Haz clic en el objeto Unity, luego usa las teclas numéricas para cambiar entre demos. Algunos demos requieren que hagas clic en una o dos ubicaciones para activarlos.
Configuración básica
Para comenzar, deberás crear un nuevo proyecto 2D en Unity. Nómbralo como quieras. En Unity, crea cuatro carpetas: Materials
, Prefabs
, Scripts
y Sprites
.



A continuación, haz clic en la Cámara Principal y asegúrate de que tu Proyección esté configurada en Orthographic
. Establece el Tamaño de la cámara en 10
.
Haz clic derecho en la carpeta Materials y selecciona Crear > Material. Renómbrala a Aditivo
. Selecciona ese material y cambia su Sombreador a Partículas> Aditivo. Esto ayudará al "pop"de tu relámpago 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. Comienza abriendo tu software favorito de edición de imágenes y dibujando una línea recta con un efecto de brillo. Así es como se ve la mía:

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 (recorta tu imagen según sea necesario). Esto nos permitirá estirar el segmento medio a cualquier longitud que queramos. Ya que vamos a estirar el segmento medio, podemos guardarlo como un solo píxel grueso. Además, como las piezas de la izquierda y la derecha son imágenes espejadas entre sí, solo necesitamos guardar una de ellas; Podemos darle la vuelta en el código.

Arrastra tus archivos de imagen a la carpeta Sprites en el panel Proyecto. Esto importará los archivos de imagen al proyecto de Unity. Haz clic en los sprites para verlos en el panel Inspector. Asegúrate de que el Tipo de Textura esté establecido en Sprite (2D \ uGUI)
y configura el Packing Tag en Línea
.
El Packing Tag ayudará a Unity a ahorrar en los llamados dibujos de nuestro relámpago, así que asegúrate de darles a ambos sprites el mismo Packing Tag, de lo contrario no mejorará el rendimiento.



Ahora, declaremos una nueva clase para manejar segmentos de líneas de dibujo:
1 |
#pragma strict |
2 |
|
3 |
class LineJS extends MonoBehaviour |
4 |
{
|
5 |
//Start
|
6 |
public var A: Vector2; |
7 |
|
8 |
//End
|
9 |
public var B: Vector2; |
10 |
|
11 |
//Thickness of line
|
12 |
public var Thickness: float; |
13 |
|
14 |
//Children that contain the pieces that make up the line
|
15 |
public var StartCapChild : GameObject; |
16 |
public var LineChild : GameObject; |
17 |
public var EndCapChild : GameObject; |
18 |
|
19 |
//Create a new line
|
20 |
public function Line(a : Vector2, b : Vector2, thickness : float) |
21 |
{
|
22 |
A = a; |
23 |
B = b; |
24 |
Thickness = thickness; |
25 |
}
|
26 |
|
27 |
//Used to set the color of the line
|
28 |
public function SetColor(color : Color) |
29 |
{
|
30 |
StartCapChild.GetComponent(SpriteRenderer).color = color; |
31 |
LineChild.GetComponent(SpriteRenderer).color = color; |
32 |
EndCapChild.GetComponent(SpriteRenderer).color = color; |
33 |
}
|
34 |
|
35 |
//...
|
36 |
}
|
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 grosor, longitud y orientación.
Agrega el siguiente método Draw ()
al final de la clase LineJS
:
1 |
//Will actually draw the line
|
2 |
public function Draw() |
3 |
{
|
4 |
var difference : Vector2 = B - A; |
5 |
var rotation : float = 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 |
var lineChildWorldAdjust : float = LineChild.transform.localScale.x * LineChild.GetComponent(SpriteRenderer).sprite.rect.width / 2f; |
35 |
var startCapChildWorldAdjust : float = StartCapChild.transform.localScale.x * StartCapChild.GetComponent(SpriteRenderer).sprite.rect.width / 2f; |
36 |
var endCapChildWorldAdjust : float = 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 los limites los unirá sin problemas cuando los dibujemos. El extremo de inicio se coloca en el punto A, el segmento medio se estira hasta el ancho deseado y la punta del otro extremo se gira 180 ° y se dibuja en el punto B.
Ahora necesitamos crear un prefab para que nuestra clase LineJS trabaje. En Unity, en el menú, selecciona GameObject > Crear vacío. El objeto aparecerá en tu panel de Jerarquía. Cámbiale el nombre a LineJS
y arrastra tu script LineJS a él. Debería verse algo 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> Crear Otros> Sprite en el menú. Cámbiales el nombre a StartCap
, MiddleSegment
y EndCap
. Arrástralos a nuestro objeto Line JS para que se conviertan en sus hijos, eso debería parecerse a la imagen de abajo.



Ve a través de cada hijo y establece su Material en el Renderizador de Sprite al material Aditivo que creamos anteriormente. Asigna a cada uno el sprite apropiado. (Las dos mayúsculas deben obtener el sprite de cap y el segmento central debe tener el sprite de línea).
Haz clic en el objeto LineJS para que puedas ver la secuencia de comandos en el panel Inspector. Asigna los "hijos" a sus lugares apropiados y luego arrastra el objeto Line JS a la carpeta Prefabs para crear uno. Ahora puedes eliminar el objeto Line JS desde el panel Jerarquía.
Paso 2: crea líneas dentadas
Los rayos tienden a formar líneas irregulares, por lo que necesitaremos un algoritmo para generarlas. Haremos esto seleccionando puntos al azar a lo largo de una línea y desplazándolos a una distancia aleatoria de la línea.
El uso de un desplazamiento completamente aleatorio tiende a hacer que la línea sea demasiado irregular, por lo que suavizaremos los resultados limitando la distancia entre los puntos vecinos que pueden desplazarse: mira la diferencia entre la segunda y la tercera línea en la siguiente figura.

Suavizamos la línea colocando puntos en un desplazamiento similar al punto anterior; esto permite que la línea en su conjunto se desplace hacia arriba y hacia abajo, al tiempo que evita que alguna parte de ella sea demasiado irregular.
Vamos a crear una clase LightningBoltJS
para manejar la creación de nuestras líneas irregulares.
1 |
#pragma strict |
2 |
import System.Collections.Generic; |
3 |
class LightningBoltJS extends MonoBehaviour |
4 |
{
|
5 |
//List of all of our active/inactive lines
|
6 |
public var ActiveLineObj : List.<GameObject>; |
7 |
public var InactiveLineObj : List.<GameObject>; |
8 |
|
9 |
//Prefab for a line
|
10 |
public var LinePrefab : GameObject; |
11 |
|
12 |
//Transparency
|
13 |
public var Alpha : float; |
14 |
|
15 |
//The speed at which our bolts will fade out
|
16 |
public var FadeOutRate : float; |
17 |
|
18 |
//The color of our bolts
|
19 |
public var Tint : Color; |
20 |
|
21 |
//The position where our bolt started
|
22 |
public function Start() |
23 |
{
|
24 |
var first : GameObject = ActiveLineObj[0]; |
25 |
return first.GetComponent(LineJS).A; |
26 |
}
|
27 |
|
28 |
//The position where our bolt ended
|
29 |
public function End() |
30 |
{
|
31 |
var last : GameObject = ActiveLineObj[ActiveLineObj.Count-1]; |
32 |
return last.GetComponent(LineJS).B; |
33 |
}
|
34 |
|
35 |
//True if the bolt has completely faded out
|
36 |
public function IsComplete() |
37 |
{
|
38 |
return Alpha <= 0; |
39 |
}
|
40 |
|
41 |
public function Initialize(maxSegments : int) |
42 |
{
|
43 |
//Initialize lists for pooling
|
44 |
ActiveLineObj = new List.<GameObject>(); |
45 |
InactiveLineObj = new List.<GameObject>(); |
46 |
|
47 |
for(var i : int = 0; i < maxSegments; i++) |
48 |
{
|
49 |
//instantiate from our Line Prefab
|
50 |
var line : GameObject = GameObject.Instantiate(LinePrefab); |
51 |
|
52 |
//parent it to our bolt object
|
53 |
line.transform.parent = transform; |
54 |
|
55 |
//set it inactive
|
56 |
line.SetActive(false); |
57 |
|
58 |
//add it to our list
|
59 |
InactiveLineObj.Add(line); |
60 |
}
|
61 |
}
|
62 |
|
63 |
public function ActivateBolt(source : Vector2, dest : Vector2, color : Color, thickness : float) |
64 |
{
|
65 |
//for use in loops later
|
66 |
var i : int; |
67 |
|
68 |
//Store tint
|
69 |
Tint = color; |
70 |
|
71 |
//Store alpha
|
72 |
Alpha = 1.5f; |
73 |
|
74 |
//Store fade out rate
|
75 |
FadeOutRate = 0.03f; |
76 |
|
77 |
//actually create the bolt
|
78 |
//Prevent from getting a 0 magnitude
|
79 |
if(Vector2.Distance(dest, source) <= 0) |
80 |
{
|
81 |
var adjust : Vector2 = Random.insideUnitCircle; |
82 |
if(adjust.magnitude <= 0) adjust.x += .1f; |
83 |
dest += adjust; |
84 |
}
|
85 |
|
86 |
//difference from source to destination
|
87 |
var slope : Vector2 = dest - source; |
88 |
var normal : Vector2 = (new Vector2(slope.y, -slope.x)).normalized; |
89 |
|
90 |
//distance between source and destination
|
91 |
var distance : float = slope.magnitude; |
92 |
|
93 |
var positions : List.<float> = new List.<float>(); |
94 |
positions.Add(0); |
95 |
|
96 |
for (i = 0; i < distance / 4; i++) |
97 |
{
|
98 |
//Generate random positions between 0 and 1 to break up the bolt
|
99 |
//positions.Add (Random.Range(0f, 1f));
|
100 |
positions.Add(Random.Range(.25f, .75f)); |
101 |
}
|
102 |
|
103 |
positions.Sort(); |
104 |
|
105 |
var Sway : float = 80; |
106 |
var Jaggedness : float = 1 / Sway; |
107 |
|
108 |
//Affects how wide the bolt is allowed to spread
|
109 |
var spread : float = 1f; |
110 |
|
111 |
//Start at the source
|
112 |
var prevPoint : Vector2 = source; |
113 |
|
114 |
//No previous displacement, so just 0
|
115 |
var prevDisplacement : float = 0; |
116 |
|
117 |
for (i = 1; i < positions.Count; i++) |
118 |
{
|
119 |
//don't allow more than we have in the pool
|
120 |
var inactiveCount : int = InactiveLineObj.Count; |
121 |
if(inactiveCount <= 0) break; |
122 |
|
123 |
var pos : float = positions[i]; |
124 |
var prevPos : float = positions[i - 1]; |
125 |
//used to prevent sharp angles by ensuring very close positions also have small perpendicular variation.
|
126 |
var scale : float = (distance * Jaggedness) * (pos - prevPos); |
127 |
|
128 |
//defines an envelope. Points near the middle of the bolt can be further from the central line.
|
129 |
var envelope : float = pos > 0.95f ? 20 * (1 - pos) : spread; |
130 |
|
131 |
//calculate the displacement
|
132 |
var displacement : float = Random.Range(-Sway, Sway); |
133 |
displacement -= (displacement - prevDisplacement) * (1 - scale); |
134 |
displacement *= envelope; |
135 |
|
136 |
//Calculate the end point
|
137 |
var point : Vector2 = source + (pos * slope) + (displacement * normal); |
138 |
|
139 |
activateLine(prevPoint, point, thickness); |
140 |
prevPoint = point; |
141 |
prevDisplacement = displacement; |
142 |
}
|
143 |
|
144 |
activateLine(prevPoint, dest, thickness); |
145 |
}
|
146 |
|
147 |
public function DeactivateSegments() |
148 |
{
|
149 |
for(var i : int = ActiveLineObj.Count - 1; i >= 0; i--) |
150 |
{
|
151 |
var line : GameObject = ActiveLineObj[i]; |
152 |
line.SetActive(false); |
153 |
|
154 |
ActiveLineObj.RemoveAt(i); |
155 |
InactiveLineObj.Add(line); |
156 |
}
|
157 |
}
|
158 |
|
159 |
function activateLine(A : Vector2, B : Vector2, thickness : float) |
160 |
{
|
161 |
//get the inactive count
|
162 |
var inactiveCount : int = InactiveLineObj.Count; |
163 |
|
164 |
//only activate if we can pull from inactive
|
165 |
if(inactiveCount <= 0) return; |
166 |
|
167 |
//pull the GameObject
|
168 |
var lineObj : GameObject = InactiveLineObj[InactiveLineObj.Count - 1]; |
169 |
|
170 |
//set it active
|
171 |
lineObj.SetActive(true); |
172 |
|
173 |
//get the Line component
|
174 |
var lineComponent : LineJS = lineObj.GetComponent(LineJS); |
175 |
lineComponent.SetColor(Color.white); |
176 |
lineComponent.A = A; |
177 |
lineComponent.B = B; |
178 |
lineComponent.Thickness = thickness; |
179 |
ActiveLineObj.Add(lineObj); |
180 |
InactiveLineObj.Remove(lineObj); |
181 |
}
|
182 |
|
183 |
public function Draw() |
184 |
{
|
185 |
//if the bolt has faded out, no need to draw
|
186 |
if (Alpha <= 0) return; |
187 |
|
188 |
for(var i : int = 0; i < ActiveLineObj.Count; i++) |
189 |
{
|
190 |
var obj : GameObject = ActiveLineObj[i]; |
191 |
var lineComponent : LineJS = obj.GetComponent(LineJS); |
192 |
lineComponent.SetColor(Tint * (Alpha * 0.6f)); |
193 |
lineComponent.Draw(); |
194 |
}
|
195 |
}
|
196 |
|
197 |
public function Update() |
198 |
{
|
199 |
Alpha -= FadeOutRate; |
200 |
}
|
201 |
|
202 |
//...
|
203 |
}
|
El código puede parecer un poco intimdante, pero no es tan malo una vez que comprendes la lógica. Antes de continuar, comprende que hemos elegido unir nuestros segmentos de línea en los rayos (ya que la constante creación de instancias y la destrucción de objetos puede ser costosa en Unity).
- La función
Initialize()
se llamará una vez en cada relámpago y determinará cuántos segmentos de línea se permite usar en cada rayo. - La función
activateLine()
activará un segmento de línea utilizando los datos de posición dados. - La función
DeactivateSegments()
desactivará cualquier segmento de línea activo en nuestro rayo. - La función
ActivateBolt()
manejará la creación de nuestras líneas irregulares y llamará a la funciónenableLine()
para activar nuestros segmentos de línea en las posiciones apropiadas.
Para crear nuestras líneas dentadas, comenzamos calculando la pendiente entre nuestros dos puntos, así como el vector normal de esa pendiente. Luego, seleccionamos un número 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, clasificamos estas posiciones para permitirnos agregar fácilmente segmentos de línea entre ellas.
El bucle atraviesa los puntos elegidos al azar y los desplaza a lo largo de la normal en una cantidad aleatoria. El factor scale
está ahí para evitar ángulos demasiado agudos, y envelope
garantiza que el rayo realmente vaya al punto de destino limitando el desplazamiento cuando estamos cerca del final. spread
es para ayudar a controlar hasta qué punto los segmentos se desvían de la pendiente de nuestra línea; un spread
de 0
esencialmente te dará una línea recta.



Entonces, como hicimos con nuestra clase LineJS
, hagamos esto un prefab. En el menú, selecciona GameObject > Crear vacío. El objeto aparecerá en tu panel de Jerarquía. Cámbiale el nombre a BoltJS
y arrastra una copia del script LightningBoltJS
aquí. Finalmente, haz clic en el objeto BoltJS y asigna el prefab LineJS, desde la carpeta Prefabs, al espacio apropiado en el script LightningBoltJS. Una vez que hayas terminado con eso, simplemente arrastra el objeto BoltJS a la carpeta Prefabs para crear un prefab.
Paso 3: Añade animación
Los rayos deberían destellar y luego desaparecer. Esto es para lo que sirven nuestras funciones Update()
y Draw()
en LightningBoltJS
. Llamar a Update()
hará que el rayo se desvanezca. Al llamar a Draw()
se actualizará el color del rayo en la pantalla. IsComplete()
te dirá cuándo el rayo se ha desvanecido por completo.
Paso 4: Crea un rayo
Ahora que tenemos nuestra clase LightningBoltJS
, vamos a darle un buen uso y configurar una escena demo rápida.
Vamos a utilizar un grupo de objetos para este demo, por lo que querremos crear un objeto vacío para mantener nuestros rayos activos e inactivos (simplemente con fines organizativos). En Unity, en el menú, seleccionar GameObject> Crear vacío. El objeto aparecerá en tu panel de Jerarquía. Cámbiale el nombre a LightningPoolHolder
.
Haz clic derecho en la carpeta Scripts y selecciona Crear> Javascript. Nombra tu script DemoScriptJS
y ábrelo. Aquí hay un código rápido para que empieces:
1 |
#pragma strict |
2 |
import System.Collections.Generic; |
3 |
|
4 |
class DemoScriptJS extends MonoBehaviour |
5 |
{
|
6 |
//Prefabs to be assigned in Editor
|
7 |
public var BoltPrefab : GameObject; |
8 |
|
9 |
//For pooling
|
10 |
var activeBoltsObj : List.<GameObject>; |
11 |
var inactiveBoltsObj : List.<GameObject>; |
12 |
var maxBolts : int = 1000; |
13 |
|
14 |
//For handling mouse clicks
|
15 |
var clicks : int = 0; |
16 |
var pos1 : Vector2; |
17 |
var pos2 : Vector2; |
18 |
|
19 |
function Start() |
20 |
{
|
21 |
//Initialize lists
|
22 |
activeBoltsObj = new List.<GameObject>(); |
23 |
inactiveBoltsObj = new List.<GameObject>(); |
24 |
|
25 |
//for use later
|
26 |
var tempV3 : Vector3; |
27 |
|
28 |
//Grab the parent we'll be assigning to our bolt pool
|
29 |
var p : GameObject = GameObject.Find("LightningPoolHolder"); |
30 |
|
31 |
//For however many bolts we've specified
|
32 |
for(var i : int = 0; i < maxBolts; i++) |
33 |
{
|
34 |
//create from our prefab
|
35 |
var bolt : GameObject = Instantiate(BoltPrefab); |
36 |
|
37 |
//Assign parent
|
38 |
bolt.transform.parent = p.transform; |
39 |
|
40 |
//Initialize our lightning with a preset number of max sexments
|
41 |
bolt.GetComponent(LightningBoltJS).Initialize(25); |
42 |
|
43 |
//Set inactive to start
|
44 |
bolt.SetActive(false); |
45 |
|
46 |
//Store in our inactive list
|
47 |
inactiveBoltsObj.Add(bolt); |
48 |
}
|
49 |
}
|
50 |
|
51 |
function Update() |
52 |
{
|
53 |
//Declare variables for use later
|
54 |
var boltObj : GameObject; |
55 |
var boltComponent : LightningBoltJS; |
56 |
var i : int; |
57 |
var tempV3 : Vector3; |
58 |
var adjust : Vector2; |
59 |
|
60 |
//store off the count for effeciency
|
61 |
var activeLineCount : int = activeBoltsObj.Count; |
62 |
|
63 |
//loop through active lines (backwards because we'll be removing from the list)
|
64 |
for (i = activeLineCount - 1; i >= 0; i--) |
65 |
{
|
66 |
//pull GameObject
|
67 |
boltObj = activeBoltsObj[i]; |
68 |
|
69 |
//get the LightningBolt component
|
70 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
71 |
|
72 |
//if the bolt has faded out
|
73 |
if(boltComponent.IsComplete()) |
74 |
{
|
75 |
//deactive the segments it contains
|
76 |
boltComponent.DeactivateSegments(); |
77 |
|
78 |
//set it inactive
|
79 |
boltObj.SetActive(false); |
80 |
|
81 |
//move it to the inactive list
|
82 |
activeBoltsObj.RemoveAt(i); |
83 |
inactiveBoltsObj.Add(boltObj); |
84 |
}
|
85 |
}
|
86 |
|
87 |
//If left mouse button pressed
|
88 |
if(Input.GetMouseButtonDown(0)) |
89 |
{
|
90 |
//if first click
|
91 |
if(clicks == 0) |
92 |
{
|
93 |
//store starting position
|
94 |
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
95 |
pos1 = new Vector2(tempV3.x, tempV3.y); |
96 |
}
|
97 |
else if(clicks == 1) //second click |
98 |
{
|
99 |
//store end position
|
100 |
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
101 |
pos2 = new Vector2(tempV3.x, tempV3.y); |
102 |
|
103 |
CreatePooledBolt(pos1,pos2, Color.white, 1f); |
104 |
}
|
105 |
|
106 |
//increment our tick count
|
107 |
clicks++; |
108 |
|
109 |
//restart the count after 2 clicks
|
110 |
if(clicks > 1) clicks = 0; |
111 |
}
|
112 |
|
113 |
//update and draw active bolts
|
114 |
for(i = 0; i < activeBoltsObj.Count; i++) |
115 |
{
|
116 |
boltObj = activeBoltsObj[i]; |
117 |
boltObj.GetComponent(LightningBoltJS).Update(); |
118 |
boltObj.GetComponent(LightningBoltJS).Draw(); |
119 |
}
|
120 |
}
|
121 |
|
122 |
function CreatePooledBolt(source : Vector2, dest : Vector2, color : Color, thickness : float) |
123 |
{
|
124 |
//if there is an inactive bolt to pull from the pool
|
125 |
if(inactiveBoltsObj.Count > 0) |
126 |
{
|
127 |
//pull the GameObject
|
128 |
var boltObj : GameObject = inactiveBoltsObj[inactiveBoltsObj.Count - 1]; |
129 |
|
130 |
//set it active
|
131 |
boltObj.SetActive(true); |
132 |
|
133 |
//move it to the active list
|
134 |
activeBoltsObj.Add(boltObj); |
135 |
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1); |
136 |
|
137 |
//get the bolt component
|
138 |
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS); |
139 |
|
140 |
//activate the bolt using the given position data
|
141 |
boltComponent.ActivateBolt(source, dest, color, thickness); |
142 |
}
|
143 |
}
|
144 |
}
|
Todo lo que hace este código es darnos una forma de crear rayos utilizando la agrupación de objetos. Hay otras formas de configurar esto, ¡pero esta con la que lo haremos! Una vez que lo hayamos configurado, todo lo que tendrás que hacer es hacer clic dos veces para crear un rayo en la pantalla: una vez para la posición inicial y otra para la posición final.
Necesitaremos un objeto para poner nuestro DemoScriptJS
. En el menú, selecciona GameObject> Crear vacío. El objeto aparecerá en tu panel de Jerarquía. Cámbiale el nombre a DemoScript
y arrastra tu script DemoScriptJS ahi. Haz clic en el objeto DemoScript para que podamos verlo en el panel Inspector. Asigna el prefab BoltJS, desde la carpeta Prefabs, al espacio correspondiente en el DemoScriptJS.



¡Eso debería ser suficiente para ponerte en marcha! ¡Ejecuta la escena en Unity y pruébala!
Paso 5: Crea las ramas del rayo
Puedes usar la clase LightningBoltJS
como un bloque de construcción para crear efectos de rayos más interesantes. Por ejemplo, puedes hacer que los rayos se bifurquen como se muestra a continuación:

Para hacer las ramas del rayo, seleccionamos puntos aleatorios a lo largo del rayo y agregamos nuevos rayos que se ramifican desde estos puntos. En el código siguiente, creamos entre tres y seis ramas las cuales se separaran del rayo principal en ángulos de 30°.
1 |
#pragma strict
|
2 |
import System.Collections.Generic; |
3 |
|
4 |
class BranchLightningJS extends MonoBehaviour |
5 |
{
|
6 |
//For holding all of our bolts in our branch
|
7 |
public var boltsObj : 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 function IsComplete() |
11 |
{
|
12 |
return boltsObj.Count <= 0; |
13 |
}
|
14 |
|
15 |
//Start position of branch
|
16 |
public var Start : Vector2; |
17 |
|
18 |
//End position of branch
|
19 |
public var End : Vector2; |
20 |
|
21 |
static var rand : Random = new Random(); |
22 |
|
23 |
public function Initialize(start : Vector2, end : Vector2, boltPrefab : GameObject) |
24 |
{
|
25 |
//for use lateer
|
26 |
var i : int; |
27 |
|
28 |
//store start and end positions
|
29 |
Start = start; |
30 |
End = end; |
31 |
|
32 |
//create the main bolt from our bolt prefab
|
33 |
var mainBoltObj : GameObject = GameObject.Instantiate(boltPrefab); |
34 |
|
35 |
//get the LightningBolt component
|
36 |
var mainBoltComponent : LightningBoltJS = mainBoltObj.GetComponent(LightningBoltJS); |
37 |
|
38 |
//initialize our bolt with a max of 5 segments
|
39 |
mainBoltComponent.Initialize(5); |
40 |
|
41 |
//activate the bolt with our position data
|
42 |
mainBoltComponent.ActivateBolt(start, end, Color.white, 1f); |
43 |
|
44 |
//add it to our list
|
45 |
boltsObj.Add(mainBoltObj); |
46 |
|
47 |
//randomly determine how many sub branches there will be (3-6)
|
48 |
var numBranches : int = Random.Range(3,6); |
49 |
|
50 |
//calculate the difference between our start and end points
|
51 |
var diff : Vector2 = end - start; |
52 |
|
53 |
// pick a bunch of random points between 0 and 1 and sort them
|
54 |
var branchPoints : List.<float> = new List.<float>(); |
55 |
for(i = 0; i < numBranches; i++) branchPoints.Add(Random.value); |
56 |
branchPoints.Sort(); |
57 |
|
58 |
//go through those points
|
59 |
for (i = 0; i < branchPoints.Count; i++) |
60 |
{
|
61 |
// Bolt.GetPoint() gets the position of the lightning bolt based on the percentage passed in (0 = start of bolt, 1 = end)
|
62 |
var boltStart : Vector2 = mainBoltComponent.GetPoint(branchPoints[i]); |
63 |
|
64 |
//get rotation of 30 degrees. Alternate between rotating left and right. (i & 1 will be true for all odd numbers...yay bitwise operators!)
|
65 |
var rot : Quaternion = Quaternion.AngleAxis(30 * ((i & 1) == 0 ? 1 : -1), new Vector3(0,0,1)); |
66 |
|
67 |
var point : float = branchPoints[i]; |
68 |
|
69 |
//calculate how much to adjust for our end position
|
70 |
var adjust : Vector2 = rot * (Random.Range(.5f, .75f) * diff * (1 - point)); |
71 |
|
72 |
//get the end position
|
73 |
var boltEnd : Vector2 = adjust + boltStart; |
74 |
|
75 |
//instantiate from our bolt prefab
|
76 |
var boltObj : GameObject = GameObject.Instantiate(boltPrefab); |
77 |
|
78 |
//get the LightningBolt component
|
79 |
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS); |
80 |
|
81 |
//initialize our bolt with a max of 5 segments
|
82 |
boltComponent.Initialize(5); |
83 |
|
84 |
//activate the bolt with our position data
|
85 |
boltComponent.ActivateBolt(boltStart, boltEnd, Color.white, 1f); |
86 |
|
87 |
//add it to the list
|
88 |
boltsObj.Add(boltObj); |
89 |
}
|
90 |
}
|
91 |
|
92 |
public function Update() |
93 |
{
|
94 |
//go through our active bolts
|
95 |
for (var i : int = boltsObj.Count - 1; i >= 0; i--) |
96 |
{
|
97 |
//get the GameObject
|
98 |
var boltObj : GameObject = boltsObj[i]; |
99 |
|
100 |
//get the LightningBolt component
|
101 |
var boltComp : LightningBoltJS = boltObj.GetComponent(LightningBoltJS); |
102 |
|
103 |
//update/fade out the bolt
|
104 |
boltComp.Update(); |
105 |
|
106 |
//if the bolt has faded
|
107 |
if(boltComp.IsComplete()) |
108 |
{
|
109 |
//remove it from our list
|
110 |
boltsObj.RemoveAt(i); |
111 |
|
112 |
//destroy it (would be better to pool but I'll let you figure out how to do that =P)
|
113 |
Destroy(boltObj); |
114 |
}
|
115 |
}
|
116 |
}
|
117 |
|
118 |
//Draw our active bolts on screen
|
119 |
public function Draw() |
120 |
{
|
121 |
var boltObj : GameObject; |
122 |
for(var i : int; i < boltsObj.Count; i++) |
123 |
{
|
124 |
boltObj = boltsObj[i]; |
125 |
boltObj.GetComponent(LightningBoltJS).Draw(); |
126 |
}
|
127 |
}
|
128 |
}
|
Este código funciona de manera muy similar a nuestra clase LightningBoltJS
con la excepción de que no usa la agrupación de objetos. Llamar a Initialize()
es todo lo que necesitas hacer para crear un rayo ramificado; después de eso, solo tendrás que llamar a Update()
y Draw()
. Te mostraré exactamente cómo hacerlo en nuestro DemoScriptJS
más adelante en el tutorial.
Es posible que hayas notado la referencia a una función GetPoint()
en la clase LightningBoltJS
. Todavía no hemos implementado esa función, así que vamos a ocuparnos de eso ahora.
Agrega la siguiente función en la parte inferior de la clase LightningBoltJS
:
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 function GetPoint(position : float) |
4 |
{
|
5 |
var start : Vector2 = Start(); |
6 |
var length : float = Vector2.Distance(start, End()); |
7 |
var dir : Vector2 = (End() - start) / length; |
8 |
position *= length; |
9 |
|
10 |
var line : LineJS; |
11 |
|
12 |
//find the appropriate line
|
13 |
for(var i : int = 0; i < ActiveLineObj.Count; i++) |
14 |
{
|
15 |
var x : GameObject = ActiveLineObj[i]; |
16 |
|
17 |
if(Vector2.Dot(x.GetComponent(LineJS).B - start, dir) >= position) |
18 |
{
|
19 |
line = x.GetComponent(LineJS); |
20 |
break; |
21 |
}
|
22 |
}
|
23 |
var lineStartPos : float = Vector2.Dot(line.A - start, dir); |
24 |
var lineEndPos : float = Vector2.Dot(line.B - start, dir); |
25 |
var linePos : float = (position - lineStartPos) / (lineEndPos - lineStartPos); |
26 |
|
27 |
return Vector2.Lerp(line.A, line.B, linePos); |
28 |
}
|
Paso 6: Crea un texto relámpago
A continuación se muestra un video de otro efecto que puedes hacer con los rayos:
Tendremos que hacer un poco más de configuración para este. Primero, desde el panel Proyecto, selecciona Crear > RenderTexture. Cambia el nombre a RenderText
y establece su tamaño a 256x256px
. (No necesariamente tiene que ser ese tamaño exacto, pero cuanto más pequeño sea, más rápido se ejecutará el programa).
En el menú, selecciona Editar > Configuración del proyecto > Etiquetas y capas. Luego, en el panel Inspector, expande el menú desplegable Capas y agrega Text
en la capa de usuario 8.



Entonces necesitaremos crear una segunda cámara. En el menú, selecciona GameObject > Crear otro > Cámara. Cámbiale el nombre a TextCamera
y establece su Proyección a Orthographic
y sus Indicadores claros a Solid Color
. Establece su color de fondo en (R: 0, G: 0, B: 0, A: 0)
y establece su máscara de eliminación para que sea solo Text
(la capa que acabamos de crear). Finalmente, establece su Textura objetivo en RenderText
(RenderTexture que creamos anteriormente). Probablemente tendrás que jugar con el tamaño de la cámara más adelante, para que todo encaje en la pantalla.



Ahora tendremos que crear el texto real que dibujaremos con nuestro relámpago. En el menú, selecciona GameObject > Crear otro > Texto GUI. Selecciona el objeto de Texto GUI en el panel Jerarquía y establece su Texto en LIGHTNING
, su Ancla en middle center
y su Alineación en center
. Luego, establece su capa en la capa Text
que creamos anteriormente. Probablemente tendrás que jugar un poco con el tamaño de fuente para ajustar el texto en la pantalla.



Ahora selecciona la cámara principal y configura su máscara de eliminación para que sea todo menos nuestra capa de texto. Esto hará que nuestro Texto GUI desaparezca aparentemente de la pantalla, pero debe dibujarse en la RenderTexture que creamos anteriormente: selecciona RenderText en el panel Proyecto y deberías poder ver la palabra LIGHTNING en la vista previa en la parte inferior del panel.
Si no puedes ver la palabra LIGHTNING, deberás jugar con la posición, el tamaño de la fuente y el tamaño de la cámara (texto). Para ayudarte a colocar tu texto, haz clic en TextCamera en el panel Jerarquía y establece la Textura objetivo en None
. Ahora podrás ver tu texto GUI si lo centras en la cámara de texto. Una vez que hayas colocado todo, establece la Textura objetivo de la cámara de texto de nuevo en RenderText
.
¡Ahora el código! Necesitaremos obtener los píxeles del texto que estamos dibujando. Podemos hacerlo dibujando nuestro texto en un RenderTarget
y leyendo los datos de píxeles en un Texture2D
con Texture2D.ReadPixels()
. Luego, 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 |
function TextCapture() |
3 |
{
|
4 |
//must wait until end of frame so something is actually drawn or else it will error
|
5 |
yield WaitForEndOfFrame(); |
6 |
|
7 |
//get the camera that draws our text
|
8 |
var cam : Camera = 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 |
var image : Texture2D = 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 |
var interval : int = 2; |
30 |
|
31 |
//loop through pixels
|
32 |
for(var y : int = 0; y < image.height; y += interval) |
33 |
{
|
34 |
for(var x : int = 0; x < image.width; x += interval) |
35 |
{
|
36 |
//get the color of the pixel
|
37 |
var color : Color = image.GetPixel(x,y); |
38 |
|
39 |
//if the color has an 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 Co-rutina al inicio de nuestro programa para que funcione correctamente.
Después de eso, en cada cuadro, podemos elegir al azar pares de estos puntos y crear un rayo entre ellos. Queremos diseñarlo de modo que cuanto más cerca estén los dos puntos entre sí, mayor será la posibilidad de que creemos un rayo entre ellos.
Hay una técnica simple que podemos usar para lograr esto: elegiremos el primer punto al azar, y luego elegiremos 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 DemoScriptJS
más adelante):
1 |
//go through the points we capture earlier
|
2 |
for (var i1 : int = 0; i1 < textPoints.Count; i1++) |
3 |
{
|
4 |
var point : Vector2 = textPoints[i1]; |
5 |
//randomly ignore certain points
|
6 |
if(Random.Range(0,75) != 0) continue; |
7 |
|
8 |
//placeholder values
|
9 |
var nearestParticle : Vector2 = Vector2.zero; |
10 |
var nearestDistSquared : float = float.MaxValue; |
11 |
|
12 |
for (i = 0; i < 50; i++) |
13 |
{
|
14 |
//select a random point
|
15 |
var other : Vector2 = textPoints[Random.Range(0, textPoints.Count)]; |
16 |
|
17 |
//calculate the distance (squared for performance benefits) between the two points
|
18 |
var distSquared : float = DistanceSquared(point, other); |
19 |
|
20 |
//If this point is the nearest point (but not too near!)
|
21 |
if (distSquared < nearestDistSquared && distSquared > 3 * 3) |
22 |
{
|
23 |
//store off the data
|
24 |
nearestDistSquared = distSquared; |
25 |
nearestParticle = other; |
26 |
}
|
27 |
}
|
28 |
|
29 |
//if the point we found isn't too near/far
|
30 |
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3) |
31 |
{
|
32 |
//create a (pooled) bolt at the corresponding screen position
|
33 |
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f); |
34 |
}
|
35 |
}
|
36 |
|
37 |
/* The code above uses the following function
|
38 |
* It'll need to be placed appropriately
|
39 |
---------------------------------------------
|
40 |
//calculate distance squared (no square root = performance boost)
|
41 |
public function DistanceSquared(a : Vector2, b : Vector2)
|
42 |
{
|
43 |
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
|
44 |
}
|
45 |
---------------------------------------------*/
|
El número de puntos candidatos que probamos afectará el aspecto del texto de rayo; si se comprueba un número mayor de puntos, podremos encontrar puntos muy cercanos entre los cuales dibujar rayos, lo que hará que el texto sea muy limpio y legible, pero con menos relámpagos 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 cómo hacer un relámpago y un texto de rayo, pero esos no son los únicos efectos que puedes hacer. Echemos un vistazo a un par de otras variaciones en el rayo que podrías querer utilizar.
Relámpago en movimiento
A menudo es posible que desees hacer un rayo en movimiento. Puedes hacer esto agregando un nuevo rayo corto a cada marco en el punto final del rayo del marco anterior
1 |
//Will contain all of the pieces for the moving bolt
|
2 |
var movingBolt : List.<GameObject>; |
3 |
|
4 |
//used for actually moving the moving bolt
|
5 |
var lightningEnd : Vector2 = new Vector2(100, 100); |
6 |
var lightningVelocity : Vector2 = new Vector2(1, 0); |
7 |
|
8 |
function Update() |
9 |
{
|
10 |
//loop through all of our bolts that make up the moving bolt
|
11 |
for(i = movingBolt.Count - 1; i >= 0; i--) |
12 |
{
|
13 |
boltObj = movingBolt[i]; |
14 |
//get the bolt component
|
15 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
16 |
|
17 |
//if the bolt has faded out
|
18 |
if(boltComponent.IsComplete()) |
19 |
{
|
20 |
//destroy it
|
21 |
Destroy(movingBolt[i]); |
22 |
|
23 |
//remove it from our list
|
24 |
movingBolt.RemoveAt(i); |
25 |
|
26 |
//on to the next one, on on to the next one
|
27 |
continue; |
28 |
}
|
29 |
|
30 |
//update and draw bolt
|
31 |
boltComponent.Update(); |
32 |
boltComponent.Draw(); |
33 |
}
|
34 |
|
35 |
//if our moving bolt is active
|
36 |
if(movingBolt.Count > 0) |
37 |
{
|
38 |
boltObj = movingBolt[movingBolt.Count-1]; |
39 |
//calculate where it currently ends
|
40 |
lightningEnd = boltObj.GetComponent(LightningBoltJS).End(); |
41 |
|
42 |
//if the end of the bolt is within 25 units of the camera
|
43 |
if(Vector2.Distance(lightningEnd,Camera.main.transform.position) < 25) |
44 |
{
|
45 |
//instantiate from our bolt prefab
|
46 |
boltObj = GameObject.Instantiate(BoltPrefab); |
47 |
|
48 |
//get the bolt component
|
49 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
50 |
|
51 |
//initialize it with a maximum of 5 segments
|
52 |
boltComponent.Initialize(5); |
53 |
|
54 |
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
|
55 |
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f); |
56 |
|
57 |
//add it to our list
|
58 |
movingBolt.Add(boltObj); |
59 |
|
60 |
//update and draw our new bolt
|
61 |
boltComponent.Update(); |
62 |
boltComponent.Draw(); |
63 |
}
|
64 |
}
|
65 |
}
|
Ráfaga de relámpago
Esta variación ofrece un efecto dramático que dispara un rayo en un círculo desde el punto central:
1 |
//get the difference between our two positions (destination - source = vector from source to destination)
|
2 |
var diff : Vector2 = pos2 - pos1; |
3 |
|
4 |
function Update() |
5 |
{
|
6 |
//define how many bolts we want in our circle
|
7 |
var boltsInBurst : int = 10; |
8 |
|
9 |
for(i = 0; i < boltsInBurst; i++) |
10 |
{
|
11 |
//rotate around the z axis to the appropriate angle
|
12 |
var rot : Quaternion = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1)); |
13 |
|
14 |
adjust = rot * diff; |
15 |
|
16 |
//Calculate the end position for the bolt
|
17 |
var boltEnd : Vector2 = adjust + pos1; |
18 |
|
19 |
//create a (pooled) bolt from pos1 to boltEnd
|
20 |
CreatePooledBolt(pos1, boltEnd, Color.white, 1f); |
21 |
}
|
22 |
}
|
Paso 8: Ponlo todo junto en DemoScriptJS
Querrás poder probar todos estos efectos sofisticados que hemos creado hasta ahora, así que pongámoslos todos en el DemoScriptJS
que creamos anteriormente. Podrás alternar entre los efectos presionando las teclas numéricas de tu teclado para seleccionar el efecto, y luego simplemente haciendo clic dos veces como hicimos con nuestros rayos antes.
Aquí está el código completo:
1 |
#pragma strict
|
2 |
#pragma strict
|
3 |
import System.Collections.Generic; |
4 |
|
5 |
class DemoScriptJS extends MonoBehaviour |
6 |
{
|
7 |
//Prefabs to be assigned in Editor
|
8 |
public var BoltPrefab : GameObject; |
9 |
public var BranchPrefab : GameObject; |
10 |
|
11 |
//For pooling
|
12 |
var activeBoltsObj : List.<GameObject>; |
13 |
var inactiveBoltsObj : List.<GameObject>; |
14 |
var maxBolts : int = 1000; |
15 |
|
16 |
var scaleText : float; |
17 |
var positionText : Vector2; |
18 |
|
19 |
//Different modes for the demo
|
20 |
class Mode |
21 |
{
|
22 |
public static final var bolt : byte = 0; |
23 |
public static final var branch : byte = 1; |
24 |
public static final var moving : byte = 2; |
25 |
public static final var text : byte = 3; |
26 |
public static final var nodes : byte = 4; |
27 |
public static final var burst : byte = 5; |
28 |
}
|
29 |
|
30 |
//The current mode the demo is in
|
31 |
var currentMode : byte = Mode.bolt; |
32 |
|
33 |
//Will contain all of the pieces for the moving bolt
|
34 |
var movingBolt : List.<GameObject>; |
35 |
|
36 |
//used for actually moving the moving bolt
|
37 |
var lightningEnd : Vector2 = new Vector2(100, 100); |
38 |
var lightningVelocity : Vector2 = new Vector2(1, 0); |
39 |
|
40 |
//Will contain all of the pieces for the branches
|
41 |
var branchesObj : List.<GameObject>; |
42 |
|
43 |
//For handling mouse clicks
|
44 |
var clicks : int = 0; |
45 |
var pos1 : Vector2; |
46 |
var pos2 : Vector2; |
47 |
|
48 |
//For storing all of the pixels that need to be drawn by the bolts
|
49 |
var textPoints : List.<Vector2>; |
50 |
|
51 |
//true in text mode
|
52 |
var shouldText : boolean = false; |
53 |
|
54 |
function Start() |
55 |
{
|
56 |
//Initialize lists
|
57 |
activeBoltsObj = new List.<GameObject>(); |
58 |
inactiveBoltsObj = new List.<GameObject>(); |
59 |
branchesObj = new List.<GameObject>(); |
60 |
|
61 |
//for use later
|
62 |
var tempV3 : Vector3; |
63 |
|
64 |
//Grab the parent we'll be assigning to our bolt pool
|
65 |
var p : GameObject = GameObject.Find("LightningPoolHolder"); |
66 |
|
67 |
//For however many bolts we've specified
|
68 |
for(var i : int = 0; i < maxBolts; i++) |
69 |
{
|
70 |
//create from our prefab
|
71 |
var bolt : GameObject = Instantiate(BoltPrefab); |
72 |
|
73 |
//Assign parent
|
74 |
bolt.transform.parent = p.transform; |
75 |
|
76 |
//Initialize our lightning with a preset number of max sexments
|
77 |
bolt.GetComponent(LightningBoltJS).Initialize(25); |
78 |
|
79 |
//Set inactive to start
|
80 |
bolt.SetActive(false); |
81 |
|
82 |
//Store in our inactive list
|
83 |
inactiveBoltsObj.Add(bolt); |
84 |
}
|
85 |
|
86 |
//Start up a coroutine to capture the pixels we'll be drawing from our text (need the coroutine or error)
|
87 |
StartCoroutine(TextCapture()); |
88 |
}
|
89 |
|
90 |
function Update() |
91 |
{
|
92 |
//Declare variables for use later
|
93 |
var boltObj : GameObject; |
94 |
var boltComponent : LightningBoltJS; |
95 |
var i : int; |
96 |
var tempV3 : Vector3; |
97 |
var adjust : Vector2; |
98 |
var branchObj : GameObject; |
99 |
var branchComponent : BranchLightningJS; |
100 |
|
101 |
//store off the count for effeciency
|
102 |
var activeLineCount : int = activeBoltsObj.Count; |
103 |
|
104 |
//loop through active lines (backwards because we'll be removing from the list)
|
105 |
for (i = activeLineCount - 1; i >= 0; i--) |
106 |
{
|
107 |
//pull GameObject
|
108 |
boltObj = activeBoltsObj[i]; |
109 |
|
110 |
//get the LightningBolt component
|
111 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
112 |
|
113 |
//if the bolt has faded out
|
114 |
if(boltComponent.IsComplete()) |
115 |
{
|
116 |
//deactive the segments it contains
|
117 |
boltComponent.DeactivateSegments(); |
118 |
|
119 |
//set it inactive
|
120 |
boltObj.SetActive(false); |
121 |
|
122 |
//move it to the inactive list
|
123 |
activeBoltsObj.RemoveAt(i); |
124 |
inactiveBoltsObj.Add(boltObj); |
125 |
}
|
126 |
}
|
127 |
|
128 |
//check for key press and set mode accordingly
|
129 |
if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) |
130 |
{
|
131 |
shouldText = false; |
132 |
currentMode = Mode.bolt; |
133 |
}
|
134 |
else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) |
135 |
{
|
136 |
shouldText = false; |
137 |
currentMode = Mode.branch; |
138 |
}
|
139 |
else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) |
140 |
{
|
141 |
shouldText = false; |
142 |
currentMode = Mode.moving; |
143 |
}
|
144 |
else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) |
145 |
{
|
146 |
shouldText = true; |
147 |
currentMode = Mode.text; |
148 |
}
|
149 |
else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) |
150 |
{
|
151 |
shouldText = false; |
152 |
currentMode = Mode.nodes; |
153 |
}
|
154 |
else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6)) |
155 |
{
|
156 |
shouldText = false; |
157 |
currentMode = Mode.burst; |
158 |
}
|
159 |
|
160 |
//If left mouse button pressed
|
161 |
if(Input.GetMouseButtonDown(0)) |
162 |
{
|
163 |
//if first click
|
164 |
if(clicks == 0) |
165 |
{
|
166 |
//store starting position
|
167 |
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
168 |
pos1 = new Vector2(tempV3.x, tempV3.y); |
169 |
}
|
170 |
else if(clicks == 1) //second click |
171 |
{
|
172 |
//store end position
|
173 |
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition); |
174 |
pos2 = new Vector2(tempV3.x, tempV3.y); |
175 |
|
176 |
//Handle the current mode appropriately
|
177 |
switch (currentMode) |
178 |
{
|
179 |
case Mode.bolt: |
180 |
//create a (pooled) bolt from pos1 to pos2
|
181 |
CreatePooledBolt(pos1,pos2, Color.white, 1f); |
182 |
break; |
183 |
|
184 |
case Mode.branch: |
185 |
//instantiate from our branch prefab
|
186 |
branchObj = GameObject.Instantiate(BranchPrefab); |
187 |
|
188 |
//get the branch component
|
189 |
branchComponent = branchObj.GetComponent(BranchLightningJS); |
190 |
|
191 |
//initialize the branch component using our position data
|
192 |
branchComponent.Initialize(pos1, pos2, BoltPrefab); |
193 |
|
194 |
//add it to the list of active branches
|
195 |
branchesObj.Add(branchObj); |
196 |
break; |
197 |
|
198 |
case Mode.moving: |
199 |
//Prevent from getting a 0 magnitude (0 causes errors
|
200 |
if(Vector2.Distance(pos1, pos2) <= 0) |
201 |
{
|
202 |
//Try a random position
|
203 |
adjust = Random.insideUnitCircle; |
204 |
|
205 |
//failsafe
|
206 |
if(adjust.magnitude <= 0) adjust.x += .1f; |
207 |
|
208 |
//Adjust the end position
|
209 |
pos2 += adjust; |
210 |
}
|
211 |
|
212 |
//Clear out any old moving bolt (this is designed for one moving bolt at a time)
|
213 |
for(i = movingBolt.Count - 1; i >= 0; i--) |
214 |
{
|
215 |
Destroy(movingBolt[i]); |
216 |
movingBolt.RemoveAt(i); |
217 |
}
|
218 |
|
219 |
//get the "velocity" so we know what direction to send the bolt in after initial creation
|
220 |
lightningVelocity = (pos2 - pos1).normalized; |
221 |
|
222 |
//instantiate from our bolt prefab
|
223 |
boltObj = GameObject.Instantiate(BoltPrefab); |
224 |
|
225 |
//get the bolt component
|
226 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
227 |
|
228 |
//initialize it with 5 max segments
|
229 |
boltComponent.Initialize(5); |
230 |
|
231 |
//activate the bolt using our position data
|
232 |
boltComponent.ActivateBolt(pos1, pos2, Color.white, 1f); |
233 |
|
234 |
//add it to our list
|
235 |
movingBolt.Add(boltObj); |
236 |
break; |
237 |
|
238 |
case Mode.burst: |
239 |
//get the difference between our two positions (destination - source = vector from source to destination)
|
240 |
var diff : Vector2 = pos2 - pos1; |
241 |
|
242 |
//define how many bolts we want in our circle
|
243 |
var boltsInBurst : int = 10; |
244 |
|
245 |
for(i = 0; i < boltsInBurst; i++) |
246 |
{
|
247 |
//rotate around the z axis to the appropriate angle
|
248 |
var rot : Quaternion = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1)); |
249 |
|
250 |
adjust = rot * diff; |
251 |
|
252 |
//Calculate the end position for the bolt
|
253 |
var boltEnd : Vector2 = adjust + pos1; |
254 |
|
255 |
//create a (pooled) bolt from pos1 to boltEnd
|
256 |
CreatePooledBolt(pos1, boltEnd, Color.white, 1f); |
257 |
}
|
258 |
|
259 |
break; |
260 |
}
|
261 |
}
|
262 |
|
263 |
//increment our tick count
|
264 |
clicks++; |
265 |
|
266 |
//restart the count after 2 clicks
|
267 |
if(clicks > 1) clicks = 0; |
268 |
}
|
269 |
|
270 |
//if in node mode
|
271 |
if(currentMode == Mode.nodes) |
272 |
{
|
273 |
//constantly create a (pooled) bolt between the two assigned positions
|
274 |
CreatePooledBolt(pos1, pos2, Color.white, 1f); |
275 |
}
|
276 |
|
277 |
//loop through any active branches
|
278 |
for(i = branchesObj.Count - 1; i >= 0; i--) |
279 |
{
|
280 |
branchObj = branchesObj[i]; |
281 |
|
282 |
//pull the branch lightning component
|
283 |
branchComponent = branchObj.GetComponent(BranchLightningJS); |
284 |
|
285 |
//If it's faded out already
|
286 |
if(branchComponent.IsComplete()) |
287 |
{
|
288 |
//destroy it
|
289 |
Destroy(branchesObj[i]); |
290 |
|
291 |
//take it out of our list
|
292 |
branchesObj.RemoveAt(i); |
293 |
|
294 |
//move on to the next branch
|
295 |
continue; |
296 |
}
|
297 |
|
298 |
//draw and update the branch
|
299 |
branchComponent.Update(); |
300 |
branchComponent.Draw(); |
301 |
}
|
302 |
|
303 |
//loop through all of our bolts that make up the moving bolt
|
304 |
for(i = movingBolt.Count - 1; i >= 0; i--) |
305 |
{
|
306 |
boltObj = movingBolt[i]; |
307 |
//get the bolt component
|
308 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
309 |
|
310 |
//if the bolt has faded out
|
311 |
if(boltComponent.IsComplete()) |
312 |
{
|
313 |
//destroy it
|
314 |
Destroy(movingBolt[i]); |
315 |
|
316 |
//remove it from our list
|
317 |
movingBolt.RemoveAt(i); |
318 |
|
319 |
//on to the next one, on on to the next one
|
320 |
continue; |
321 |
}
|
322 |
|
323 |
//update and draw bolt
|
324 |
boltComponent.Update(); |
325 |
boltComponent.Draw(); |
326 |
}
|
327 |
|
328 |
//if our moving bolt is active
|
329 |
if(movingBolt.Count > 0) |
330 |
{
|
331 |
boltObj = movingBolt[movingBolt.Count-1]; |
332 |
//calculate where it currently ends
|
333 |
lightningEnd = boltObj.GetComponent(LightningBoltJS).End(); |
334 |
|
335 |
//if the end of the bolt is within 25 units of the camera
|
336 |
if(Vector2.Distance(lightningEnd,Camera.main.transform.position) < 25) |
337 |
{
|
338 |
//instantiate from our bolt prefab
|
339 |
boltObj = GameObject.Instantiate(BoltPrefab); |
340 |
|
341 |
//get the bolt component
|
342 |
boltComponent = boltObj.GetComponent(LightningBoltJS); |
343 |
|
344 |
//initialize it with a maximum of 5 segments
|
345 |
boltComponent.Initialize(5); |
346 |
|
347 |
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
|
348 |
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f); |
349 |
|
350 |
//add it to our list
|
351 |
movingBolt.Add(boltObj); |
352 |
|
353 |
//update and draw our new bolt
|
354 |
boltComponent.Update(); |
355 |
boltComponent.Draw(); |
356 |
}
|
357 |
}
|
358 |
|
359 |
//if in text mode
|
360 |
if(shouldText) |
361 |
{
|
362 |
//go through the points we capture earlier
|
363 |
for (var i1 : int = 0; i1 < textPoints.Count; i1++) |
364 |
{
|
365 |
var point : Vector2 = textPoints[i1]; |
366 |
//randomly ignore certain points
|
367 |
if(Random.Range(0,75) != 0) continue; |
368 |
|
369 |
//placeholder values
|
370 |
var nearestParticle : Vector2 = Vector2.zero; |
371 |
var nearestDistSquared : float = float.MaxValue; |
372 |
|
373 |
for (i = 0; i < 50; i++) |
374 |
{
|
375 |
//select a random point
|
376 |
var other : Vector2 = textPoints[Random.Range(0, textPoints.Count)]; |
377 |
|
378 |
//calculate the distance (squared for performance benefits) between the two points
|
379 |
var distSquared : float = DistanceSquared(point, other); |
380 |
|
381 |
//If this point is the nearest point (but not too near!)
|
382 |
if (distSquared < nearestDistSquared && distSquared > 3 * 3) |
383 |
{
|
384 |
//store off the data
|
385 |
nearestDistSquared = distSquared; |
386 |
nearestParticle = other; |
387 |
}
|
388 |
}
|
389 |
|
390 |
//if the point we found isn't too near/far
|
391 |
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3) |
392 |
{
|
393 |
//create a (pooled) bolt at the corresponding screen position
|
394 |
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f); |
395 |
}
|
396 |
}
|
397 |
}
|
398 |
|
399 |
//update and draw active bolts
|
400 |
for(i = 0; i < activeBoltsObj.Count; i++) |
401 |
{
|
402 |
boltObj = activeBoltsObj[i]; |
403 |
boltObj.GetComponent(LightningBoltJS).Update(); |
404 |
boltObj.GetComponent(LightningBoltJS).Draw(); |
405 |
}
|
406 |
}
|
407 |
|
408 |
//calculate distance squared (no square root = performance boost)
|
409 |
public function DistanceSquared(a : Vector2, b : Vector2) |
410 |
{
|
411 |
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)); |
412 |
}
|
413 |
|
414 |
function CreatePooledBolt(source : Vector2, dest : Vector2, color : Color, thickness : float) |
415 |
{
|
416 |
//if there is an inactive bolt to pull from the pool
|
417 |
if(inactiveBoltsObj.Count > 0) |
418 |
{
|
419 |
//pull the GameObject
|
420 |
var boltObj : GameObject = inactiveBoltsObj[inactiveBoltsObj.Count - 1]; |
421 |
|
422 |
//set it active
|
423 |
boltObj.SetActive(true); |
424 |
|
425 |
//move it to the active list
|
426 |
activeBoltsObj.Add(boltObj); |
427 |
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1); |
428 |
|
429 |
//get the bolt component
|
430 |
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS); |
431 |
|
432 |
//activate the bolt using the given position data
|
433 |
boltComponent.ActivateBolt(source, dest, color, thickness); |
434 |
}
|
435 |
}
|
436 |
|
437 |
//Capture the important points of our text for later
|
438 |
function TextCapture() |
439 |
{
|
440 |
//must wait until end of frame so something is actually drawn or else it will error
|
441 |
yield WaitForEndOfFrame(); |
442 |
|
443 |
//get the camera that draws our text
|
444 |
var cam : Camera = GameObject.Find("TextCamera").GetComponent(Camera); |
445 |
|
446 |
//make sure it has an assigned RenderTexture
|
447 |
if(cam.targetTexture != null) |
448 |
{
|
449 |
//pull the active RenderTexture
|
450 |
RenderTexture.active = cam.targetTexture; |
451 |
|
452 |
//capture the image into a Texture2D
|
453 |
var image : Texture2D = new Texture2D(cam.targetTexture.width, cam.targetTexture.height); |
454 |
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0); |
455 |
image.Apply(); |
456 |
|
457 |
//calculate how the text will be scaled when it is displayed as lightning on the screen
|
458 |
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x); |
459 |
|
460 |
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
|
461 |
positionText.x -= image.width * scaleText * .5f; |
462 |
positionText.y -= image.height * scaleText * .5f; |
463 |
|
464 |
//basically determines how many pixels we skip/check
|
465 |
var interval : int = 2; |
466 |
|
467 |
//loop through pixels
|
468 |
for(var y : int = 0; y < image.height; y += interval) |
469 |
{
|
470 |
for(var x : int = 0; x < image.width; x += interval) |
471 |
{
|
472 |
//get the color of the pixel
|
473 |
var color : Color = image.GetPixel(x,y); |
474 |
|
475 |
//if the color has an r (red) value
|
476 |
if(color.r > 0) |
477 |
{
|
478 |
//add it to our points for drawing
|
479 |
textPoints.Add(new Vector2(x,y)); |
480 |
}
|
481 |
}
|
482 |
}
|
483 |
}
|
484 |
}
|
485 |
}
|
Conclusión
El rayo es un gran efecto especial para arreglar 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 efectos de iluminación impresionantes! Descarga el código fuente y experimenta por tu cuenta.