Menciptakan Tower Defense in Unity: Enemies

[ Bagian pertama: ubin dan menemukan jalan ]

  • Penempatan poin penciptaan musuh.
  • Munculnya musuh dan gerakan mereka melintasi lapangan.
  • Menciptakan gerakan halus dengan kecepatan konstan.
  • Ubah ukuran, kecepatan, dan penempatan musuh.

Ini adalah bagian kedua dari tutorial tentang permainan menara pertahanan sederhana. Ini meneliti proses menciptakan musuh dan gerakan mereka ke titik akhir terdekat.

Tutorial ini dibuat di Unity 2018.3.0f2.


Musuh dalam perjalanan ke titik akhir.

Poin penciptaan musuh (spawn)


Sebelum kita mulai membuat musuh, kita perlu memutuskan di mana menempatkan mereka di lapangan. Untuk melakukan ini, kami akan membuat spawn point.

Konten Ubin


Spawn point adalah tipe lain dari konten ubin, jadi tambahkan entri untuk itu di GameTileContentType .

 public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint } 

Dan kemudian membuat cetakan untuk memvisualisasikannya. Duplikat cetakan dari titik awal cukup cocok untuk kita, cukup ubah jenis kontennya dan berikan materi lain. Saya membuatnya oranye.


Konfigurasi titik spawn.

Tambahkan dukungan spawn point ke pabrik konten dan berikan tautan ke cetakan.

  [SerializeField] GameTileContent spawnPointPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); case GameTileContentType.SpawnPoint: return Get(spawnPointPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; } 


Pabrik dengan dukungan untuk poin spawn.

Aktifkan atau nonaktifkan poin spawn


Metode untuk beralih status titik spawn, seperti metode beralih lainnya, kami akan tambahkan ke GameBoard . Tetapi spawn point tidak mempengaruhi pencarian path, jadi setelah perubahan kita tidak perlu mencari path baru.

  public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { tile.Content = contentFactory.Get(GameTileContentType.Empty); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); } } 

Permainan itu masuk akal hanya jika kita memiliki musuh, dan mereka membutuhkan poin spawn. Oleh karena itu, bidang permainan harus mengandung setidaknya satu titik spawn. Kami juga akan memerlukan akses ke titik spawn di masa depan, ketika kami akan menambahkan musuh, jadi mari kita gunakan daftar untuk melacak semua ubin dengan titik-titik ini. Kami akan memperbarui daftar ketika mengalihkan status titik spawn dan mencegah penghapusan titik spawn terakhir.

  List<GameTile> spawnPoints = new List<GameTile>(); … public void ToggleSpawnPoint (GameTile tile) { if (tile.Content.Type == GameTileContentType.SpawnPoint) { if (spawnPoints.Count > 1) { spawnPoints.Remove(tile); tile.Content = contentFactory.Get(GameTileContentType.Empty); } } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint); spawnPoints.Add(tile); } } 

Metode Initialize sekarang harus mengatur spawn point untuk membuat keadaan awal bidang yang benar. Mari kita sertakan ubin pertama, yang ada di sudut kiri bawah.

  public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); } 

Kami akan membuat sentuhan alternatif sekarang mengganti status titik spawn, tetapi ketika Anda menahan tombol Shift kiri (keystroke diperiksa oleh metode Input.GetKey ), keadaan titik akhir akan beralih

  void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleDestination(tile); } else { board.ToggleSpawnPoint(tile); } } } 


Lapangan dengan titik bertelur.

Dapatkan akses ke titik spawn


Lapangan berurusan dengan semua ubinnya, tetapi musuh bukan tanggung jawabnya. Kami akan memungkinkan untuk mengakses spawn point-nya melalui metode GetSpawnPoint umum dengan parameter indeks.

  public GameTile GetSpawnPoint (int index) { return spawnPoints[index]; } 

Untuk mengetahui indeks mana yang benar, diperlukan informasi tentang jumlah spawn point, jadi kami akan membuatnya umum menggunakan properti pengambil umum.

  public int SpawnPointCount => spawnPoints.Count; 

Musuh muncul


Memunculkan musuh agak mirip dengan membuat konten ubin. Kami membuat contoh cetakan melalui pabrik, yang kemudian kami tempatkan di lapangan.

Pabrik


Kami akan membuat pabrik untuk musuh yang akan meletakkan semua yang dibuatnya di panggungnya sendiri. Fungsionalitas ini sama dengan pabrik yang sudah kita miliki, jadi mari kita meletakkan kode untuknya di kelas dasar umum GameObjectFactory . Kita hanya akan memerlukan satu metode CreateGameObjectInstance dengan parameter prefab umum, yang membuat dan mengembalikan sebuah instance, dan juga mengelola seluruh adegan. Kami membuat metode yang protected , yaitu, itu akan tersedia hanya untuk kelas dan semua tipe yang mewarisi darinya. Itulah yang dilakukan oleh kelas, itu tidak dimaksudkan untuk digunakan sebagai pabrik yang berfungsi penuh. Oleh karena itu, kami menandainya sebagai abstract , yang tidak akan memungkinkan kami untuk membuat instance objeknya.

 using UnityEngine; using UnityEngine.SceneManagement; public abstract class GameObjectFactory : ScriptableObject { Scene scene; protected T CreateGameObjectInstance<T> (T prefab) where T : MonoBehaviour { if (!scene.isLoaded) { if (Application.isEditor) { scene = SceneManager.GetSceneByName(name); if (!scene.isLoaded) { scene = SceneManager.CreateScene(name); } } else { scene = SceneManager.CreateScene(name); } } T instance = Instantiate(prefab); SceneManager.MoveGameObjectToScene(instance.gameObject, scene); return instance; } } 

Ubah GameTileContentFactory sehingga GameTileContentFactory jenis pabrik ini dan menggunakan CreateGameObjectInstance dalam metode Get -nya, lalu hapus kode kontrol adegan dari sana.

 using UnityEngine; [CreateAssetMenu] public class GameTileContentFactory : GameObjectFactory { … //Scene contentScene; … GameTileContent Get (GameTileContent prefab) { GameTileContent instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; //MoveToFactoryScene(instance.gameObject); return instance; } //void MoveToFactoryScene (GameObject o) { // … //} } 

Setelah itu, buat tipe EnemyFactory baru yang membuat instance satu prefab Enemy menggunakan metode Get bersama dengan metode Reclaim menyertainya.

 using UnityEngine; [CreateAssetMenu] public class EnemyFactory : GameObjectFactory { [SerializeField] Enemy prefab = default; public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (Enemy enemy) { Debug.Assert(enemy.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(enemy.gameObject); } } 

Tipe Enemy baru awalnya hanya harus melacak pabrik aslinya.

 using UnityEngine; public class Enemy : MonoBehaviour { EnemyFactory originFactory; public EnemyFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } } 

Cetakan rumah


Musuh membutuhkan visualisasi, yang bisa berupa apa saja - robot, laba-laba, hantu, sesuatu yang lebih sederhana, misalnya, kubus, yang kita gunakan. Namun secara umum, musuh memiliki model 3D dengan kompleksitas apa pun. Untuk memastikan dukungannya yang mudah, kami akan menggunakan objek root untuk hierarki prefab musuh, yang hanya dilampirkan komponen Enemy .


Prefab Root

Mari kita buat objek ini satu-satunya elemen anak, yang akan menjadi akar dari model. Itu harus memiliki nilai Transform unit.


Akar model.

Tugas root model ini adalah memposisikan model 3D relatif terhadap titik asal lokal koordinat musuh, sehingga ia menganggapnya sebagai titik referensi di mana musuh berdiri atau menggantung. Dalam kasus kami, model akan menjadi kubus setengah ukuran standar, yang akan saya beri warna biru tua. Kami membuatnya menjadi anak dari model root dan mengatur posisi Y ke 0,25 sehingga berdiri di tanah.


Model kubus

Dengan demikian, prefab musuh terdiri dari tiga objek bersarang: root prefab, root model, dan kubus. Ini mungkin tampak seperti bust untuk kubus sederhana, tetapi sistem seperti itu memungkinkan Anda untuk bergerak dan menghidupkan musuh tanpa khawatir tentang fitur-fiturnya.


Hirarki prefab musuh.

Ayo buat pabrik musuh dan tetapkan prefab untuknya.


Pabrik aset.

Menempatkan musuh di lapangan


Untuk menempatkan musuh di lapangan, Game harus menerima tautan ke pabrik musuh. Karena kami membutuhkan banyak musuh, kami akan menambahkan opsi konfigurasi untuk menyesuaikan kecepatan pemijahan, yang dinyatakan dalam jumlah musuh per detik. Kisaran yang dapat diterima adalah 0,1-10 dengan nilai default 1.

  [SerializeField] EnemyFactory enemyFactory = default; [SerializeField, Range(0.1f, 10f)] float spawnSpeed = 1f; 


Game dengan pabrik musuh dan kecepatan pemijahan 4.

Kami akan melacak perkembangan pemijahan di Update , meningkatkannya dengan kecepatan kali waktu delta. Jika nilai prggress melebihi 1, maka kami menurunkannya dan menelurkan musuh menggunakan metode SpawnEnemy baru. Kami terus melakukan ini sampai kemajuan melebihi 1 jika kecepatannya terlalu tinggi dan waktu bingkai sangat lama sehingga beberapa musuh tidak diciptakan pada waktu yang sama.

  float spawnProgress; … void Update () { … spawnProgress += spawnSpeed * Time.deltaTime; while (spawnProgress >= 1f) { spawnProgress -= 1f; SpawnEnemy(); } } 

Apakah tidak perlu memperbarui kemajuan di FixedUpdate?
Ya, itu mungkin, tetapi pengaturan waktu yang akurat tidak diperlukan untuk permainan menara pertahanan. Kami hanya akan memperbarui keadaan permainan setiap frame dan membuatnya bekerja dengan cukup baik untuk setiap delta waktu.

Biarkan SpawnEnemy mendapatkan titik spawn acak dari lapangan dan membuat musuh di ubin ini. Kami akan memberikan metode Enemy SpawnOn untuk SpawnOn dirinya dengan benar.

  void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get(); enemy.SpawnOn(spawnPoint); } 

Untuk saat ini, yang harus dilakukan SpawnOn adalah mengatur posisinya sendiri sama dengan pusat ubin. Karena model cetakan diposisikan dengan benar, kubus musuh akan berada di atas ubin ini.

  public void SpawnOn (GameTile tile) { transform.localPosition = tile.transform.localPosition; } 


Musuh muncul di titik spawn.

Musuh yang bergerak


Setelah musuh muncul, ia harus mulai bergerak di sepanjang jalan ke titik akhir terdekat. Untuk mencapai ini, Anda perlu menghidupkan musuh. Kami mulai dengan luncuran halus sederhana dari ubin ke ubin, dan kemudian membuat gerakan mereka lebih sulit.

Koleksi musuh


