子彈系統和粒子系統比較類似,為了創建出五花八門的子彈,例如追蹤,連續繼承,散彈等,需要一個擁有眾多參數的子彈生成器,這里叫它Shooter好了。
Shooter負責把玩各類子彈造型和參數,創建出子彈,創建完了之后接下來就交給子彈自己來管理自己了。
所以,一個子彈系統包含:
1.ShooterSystem類
一個能生成各種類型子彈的發射器。
2.Bullet類
按照給定的初始參數不斷向前飛行的子彈個體。
先思考每一個單獨的子彈需要有哪些物理參數:
1 //目標 2 public GameObject Target { get; set; } 3 //瞬時速度 4 public float Velocity { get; set; } 5 //剩余生命周期 6 public float LifeTime { get; set; } 7 //角速度 8 public float Palstance { get; set; } 9 //線性加速度 10 public float Acceleration { get; set; } 11 //最大速度 12 public float MaxVelocity { get; set; }
這些參數不需要子彈自己來配置,而是交給把玩它們的Shooter來進行,但是子彈自身需要知道這些參數。
其中指得一提的是角速度,正常的子彈是沒有追蹤功能的,生成之后就只能自動向前飛,但一旦設置了子彈的目標后,子彈就必須根據角速度轉向目標位置的向量,保證自己的前向能盡快和目標向量對齊;而這一對齊的過程,就需要用角速度來描述。
子彈在生命周期到了之后要自動銷毀,因為它經常反復創建和銷毀,最好使用對象池來進行這一過程:
https://www.cnblogs.com/koshio0219/p/11572567.html
調用如下:
1 public IEnumerator AutoRecycle() 2 { 3 yield return new WaitForSeconds(LifeTime); 4 ObjectPool.Instance.RecycleObj(gameObject); 5 }
子彈每一幀的狀態都會有所變化,例如位置,速度的更新,向前運行的方向的更新等:
1 private void Update() 2 { 3 float deltaTime = Time.deltaTime; 4 //由當前子彈位置指向目標位置的向量,記為瞬時偏移向量 5 Vector3 offset = (Target.transform.position - transform.position).normalized; 6 //子彈的當前前進方向與瞬時偏移向量之間的夾角 7 float angle = Vector3.Angle(transform.forward, offset); 8 //夾角除以角速度計算需要轉到相同方向所需要的總時間 9 float needTime = angle*1.0f / Palstance; 10 //插值運算出當前幀的前向方向向量,也即是需要偏移的角度 11 transform.forward = Vector3.Lerp(transform.forward, offset, deltaTime / needTime).normalized; 12 //處理線性加速度對於速度的增量 13 if (Velocity < MaxVelocity) 14 { 15 Velocity += deltaTime * Acceleration; 16 } 17 //按當前速度向前移動一幀的距離,賦值給當前位置 18 transform.position += transform.forward * Velocity * deltaTime; 19 }
如果不想讓子彈追蹤,也很簡單,把角速度傳為0即可,float除數為0也是沒有問題的。
子彈生成器主要是創建子彈,所以需要包含子彈類的所有參數,除此之外,還需要有一些其他的參數:
1 public bool bAuto = false; 2 3 public GameObject bulletPrefab; 4 //子彈目標 5 public GameObject target; 6 //初速度 7 public float velocity = 0f; 8 //加速度 9 public float acceleration = 30f; 10 //總生命周期 11 public float lifeTime = 3f; 12 //初始方向 13 public Vector2 direction = Vector2.zero; 14 //最大速度 15 public float maxVelocity = 600; 16 //角速度 17 public float palstance = 120; 18 //角度波動范圍 19 public float angelRange = 0f; 20 //延遲 21 public float delay = 1f; 22 //是否循環 23 public bool bLoop = false; 24 //時間間隔 25 public float timeCell = .1f; 26 //生成數量 27 public int count = 1; 28 //傷害 29 public float damage; 30 //碰撞類型 31 public CollisionType collisionType; 32 //是否有子系統 33 public bool bChildShooter = false; 34 //子系統是誰 35 public GameObject childShooter;
初始方向就是子彈生成后的前向方向,如果想制造散彈效果,則子彈就需要在一定的角度波動范圍內生成前向方向,但生成的位置依然是統一的。
生成器還需要能循環生成子彈,能夠在生成的子彈飛行過程中繼續生成不一樣效果的分裂子彈,所以還需要子系統,子系統和父系統可以寫為同一個生成器類。需要注意的就是,子系統的生命周期需要依賴父系統生成的子彈的生命周期。
生成單個子彈的方法:
1 private void Creat(Transform parent) 2 { 3 //從對象池中取對象生成到指定物體下,復位坐標 4 var ins = ObjectPool.Instance.GetObj(bulletPrefab, parent.transform); 5 ins.transform.ResetLocal(); 6 7 //對子彈的屬性賦值 8 var bullet = ins.GetComponent<Bullet>(); 9 bullet.Target = target; 10 bullet.Velocity = velocity; 11 bullet.Acceleration = acceleration; 12 bullet.LifeTime = lifeTime; 13 bullet.MaxVelocity = maxVelocity; 14 bullet.Palstance = palstance; 15 16 //確定子彈生成方向的范圍,默認Z軸正方向為子彈飛行方向 17 float x = Random.Range(direction.x - angelRange / 2, direction.x + angelRange / 2); 18 float y = Random.Range(direction.y - angelRange / 2, direction.y + angelRange / 2); 19 bullet.transform.localEulerAngles = new Vector3(x, y, 0); 20 21 parent.DetachChildren(); 22 23 //開啟子彈自動回收 24 bullet.StartCoroutine(bullet.AutoRecycle()); 25 26 //判斷子生成器並自動運行 27 if (bChildShooter) 28 { 29 var cscs = childShooter.GetComponent<ShooterSystem>(); 30 if (lifeTime > cscs.delay) 31 StartCoroutine(cscs.AutoCreat(bullet.transform, this)); 32 else 33 Debug.Log("子發射器延遲時間設置有誤!"); 34 } 35 }
對於子生成器來說,它也同樣可能擁有自己的子生成器,在AutoCreat的方法中需要傳遞它的父生成器是誰,默認情況下為空:
1 IEnumerator AutoCreat(Transform parent, ShooterSystem parShooter = null) 2 { 3 yield return new WaitForSeconds(delay); 4 if (bLoop) 5 { 6 if (parShooter != null) 7 { 8 //子生成器需要計算循環的次數,父生成器則是無限循環 9 int loopCount = (int)((parShooter.lifeTime - delay) / timeCell); 10 for (; loopCount > 0; loopCount--) 11 { 12 //每次循環生成的子彈數量 13 for (int i = 0; i < count; i++) 14 Creat(parent); 15 yield return new WaitForSeconds(timeCell); 16 } 17 } 18 else 19 { 20 for (; ; ) 21 { 22 for (int i = 0; i < count; i++) 23 Creat(parent); 24 yield return new WaitForSeconds(timeCell); 25 } 26 } 27 } 28 else 29 { 30 for (int i = 0; i < count; i++) 31 Creat(parent); 32 } 33 }
有關傷害判斷和碰撞檢測不在此篇討論范圍內。
2019年12月12日更新:
增加以下幾個功能:
1.可以控制子彈僅在單軸向的角度范圍內散射,比如有時想讓子彈只在同一個平面內散射,而不是在三維空間中。
2.可以控制子彈在散射范圍內平均分布,而不是僅能隨機分布。
3.可以控制子彈在非循環發射狀態下按照固定時間間隔先后發射,比如追蹤導彈一發發有序射擊。
在此之前,先優化子彈中的一個小問題,子彈類的Update方法中,僅當存在追蹤目標且角速度大於零時追蹤目標:
1 private void Update() 2 { 3 float deltaTime = Time.deltaTime; 4 if (Target != null && Palstance > 0) 5 { 6 //由當前子彈位置指向目標位置的向量,記為瞬時偏移向量 7 Vector3 offset = (Target.transform.position - transform.position).normalized; 8 //子彈的當前前進方向與瞬時偏移向量之間的夾角 9 float angle = Vector3.Angle(transform.forward, offset); 10 //夾角除以角速度計算需要轉到相同方向所需要的總時間 11 float needTime = angle * 1.0f / Palstance; 12 //插值運算出當前幀的前向方向向量,也即是需要偏移的角度 13 transform.forward = Vector3.Lerp(transform.forward, offset, deltaTime / needTime).normalized; 14 } 15 //處理線性加速度對於速度的增量 16 if (Velocity < MaxVelocity) 17 { 18 Velocity += deltaTime * Acceleration; 19 } 20 //按當前速度向前移動一幀的距離,賦值給當前位置 21 transform.position += transform.forward * Velocity * deltaTime; 22 }
下面開始實現前面的幾個功能:
定義可選軸向,理論上只要繞兩個方向的軸向就可以定義三維空間中的任何一個方向,這里將Z軸作為初始的前進方向因此不對Z軸作任何操作和改變。
1 public enum AngelRangeAxis 2 { 3 //僅在繞Y軸的平面上,也即是X-Z平面 4 RYAxis, 5 //僅在繞X軸的平面上,也即是Y-Z平面 6 RXAxis, 7 //三維空間中 8 BothXY 9 }
在ShooterSystem類中增加定義以下屬性:
1 //是否固定單位角度 2 public bool bFixedAngel = false; 3 //單數量時間間隔 4 public float EachCountDur = 0f; 5 //計算得出的固定單位角度 6 private float FixAngel; 7 //范圍軸向設置 8 public AngelRangeAxis RangeAxis;
在Start方法中根據一次發射數量計算單位角度:
1 if (bFixedAngel) 2 { 3 FixAngel = AngelRange / (Count - 1); 4 }
在Creat方法中增加參數——當前創建的子彈索引idx,默認值為-1,可以不傳遞該參數,當傳遞該參數時,用於計算每一子彈在范圍內應處於的角度:
1 //確定子彈生成方向的范圍,默認z軸正方向為子彈飛行方向 2 switch (RangeAxis) 3 { 4 case AngelRangeAxis.RYAxis: 5 bullet.transform.localEulerAngles = new Vector3(Direction.x, GetLocalEulerAxis(Direction.y, idx), 0); 6 break; 7 case AngelRangeAxis.RXAxis: 8 bullet.transform.localEulerAngles = new Vector3(GetLocalEulerAxis(Direction.x, idx), Direction.y, 0); 9 break; 10 case AngelRangeAxis.Both: 11 bullet.transform.localEulerAngles = new Vector3(GetLocalEulerAxis(Direction.x, idx), GetLocalEulerAxis(Direction.y, idx), 0); 12 break; 13 }
其中方法GetLocalEulerAxis定義如下,主要用於確定軸向的最終值(無論是固定角度還是隨機):
1 private float GetLocalEulerAxis(float dirAxis,int idx) 2 { 3 if (bFixedAngel) 4 return dirAxis- AngelRange / 2 + FixAngel * idx; 5 else 6 return Random.Range(dirAxis - AngelRange / 2, dirAxis + AngelRange / 2); 7 }
在AutoCreat協程中的非循環生成條件中進行如下修改:
1 for (int i = 0; i < Count; i++) 2 { 3 if (bFixedAngel) 4 Creat(parent, i); 5 else 6 Creat(parent); 7 yield return new WaitForSeconds(EachCountDur); 8 }
修改后可以發射出類似於這樣的追蹤導彈:
或者這樣同一平面內的等間距子彈: