在进行激光攻击的脚本编写前,我们先进行一定程度的想象,思考激光和普通的远程攻击有哪些不太一样的地方。
正常的远程攻击例如子弹,箭矢,技能波等,都有明确的弹道,且无法同时命中多个敌人,只要命中敌人后就会被销毁。(特殊技能除外)
但激光可以认为是一种持续性的范围伤害,只是它的范围(长度)是不固定的,在激光的发射阶段,它会在第一个被命中的目标或障碍物处截断。
激光成型后,一般来说,它将不再被运动的生命体目标截断,反而是依靠它已经生成的光柱将该目标弹开并造成伤害。
当然,如果之前被命中的目标从激光的光柱范围内移开,这时激光会自动延长至下一被命中的目标或障碍物位置。
激光一旦发出,在它的生命周期内,只延长不缩短,直至最后衰减消失。
激光发射的过程如下:
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(); 9 EleLightningInstance = ObjectPool.Instance.GetObj(EleLightningPerfab.gameObject, FirePos).GetComponent (); 10 //设置状态 11 State = EmissionRayState.On; 12 LifeSate = EmissionLifeSate.Creat; 13 //初始化属性 14 RayCurrentPos = FirePos.position; 15 LineRayInstance.GetComponent ().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(); 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 bHitObject = true; 21 } 22 23 if (Physics.Raycast(rayR, out hitR, RayLength)) 24 { 25 var hitDir = (-hitL.transform.forward).GetVector3XZ().normalized; 26 SendActorHit(hitR.transform.gameObject,hitDir); 27 bHitObject = true; 28 } 29 //如果两条射线都未有击中任何目标,则代表激光需要继续延长 30 if (!bHitObject) 31 ExtendLine(); 32 }
1 //延长激光 2 private void ExtendLine() 3 { 4 var dt = Time.deltaTime; 5 RayCurrentPos += FirePos.forward * FireSpeed * dt; 6 7 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 8 RaycastHit hit; 9 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 10 { 11 RayCurrentPos = hit.point; 12 SendActorHit(hit.transform.gameObject,FirePos.forward.GetVector3XZ().normalized); 13 RayLength = (RayCurrentPos - FirePos.position).magnitude; 14 CreatKeepEleLightning(); 15 } 16 //更新当前帧终点位置,延长不用再设置起点位置 17 LineRayInstance.SetPosition(1, RayCurrentPos); 18 }
激光衰减阶段:
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 EmissionRayState 4 { 5 Off, 6 On 7 } 8 9 public enum EmissionLifeSate 10 { 11 None, 12 //创建阶段 13 Creat, 14 //生命周期阶段 15 Keep, 16 //衰减阶段 17 Attenuate 18 } 19 20 public class EmissionRayCtrl : MonoBehaviour 21 { 22 public LineRenderer LineRayPrefab; 23 public LineRenderer EleLightningPerfab; 24 25 private LineRenderer LineRayInstance; 26 private LineRenderer EleLightningInstance; 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 ObjectType TargetDamageType = ObjectType.Player; 52 53 private EmissionRayState State; 54 private EmissionLifeSate LifeSate; 55 56 private Vector3 RayCurrentPos; 57 private float RayOriginWidth; 58 private float RayCurrentWidth; 59 private float LifeTimer; 60 private float RayLength; 61 void Start() 62 { 63 State = EmissionRayState.Off; 64 LifeSate = EmissionLifeSate.None; 65 } 66 67 public void FireBegin() 68 { 69 switch (State) 70 { 71 //只有在状态关闭时才可以开启激光 72 case EmissionRayState.Off: 73 //实例化激光组件 74 LineRayInstance = ObjectPool.Instance.GetObj(LineRayPrefab.gameObject, FirePos).GetComponent(); 75 EleLightningInstance = ObjectPool.Instance.GetObj(EleLightningPerfab.gameObject, FirePos).GetComponent (); 76 //设置状态 77 State = EmissionRayState.On; 78 LifeSate = EmissionLifeSate.Creat; 79 //初始化属性 80 RayCurrentPos = FirePos.position; 81 LineRayInstance.GetComponent ().Damage = Damage; 82 LineRayInstance.positionCount = 2; 83 RayOriginWidth = LineRayInstance.startWidth; 84 LineRayInstance.material.SetColor("_Color", EmissionColor); 85 EleLightningInstance.material.SetColor("_Color", EleLightColor); 86 break; 87 } 88 } 89 90 void Update() 91 { 92 switch (State) 93 { 94 case EmissionRayState.On: 95 switch (LifeSate) 96 { 97 case EmissionLifeSate.Creat: 98 ShootLine(); 99 break; 100 case EmissionLifeSate.Keep: 101 ExtendLineWidth(); 102 break; 103 case EmissionLifeSate.Attenuate: 104 CutDownRayLine(); 105 break; 106 } 107 break; 108 } 109 } 110 111 //生成射线 112 private void ShootLine() 113 { 114 //设置激光起点 115 LineRayInstance.SetPosition(0, FirePos.position); 116 var dt = Time.deltaTime; 117 118 //激光的终点按发射速度进行延伸 119 RayCurrentPos += FirePos.forward * FireSpeed * dt; 120 121 //在激光运动过程中创建短射线用来检测碰撞 122 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 123 RaycastHit hit; 124 //射线长度稍大于一帧的运动距离,保证不会因为运动过快而丢失 125 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 126 { 127 RayCurrentPos = hit.point; 128 //向命中物体发送被击信号,被击方向为激光发射方向 129 SendActorHit(hit.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 130 131 //激光接触到目标后自动切换至下一生命周期状态 132 LifeSate = EmissionLifeSate.Keep; 133 //保存当前激光的长度 134 RayLength = (RayCurrentPos - FirePos.position).magnitude; 135 136 RayCurrentWidth = RayOriginWidth; 137 //创建激光周围电光 138 CreatKeepEleLightning(); 139 //开始计算生命周期 140 LifeTimer = 0f; 141 } 142 //设置当前帧终点位置 143 LineRayInstance.SetPosition(1, RayCurrentPos); 144 } 145 146 //发送受击信号 147 private void SendActorHit(GameObject HitObject, Vector2 dir) 148 { 149 //判断激光击中目标是否是指定的目标类型 150 if (HitObject.GetTagType() == TargetDamageType) 151 { 152 var actor = HitObject.GetComponent (); 153 if (actor != null) 154 { 155 actor.OnHit(LineRayInstance.gameObject); 156 actor.OnHitReAction(LineRayInstance.gameObject, dir); 157 } 158 } 159 } 160 161 private void CheckRayHit() 162 { 163 var offse = (RayCurrentWidth + EleLightOffse) * .5f; 164 //向量运算出左右的起始位置 165 var startL = FirePos.position - FirePos.right * offse; 166 var startR = FirePos.position + FirePos.right * offse; 167 //创建基于当前激光宽度的左右两条检测射线 168 Ray rayL = new Ray(startL, FirePos.forward); 169 Ray rayR = new Ray(startR, FirePos.forward); 170 RaycastHit hitL; 171 RaycastHit hitR; 172 173 bool bHitObject = false; 174 //按当前激光长度检测,若没有碰到任何物体,则延长激光 175 if (Physics.Raycast(rayL, out hitL, RayLength)) 176 { 177 //左右击中目标是击中方向为该角色运动前向的反方向 178 var hitDir = (-hitL.transform.forward).GetVector3XZ().normalized; 179 SendActorHit(hitL.transform.gameObject, hitDir); 180 bHitObject = true; 181 } 182 183 if (Physics.Raycast(rayR, out hitR, RayLength)) 184 { 185 var hitDir = (-hitL.transform.forward).GetVector3XZ().normalized; 186 SendActorHit(hitR.transform.gameObject, hitDir); 187 bHitObject = true; 188 } 189 //如果两条射线都未有击中任何目标,则代表激光需要继续延长 190 if (!bHitObject) 191 ExtendLine(); 192 } 193 194 //延长激光 195 private void ExtendLine() 196 { 197 var dt = Time.deltaTime; 198 RayCurrentPos += FirePos.forward * FireSpeed * dt; 199 200 Ray ray = new Ray(RayCurrentPos, FirePos.forward); 201 RaycastHit hit; 202 if (Physics.Raycast(ray, out hit, 1.2f * dt * FireSpeed)) 203 { 204 RayCurrentPos = hit.point; 205 SendActorHit(hit.transform.gameObject, FirePos.forward.GetVector3XZ().normalized); 206 RayLength = (RayCurrentPos - FirePos.position).magnitude; 207 CreatKeepEleLightning(); 208 } 209 //更新当前帧终点位置,延长不用再设置起点位置 210 LineRayInstance.SetPosition(1, RayCurrentPos); 211 } 212 213 private void ExtendLineWidth() 214 { 215 //每帧检测射线碰撞 216 CheckRayHit(); 217 var dt = Time.deltaTime; 218 //按速度扩展宽度直到最大宽度 219 if (RayCurrentWidth < MaxRayWidth) 220 { 221 RayCurrentWidth += dt * WidthExtendSpeed; 222 LineRayInstance.startWidth = RayCurrentWidth; 223 LineRayInstance.endWidth = RayCurrentWidth; 224 } 225 //生命周期结束后切换为衰减状态 226 LifeTimer += dt; 227 if (LifeTimer > LifeTime) 228 { 229 LifeSate = EmissionLifeSate.Attenuate; 230 } 231 } 232 233 //生成电光 234 private void CreatKeepEleLightning() 235 { 236 var EleLightCount = (int)(RayLength / EachEleLightDistance); 237 EleLightningInstance.positionCount = EleLightCount; 238 for (int i = 0; i < EleLightCount; i++) 239 { 240 //计算偏移值 241 var offse = RayCurrentWidth * .5f + EleLightOffse; 242 //计算未偏移时每个电光的线段中轴位置 243 var eleo = FirePos.position + (RayCurrentPos - FirePos.position) * (i + 1) / EleLightCount; 244 //在射线的左右间隔分布,按向量运算进行偏移 245 var pos = i % 2 == 0 ? eleo - offse * FirePos.right : eleo + offse * FirePos.right; 246 EleLightningInstance.SetPosition(i, pos); 247 } 248 } 249 250 private void CutDownRayLine() 251 { 252 var dt = Time.deltaTime; 253 //宽度衰减为零后意味着整个激光关闭完成 254 if (RayCurrentWidth > 0) 255 { 256 RayCurrentWidth -= dt * FadeOutSpeed; 257 LineRayInstance.startWidth = RayCurrentWidth; 258 LineRayInstance.endWidth = RayCurrentWidth; 259 } 260 else 261 FireShut(); 262 } 263 264 public void FireShut() 265 { 266 switch (State) 267 { 268 case EmissionRayState.On: 269 EleLightningInstance.positionCount = 0; 270 LineRayInstance.positionCount = 0; 271 LineRayInstance.startWidth = RayOriginWidth; 272 LineRayInstance.endWidth = RayOriginWidth; 273 //回收实例化个体 274 ObjectPool.Instance.RecycleObj(LineRayInstance.gameObject); 275 ObjectPool.Instance.RecycleObj(EleLightningInstance.gameObject); 276 State = EmissionRayState.Off; 277 //发送射线已关闭的事件 278 EventManager.QueueEvent(new EmissionShutEvent(gameObject)); 279 break; 280 } 281 } 282 }