Untuk memperbarui status musuh, kami akan menggunakan pendekatan yang sama yang digunakan dalam rangkaian tutorial Manajemen Objek . Kami menambahkan Enemy metode GameUpdate umum, yang mengembalikan informasi tentang apakah dia masih hidup, yang pada tahap ini akan selalu benar. Untuk saat ini, hanya membuatnya bergerak maju sesuai dengan delta waktu.

  public bool GameUpdate () { transform.localPosition += Vector3.forward * Time.deltaTime; return true; } 

Selain itu, kita perlu memelihara daftar musuh yang hidup dan memperbarui mereka semua, menghilangkannya dari daftar musuh yang mati. Kami dapat memasukkan semua kode ini ke dalam sebuah Game , tetapi sebaliknya, mengisolasinya dan membuat tipe EnemyCollection . Ini adalah kelas serial yang tidak mewarisi dari apa pun. Kami memberinya metode umum untuk menambahkan musuh dan metode lain untuk memperbarui seluruh koleksi.

 using System.Collections.Generic; [System.Serializable] public class EnemyCollection { List<Enemy> enemies = new List<Enemy>(); public void Add (Enemy enemy) { enemies.Add(enemy); } public void GameUpdate () { for (int i = 0; i < enemies.Count; i++) { if (!enemies[i].GameUpdate()) { int lastIndex = enemies.Count - 1; enemies[i] = enemies[lastIndex]; enemies.RemoveAt(lastIndex); i -= 1; } } } } 

Sekarang Game akan cukup untuk membuat hanya satu koleksi seperti itu, di setiap frame perbarui dan tambahkan musuh yang dibuat untuk itu. Kami akan memperbarui musuh segera setelah kemungkinan pemunculan musuh baru sehingga pembaruan terjadi secara instan.

  EnemyCollection enemies = new EnemyCollection(); … void Update () { … enemies.GameUpdate(); } … void SpawnEnemy () { … enemies.Add(enemy); } 


Musuh bergerak maju.

Gerakan di sepanjang jalan


Musuh sudah bergerak, tetapi sejauh ini tidak mengikuti jalan. Untuk melakukan ini, mereka perlu tahu ke mana harus pergi selanjutnya. Karena itu, mari berikan GameTile properti pengambil yang umum untuk mendapatkan ubin berikutnya di jalur.

  public GameTile NextTileOnPath => nextOnPath; 

Mengetahui ubin tempat Anda ingin keluar, dan ubin tempat Anda perlu mendapatkan, musuh dapat menentukan titik awal dan akhir untuk memindahkan satu ubin. Musuh dapat menginterpolasi posisi antara dua titik ini, melacak pergerakan mereka. Setelah langkah selesai, proses ini diulang untuk ubin berikutnya. Tapi jalurnya bisa berubah kapan saja. Alih-alih menentukan ke mana harus bergerak lebih jauh dalam proses perpindahan, kami hanya melanjutkan untuk bergerak di sepanjang rute yang direncanakan dan memeriksanya, mencapai ubin berikutnya.

Biarkan Enemy melacak kedua ubin sehingga tidak terpengaruh oleh perubahan jalur. Dia juga akan melacak posisi sehingga kita tidak harus menerimanya di setiap bingkai, dan melacak proses pemindahan.

  GameTile tileFrom, tileTo; Vector3 positionFrom, positionTo; float progress; 

Inisialisasi bidang ini di SpawnOn . Titik pertama adalah ubin tempat musuh bergerak, dan titik akhir adalah ubin berikutnya di jalan. Ini mengasumsikan bahwa ubin berikutnya ada, kecuali musuh dibuat di titik akhir, yang seharusnya tidak mungkin. Lalu kami menyimpan posisi ubin dan mengatur ulang kemajuan. Kami tidak perlu mengatur posisi musuh di sini, karena metode GameUpdate nya disebut dalam bingkai yang sama.

  public void SpawnOn (GameTile tile) { //transform.localPosition = tile.transform.localPosition; Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this); tileFrom = tile; tileTo = tile.NextTileOnPath; positionFrom = tileFrom.transform.localPosition; positionTo = tileTo.transform.localPosition; progress = 0f; } 

Peningkatan progres akan dilakukan di GameUpdate . Mari kita tambahkan delta waktu yang konstan sehingga musuh bergerak dengan kecepatan satu ubin per detik. Saat progres selesai, kami menggeser data sehingga To menjadi nilai From , dan To baru adalah ubin berikutnya di jalur. Lalu kami mengurangi kemajuan. Ketika data menjadi relevan, kami menginterpolasi posisi musuh antara From dan To . Karena interpolator mengalami kemajuan, nilainya harus dalam kisaran 0 dan 1, jadi kita dapat menggunakan s Vector3.LerpUnclamped .

  public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; } 

Ini memaksa musuh untuk mengikuti jalan, tetapi tidak akan bertindak ketika mencapai titik akhir. Oleh karena itu, sebelum mengubah posisi From dan To , Anda perlu membandingkan ubin berikutnya di jalur dengan null . Jika demikian, maka kita telah mencapai titik akhir dan musuh telah menyelesaikan pergerakan. Kami menjalankan Reklamasi untuk itu dan mengembalikan false .

  while (progress >= 1f) { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; if (tileTo == null) { OriginFactory.Reclaim(this); return false; } positionFrom = positionTo; positionTo = tileTo.transform.localPosition; progress -= 1f; } 



Musuh mengikuti jalur terpendek.

Musuh sekarang bergerak dari pusat satu ubin ke yang lain. Perlu dipertimbangkan bahwa mereka mengubah keadaan pergerakan mereka hanya di tengah-tengah ubin, karena itu mereka tidak dapat segera menanggapi perubahan di lapangan. Ini berarti bahwa kadang-kadang musuh akan bergerak melalui dinding yang baru saja diatur. Begitu mereka mulai bergerak menuju sel, tidak ada yang akan menghentikan mereka. Itu sebabnya dinding juga membutuhkan jalur nyata.


