Menciptakan Tower Defense in Unity: Ballistics

[ Bagian pertama , kedua dan ketiga dari tutorial]

  • Dukungan untuk berbagai jenis menara.
  • Membuat menara mortar.
  • Perhitungan lintasan parabola.
  • Meluncurkan shell yang meledak.

Ini adalah bagian keempat dari tutorial tentang cara membuat game menara pertahanan sederhana. Di dalamnya kita akan menambahkan menara mortir menembakkan peluru yang meledak di tabrakan.

Tutorial ini dibuat di Unity 2018.4.4f1.


Musuh dibom.

Jenis menara


Laser bukan satu-satunya jenis senjata yang bisa diletakkan di menara. Dalam tutorial ini kita akan menambahkan jenis kedua menara, yang akan menembakkan peluru yang meledak pada kontak, merusak semua musuh di dekatnya. Untuk melakukan ini, kita memerlukan dukungan untuk berbagai jenis menara.

Menara abstrak


Deteksi dan pelacakan target adalah fungsi yang dapat digunakan menara mana pun, jadi kami akan memasukkannya ke dalam kelas dasar abstrak menara. Untuk melakukan ini, kita cukup menggunakan kelas Tower , tapi pertama, duplikat isinya untuk digunakan nanti di kelas LaserTower tertentu. Lalu kami menghapus semua kode terkait laser dari Tower . Menara mungkin tidak melacak target tertentu, jadi hapus bidang target dan ubah AcquireTarget dan TrackTarget sehingga parameter output digunakan sebagai parameter tautan. Kemudian kami akan menghapus visualisasi OnDrawGizmosSelected dari OnDrawGizmosSelected , tetapi kami akan meninggalkan rentang sasaran, karena digunakan untuk semua menara.

 using UnityEngine; public abstract class Tower : GameTileContent { const int enemyLayerMask = 1 << 9; static Collider[] targetsBuffer = new Collider[100]; [SerializeField, Range(1.5f, 10.5f)] protected float targetingRange = 1.5f; protected bool AcquireTarget (out TargetPoint target) { … } protected bool TrackTarget (ref TargetPoint target) { … } void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } } 

Mari kita ubah kelas duplikat sehingga berubah menjadi LaserTower yang memperluas Tower dan menggunakan fungsionalitas kelas dasar, menyingkirkan kode duplikat.

 using UnityEngine; public class LaserTower : Tower { [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; [SerializeField] Transform turret = default, laserBeam = default; TargetPoint target; Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } public override void GameUpdate () { if (TrackTarget(ref target) || AcquireTarget(out target)) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } void Shoot () { … } } 

Kemudian perbarui cetakan menara laser untuk menggunakan komponen baru.


Komponen menara laser.

Membuat jenis menara tertentu


Untuk dapat memilih menara mana yang akan ditempatkan di lapangan, kami akan menambahkan enumerasi TowerType mirip dengan GameTileContentType . Kami akan membuat dukungan untuk menara laser dan menara mortir yang ada, yang akan kami buat nanti.

 public enum TowerType { Laser, Mortar } 

Karena kita akan membuat kelas untuk setiap jenis menara, kita akan menambahkan properti pengambil abstrak ke Tower untuk menunjukkan jenisnya. Ini bekerja mirip dengan jenis perilaku tokoh dalam seri tutorial Manajemen Objek .

  public abstract TowerType TowerType€ { get; } 

LaserTower ulang dalam LaserTower sehingga mengembalikan jenis yang benar.

  public override TowerType TowerType€ => TowerType.Laser; 

Selanjutnya, ubah GameTileContentFactory sehingga pabrik dapat menghasilkan menara dari tipe yang diinginkan. Kami menerapkan ini dengan berbagai menara dan menambahkan metode Get publik alternatif dengan parameter TowerType . Untuk memverifikasi bahwa array dikonfigurasi dengan benar, kami akan menggunakan pernyataan. Metode Get publik lainnya sekarang hanya akan berlaku untuk konten ubin tanpa menara.

  [SerializeField] Tower[] towerPrefabs = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … } Debug.Assert(false, "Unsupported non-tower type: " + type); return null; } public GameTileContent Get (TowerType type) { Debug.Assert((int)type < towerPrefabs.Length, "Unsupported tower type!"); Tower prefab = towerPrefabs[(int)type]; Debug.Assert(type == prefab.TowerType€, "Tower prefab at wrong index!"); return Get(prefab); } 

Adalah logis untuk mengembalikan tipe yang paling spesifik, jadi idealnya, tipe pengembalian dari metode Get baru harus Tower . Tetapi metode Get privat yang digunakan untuk membuat cetakan menghasilkan GameTileContent . Di sini Anda dapat melakukan konversi, atau menjadikan metode Get privat menjadi umum. Mari kita pilih opsi kedua.

  public Tower Get (TowerType type) { … } T Get<T> (T prefab) where T : GameTileContent { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } 

Meskipun kami hanya memiliki menara laser, kami akan menjadikannya satu-satunya elemen array menara pabrik.


Array menara cetakan.

