效果展示:
在進行激光攻擊的腳本編寫前,我們先進行一定程度的想象,思考激光和普通的遠程攻擊有哪些不太一樣的地方。
正常的遠程攻擊例如子彈,箭矢,技能波等,都有明確的彈道,且無法同時命中多個敵人,只要命中敵人后就會被銷毀。(特殊技能除外)
但激光可以認為是一種持續性的范圍傷害,只是它的范圍(長度)是不固定的,在激光的發射階段,它會在第一個被命中的目標或障礙物處截斷。
激光成型后,在它的生命周期內,可能會延長或被路徑上的障礙物截斷。當然,如果之前被命中的目標從激光的光柱范圍內移開,這時激光會自動延長至下一被命中的目標或障礙物位置。
激光發射的過程如下:
1.從起始的發射點射出一條不斷向前運動的射線,到達目標點的速度非常快,一般肉眼很難捕捉。直到遇到障礙物截斷,不然持續向前延伸。
2.激光一開始是以極小的寬度開始擴散它的能量,它的寬度在發射過程中是由細到寬最終到達極限寬度的。而不是恆定不變的。
3.激光由於快速運動勢必會與空氣產生摩擦,一部分電光會在激光運動的軌跡周圍閃現。
4.激光有生命周期,也可以是停止持續供能后衰減。但激光衰減的過程中長度不會發生變化,而是通過類似於能量迅速收束的方式使整個光柱逐漸變細直至消失,周圍的電光也在此衰減過程中逐漸消失。
上面想象模擬了一束激光從生成到凋亡的整個過程,基於此,先定義幾種狀態:
1 public enum EmissionRayState 2 { 3 Off, 4 On 5 } 6 7 public enum EmissionLifeSate 8 { 9 None, 10 //創建階段 11 Creat, 12 //生命周期階段 13 Keep, 14 //衰減階段 15 Attenuate 16 }
主循環的狀態切換:
1 void Update() 2 { 3 switch (State) 4 { 5 case EmissionRayState.On: 6 switch (LifeSate) 7 { 8 case EmissionLifeSate.Creat: 9 ShootLine(); 10 break; 11 case EmissionLifeSate.Keep: 12 ExtendLineWidth(); 13 break; 14 case EmissionLifeSate.Attenuate: 15 CutDownRayLine(); 16 break; 17 } 18 break; 19 } 20 }
屬性列表:
1 //發射位置 2 public Transform FirePos; 3 //激光顏色 4 public Color EmissionColor = Color.blue; 5 //電光顏色 6 public Color EleLightColor = Color.blue; 7 //發射速度 8 public float FireSpeed = 30f; 9 //生命周期 10 public float LifeTime = .3f; 11 //最大到達寬度 12 public float MaxRayWidth = .1f; 13 //寬度擴展速度 14 public float WidthExtendSpeed = .5f; 15 //漸隱速度 16 public float FadeOutSpeed = 1f; 17 //單位電光的距離 18 public float EachEleLightDistance = 2f; 19 //電光左右偏移值 20 public float EleLightOffse = .5f; 21 //擊中傷害 22 public int Damage = 121; 23 //接收傷害角色類型 24 public ObjectType TargetDamageType = ObjectType.Player;
每次發射激光時創建一個附帶LineRenderer組件的物體,在發射前對其中的一些屬性賦值:
1 public void FireBegin() 2 { 3 switch (State) 4 { 5 //只有在狀態關閉時才可以開啟激光 6 case EmissionRayState.Off: 7 //實例化激光組件 8 LineRayInstance = ObjectPool.Instance.GetObj(LineRayPrefab.gameObject, FirePos).GetComponent<LineRenderer>(); 9 EleLightningInstance = ObjectPool.Instance.GetObj(EleLightningPerfab.gameObject, FirePos).GetComponent<LineRenderer>(); 10 //設置狀態 11 State = EmissionRayState.On; 12 LifeSate = EmissionLifeSate.Creat; 13 //初始化屬性 14 RayCurrentPos = FirePos.position; 15 LineRayInstance.GetComponent<EmissionRay>().Damage = Damage; 16 LineRayInstance.positionCount = 2; 17 RayOriginWidth = LineRayInstance.startWidth; 18 LineRayInstance.material.SetColor("_Color", EmissionColor); 19 EleLightningInstance.material.SetColor("_Color", EleLightColor); 20 break; 21 } 22 }
該方法外部調用后將自動切換到激光的生命周期循環,其中用到的對象池可詳見:
https://www.cnblogs.com/koshio0219/p/11572567.html
生成射線階段:
1 //生成射線 2 private void ShootLine() 3 { 4 //設置激光起點 5 LineRayInstance.SetPosition(0, FirePos.position); 6 var dt = Time.deltaTime; 7 8 //激光的終點按發射速度進行延伸 9 RayCurrentPos += FirePos.forward * FireSpeed * dt; 10 11 //在激光運動過程中創建短射線用來檢測碰撞 12 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 13 RaycastHit hit; 14 //射線長度稍大於一幀的運動距離,保證不會因為運動過快而丟失 15 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 16 { 17 RayCurrentPos = hit.point; 18 //向命中物體發送被擊信號,被擊方向為激光發射方向 19 SendActorHit(hit.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 20 21 //激光接觸到目標后自動切換至下一生命周期狀態 22 LifeSate = EmissionLifeSate.Keep; 23 //保存當前激光的長度 24 RayLength = (RayCurrentPos - FirePos.position).magnitude; 25 26 RayCurrentWidth = RayOriginWidth; 27 //創建激光周圍電光 28 CreatKeepEleLightning(); 29 //開始計算生命周期 30 LifeTimer = 0f; 31 } 32 //設置當前幀終點位置 33 LineRayInstance.SetPosition(1, RayCurrentPos); 34 }
1 //發送受擊信號 2 private void SendActorHit(GameObject HitObject,Vector2 dir) 3 { 4 //判斷激光擊中目標是否是指定的目標類型 5 if (HitObject.GetTagType() == TargetDamageType) 6 { 7 var actor = HitObject.GetComponent<Actor>(); 8 if (actor != null) 9 { 10 actor.OnHit(LineRayInstance.gameObject); 11 actor.OnHitReAction(LineRayInstance.gameObject, dir); 12 } 13 } 14 }
這里寫了一個GameObject的擴展方法,將物體的標簽轉為自定義的枚舉類型,以防在代碼中或編輯器中經常要輸入標簽的字符串,很是繁瑣:
1 public static ObjectType GetTagType(this GameObject gameObject) 2 { 3 switch (gameObject.tag) 4 { 5 case "Player": 6 return ObjectType.Player; 7 case "Enemy": 8 return ObjectType.Enemy; 9 case "Bullets": 10 return ObjectType.Bullet; 11 case "Emission": 12 return ObjectType.Emission; 13 case "Collider": 14 return ObjectType.Collider; 15 default: 16 return ObjectType.Undefined; 17 } 18 }
1 public enum ObjectType 2 { 3 Player, 4 Enemy, 5 Bullet, 6 Emission, 7 Collider, 8 Undefined 9 }
創建激光周圍的電光:
1 private void CreatKeepEleLightning() 2 { 3 var EleLightCount = (int)(RayLength / EachEleLightDistance); 4 EleLightningInstance.positionCount = EleLightCount; 5 for (int i = 0; i < EleLightCount; i++) 6 { 7 //計算偏移值 8 var offse = RayCurrentWidth *.5f + EleLightOffse; 9 //計算未偏移時的線段中軸位置 10 var eleo = FirePos.position + (RayCurrentPos - FirePos.position) * (i + 1) / EleLightCount; 11 //在射線的左右間隔分布,按向量運算進行偏移 12 var pos = i % 2 == 0 ? eleo - offse * FirePos.right : eleo + offse * FirePos.right; 13 EleLightningInstance.SetPosition(i, pos); 14 } 15 }
注意本例中不用任何碰撞體來檢測碰撞,而是單純用射線檢測。
真實生命周期階段:
1 private void ExtendLineWidth() 2 { 3 //每幀檢測射線碰撞 4 CheckRayHit(); 5 var dt = Time.deltaTime; 6 //按速度擴展寬度直到最大寬度 7 if (RayCurrentWidth < MaxRayWidth) 8 { 9 RayCurrentWidth += dt * WidthExtendSpeed; 10 LineRayInstance.startWidth = RayCurrentWidth; 11 LineRayInstance.endWidth = RayCurrentWidth; 12 } 13 //生命周期結束后切換為衰減狀態 14 LifeTimer += dt; 15 if (LifeTimer > LifeTime) 16 { 17 LifeSate = EmissionLifeSate.Attenuate; 18 } 19 }
在真實生命周期階段需要每幀檢測激光的射線范圍內是否有目標靠近,激光是否因為阻礙物而需要延長或截斷等:
1 private void CheckRayHit() 2 { 3 var offse = (RayCurrentWidth + EleLightOffse) * .5f; 4 //向量運算出左右的起始位置 5 var startL = FirePos.position - FirePos.right * offse; 6 var startR = FirePos.position + FirePos.right * offse; 7 //創建基於當前激光寬度的左右兩條檢測射線 8 Ray rayL = new Ray(startL, FirePos.forward); 9 Ray rayR = new Ray(startR, FirePos.forward); 10 RaycastHit hitL; 11 RaycastHit hitR; 12 13 //bool bHitObject = false; 14 //按當前激光長度檢測,若沒有碰到任何物體,則延長激光 15 if (Physics.Raycast(rayL, out hitL, RayLength)) 16 { 17 //左右擊中目標是擊中方向為該角色運動前向的反方向 18 var hitDir = (-hitL.transform.forward).GetVector3XZ().normalized; 19 SendActorHit(hitL.transform.gameObject, hitDir); 20 } 21 22 if (Physics.Raycast(rayR, out hitR, RayLength)) 23 { 24 var hitDir = (-hitR.transform.forward).GetVector3XZ().normalized; 25 SendActorHit(hitR.transform.gameObject, hitDir); 26 } 27 ChangeLine(); 28 }
1 private void ChangeLine() 2 { 3 RaycastHit info; 4 if (Physics.Raycast(new Ray(FirePos.position, FirePos.forward), out info)) 5 { 6 RayCurrentPos = info.point; 7 SendActorHit(info.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 8 RayLength = (RayCurrentPos - FirePos.position).magnitude; 9 LineRayInstance.SetPosition(1, RayCurrentPos); 10 CreatKeepEleLightning(); 11 } 12 }
激光衰減階段:
1 private void CutDownRayLine() 2 { 3 var dt = Time.deltaTime; 4 //寬度衰減為零后意味着整個激光關閉完成 5 if (RayCurrentWidth > 0) 6 { 7 RayCurrentWidth -= dt * FadeOutSpeed; 8 LineRayInstance.startWidth = RayCurrentWidth; 9 LineRayInstance.endWidth = RayCurrentWidth; 10 } 11 else 12 FireShut(); 13 }
關閉激光並還原設置:
1 public void FireShut() 2 { 3 switch (State) 4 { 5 case EmissionRayState.On: 6 EleLightningInstance.positionCount = 0; 7 LineRayInstance.positionCount = 0; 8 LineRayInstance.startWidth = RayOriginWidth; 9 LineRayInstance.endWidth = RayOriginWidth; 10 //回收實例化個體 11 ObjectPool.Instance.RecycleObj(LineRayInstance.gameObject); 12 ObjectPool.Instance.RecycleObj(EleLightningInstance.gameObject); 13 State = EmissionRayState.Off; 14 //發送當前物體激光已關閉的事件 15 EventManager.QueueEvent(new EmissionShutEvent(gameObject)); 16 break; 17 } 18 }
這里用到的事件系統可以詳見:
https://www.cnblogs.com/koshio0219/p/11209191.html
完整腳本:

1 using UnityEngine; 2 3 public enum EmissionLifeSate 4 { 5 None, 6 //創建階段 7 Creat, 8 //生命周期階段 9 Keep, 10 //衰減階段 11 Attenuate 12 } 13 14 public class EmissionRayCtrl : FireBase 15 { 16 public LineRenderer LineRayPrefab; 17 public LineRenderer EleLightningPerfab; 18 19 private LineRenderer LineRayInstance; 20 private LineRenderer EleLightningInstance; 21 22 public GameObject FirePrefab; 23 public GameObject HitPrefab; 24 25 private GameObject FireInstance; 26 private GameObject HitInstance; 27 28 //發射位置 29 public Transform FirePos; 30 //激光顏色 31 public Color EmissionColor = Color.blue; 32 //電光顏色 33 public Color EleLightColor = Color.blue; 34 //發射速度 35 public float FireSpeed = 30f; 36 //生命周期 37 public float LifeTime = .3f; 38 //最大到達寬度 39 public float MaxRayWidth = .1f; 40 //寬度擴展速度 41 public float WidthExtendSpeed = .5f; 42 //漸隱速度 43 public float FadeOutSpeed = 1f; 44 //單位電光的距離 45 public float EachEleLightDistance = 2f; 46 //電光左右偏移值 47 public float EleLightOffse = .5f; 48 //擊中傷害 49 public int Damage = 121; 50 //傷害結算間隔 51 public float DamageCD = .1f; 52 //冷卻時間 53 public float CD = 0f; 54 //接收傷害角色類型 55 public ObjectType TargetDamageType = ObjectType.Player; 56 57 public bool bHaveEleLight = false; 58 59 private FireState State; 60 private EmissionLifeSate LifeSate; 61 62 private Vector3 RayCurrentPos; 63 private float RayOriginWidth; 64 private float RayCurrentWidth; 65 private float LifeTimer; 66 private float CDTimer; 67 private float DamageCDTimer; 68 private float RayLength; 69 70 void Start() 71 { 72 State = FireState.Off; 73 LifeSate = EmissionLifeSate.None; 74 CDTimer = 0f; 75 DamageCDTimer = 0f; 76 } 77 78 public override void FireBegin() 79 { 80 switch (State) 81 { 82 //只有在狀態關閉時才可以開啟激光 83 case FireState.Off: 84 if (CDTimer <= 0) 85 { 86 //實例化激光組件 87 LineRayInstance = ObjectPool.Instance.GetObj(LineRayPrefab.gameObject, FirePos).GetComponent<LineRenderer>(); 88 EleLightningInstance = ObjectPool.Instance.GetObj(EleLightningPerfab.gameObject, FirePos).GetComponent<LineRenderer>(); 89 FireInstance = ObjectPool.Instance.GetObj(FirePrefab, FirePos); 90 HitInstance = ObjectPool.Instance.GetObj(HitPrefab, FirePos); 91 //設置狀態 92 State = FireState.On; 93 LifeSate = EmissionLifeSate.Creat; 94 HitInstance.SetActive(false); 95 //初始化屬性 96 RayCurrentPos = FirePos.position; 97 LineRayInstance.GetComponent<EmissionRay>().Damage = Damage; 98 LineRayInstance.positionCount = 2; 99 RayOriginWidth = LineRayInstance.startWidth; 100 LineRayInstance.material.SetColor("_Color", EmissionColor); 101 EleLightningInstance.material.SetColor("_Color", EleLightColor); 102 CDTimer = CD; 103 } 104 break; 105 } 106 } 107 108 void FixedUpdate() 109 { 110 switch (State) 111 { 112 case FireState.On: 113 switch (LifeSate) 114 { 115 case EmissionLifeSate.Creat: 116 ShootLine(); 117 break; 118 case EmissionLifeSate.Keep: 119 ExtendLineWidth(); 120 break; 121 case EmissionLifeSate.Attenuate: 122 CutDownRayLine(); 123 break; 124 } 125 break; 126 case FireState.Off: 127 CDTimer -= Time.fixedDeltaTime; 128 break; 129 } 130 } 131 132 //生成射線 133 private void ShootLine() 134 { 135 //設置激光起點 136 LineRayInstance.SetPosition(0, FirePos.position); 137 var dt = Time.fixedDeltaTime; 138 139 //激光的終點按發射速度進行延伸 140 RayCurrentPos += FirePos.forward * FireSpeed * dt; 141 142 //在激光運動過程中創建短射線用來檢測碰撞 143 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 144 RaycastHit hit; 145 //射線長度稍大於一幀的運動距離,保證不會因為運動過快而丟失 146 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 147 { 148 RayCurrentPos = hit.point; 149 //向命中物體發送被擊信號,被擊方向為激光發射方向 150 SendActorHit(hit.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 151 152 //激光接觸到目標后自動切換至下一生命周期狀態 153 LifeSate = EmissionLifeSate.Keep; 154 //保存當前激光的長度 155 RayLength = (RayCurrentPos - FirePos.position).magnitude; 156 157 RayCurrentWidth = RayOriginWidth; 158 HitInstance.SetActive(true); 159 //開始計算生命周期 160 LifeTimer = 0f; 161 } 162 //設置當前幀終點位置 163 LineRayInstance.SetPosition(1, RayCurrentPos); 164 } 165 166 //發送受擊信號 167 private void SendActorHit(GameObject HitObject, Vector2 dir) 168 { 169 //判斷激光擊中目標是否是指定的目標類型 170 if (HitObject.GetTagType() == TargetDamageType) 171 { 172 var actor = HitObject.GetComponent<Actor>(); 173 if (actor != null) 174 { 175 if (DamageCDTimer <= 0) 176 { 177 actor.OnHit(LineRayInstance.gameObject); 178 actor.OnHitReAction(LineRayInstance.gameObject, dir); 179 DamageCDTimer = DamageCD; 180 } 181 DamageCDTimer -= Time.deltaTime; 182 } 183 } 184 } 185 186 private void CheckRayHit() 187 { 188 var offse = (RayCurrentWidth + EleLightOffse) * .5f; 189 //向量運算出左右的起始位置 190 var startL = FirePos.position - FirePos.right * offse; 191 var startR = FirePos.position + FirePos.right * offse; 192 //創建基於當前激光寬度的左右兩條檢測射線 193 Ray rayL = new Ray(startL, FirePos.forward); 194 Ray rayR = new Ray(startR, FirePos.forward); 195 RaycastHit hitL; 196 RaycastHit hitR; 197 198 //bool bHitObject = false; 199 //按當前激光長度檢測,若沒有碰到任何物體,則延長激光 200 if (Physics.Raycast(rayL, out hitL, RayLength)) 201 { 202 //左右擊中目標是擊中方向為該角色運動前向的反方向 203 var hitDir = (-hitL.transform.forward).GetVector3XZ().normalized; 204 SendActorHit(hitL.transform.gameObject, hitDir); 205 } 206 207 if (Physics.Raycast(rayR, out hitR, RayLength)) 208 { 209 var hitDir = (-hitR.transform.forward).GetVector3XZ().normalized; 210 SendActorHit(hitR.transform.gameObject, hitDir); 211 } 212 ChangeLine(); 213 } 214 215 private void ChangeLine() 216 { 217 RaycastHit info; 218 if (Physics.Raycast(new Ray(FirePos.position, FirePos.forward), out info)) 219 { 220 RayCurrentPos = info.point; 221 SendActorHit(info.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 222 RayLength = (RayCurrentPos - FirePos.position).magnitude; 223 LineRayInstance.SetPosition(1, RayCurrentPos); 224 CreatKeepEleLightning(); 225 } 226 } 227 228 //延長激光 229 private void ExtendLine() 230 { 231 var dt = Time.fixedDeltaTime; 232 RayCurrentPos += FirePos.forward * FireSpeed * dt; 233 234 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 235 RaycastHit hit; 236 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 237 { 238 RayCurrentPos = hit.point; 239 SendActorHit(hit.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 240 RayLength = (RayCurrentPos - FirePos.position).magnitude; 241 CreatKeepEleLightning(); 242 } 243 //更新當前幀終點位置,延長不用再設置起點位置 244 LineRayInstance.SetPosition(1, RayCurrentPos); 245 } 246 247 private void ExtendLineWidth() 248 { 249 var dt = Time.fixedDeltaTime; 250 //按速度擴展寬度直到最大寬度 251 if (RayCurrentWidth < MaxRayWidth) 252 { 253 RayCurrentWidth += dt * WidthExtendSpeed; 254 LineRayInstance.startWidth = RayCurrentWidth; 255 LineRayInstance.endWidth = RayCurrentWidth; 256 } 257 //每幀檢測射線碰撞 258 CheckRayHit(); 259 //生命周期結束后切換為衰減狀態 260 LifeTimer += dt; 261 if (LifeTimer > LifeTime) 262 { 263 LifeSate = EmissionLifeSate.Attenuate; 264 } 265 ReBuildLine(); 266 } 267 268 //刷新激光位置,用於動態旋轉的發射源 269 private void ReBuildLine() 270 { 271 LineRayInstance.SetPosition(0, FirePos.position); 272 LineRayInstance.SetPosition(1, FirePos.position + FirePos.forward * RayLength); 273 HitInstance.transform.position = FirePos.position + FirePos.forward * RayLength; 274 CreatKeepEleLightning(); 275 } 276 277 //生成電光 278 private void CreatKeepEleLightning() 279 { 280 if (bHaveEleLight) 281 { 282 var EleLightCount = (int)(RayLength / EachEleLightDistance); 283 EleLightningInstance.positionCount = EleLightCount; 284 for (int i = 0; i < EleLightCount; i++) 285 { 286 //計算偏移值 287 var offse = RayCurrentWidth * .5f + EleLightOffse; 288 //計算未偏移時每個電光的線段中軸位置 289 var eleo = FirePos.position + (RayCurrentPos - FirePos.position) * (i + 1) / EleLightCount; 290 //在射線的左右間隔分布,按向量運算進行偏移 291 var pos = i % 2 == 0 ? eleo - offse * FirePos.right : eleo + offse * FirePos.right; 292 EleLightningInstance.SetPosition(i, pos); 293 } 294 } 295 } 296 297 private void CutDownRayLine() 298 { 299 ReBuildLine(); 300 var dt = Time.fixedDeltaTime; 301 //寬度衰減為零后意味着整個激光關閉完成 302 if (RayCurrentWidth > 0) 303 { 304 RayCurrentWidth -= dt * FadeOutSpeed; 305 LineRayInstance.startWidth = RayCurrentWidth; 306 LineRayInstance.endWidth = RayCurrentWidth; 307 } 308 else 309 FireShut(); 310 } 311 312 public override void FireShut() 313 { 314 switch (State) 315 { 316 case FireState.On: 317 EleLightningInstance.positionCount = 0; 318 LineRayInstance.positionCount = 0; 319 LineRayInstance.startWidth = RayOriginWidth; 320 LineRayInstance.endWidth = RayOriginWidth; 321 //回收實例化個體 322 ObjectPool.Instance.RecycleObj(LineRayInstance.gameObject); 323 ObjectPool.Instance.RecycleObj(EleLightningInstance.gameObject); 324 ObjectPool.Instance.RecycleObj(FireInstance); 325 ObjectPool.Instance.RecycleObj(HitInstance); 326 State = FireState.Off; 327 //發送射線已關閉的事件 328 EventManager.QueueEvent(new EmissionShutEvent(gameObject)); 329 break; 330 } 331 } 332 333 public override void SetDamage(int damage) 334 { 335 Damage = damage; 336 } 337 338 public override void SetFirePos(Transform pos) 339 { 340 FirePos = pos; 341 } 342 343 public override void SetCD(float cd) 344 { 345 CD = cd; 346 } 347 348 public override string GetAniName() 349 { 350 return "ANI_Aim_01"; 351 } 352 353 public override FireState GetFireState() 354 { 355 return State; 356 } 357 }