Musuh bereaksi terhadap mengubah jalur.

Gerakan ujung ke ujung


Gerakan antara pusat ubin dan perubahan arah yang tajam terlihat normal untuk permainan abstrak di mana musuh bergerak kubus, tetapi biasanya gerakan halus terlihat lebih indah. Langkah pertama untuk implementasinya adalah tidak bergerak di sepanjang pusat, tetapi di sepanjang tepi ubin.

Titik tepi antara ubin yang berdekatan dapat ditemukan dengan rata-rata posisi mereka. Alih-alih menghitungnya di setiap langkah untuk setiap musuh, kami hanya akan menghitungnya saat mengubah jalur di GameTile.GrowPathTo . Jadikan tersedia menggunakan properti ExitPoint .

  public Vector3 ExitPoint { get; private set; } … GameTile GrowPathTo (GameTile neighbor) { … neighbor.ExitPoint = (neighbor.transform.localPosition + transform.localPosition) * 0.5f; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; } 

Satu-satunya kasus khusus adalah sel terakhir, titik keluar yang akan menjadi pusatnya.

  public void BecomeDestination () { distance = 0; nextOnPath = null; ExitPoint = transform.localPosition; } 

Ubah Enemy sehingga ia menggunakan titik keluar, bukan pusat ubin.

  public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … positionTo = tileFrom.ExitPoint; progress -= 1f; } transform.localPosition = Vector3.Lerp(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … positionTo = tileFrom.ExitPoint; progress = 0f; } 


Musuh bergerak di antara tepi.

Efek samping dari perubahan ini adalah ketika musuh berbalik karena perubahan jalur, mereka tetap tidak bergerak selama sedetik.


Saat berputar, musuh berhenti.

Orientasi


Meskipun musuh bergerak di sepanjang jalan sampai mereka mengubah orientasi mereka. Agar mereka dapat melihat ke arah gerakan, mereka perlu mengetahui arah jalan yang mereka ikuti. Kami juga akan menentukan ini selama mencari cara, sehingga ini tidak harus dilakukan oleh musuh.

Kami memiliki empat arah: utara, timur, selatan dan barat. Mari kita sebutkan.

 public enum Direction { North, East, South, West } 

Lalu kami memberikan properti GameTile untuk menyimpan arah jalannya.

  public Direction PathDirection { get; private set; } 

Tambahkan parameter arah ke GrowTo , yang menetapkan properti. Karena kita menumbuhkan jalan dari ujung ke awal, arahnya akan berlawanan dengan tempat kita menumbuhkan jalan.

  public GameTile GrowPathNorth () => GrowPathTo(north, Direction.South); public GameTile GrowPathEast () => GrowPathTo(east, Direction.West); public GameTile GrowPathSouth () => GrowPathTo(south, Direction.North); public GameTile GrowPathWest () => GrowPathTo(west, Direction.East); GameTile GrowPathTo (GameTile neighbor, Direction direction) { … neighbor.PathDirection = direction; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; } 

Kita perlu mengubah arah menjadi belokan yang dinyatakan sebagai angka empat. Akan lebih mudah jika kita bisa memanggil GetRotation untuk arahannya, jadi mari kita lakukan ini dengan membuat metode ekstensi. Tambahkan metode statis DirectionExtensions umum, berikan array untuk cache angka empat yang diperlukan, serta metode GetRotation untuk mengembalikan nilai arah yang sesuai. Dalam hal ini, masuk akal untuk menempatkan kelas ekstensi dalam file yang sama dengan tipe enumerasi.

 using UnityEngine; public enum Direction { North, East, South, West } public static class DirectionExtensions { static Quaternion[] rotations = { Quaternion.identity, Quaternion.Euler(0f, 90f, 0f), Quaternion.Euler(0f, 180f, 0f), Quaternion.Euler(0f, 270f, 0f) }; public static Quaternion GetRotation (this Direction direction) { return rotations[(int)direction]; } } 

Apa itu metode ekstensi?
Metode ekstensi adalah metode statis di dalam kelas statis yang berperilaku seperti metode contoh dari beberapa jenis. Tipe ini bisa berupa kelas, antarmuka, struktur, nilai primitif, atau enumerasi. Argumen pertama untuk metode ekstensi harus memiliki this . Ini mendefinisikan nilai dari jenis dan contoh dengan metode yang akan bekerja. Pendekatan ini berarti bahwa memperluas properti tidak dimungkinkan.

Apakah ini memungkinkan Anda untuk menambahkan metode apa pun? Ya, sama seperti Anda dapat menulis metode statis apa pun yang parameternya adalah jenis apa pun.

Sekarang kita dapat memutar Enemy ketika memijah dan setiap kali kita memasukkan ubin baru. Setelah memperbarui data, ubin From memberi kami arahan.

  public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress -= 1f; } transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); return true; } public void SpawnOn (GameTile tile) { … transform.localRotation = tileFrom.PathDirection.GetRotation(); progress = 0f; } 

Ubah arah


Alih-alih mengubah arah secara instan, lebih baik untuk menginterpolasi nilai antar belokan, mirip dengan cara kami menginterpolasi di antara posisi. Untuk berpindah dari satu orientasi ke orientasi lainnya, kita perlu mengetahui perubahan arah yang perlu dilakukan: tanpa berbelok, berbelok ke kanan, berbelok ke kiri atau berbelok ke belakang. Kami menambahkan ini enumerasi, yang lagi-lagi dapat ditempatkan dalam file yang sama dengan Direction , karena mereka kecil dan terkait erat.

 public enum Direction { North, East, South, West } public enum DirectionChange { None, TurnRight, TurnLeft, TurnAround } 