Membuat instance dari jenis menara tertentu


Untuk membuat menara dari jenis tertentu, kami GameBoard.ToggleTower sehingga membutuhkan parameter TowerType dan TowerType ke pabrik.

  public void ToggleTower (GameTile tile, TowerType towerType) { if (tile.Content.Type == GameTileContentType.Tower€) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(towerType); … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } } 

Ini menciptakan peluang baru: keadaan menara beralih ketika sudah ada, tetapi menara dari berbagai jenis. Sejauh ini, beralih hanya menghilangkan menara yang ada, tetapi akan logis untuk diganti dengan jenis baru, jadi mari kita terapkan ini. Karena ubin tetap sibuk, Anda tidak perlu mencari jalan lagi.

  if (tile.Content.Type == GameTileContentType.Tower€) { updatingContent.Remove(tile.Content); if (((Tower)tile.Content).TowerType€ == towerType) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } } 

Game sekarang harus melacak jenis menara yang dapat dipindahkan. Kami cukup menunjukkan setiap jenis menara dengan angka. Menara laser adalah 1, itu akan menjadi menara default, dan menara mortar adalah 2. Dengan menekan tombol angka, kita akan memilih jenis menara yang sesuai.

  TowerType selectedTowerType; … void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } if (Input.GetKeyDown(KeyCode.Alpha1)) { selectedTowerType = TowerType.Laser; } else if (Input.GetKeyDown(KeyCode.Alpha2)) { selectedTowerType = TowerType.Mortar; } … } … void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile, selectedTowerType); } else { board.ToggleWall(tile); } } } 

Menara mortar


Menara mortir belum dimungkinkan untuk dipasang, karena belum memiliki cetakan. Mari kita mulai dengan membuat jenis MortarTower minimal. Mortar memiliki frekuensi kebakaran, untuk menunjukkan yang Anda dapat menggunakan bidang konfigurasi "tembakan per detik". Selain itu, kita akan membutuhkan tautan ke mortar agar dapat membidik.

 using UnityEngine; public class MortarTower : Tower { [SerializeField, Range(0.5f, 2f)] float shotsPerSecond = 1f; [SerializeField] Transform mortar = default; public override TowerType TowerType€ => TowerType.Mortar; } 

Sekarang buat cetakan untuk menara mortar. Ini dapat dilakukan dengan menduplikasi cetakan dari menara laser dan mengganti komponen menara. Lalu kita menyingkirkan benda-benda menara dan sinar laser. Ganti nama turret menjadi mortar , gerakkan ke bawah sehingga berdiri di atas alas, beri warna abu-abu terang dan pasangkan. Kita dapat meninggalkan mortar collider, dalam hal ini, menggunakan objek terpisah, yang merupakan collider sederhana ditumpangkan pada orientasi default mortar. Saya menetapkan kisaran mortar 3,5 dan frekuensi 1 tembakan per detik.

pemandangan

hierarki

inspektur

Rumah pabrikan dari menara mortir.

Mengapa mereka disebut mortir?
Varietas pertama dari senjata ini pada dasarnya adalah mangkuk besi, mirip dengan mortir, di mana bahan-bahannya digiling menggunakan alu.

Tambahkan mortar cetakan ke larik pabrik sehingga menara mortar dapat ditempatkan di lapangan. Namun, mereka belum melakukan apa pun.

inspektur

pemandangan

Dua jenis menara, salah satunya tidak aktif

Perhitungan lintasan


Mortira menembakkan sebuah peluru pada sudut, sehingga ia terbang melewati rintangan dan mengenai target dari atas. Biasanya, kerang digunakan yang meledak ketika bertabrakan dengan atau di atas target. Agar tidak menyulitkan hal-hal, kita akan selalu membidik tanah sehingga kerang meledak ketika tingginya turun ke nol.

Membidik horizontal


Untuk mengarahkan mortar, kita perlu mengarahkannya secara horizontal pada target, dan kemudian mengubah posisi vertikal sehingga proyektil mendarat pada jarak yang tepat. Kami akan mulai dengan langkah pertama. Pertama, kita akan menggunakan titik relatif tetap, bukan target yang bergerak, untuk memastikan perhitungan kita benar.

Tambahkan metode MortarTower ke GameUpdate , yang selalu memanggil metode Launch . Alih-alih menembakkan proyektil nyata, kami akan memvisualisasikan perhitungan matematika untuk saat ini. Titik tembakan adalah posisi mortir di dunia, yang terletak tepat di atas tanah. Kami menempatkan titik target tiga unit dari itu di sepanjang sumbu X, dan nol komponen Y, karena kami selalu membidik tanah. Kemudian kita akan menunjukkan poin dengan Debug.DrawLine garis kuning di antara mereka dengan memanggil Debug.DrawLine . Garis akan terlihat dalam mode pemandangan untuk satu bingkai, tetapi ini sudah cukup, karena di setiap bingkai kita menggambar garis baru.

  public override void GameUpdate () { Launch(); } public void Launch () { Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); Debug.DrawLine(launchPoint, targetPoint, Color.yellow); } 


