// // Lightning Bolt for Unity // (c) 2016 Digital Ruby, LLC // Source code may be used for personal or commercial projects. // Source code may NOT be redistributed or sold. // using UnityEngine; using System.Collections.Generic; namespace DigitalRuby.LightningBolt { /// /// Types of animations for lightning bolts /// public enum LightningBoltAnimationMode { /// /// No animation /// None, /// /// Pick a random frame /// Random, /// /// Loop through each frame and restart at the beginning /// Loop, /// /// Loop through each frame then go backwards to the beginning then forward, etc. /// PingPong } /// /// Allows creation of simple lightning bolts /// [RequireComponent(typeof(LineRenderer))] public class LightningBoltScript : MonoBehaviour { [Tooltip("The game object where the lightning will emit from. If null, StartPosition is used.")] public GameObject StartObject; [Tooltip("The start position where the lightning will emit from. This is in world space if StartObject is null, otherwise this is offset from StartObject position.")] public Vector3 StartPosition; [Tooltip("The game object where the lightning will end at. If null, EndPosition is used.")] public GameObject EndObject; [Tooltip("The end position where the lightning will end at. This is in world space if EndObject is null, otherwise this is offset from EndObject position.")] public Vector3 EndPosition; [Range(0, 8)] [Tooltip("How manu generations? Higher numbers create more line segments.")] public int Generations = 6; [Range(0.01f, 1.0f)] [Tooltip("How long each bolt should last before creating a new bolt. In ManualMode, the bolt will simply disappear after this amount of seconds.")] public float Duration = 0.05f; private float timer; [Range(0.0f, 1.0f)] [Tooltip("How chaotic should the lightning be? (0-1)")] public float ChaosFactor = 0.15f; [Tooltip("In manual mode, the trigger method must be called to create a bolt")] public bool ManualMode; [Range(1, 64)] [Tooltip("The number of rows in the texture. Used for animation.")] public int Rows = 1; [Range(1, 64)] [Tooltip("The number of columns in the texture. Used for animation.")] public int Columns = 1; [Tooltip("The animation mode for the lightning")] public LightningBoltAnimationMode AnimationMode = LightningBoltAnimationMode.PingPong; /// /// Assign your own random if you want to have the same lightning appearance /// [HideInInspector] [System.NonSerialized] public System.Random RandomGenerator = new System.Random(); private LineRenderer lineRenderer; private List> segments = new List>(); private int startIndex; private Vector2 size; private Vector2[] offsets; private int animationOffsetIndex; private int animationPingPongDirection = 1; private bool orthographic; private void GetPerpendicularVector(ref Vector3 directionNormalized, out Vector3 side) { if (directionNormalized == Vector3.zero) { side = Vector3.right; } else { // use cross product to find any perpendicular vector around directionNormalized: // 0 = x * px + y * py + z * pz // => pz = -(x * px + y * py) / z // for computational stability use the component farthest from 0 to divide by float x = directionNormalized.x; float y = directionNormalized.y; float z = directionNormalized.z; float px, py, pz; float ax = Mathf.Abs(x), ay = Mathf.Abs(y), az = Mathf.Abs(z); if (ax >= ay && ay >= az) { // x is the max, so we can pick (py, pz) arbitrarily at (1, 1): py = 1.0f; pz = 1.0f; px = -(y * py + z * pz) / x; } else if (ay >= az) { // y is the max, so we can pick (px, pz) arbitrarily at (1, 1): px = 1.0f; pz = 1.0f; py = -(x * px + z * pz) / y; } else { // z is the max, so we can pick (px, py) arbitrarily at (1, 1): px = 1.0f; py = 1.0f; pz = -(x * px + y * py) / z; } side = new Vector3(px, py, pz).normalized; } } private void GenerateLightningBolt(Vector3 start, Vector3 end, int generation, int totalGenerations, float offsetAmount) { if (generation < 0 || generation > 8) { return; } else if (orthographic) { start.z = end.z = Mathf.Min(start.z, end.z); } segments.Add(new KeyValuePair(start, end)); if (generation == 0) { return; } Vector3 randomVector; if (offsetAmount <= 0.0f) { offsetAmount = (end - start).magnitude * ChaosFactor; } while (generation-- > 0) { int previousStartIndex = startIndex; startIndex = segments.Count; for (int i = previousStartIndex; i < startIndex; i++) { start = segments[i].Key; end = segments[i].Value; // determine a new direction for the split Vector3 midPoint = (start + end) * 0.5f; // adjust the mid point to be the new location RandomVector(ref start, ref end, offsetAmount, out randomVector); midPoint += randomVector; // add two new segments segments.Add(new KeyValuePair(start, midPoint)); segments.Add(new KeyValuePair(midPoint, end)); } // halve the distance the lightning can deviate for each generation down offsetAmount *= 0.5f; } } public void RandomVector(ref Vector3 start, ref Vector3 end, float offsetAmount, out Vector3 result) { if (orthographic) { Vector3 directionNormalized = (end - start).normalized; Vector3 side = new Vector3(-directionNormalized.y, directionNormalized.x, directionNormalized.z); float distance = ((float)RandomGenerator.NextDouble() * offsetAmount * 2.0f) - offsetAmount; result = side * distance; } else { Vector3 directionNormalized = (end - start).normalized; Vector3 side; GetPerpendicularVector(ref directionNormalized, out side); // generate random distance float distance = (((float)RandomGenerator.NextDouble() + 0.1f) * offsetAmount); // get random rotation angle to rotate around the current direction float rotationAngle = ((float)RandomGenerator.NextDouble() * 360.0f); // rotate around the direction and then offset by the perpendicular vector result = Quaternion.AngleAxis(rotationAngle, directionNormalized) * side * distance; } } private void SelectOffsetFromAnimationMode() { int index; if (AnimationMode == LightningBoltAnimationMode.None) { lineRenderer.material.mainTextureOffset = offsets[0]; return; } else if (AnimationMode == LightningBoltAnimationMode.PingPong) { index = animationOffsetIndex; animationOffsetIndex += animationPingPongDirection; if (animationOffsetIndex >= offsets.Length) { animationOffsetIndex = offsets.Length - 2; animationPingPongDirection = -1; } else if (animationOffsetIndex < 0) { animationOffsetIndex = 1; animationPingPongDirection = 1; } } else if (AnimationMode == LightningBoltAnimationMode.Loop) { index = animationOffsetIndex++; if (animationOffsetIndex >= offsets.Length) { animationOffsetIndex = 0; } } else { index = RandomGenerator.Next(0, offsets.Length); } if (index >= 0 && index < offsets.Length) { lineRenderer.material.mainTextureOffset = offsets[index]; } else { lineRenderer.material.mainTextureOffset = offsets[0]; } } private void UpdateLineRenderer() { int segmentCount = (segments.Count - startIndex) + 1; lineRenderer.positionCount = segmentCount; if (segmentCount < 1) { return; } int index = 0; lineRenderer.SetPosition(index++, segments[startIndex].Key); for (int i = startIndex; i < segments.Count; i++) { lineRenderer.SetPosition(index++, segments[i].Value); } segments.Clear(); SelectOffsetFromAnimationMode(); } private void Start() { orthographic = (Camera.main != null && Camera.main.orthographic); lineRenderer = GetComponent(); lineRenderer.positionCount = 0; UpdateFromMaterialChange(); } private void Update() { orthographic = (Camera.main != null && Camera.main.orthographic); if (timer <= 0.0f) { if (ManualMode) { timer = Duration; lineRenderer.positionCount = 0; } else { Trigger(); } } timer -= Time.deltaTime; } /// /// Trigger a lightning bolt. Use this if ManualMode is true. /// public void Trigger() { Vector3 start, end; timer = Duration + Mathf.Min(0.0f, timer); if (StartObject == null) { start = StartPosition; } else { start = StartObject.transform.position + StartPosition; } if (EndObject == null) { end = EndPosition; } else { end = EndObject.transform.position + EndPosition; } startIndex = 0; GenerateLightningBolt(start, end, Generations, Generations, 0.0f); UpdateLineRenderer(); } /// /// Call this method if you change the material on the line renderer /// public void UpdateFromMaterialChange() { size = new Vector2(1.0f / (float)Columns, 1.0f / (float)Rows); lineRenderer.material.mainTextureScale = size; offsets = new Vector2[Rows * Columns]; for (int y = 0; y < Rows; y++) { for (int x = 0; x < Columns; x++) { offsets[x + (y * Columns)] = new Vector2((float)x / Columns, (float)y / Rows); } } } } }