Tambahkan metode ekstensi lain, kali ini GetDirectionChangeTo , yang mengembalikan perubahan arah dari arah saat ini ke yang berikutnya. Jika arahnya bertepatan, maka tidak ada pergeseran. Jika yang berikutnya lebih dari yang sekarang, maka ini adalah belokan ke kanan. Tetapi karena arah diulang, situasi yang sama akan terjadi ketika yang berikutnya adalah tiga kurang dari yang saat ini. Dengan belokan kiri akan sama, hanya penambahan dan pengurangan yang akan mengubah tempat. Satu-satunya kasing yang tersisa adalah giliran kembali.

  public static DirectionChange GetDirectionChangeTo ( this Direction current, Direction next ) { if (current == next) { return DirectionChange.None; } else if (current + 1 == next || current - 3 == next) { return DirectionChange.TurnRight; } else if (current - 1 == next || current + 3 == next) { return DirectionChange.TurnLeft; } return DirectionChange.TurnAround; } 

Kami membuat rotasi hanya dalam satu dimensi, jadi interpolasi linear sudut akan cukup bagi kami. Tambahkan metode ekspansi lain yang mendapatkan sudut arah dalam derajat.

  public static float GetAngle (this Direction direction) { return (float)direction * 90f; } 

Sekarang Anda harus Enemymelacak arah, perubahan arah dan sudut di mana Anda perlu melakukan interpolasi.

  Direction direction; DirectionChange directionChange; float directionAngleFrom, directionAngleTo; 

SpawnOnsemakin sulit, jadi mari kita pindahkan kode persiapan negara ke metode lain. Kami akan menunjuk negara awal musuh sebagai negara pengantar, jadi kami akan menyebutnya PrepareIntro. Dalam keadaan ini, musuh bergerak dari pusat ke tepi ubin awal, sehingga tidak ada perubahan arah. The sudut Fromdan Tosama.

  public void SpawnOn (GameTile tile) { Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this); tileFrom = tile; tileTo = tile.NextTileOnPath; //positionFrom = tileFrom.transform.localPosition; //positionTo = tileFrom.ExitPoint; //transform.localRotation = tileFrom.PathDirection.GetRotation(); progress = 0f; PrepareIntro(); } void PrepareIntro () { positionFrom = tileFrom.transform.localPosition; positionTo = tileFrom.ExitPoint; direction = tileFrom.PathDirection; directionChange = DirectionChange.None; directionAngleFrom = directionAngleTo = direction.GetAngle(); transform.localRotation = direction.GetRotation(); } 

Pada tahap ini, kami membuat sesuatu seperti mesin negara kecil. Untuk mempermudah GameUpdate, pindahkan kode status ke metode baru PrepareNextState. Kami hanya akan meninggalkan perubahan ubin Fromdan To, karena kami menggunakannya di sini untuk memeriksa apakah musuh telah menyelesaikan jalan.

  public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … //positionFrom = positionTo; //positionTo = tileFrom.ExitPoint; //transform.localRotation = tileFrom.PathDirection.GetRotation(); progress -= 1f; PrepareNextState(); } … } 

Saat bertransisi ke keadaan baru, Anda selalu perlu mengubah posisi, menemukan perubahan arah, memperbarui arah saat ini dan menggeser sudut Toke From. Kami tidak lagi mengatur giliran.

  void PrepareNextState () { positionFrom = positionTo; positionTo = tileFrom.ExitPoint; directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection); direction = tileFrom.PathDirection; directionAngleFrom = directionAngleTo; } 

Tindakan lain bergantung pada perubahan arah. Mari kita tambahkan metode untuk setiap opsi. Jika kita bergerak maju, maka sudutnya Tobertepatan dengan arah jalur sel saat ini. Selain itu, kita perlu mengatur rotasi sehingga musuh melihat lurus ke depan.

  void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); } 

Dalam hal belokan, kami tidak berbalik secara instan. Kita perlu melakukan interpolasi ke sudut yang berbeda: 90 Β° lebih untuk berbelok ke kanan, 90 Β° lebih sedikit untuk berbelok ke kiri, dan 180 Β° lebih untuk berbelok ke belakang. Untuk menghindari membelok ke arah yang salah karena perubahan nilai sudut dari 359 Β° ke 0 Β°, sudut Toharus ditunjukkan relatif terhadap arah saat ini. Kami tidak perlu khawatir bahwa sudutnya akan menjadi kurang dari 0 Β° atau lebih dari 360 Β°, karena kami Quaternion.Eulerdapat mengatasinya.

  void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; } 

Pada akhirnya, PrepareNextStatekita dapat menggunakan switchuntuk mengubah arah untuk memutuskan mana dari empat metode untuk memanggil.

  void PrepareNextState () { … switch (directionChange) { case DirectionChange.None: PrepareForward(); break; case DirectionChange.TurnRight: PrepareTurnRight(); break; case DirectionChange.TurnLeft: PrepareTurnLeft(); break; default: PrepareTurnAround(); break; } } 

Sekarang pada akhirnya GameUpdatekita perlu memeriksa apakah arahnya telah berubah. Jika demikian, lalu sisipkan antara kedua sudut dan atur rotasi.

  public bool GameUpdate () { … transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); if (directionChange != DirectionChange.None) { float angle = Mathf.LerpUnclamped( directionAngleFrom, directionAngleTo, progress ); transform.localRotation = Quaternion.Euler(0f, angle, 0f); } return true; } 


Musuh berbalik.

Gerakan kurva