Kami membidik titik yang diperbaiki relatif terhadap menara.

Dengan menggunakan baris ini kita dapat mendefinisikan segitiga siku-siku. Titik atasnya berada di posisi mortir. Mengenai mortir, ini  beginbmatrix00 endbmatrix . Titik di bawah, di dasar menara, adalah  beginbmatrix0y endbmatrix , dan intinya adalah  beginbmatrixxy endbmatrix dimana x sama dengan 3, dan y Apakah posisi vertikal mortir negatif. Kita perlu melacak kedua nilai ini.

  Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); float x = 3f; float y = -launchPoint.y; 


Membidik segitiga.

Secara umum, target bisa di mana saja dalam jangkauan menara, jadi Z juga harus diperhitungkan. Namun, segitiga bertujuan masih tetap dua dimensi, hanya berputar di sekitar sumbu Y. Untuk mengilustrasikannya, kita akan menambahkan parameter vektor perpindahan relatif dalam Launch dan akan memanggilnya dengan empat perpindahan di XZ:  beginbmatrix30 endbmatrix ,  beginbmatrix01 endbmatrix ,  beginbmatrix11 endbmatrix dan  beginbmatrix31 endbmatrix . Ketika titik bidik menjadi sama dengan titik tembakan plus offset ini, dan koordinat Y-nya menjadi sama dengan nol.

  public override void GameUpdate () { Launch(new Vector3(3f, 0f, 0f)); Launch(new Vector3(0f, 0f, 1f)); Launch(new Vector3(1f, 0f, 1f)); Launch(new Vector3(3f, 0f, 1f)); } public void Launch (Vector3 offset) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = launchPoint + offset; targetPoint.y = 0f; … } 

Sekarang x dari segitiga bertujuan sama dengan panjang vektor 2D yang menunjuk dari dasar menara ke titik tujuan. Dengan menormalkan vektor ini, kita juga mendapatkan vektor arah XZ, yang dapat digunakan untuk menyelaraskan segitiga. Anda dapat menunjukkannya dengan menggambar bagian bawah segitiga sebagai garis putih yang diperoleh dari arah dan x.

  Vector2 dir; dir.x = targetPoint.x - launchPoint.x; dir.y = targetPoint.z - launchPoint.z; float x = dir.magnitude; float y = -launchPoint.y; dir /= x; Debug.DrawLine(launchPoint, targetPoint, Color.yellow); Debug.DrawLine( new Vector3(launchPoint.x, 0.01f, launchPoint.z), new Vector3( launchPoint.x + dir.x * x, 0.01f, launchPoint.z + dir.y * x ), Color.white ); 


Segitiga bertujuan bertujuan.

Sudut tembakan


Selanjutnya, kita harus mencari tahu sudut untuk menembak proyektil. Adalah perlu untuk menurunkannya dari fisika lintasan proyektil. Kami tidak akan memperhitungkan hambatan, angin, dan halangan lainnya, hanya kecepatan tembakan v dan gravitasi g=9,81 .

Offset d proyektil ini sejalan dengan segitiga bertujuan dan dapat dijelaskan oleh dua komponen. Dengan perpindahan horizontal, sederhana: sederhana dx=vxt dimana t - waktu setelah tembakan. Dengan komponen vertikal semuanya serupa, maka ia mengalami akselerasi negatif karena gravitasi, oleh karena itu ia memiliki bentuk dy=vyt(gt2)/2 .

Bagaimana perhitungan offset dilakukan?
Kecepatan v ditentukan oleh jarak per detik, oleh karena itu, mengalikan kecepatan dengan durasi t kami mendapatkan jarak d = v t . Saat akselerasi terlibat a , kecepatannya variabel. Akselerasi adalah perubahan kecepatan per detik, yaitu jarak kuadrat per detik. Kapan saja, kecepatannya v = a t . Dalam kasus kami, ada akselerasi yang konstan a = - g , jadi kita dapat membaginya menjadi dua untuk mendapatkan kecepatan rata-rata, dan kalikan dengan waktu untuk menemukan offset d = ( a t 2 ) / 2 disebabkan oleh gravitasi.

Kami menembakkan peluru dengan kecepatan yang sama s yang tidak tergantung pada sudut bidikan  theta (theta). Yaitu vx=s cos theta dan vy=s sin theta .


Perhitungan kecepatan tembakan.

Melakukan substitusi, kita dapatkan dx=st cos theta dan dy=st sin theta(gt2)/2 .

Proyektilnya ditembakkan sehingga waktu terbangnya t adalah nilai tepat yang dibutuhkan untuk mencapai tujuan. Karena lebih mudah untuk bekerja dengan perpindahan horizontal, kami dapat menyatakan waktu sebagai t=dx/vx . Pada titik akhir dx=x itu adalah t=x/(s cos theta) . Ini artinya y=x tan theta(gx2)/(2s2 cos2 theta) .

Bagaimana cara mendapatkan persamaan y?
y=dy=s(x/(s cos theta)) sin theta(g(x/(s cos theta))2)/2=x sin theta/ cos theta(gx2)/(2s2 cos2 theta) dan  tan theta= sin theta/ cos theta .

