效果展示:
在進行雷射攻擊的腳本編寫前,我們先進行一定程度的想象,思考雷射和普通的遠端攻擊有哪些不太一樣的地方。
正常的遠端攻擊例如子彈,箭矢,技能波等,都有明确的彈道,且無法同時命中多個敵人,隻要命中敵人後就會被銷毀。(特殊技能除外)
但雷射可以認為是一種持續性的範圍傷害,隻是它的範圍(長度)是不固定的,在雷射的發射階段,它會在第一個被命中的目标或障礙物處截斷。
雷射成型後,在它的生命周期内,可能會延長或被路徑上的障礙物截斷。當然,如果之前被命中的目标從雷射的光柱範圍内移開,這時雷射會自動延長至下一被命中的目标或障礙物位置。
雷射發射的過程如下:
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 }
複制