Kita dapat meningkatkan gerakan dengan membuat musuh bergerak sepanjang kurva saat berbelok. Alih-alih berjalan dari tepi ke tepi ubin, biarkan mereka berjalan seperempat lingkaran. Pusat lingkaran ini terletak di sudut umum ubin Fromdan Todi tepi yang sama di mana musuh memasuki ubin From.


Rotasi seperempat lingkaran untuk berbelok ke kanan.

Kita bisa menyadari ini dengan menggerakkan musuh dalam busur menggunakan trigonometri, sementara pada saat yang sama mengubahnya. Tetapi ini dapat disederhanakan dengan hanya menggunakan rotasi, untuk sementara memindahkan asal lokal koordinat musuh ke pusat lingkaran. Untuk melakukan ini, kita perlu mengubah posisi model musuh, jadi kami akan memberikan Enemytautan ke model ini, dapat diakses melalui bidang konfigurasi.

  [SerializeField] Transform model = default; 


Musuh dengan mengacu pada model.

Dalam persiapan untuk bergerak maju atau mundur, model harus bergerak ke posisi standar, ke titik asal koordinat musuh. Jika tidak, model harus digeser setengah unit pengukuran - jari-jari lingkaran rotasi, jauh dari titik balik.

  void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; } 

Sekarang musuh sendiri perlu dipindahkan ke titik balik. Untuk melakukan ini, itu juga harus dipindahkan setengah dari satuan ukuran, tetapi offset yang tepat tergantung pada arah. Mari kita tambahkan Directionmetode ekstensi tambahan untuk ini GetHalfVector.

  static Vector3[] halfVectors = { Vector3.forward * 0.5f, Vector3.right * 0.5f, Vector3.back * 0.5f, Vector3.left * 0.5f }; … public static Vector3 GetHalfVector (this Direction direction) { return halfVectors[(int)direction]; } 

Tambahkan vektor yang sesuai saat berbelok ke kanan atau kiri.

  void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(-0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); } 

Dan ketika kembali, posisi harus menjadi titik awal yang biasa.

  void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = Vector3.zero; transform.localPosition = positionFrom; } 

Selain itu, ketika menghitung titik keluar, kita dapat menggunakan GameTile.GrowPathTosetengah vektor sehingga kita tidak perlu akses ke dua posisi ubin.

  neighbor.ExitPoint = neighbor.transform.localPosition + direction.GetHalfVector(); 