Menggunakan persamaan ini kita temukan  tan theta=(s2+ sqrt(s4g(gx2+2ys2)))/(gx) .
Bagaimana cara mendapatkan persamaan tan θ?
Pertama kita akan menggunakan identitas trigonometri  sec theta=1/ cos theta dan 1+ tan2 theta= dt2 theta untuk datang ke y=x tan theta(gx2)/(2s2)(1+ tan2 theta)=(gx2)/(2s2) tan2 theta+x tan theta(gx2)/(2s2) .

Ini adalah ekspresi dari formulir au2+bu+c=0 dimana u= tan theta , a=(gx2)/(2s2) , b=x , dan c=ay .

Kita bisa menyelesaikannya menggunakan rumus akar persamaan kuadrat u=(b+ sqrt(b24ac))/(2a) .

Setelah substitusi, persamaannya akan membingungkan, tetapi Anda dapat menyederhanakannya dengan mengalikannya dengan m=s2/x jadi untuk mendapatkan  tan theta=(mb+m sqrtr)/(2ma) dimana r=b24ac .

Dalam hal ini, kami memperoleh  tan theta=(s2+ sqrt(m2r))/(gx) .

Hasilnya m2r=(s4/x2)r=s4+2gs2c=s4g2x22gys2=s4g(gx2+2ys2) .

Ada dua sudut yang memungkinkan, karena Anda dapat membidik tinggi atau rendah. Lintasan rendah lebih cepat karena lebih dekat ke garis lurus ke target. Tapi lintasan tinggi terlihat lebih menarik, jadi kami akan memilihnya. Ini berarti bahwa kita hanya perlu menggunakan solusi terbesar.  tan theta=(s2+ sqrt(s4g(gx2+2ys2)))/(gx) . Kami menghitungnya, dan juga  cos theta dengan  sin theta , karena kita membutuhkan mereka untuk mendapatkan vektor kecepatan tembakan. Untuk ini, Anda perlu mengonversi  tan theta ke sudut radian menggunakan Mathf.Atan . Pertama, mari gunakan kecepatan tembakan konstan 5.

  float x = dir.magnitude; float y = -launchPoint.y; dir /= x; float g = 9.81f; float s = 5f; float s2 = s * s; float r = s2 * s2 - g * (g * x * x + 2f * y * s2); float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta; 

Mari kita memvisualisasikan lintasan dengan menggambar sepuluh segmen biru yang menunjukkan detik pertama penerbangan.

  float sinTheta = cosTheta * tanTheta; Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { float t = i / 10f; float dx = s * cosTheta * t; float dy = s * sinTheta * t - 0.5f * g * t * t; next = launchPoint + new Vector3(dir.x * dx, dy, dir.y * dx); Debug.DrawLine(prev, next, Color.blue); prev = next; } 


Jalur penerbangan Parabola berlangsung selama satu detik.

Dua titik terjauh dapat dicapai dalam waktu kurang dari satu detik, jadi kami melihat seluruh lintasannya, dan segmen-segmen berlanjut sedikit lebih jauh di bawah tanah. Untuk dua titik lainnya, sudut bidikan yang lebih besar diperlukan, karena lintasan menjadi lebih lama, dan penerbangan berlangsung lebih dari satu detik.

Kecepatan tembakan


Jika Anda ingin mencapai dua titik terdekat dalam sedetik, maka Anda perlu mengurangi kecepatan tembakan. Mari kita membuatnya sama dengan 4.

  float s = 4f; 


Kecepatan tembakan dikurangi menjadi 4.

Lintasan mereka sekarang lengkap, tetapi dua lainnya hilang. Ini terjadi karena kecepatan tembakan sekarang tidak cukup untuk mencapai titik-titik ini. Dalam kasus seperti itu, solusi untuk  tan theta tidak, yaitu, kita mendapatkan akar kuadrat dari angka negatif, yang mengarah ke nilai NaN dan hilangnya garis. Kami dapat mengenali ini dengan memeriksa r ke negativitas.

  float r = s2 * s2 - g * (g * x * x + 2f * y * s2); Debug.Assert(r >= 0f, "Launch velocity insufficient for range!"); 

Situasi ini dapat dihindari dengan mengatur kecepatan bidikan yang cukup tinggi. Tetapi jika terlalu besar, maka untuk mencapai target di dekat menara akan membutuhkan lintasan yang sangat tinggi dan waktu penerbangan yang lama, jadi Anda harus meninggalkan kecepatan serendah mungkin. Kecepatan tembakan harus cukup untuk mengenai target pada jarak maksimum.

Pada rentang maksimum r=0 , mis. untuk  tan theta hanya ada satu solusi, sesuai dengan jalan yang rendah. Ini berarti bahwa kita mengetahui kecepatan tembakan yang diperlukan. s = s q r t ( g ( y + s q r t ( x 2 + y 2 ) ) )   .

Bagaimana cara menurunkan persamaan ini untuk s?
Perlu memutuskan s4g(gx2+2ys2)=s42gys2g2x2=0 untuk s .

