Unity LineRenderer 射线检测 激光攻击
效果展示:
在进行激光攻击的脚本编写前,我们先进行一定程度的想象,思考激光和普通的远程攻击有哪些不太一样的地方。
正常的远程攻击例如子弹,箭矢,技能波等,都有明确的弹道,且无法同时命中多个敌人,只要命中敌人后就会被销毁。(特殊技能除外)
但激光可以认为是一种持续性的范围伤害,只是它的范围(长度)是不固定的,在激光的发射阶段,它会在第一个被命中的目标或障碍物处截断。
激光成型后,在它的生命周期内,可能会延长或被路径上的障碍物截断。当然,如果之前被命中的目标从激光的光柱范围内移开,这时激光会自动延长至下一被命中的目标或障碍物位置。
激光发射的过程如下:
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 }