Sekarang, ketika mengubah arah, kita tidak perlu menginterpolasi posisi Enemy.GameUpdate, karena rotasi terlibat dalam gerakan.

  public bool GameUpdate () { … if (directionChange == DirectionChange.None) { transform.localPosition = Vector3.LerpUnclamped(positionFrom, positionTo, progress); } //if (directionChange != DirectionChange.None) { else { float angle = Mathf.LerpUnclamped( directionAngleFrom, directionAngleTo, progress ); transform.localRotation = Quaternion.Euler(0f, angle, 0f); } return true; } 


Musuh dengan lembut menekuk di sudut-sudut.

Kecepatan konstan


Hingga saat ini, kecepatan musuh selalu sama dengan satu ubin per detik, terlepas dari bagaimana mereka bergerak di dalam ubin. Tetapi jarak yang mereka tempuh tergantung pada kondisinya, sehingga kecepatan mereka, dinyatakan dalam satuan per detik, bervariasi. Agar kecepatan ini konstan, kita perlu mengubah kecepatan kemajuan tergantung pada keadaan. Oleh karena itu, tambahkan bidang pengali kemajuan dan gunakan untuk mengatur skala delta di GameUpdate.

  float progress, progressFactor; … public bool GameUpdate () { progress += Time.deltaTime * progressFactor; … } 

Tetapi jika kemajuan berubah tergantung pada negara, nilai kemajuan yang tersisa tidak dapat digunakan secara langsung untuk keadaan berikutnya. Karena itu, sebelum mempersiapkan negara baru, kita perlu menormalkan kemajuan dan menerapkan pengganda baru yang sudah ada di negara baru.

  public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) { … //progress -= 1f; progress = (progress - 1f) / progressFactor; PrepareNextState(); progress *= progressFactor; } … } 

Bergerak maju tidak memerlukan perubahan, oleh karena itu, ia menggunakan faktor 1. Ketika berbelok ke kanan atau kiri, musuh melewati seperempat lingkaran dengan jari-jari Β½, sehingga jarak yang ditempuh adalah ΒΌΟ€. progresssama dengan satu dibagi dengan nilai ini. Menghidupkan kembali seharusnya tidak terlalu banyak waktu, jadi gandakan kemajuannya sehingga butuh setengah detik. Akhirnya, gerakan pengantar hanya mencakup setengah dari ubin, oleh karena itu, untuk mempertahankan kecepatan konstan, kemajuannya juga perlu digandakan.

  void PrepareForward () { … progressFactor = 1f; } void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { … progressFactor = 2f; } void PrepareIntro () { … progressFactor = 2f; } 

Mengapa jaraknya sama dengan 1/4 * pi?
Lingkar adalah 2Ο€ kali radius. Beralih ke kanan atau kiri hanya mencakup seperempat dari panjang ini, dan jari-jarinya Β½, sehingga jaraknya Β½Ο€ Γ— Β½.

Keadaan akhir


Karena kita memiliki status pengantar, mari kita tambahkan yang terakhir. Musuh saat ini menghilang segera setelah mencapai titik akhir, tetapi mari kita menunda hilangnya mereka sampai mereka mencapai pusat ubin akhir. Mari kita buat metode untuk ini PrepareOutro, atur gerakan maju, tetapi hanya ke tengah ubin dengan kemajuan berlipat untuk mempertahankan kecepatan konstan.

  void PrepareOutro () { positionTo = tileFrom.transform.localPosition; directionChange = DirectionChange.None; directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; transform.localRotation = direction.GetRotation(); progressFactor = 2f; } 

Agar GameUpdatetidak menghancurkan musuh terlalu dini, kami akan menghapus pergeseran ubin dari itu. Dia akan melakukannya sekarang PrepareNextState. Dengan demikian, memeriksa nullpengembalian truehanya setelah akhir dari keadaan akhir.

  public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) { //tileFrom = tileTo; //tileTo = tileTo.NextTileOnPath; if (tileTo == null) { OriginFactory.Reclaim(this); return false; } … } … } 

Di PrepareNextStatekita akan mulai dengan pergeseran ubin. Kemudian, setelah mengatur posisi From, tetapi sebelum mengatur posisi, Tokami akan memeriksa apakah ubin sama dengan Tonilainya null. Jika demikian, maka persiapkan keadaan akhir dan lewati sisa metode.

  void PrepareNextState () { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; if (tileTo == null) { PrepareOutro(); return; } positionTo = tileFrom.ExitPoint; … } 


Musuh dengan kecepatan konstan dan keadaan akhir.

Variabilitas musuh


Kami memiliki aliran musuh, dan mereka semua adalah kubus yang sama, bergerak dengan kecepatan yang sama. Hasilnya lebih seperti ular panjang daripada musuh individu. Mari kita membuatnya lebih berbeda dengan mengacak ukuran, perpindahan, dan kecepatan mereka.

Rentang Nilai Float


Kami akan mengubah parameter musuh, secara acak memilih karakteristik mereka dari rentang nilai. Struktur FloatRangeyang kami buat di artikel Manajemen Objek, Konfigurasi Bentuk akan berguna di sini , jadi mari kita salin. Satu-satunya perubahan adalah menambahkan konstruktor dengan satu parameter dan membuka akses ke minimum dan maksimum menggunakan properti readonly, sehingga intervalnya tidak dapat diubah.

 using UnityEngine; [System.Serializable] public struct FloatRange { [SerializeField] float min, max; public float Min => min; public float Max => max; public float RandomValueInRange { get { return Random.Range(min, max); } } public FloatRange(float value) { min = max = value; } public FloatRange (float min, float max) { this.min = min; this.max = max < min ? min : max; } } 

Kami juga menyalin atribut yang disetel untuk membatasi intervalnya.

 using UnityEngine; public class FloatRangeSliderAttribute : PropertyAttribute { public float Min { get; private set; } public float Max { get; private set; } public FloatRangeSliderAttribute (float min, float max) { Min = min; Max = max < min ? min : max; } } 

Kami hanya perlu visualisasi slider, jadi salin FloatRangeSliderDrawerke folder Editor .

 using UnityEditor; using UnityEngine; [CustomPropertyDrawer(typeof(FloatRangeSliderAttribute))] public class FloatRangeSliderDrawer : PropertyDrawer { public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { int originalIndentLevel = EditorGUI.indentLevel; EditorGUI.BeginProperty(position, label, property); position = EditorGUI.PrefixLabel( position, GUIUtility.GetControlID(FocusType.Passive), label ); EditorGUI.indentLevel = 0; SerializedProperty minProperty = property.FindPropertyRelative("min"); SerializedProperty maxProperty = property.FindPropertyRelative("max"); float minValue = minProperty.floatValue; float maxValue = maxProperty.floatValue; float fieldWidth = position.width / 4f - 4f; float sliderWidth = position.width / 2f; position.width = fieldWidth; minValue = EditorGUI.FloatField(position, minValue); position.x += fieldWidth + 4f; position.width = sliderWidth; FloatRangeSliderAttribute limit = attribute as FloatRangeSliderAttribute; EditorGUI.MinMaxSlider( position, ref minValue, ref maxValue, limit.Min, limit.Max ); position.x += sliderWidth + 4f; position.width = fieldWidth; maxValue = EditorGUI.FloatField(position, maxValue); if (minValue < limit.Min) { minValue = limit.Min; } if (maxValue < minValue) { maxValue = minValue; } else if (maxValue > limit.Max) { maxValue = limit.Max; } minProperty.floatValue = minValue; maxProperty.floatValue = maxValue; EditorGUI.EndProperty(); EditorGUI.indentLevel = originalIndentLevel; } } 

Skala model


Kami akan mulai dengan mengubah skala musuh. Tambahkan EnemyFactorypengaturan skala ke opsi. Interval skala seharusnya tidak terlalu besar, tetapi cukup untuk membuat varietas musuh miniatur dan raksasa. Apa pun dalam 0,5-2 dengan nilai standar 1. Kami akan memilih skala acak dalam interval ini Getdan memberikannya kepada musuh melalui metode baru Initialize.

  [SerializeField, FloatRangeSlider(0.5f, 2f)] FloatRange scale = new FloatRange(1f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize(scale.RandomValueInRange); return instance; } 

Metode ini Enemy.Initializehanya mengatur skala modelnya yang sama di semua dimensi.

  public void Initialize (float scale) { model.localScale = new Vector3(scale, scale, scale); } 

inspektur

pemandangan

Kisaran skala adalah 0,5-1,5.

Jalur offset


Untuk lebih menghancurkan keseragaman aliran musuh, kita dapat mengubah posisi relatif mereka di dalam ubin. Mereka bergerak maju, sehingga pergeseran ke arah ini hanya mengubah waktu gerakan mereka, yang tidak terlalu terlihat. Oleh karena itu, kami akan menggeser mereka ke samping, menjauh dari jalur ideal yang melewati pusat ubin. Tambahkan EnemyFactoryjalur offset ke interval dan berikan offset acak ke metode Initialize. Offset bisa negatif atau positif, tetapi tidak lebih dari Β½, karena ini akan memindahkan musuh ke ubin tetangga. Selain itu, kami tidak ingin musuh melampaui ubin yang mereka ikuti, jadi sebenarnya intervalnya akan berkurang, misalnya 0,4, tetapi batas sebenarnya tergantung pada ukuran musuh.

  [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; } 

Karena perpindahan jalur memengaruhi jalur yang dilalui, Enemymaka perlu dilacak.

  float pathOffset; … public void Initialize (float scale, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.pathOffset = pathOffset; } 

Saat bergerak tepat lurus (selama gerakan pengantar, akhir, atau normal ke depan), kami cukup menerapkan offset langsung ke model. Hal yang sama terjadi ketika Anda kembali. Dengan belokan kanan atau kiri, kami telah memindahkan model, yang menjadi relatif terhadap perpindahan jalur.

  void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); model.localPosition = new Vector3(pathOffset, 0f); progressFactor = 1f; } void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; model.localPosition = new Vector3(pathOffset - 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; model.localPosition = new Vector3(pathOffset + 0.5f, 0f); transform.localPosition = positionFrom + direction.GetHalfVector(); progressFactor = 1f / (Mathf.PI * 0.25f); } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 2f; } void PrepareIntro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; } void PrepareOutro () { … model.localPosition = new Vector3(pathOffset, 0f); transform.localRotation = direction.GetRotation(); progressFactor = 2f; } 