Ini adalah ekspresi dari formulir au2+bu+c=0 dimana u=s2 , a=1 , b=2gy dan c=g2x2 .

Anda dapat menyelesaikannya menggunakan rumus akar persamaan kuadrat yang disederhanakan u=(b+ sqrt(b24c))/2 .

Setelah penggantian kami dapatkan s2=(2gy+ sqrt(4g2y2+4g2x2))/2=gy+g sqrt(x2+y2) .

Kami membutuhkan solusi positif, jadi kami sadar s2=g(y+ sqrt(x2+y2)) .

Kita perlu menentukan kecepatan yang diperlukan hanya ketika mortar bangun (Sedar) atau ketika kita mengubah jangkauannya dalam mode Putar. Karenanya, kami akan melacaknya menggunakan bidang dan menghitungnya di Awake dan OnValidate .

  float launchSpeed; void Awake () { OnValidate(); } void OnValidate () { float x = targetingRange; float y = -mortar.position.y; launchSpeed = Mathf.Sqrt(9.81f * (y + Mathf.Sqrt(x * x + y * y))); } 

Namun, karena keterbatasan dalam keakuratan perhitungan floating point, menentukan target yang sangat dekat dengan jangkauan maksimum mungkin keliru. Oleh karena itu, ketika menghitung kecepatan yang diperlukan, kami menambahkan sejumlah kecil ke kisaran. Selain itu, jari-jari collider musuh pada dasarnya memperluas radius maksimum dari jangkauan menara. Kami membuatnya sama dengan 0,125, tetapi dengan peningkatan skala musuh, itu bisa dua kali lipat sebanyak mungkin, jadi kami akan meningkatkan kisaran sebenarnya sekitar 0,25, misalnya, sebesar 0,25001.

  float x = targetingRange + 0.25001f; 

Selanjutnya, terapkan persamaan yang diturunkan untuk kecepatan bidikan dalam Launch .

  float s = launchSpeed; 


Terapkan kecepatan yang dihitung ke kisaran sasaran 3.5.

Menembak


Memiliki perhitungan lintasan yang benar, Anda dapat menyingkirkan tujuan tes relatif. Sekarang Anda harus melewati titik Launch ke target.

  public void Launch (TargetPoint target) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = target.Position; targetPoint.y = 0f; … } 

Selain itu, tembakan tidak dilakukan di setiap frame. Kita perlu melacak proses tembakan dengan cara yang sama seperti proses menciptakan musuh dan menangkap target acak ketika saatnya tiba untuk tembakan di GameUpdate . Tetapi pada titik ini, mungkin tidak ada tujuan yang tersedia. Dalam hal ini, kami melanjutkan proses penembakan, tetapi tanpa akumulasi lebih lanjut. Untuk menghindari loop tak terbatas, Anda harus membuatnya sedikit kurang dari 1.

  float launchProgress; … public override void GameUpdate () { launchProgress += shotsPerSecond * Time.deltaTime; while (launchProgress >= 1f) { if (AcquireTarget(out TargetPoint target)) { Launch(target); launchProgress -= 1f; } else { launchProgress = 0.999f; } } } 

Kami tidak melacak target di antara tembakan, tetapi kami perlu memutar mortar dengan benar selama tembakan. Anda dapat menggunakan arah bidikan horizontal untuk memutar mortar secara horizontal menggunakan Quaternion.LookRotation . Kita juga butuh  t a n t h e t a  terapkan sudut pengambilan gambar untuk komponen Y dari vektor arah. Ini akan berfungsi karena arah horizontal memiliki panjang 1, yaitu  t a n t h e t a = s i n t h e t a    .


Dekomposisi vektor giliran tampilan.

  float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta; mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y)); 

Untuk tetap melihat lintasan tembakan, Anda dapat menambahkan parameter ke Debug.DrawLine yang memungkinkannya ditarik untuk waktu yang lama.

  Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { … Debug.DrawLine(prev, next, Color.blue, 1f); prev = next; } Debug.DrawLine(launchPoint, targetPoint, Color.yellow, 1f); Debug.DrawLine( … Color.white, 1f ); 


Bertujuan

Kerang


Arti menghitung lintasan adalah bahwa kita sekarang tahu cara menembak kerang. Selanjutnya kita perlu membuat dan menembak mereka.

Pabrik perang


Kami membutuhkan pabrik untuk membuat objek shell. Saat berada di udara, cangkang itu ada dengan sendirinya dan tidak lagi tergantung pada mortir yang menembak mereka. Oleh karena itu, mereka tidak boleh diproses oleh menara mortar, dan pabrik konten ubin juga tidak cocok untuk ini.Mari kita buat buat untuk semua yang terkait dengan senjata, pabrik baru dan menyebutnya pabrik perang. Pertama, buat abstrak WarEntitydengan properti OriginFactorydan metode Recycle.

 using UnityEngine; public abstract class WarEntity : MonoBehaviour { WarFactory originFactory; public WarFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); } } 

Kemudian buat entitas spesifik Shelluntuk shell.

 using UnityEngine; public class Shell : WarEntity { } 