Karena perpindahan jalur selama rotasi mengubah jari-jari, kita perlu mengubah proses penghitungan progres pengganda. Jalur offset harus dikurangi dari Β½ untuk mendapatkan jari-jari rotasi ke kanan, dan ditambahkan jika belok ke kiri.

  void PrepareTurnRight () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f + pathOffset)); } 

Kami juga mendapatkan radius belokan saat memutar 180 Β°. Dalam hal ini, kita menutupi setengah lingkaran dengan jari-jari yang sama dengan offset lintasan, sehingga jaraknya Ο€ kali offset. Namun, ini tidak berfungsi ketika perpindahannya nol, dan pada perpindahan kecil, belokannya terlalu cepat. Untuk menghindari belokan instan, kita dapat memaksa radius minimum untuk menghitung kecepatan, katakanlah 0,2.

  void PrepareTurnAround () { directionAngleTo = directionAngleFrom + (pathOffset < 0f ? 180f : -180f); model.localPosition = new Vector3(pathOffset, 0f); transform.localPosition = positionFrom; progressFactor = 1f / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); } 

inspektur


Jalur offset berada dalam kisaran βˆ’0.25–0.25.

Perhatikan bahwa sekarang musuh tidak pernah mengubah perpindahan jalur relatifnya, bahkan saat berbelok. Karena itu, panjang total jalur untuk setiap musuh memiliki jalurnya sendiri.

Untuk mencegah musuh mencapai ubin tetangga, seseorang juga harus memperhitungkan skala maksimum yang dimungkinkan. Saya hanya membatasi ukuran ke nilai maksimum 1, sehingga offset maksimum yang diijinkan untuk kubus adalah 0,25. Jika ukuran maksimum 1,5, maka perpindahan maksimum harus dikurangi menjadi 0,125.

Kecepatan


Hal terakhir yang kami acakkan adalah kecepatan musuh. Kami menambahkan satu interval lagi untuk itu EnemyFactorydan kami akan mentransfer nilai ke salinan musuh yang dibuat. Mari kita jadikan argumen kedua untuk metode ini Initialize. Musuh seharusnya tidak terlalu lambat atau cepat sehingga permainan tidak menjadi hal yang sederhana atau sulit. Mari kita batasi intervalnya menjadi 0,2–5. Kecepatan dinyatakan dalam satuan per detik, yang sesuai dengan ubin per detik hanya saat bergerak maju.

  [SerializeField, FloatRangeSlider(0.2f, 5f)] FloatRange speed = new FloatRange(1f); [SerializeField, FloatRangeSlider(-0.4f, 0.4f)] FloatRange pathOffset = new FloatRange(0f); public Enemy Get () { Enemy instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; instance.Initialize( scale.RandomValueInRange, speed.RandomValueInRange, pathOffset.RandomValueInRange ); return instance; } 

Sekarang saya Enemyharus melacak dan mempercepat.

  float speed; … public void Initialize (float scale, float speed, float pathOffset) { model.localScale = new Vector3(scale, scale, scale); this.speed = speed; this.pathOffset = pathOffset; } 

Ketika kami tidak menetapkan kecepatan secara eksplisit, kami hanya selalu menggunakan nilai 1. Sekarang kita hanya perlu membuat ketergantungan pengali kemajuan pada kecepatan.

  void PrepareForward () { … progressFactor = speed; } void PrepareTurnRight () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f - pathOffset)); } void PrepareTurnLeft () { … progressFactor = speed / (Mathf.PI * 0.5f * (0.5f + pathOffset)); } void PrepareTurnAround () { … progressFactor = speed / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f)); } void PrepareIntro () { … progressFactor = 2f * speed; } void PrepareOutro () { … progressFactor = 2f * speed; } 



Kecepatan di kisaran 0,75-1,25.

Jadi, kami mendapat aliran musuh yang indah bergerak ke titik akhir. Dalam tutorial selanjutnya kita akan belajar cara menghadapinya. Ingin tahu kapan akan dirilis? Ikuti halaman saya di Patreon !

repositori

artikel PDF

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


All Articles