Kemudian buat WarFactoryyang akan membuat proyektil menggunakan properti pengambil publik.

 using UnityEngine; [CreateAssetMenu] public class WarFactory : GameObjectFactory { [SerializeField] Shell shellPrefab = default; public Shell Shell€ => Get(shellPrefab); T Get<T> (T prefab) where T : WarEntity { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (WarEntity entity) { Debug.Assert(entity.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(entity.gameObject); } } 

Buat cetakan untuk proyektil. Saya menggunakan kubus sederhana dengan skala yang sama 0,25 dan material gelap, serta komponen Shell. Kemudian buat aset pabrik dan tetapkan prefab proyektilnya.


Pabrik perang.

Perilaku game


Untuk memindahkan cangkang, mereka perlu diperbarui. Anda dapat menggunakan pendekatan yang sama yang digunakan Gameuntuk memperbarui status musuh. Bahkan, kita bahkan dapat membuat pendekatan ini digeneralisasi dengan membuat komponen abstrak GameBehavioryang memperluas MonoBehaviourdan menambahkan metode virtual GameUpdate.

 using UnityEngine; public abstract class GameBehavior : MonoBehaviour { public virtual bool GameUpdate () => true; } 

Sekarang lakukan refactoring EnemyCollection, mengubahnya menjadi GameBehaviorCollection.

 public class GameBehaviorCollection { List<GameBehavior> behaviors = new List<GameBehavior>(); public void Add (GameBehavior behavior) { behaviors.Add(behavior); } public void GameUpdate () { for (int i = 0; i < behaviors.Count; i++) { if (!behaviors[i].GameUpdate()) { int lastIndex = behaviors.Count - 1; behaviors[i] = behaviors[lastIndex]; behaviors.RemoveAt(lastIndex); i -= 1; } } } } 

Mari kita membuatnya WarEntityberkembang GameBehavior, bukan MonoBehavior.

 public abstract class WarEntity : GameBehavior { … } 

Kami akan melakukan hal yang sama untuk Enemy, kali ini mengganti metode GameUpdate.

 public class Enemy : GameBehavior { … public override bool GameUpdate () { … } … } 

Mulai sekarang, Gameia harus melacak dua koleksi, satu untuk musuh, yang lain untuk non-musuh. Non-musuh harus diperbarui setelah yang lainnya.

  GameBehaviorCollection enemies = new GameBehaviorCollection(); GameBehaviorCollection nonEnemies = new GameBehaviorCollection(); … void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); } 

Langkah terakhir dalam mengimplementasikan upgrade shell adalah menambahkannya ke koleksi non-musuh. Mari kita lakukan ini dengan fungsi Gameyang akan menjadi fasad statis untuk pabrik perang sehingga proyektil dapat dibuat oleh tantangan Game.SpawnShell(). Agar ini berfungsi, Anda Gameharus memiliki tautan ke pabrik perang dan melacak instans Anda sendiri.

  [SerializeField] WarFactory warFactory = default; … static Game instance; public static Shell SpawnShell () { Shell shell = instance.warFactory.Shell€; instance.nonEnemies.Add(shell); return shell; } void OnEnable () { instance = this; } 


Game dengan pabrik perang.

Apakah fasad statis merupakan solusi yang baik?
, , .

Kami menembakkan shell


Setelah membuat turunan dari proyektil, ia harus terbang di sepanjang jalannya hingga mencapai tujuan akhir. Untuk melakukan ini, tambahkan ke Shellmetode Initializedan gunakan untuk menentukan titik tembakan, titik target dan kecepatan tembakan.

  Vector3 launchPoint, targetPoint, launchVelocity; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity ) { this.launchPoint = launchPoint; this.targetPoint = targetPoint; this.launchVelocity = launchVelocity; } 

Sekarang kita dapat membuat shell MortarTower.Launchdan mengirimkannya di jalan.

  mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y)); Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); 

Gerakan proyektil


Untuk Shellbergerak, kita perlu melacak durasi keberadaannya, yaitu waktu yang berlalu sejak tembakan. Lalu kita bisa menghitung posisinya di GameUpdate. Kami selalu melakukan ini sehubungan dengan titik tembaknya, sehingga proyektil dengan sempurna mengikuti jalan terlepas dari kecepatan refresh.

  float age; … public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; transform.localPosition = p; return true; } 


Penembakan.

Untuk menyelaraskan shell dengan lintasannya, kita perlu membuatnya melihat sepanjang vektor turunan, yang merupakan kecepatan mereka pada waktu yang sesuai.

  public override bool GameUpdate () { … Vector3 d = launchVelocity; dy -= 9.81f * age; transform.localRotation = Quaternion.LookRotation(d); return true; } 


Kerang berputar.

Kami membersihkan game


Sekarang sudah jelas bahwa shell terbang persis seperti seharusnya, Anda dapat menghapus MortarTower.Launchlintasan dari visualisasi.

  public void Launch (TargetPoint target) { … Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); } 

Selain itu, kita perlu memastikan bahwa kerang dihancurkan setelah mengenai target. Karena kami selalu membidik tanah, ini bisa dilakukan dengan memeriksa untuk Shell.GameUpdatemelihat apakah posisi vertikal di bawah nol. Anda dapat melakukan ini segera setelah menghitungnya, sebelum mengubah posisi dan memutar proyektil.

  public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; if (py <= 0f) { OriginFactory.Reclaim(this); return false; } transform.localPosition = p; … } 

Peledakan


Kami menembakkan kerang karena mengandung bahan peledak. Ketika proyektil mencapai targetnya, ia harus meledakkan dan menimbulkan kerusakan pada semua musuh di area ledakan. Jari-jari ledakan dan kerusakan yang ditangani tergantung pada jenis peluru yang ditembakkan oleh mortir, jadi kami akan menambahkan MortarToweropsi konfigurasi untuk mereka.

  [SerializeField, Range(0.5f, 3f)] float shellBlastRadius = 1f; [SerializeField, Range(1f, 100f)] float shellDamage = 10f; 


Jari-jari ledakan dan 1,5 kerusakan 15 peluru.

Konfigurasi ini penting hanya selama ledakan, sehingga harus ditambahkan Shelldan metodenya Initialize.

  float age, blastRadius, damage; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity, float blastRadius, float damage ) { … this.blastRadius = blastRadius; this.damage = damage; } 

MortarTower seharusnya hanya mengirimkan data ke proyektil setelah pembuatannya.

  Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y), shellBlastRadius, shellDamage ); 

Untuk menembak musuh dalam jangkauan, proyektil harus menangkap target. Kami sudah memiliki kode untuk ini, tetapi sudah ada dalam Tower. Karena ini berguna untuk semua yang membutuhkan tujuan, salin fungsionalitasnya TargetPointdan buat tersedia secara statis. Tambahkan metode untuk mengisi buffer, properti untuk mendapatkan jumlah buffered, dan metode untuk mendapatkan target buffered.

  const int enemyLayerMask = 1 << 9; static Collider[] buffer = new Collider[100]; public static int BufferedCount { get; private set; } public static bool FillBuffer (Vector3 position, float range) { Vector3 top = position; top.y += 3f; BufferedCount = Physics.OverlapCapsuleNonAlloc( position, top, range, buffer, enemyLayerMask ); return BufferedCount > 0; } public static TargetPoint GetBuffered (int index) { var target = buffer[index].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", buffer[0]); return target; } 

Sekarang kita dapat menerima semua target dalam jangkauan hingga ukuran buffer maksimum dan menimbulkan kerusakan pada saat ledakan Shell.

  if (py <= 0f) { TargetPoint.FillBuffer(targetPoint, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy€.ApplyDamage(damage); } OriginFactory.Reclaim(this); return false; } 


Peledakan kerang.

Anda juga dapat menambahkan ke TargetPointproperti statis untuk mendapatkan target acak dari buffer.

  public static TargetPoint RandomBuffered => GetBuffered(Random.Range(0, BufferedCount)); 

Ini akan memungkinkan kami untuk menyederhanakan Tower, karena sekarang Anda dapat menggunakan untuk mencari target acak TargetPoint.

 protected bool AcquireTarget (out TargetPoint target) { if (TargetPoint.FillBuffer(transform.localPosition, targetingRange)) { target = TargetPoint.RandomBuffered; return true; } target = null; return false; } 

Ledakan


Semuanya berfungsi, tetapi masih tidak terlihat sangat bisa dipercaya. Anda dapat meningkatkan gambar dengan menambahkan visualisasi ledakan ketika peledakan shell. Ini tidak hanya akan terlihat lebih menarik, tetapi juga memberikan umpan balik yang bermanfaat bagi pemain. Untuk melakukan ini, kita akan membuat cetakan ledakan seperti sinar laser. Hanya itu akan menjadi bidang warna transparan yang lebih transparan. Tambahkan komponen entitas baru Explosiondengan durasi khusus. Setengah detik sudah cukup. Tambahkan dia metode Initializeyang mengatur posisi dan radius ledakan. Saat mengatur skala, Anda perlu menggandakan jari-jari, karena jari-jari bola sphere adalah 0,5. Itu juga tempat yang bagus untuk memberikan damage ke semua musuh dalam jangkauan, jadi kami juga akan menambahkan parameter damage. Selain itu, ia membutuhkan metode GameUpdateuntuk memeriksa apakah waktu hampir habis.

 using UnityEngine; public class Explosion : WarEntity { [SerializeField, Range(0f, 1f)] float duration = 0.5f; float age; public void Initialize (Vector3 position, float blastRadius, float damage) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } transform.localPosition = position; transform.localScale = Vector3.one * (2f * blastRadius); } public override bool GameUpdate () { age += Time.deltaTime; if (age >= duration) { OriginFactory.Reclaim(this); return false; } return true; } } 

Tambahkan ledakan ke WarFactory.

  [SerializeField] Explosion explosionPrefab = default; [SerializeField] Shell shellPrefab = default; public Explosion Explosion€ => Get(explosionPrefab); public Shell Shell => Get(shellPrefab); 


Pabrik perang dengan ledakan.

Tambahkan juga Gamemetode fasad.

  public static Explosion SpawnExplosion () { Explosion explosion = instance.warFactory.Explosion€; instance.nonEnemies.Add(explosion); return explosion; } 

Sekarang ia Shelldapat menghasilkan dan memulai ledakan setelah mencapai target. Ledakan itu sendiri akan menyebabkan kerusakan.

  if (py <= 0f) { Game.SpawnExplosion().Initialize(targetPoint, blastRadius, damage); OriginFactory.Reclaim(this); return false; } 


Ledakan kerang.

Ledakan lebih halus


Bola yang tidak berubah dan bukannya ledakan tidak terlihat sangat indah. Anda dapat meningkatkannya dengan menjiwai opacity dan skala. Anda dapat menggunakan rumus sederhana untuk ini, tetapi mari kita gunakan kurva animasi yang lebih mudah diatur. Tambahkan untuk Explosiondua bidang konfigurasi ini AnimationCurve. Kami akan menggunakan kurva untuk menyesuaikan nilai selama masa ledakan, dan waktu 1 akan menunjukkan akhir dari ledakan, terlepas dari durasi sebenarnya. Hal yang sama berlaku untuk skala dan radius ledakan. Ini akan menyederhanakan konfigurasi mereka.

  [SerializeField] AnimationCurve opacityCurve = default; [SerializeField] AnimationCurve scaleCurve = default; 

Opacity akan dimulai dan diakhiri dengan nol, diskalakan dengan lancar ke nilai rata-rata 0,3. Skala akan mulai dari 0,7, meningkat dengan cepat, dan kemudian perlahan mendekati 1.


Kurva ledakan.

Untuk mengatur warna material, kita akan menggunakan blok properti material. di mana hitam adalah variabel opacity. Skala sekarang diatur ke GameUpdate, tetapi kita perlu melacak menggunakan bidang jari-jari. Di InitializeAnda dapat menggunakan skala penggandaan. Nilai-nilai kurva ditemukan dengan memanggil mereka Evaluatedengan argumen, dihitung sebagai masa hidup ledakan saat ini, dibagi dengan durasi ledakan.

  static int colorPropertyID = Shader.PropertyToID("_Color"); static MaterialPropertyBlock propertyBlock; … float scale; MeshRenderer meshRenderer; void Awake () { meshRenderer = GetComponent<MeshRenderer>(); Debug.Assert(meshRenderer != null, "Explosion without renderer!"); } public void Initialize (Vector3 position, float blastRadius, float damage) { … transform.localPosition = position; scale = 2f * blastRadius; } public override bool GameUpdate () { … if (propertyBlock == null) { propertyBlock = new MaterialPropertyBlock(); } float t = age / duration; Color c = Color.clear; ca = opacityCurve.Evaluate(t); propertyBlock.SetColor(colorPropertyID, c); meshRenderer.SetPropertyBlock(propertyBlock); transform.localScale = Vector3.one * (scale * scaleCurve.Evaluate(t)); return true; } 


Ledakan animasi.

Cangkang pelacak


Karena cangkang kecil dan memiliki kecepatan yang cukup tinggi, mereka mungkin sulit untuk diperhatikan. Dan jika Anda melihat tangkapan layar dari satu frame, lintasan sama sekali tidak bisa dipahami. Anda dapat membuatnya lebih jelas dengan menambahkan efek jejak ke shell Anda. Untuk cangkang konvensional, ini tidak terlalu realistis, tetapi kita dapat mengatakan bahwa ini adalah pelacak. Amunisi seperti itu dibuat secara khusus sehingga mereka meninggalkan tanda cerah, membuat lintasan mereka terlihat.

Ada berbagai cara untuk membuat jejak, tetapi Anda akan menggunakan yang sangat sederhana. Kami membuat ulang ledakan sehingga Shellmenciptakan ledakan kecil di setiap frame. Ledakan ini tidak akan menyebabkan kerusakan, jadi menangkap target akan menjadi pemborosan sumber daya. Tambahkan keExplosiondukungan untuk penggunaan ini dengan membuat kerusakan dilakukan jika lebih besar dari nol, dan kemudian membuat parameter kerusakan Initializeopsional.

  public void Initialize ( Vector3 position, float blastRadius, float damage = 0f ) { if (damage > 0f) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } } transform.localPosition = position; radius = 2f * blastRadius; } 

Kami akan membuat ledakan di ujung Shell.GameUpdatedengan jari-jari kecil, misalnya 0,1, untuk mengubahnya menjadi cangkang pelacak. Perlu dicatat bahwa dengan pendekatan ini, ledakan akan dibuat bingkai demi bingkai, yaitu, mereka bergantung pada laju bingkai, tetapi untuk efek sederhana ini dapat diterima.

  public override bool GameUpdate () { … Game.SpawnExplosion().Initialize(p, 0.1f); return true; } 

gambar

Pelacak proyektil. Artikel

Repositori Tutorial Tutorial

Source: https://habr.com/ru/post/id461605/


All Articles