Buat Shooter Vektor Neon di XNA: Lebih Banyak Gameplay
Indonesian (Bahasa Indonesia) translation by Suci Rohini (you can also view the original English article)
Dalam seri tutorial ini, saya akan menunjukkan cara membuat shooter tongkat neon, seperti Geometri Wars, di XNA. Tujuan dari tutorial ini bukan untuk meninggalkan Anda dengan replika Perang Geometri yang sama persis, melainkan untuk membahas elemen-elemen penting yang memungkinkan Anda membuat varian berkualitas tinggi sendiri.
Overview
Pada bagian ini kita akan membangun pada tutorial sebelumnya dengan menambahkan musuh, deteksi tabrakan dan scoring.
Inilah yang akan kita miliki pada akhirnya:
Kita akan menambahkan kelas-kelas baru berikut untuk menangani ini:
Musuh
-
EnemySpawner
: Bertanggung jawab untuk menciptakan musuh dan secara bertahap meningkatkan kesulitan permainan. -
PlayerStatus
: Trek Skor pemain, tinggi Skor dan kehidupan.
Anda mungkin telah memperhatikan ada dua jenis musuh dalam video, tetapi hanya ada satu kelas Enemy
. Kita dapat menurunkan subclass dari Enemy
untuk setiap tipe musuh. Namun, saya lebih memilih untuk menghindari hierarki kelas dalam karena mereka memiliki beberapa kelemahan:
- Mereka menambahkan lebih banyak kode boiler.
- Mereka dapat meningkatkan kompleksitas kode dan membuatnya lebih sulit untuk dipahami. Keadaan dan fungsionalitas suatu objek menjadi tersebar di seluruh rantai warisannya.
- Mereka tidak sangat fleksibel. Anda tidak bisa berbagi potongan fungsi antara cabang-cabang yang berbeda dari pohon warisan jika fungsi itu tidak di kelas dasar. Misalnya, pertimbangkan membuat dua kelas,
Mamalia
danBurung
, yang keduanya berasal dariHewan
. KelasBurung
memiliki metodeFly()
. Kemudian Anda memutuskan untuk menambahkan kelasKelelawar
yang berasal dariMamalia
dan juga bisa terbang. Untuk berbagi fungsi ini hanya dengan menggunakan warisan, Anda harus memindahkan metodeFly()
ke kelasHewan
yang bukan miliknya. Selain itu, Anda tidak dapat menghapus metode dari kelas turunan, jadi jika Anda membuat kelasPinguin
yang berasal dariBurung
, itu juga akan memiliki metodeFly()
.
Untuk tutorial ini, kita akan mendukung komposisi lebih dari warisan untuk mengimplementasikan berbagai jenis musuh. Kita akan melakukan ini dengan menciptakan berbagai perilaku yang dapat digunakan kembali yang dapat kita tambahkan ke musuh. Kita kemudian dapat dengan mudah mencampur dan mencocokkan perilaku ketika kita membuat jenis musuh baru. Sebagai contoh, jika kita sudah memiliki perilaku FollowPlayer
dan perilaku DodgeBullet
, kita bisa membuat musuh baru yang melakukan keduanya hanya dengan menambahkan kedua perilaku.
Musuh
Musuh akan memiliki beberapa properti tambahan atas entitas. Untuk memberikan pemain beberapa waktu untuk bereaksi, kita akan membuat musuh secara bertahap memudar sebelum mereka menjadi aktif dan berbahaya.
Mari kode struktur dasar dari kelas Musuh
.
class Enemy : Entity { private int timeUntilStart = 60; public bool IsActive { get { return timeUntilStart <= 0; } } public Enemy(Texture2D image, Vector2 position) { this.image = image; Position = position; Radius = image.Width / 2f; color = Color.Transparent; } public override void Update() { if (timeUntilStart <= 0) { // enemy behaviour logic goes here. } else { timeUntilStart--; color = Color.White * (1 - timeUntilStart / 60f); } Position += Velocity; Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2); Velocity *= 0.8f; } public void WasShot() { IsExpired = true; } }
Kode ini akan membuat musuh memudar untuk 60 frame dan akan memungkinkan kecepatan mereka berfungsi. Mengalikan kecepatan sebesar 0.8 memunculkan efek gesekan. Jika kita membuat musuh berakselerasi dengan laju yang konstan, gesekan ini akan menyebabkan mereka dengan lancar mendekati kecepatan maksimum. Saya suka kesederhanaan dan kehalusan dari jenis gesekan ini, tetapi Anda mungkin ingin menggunakan rumus yang berbeda tergantung pada efek yang Anda inginkan.
Metode WasShot()
akan dipanggil ketika musuh ditembak. Kita akan menambahkan lebih banyak nanti di seri.
Kita ingin jenis musuh untuk berperilaku berbeda. Kita akan mencapai hal ini dengan menetapkan perilaku. Perilaku akan menggunakan beberapa fungsi kustom yang berjalan setiap frame untuk mengontrol musuh. Kita akan menerapkan perilaku yang menggunakan sebuah iterator.
Iterators (juga disebut generator) dalam C# adalah metode khusus yang dapat berhenti di tengah jalan dan kemudian melanjutkan di mana mereka tinggalkan. Anda dapat membuat iterator dengan membuat metode dengan jenis pengembalian IEnumerable<>
dan menggunakan kata kunci yield di mana Anda ingin mengembalikannya dan kemudian melanjutkan. Iterator di C# mengharuskan Anda untuk mengembalikan sesuatu ketika Anda menghasilkan. Kita tidak benar-benar perlu mengembalikan apa pun, sehingga iterator kita hanya akan menghasilkan nol.
Perilaku paling sederhana kita adalah perilaku FollowPlayer()
yang ditunjukkan di bawah ini.
IEnumerable<int> FollowPlayer(float acceleration = 1f) { while (true) { Velocity += (PlayerShip.Instance.Position - Position).ScaleTo(acceleration); if (Velocity != Vector2.Zero) Orientation = Velocity.ToAngle(); yield return 0; } }
Ini hanya membuat musuh berakselerasi ke arah pemain dengan laju yang konstan. Gesekan yang kita tambahkan sebelumnya akan memastikannya akhirnya mencapai kecepatan maksimum (5 piksel per frame saat akselerasi 1 sejak \(0,8 \ 5 + 1 = 5 \)). Setiap frame, metode ini akan berjalan sampai menyentuh pernyataan hasil dan kemudian akan melanjutkan di mana ia meninggalkan frame berikutnya.
Anda mungkin bertanya-tanya mengapa kita terganggu dengan iterator sama sekali, karena kita bisa menyelesaikan tugas yang sama lebih mudah dengan delegasi yang sederhana. Menggunakan iterator terbayar dengan metode yang lebih kompleks yang akan mengharuskan kita untuk menyimpan status dalam variabel anggota di kelas.
Misalnya, di bawah ini adalah perilaku yang membuat musuh bergerak dalam pola persegi:
IEnumerable<int> MoveInASquare() { const int framesPerSide = 30; while (true) { // move right for 30 frames for (int i = 0; i < framesPerSide; i++) { Velocity = Vector2.UnitX; yield return 0; } // move down for (int i = 0; i < framesPerSide; i++) { Velocity = Vector2.UnitY; yield return 0; } // move left for (int i = 0; i < framesPerSide; i++) { Velocity = -Vector2.UnitX; yield return 0; } // move up for (int i = 0; i < framesPerSide; i++) { Velocity = -Vector2.UnitY; yield return 0; } } }
Apa yang baik tentang ini adalah bahwa itu tidak hanya menyelamatkan kita beberapa variabel instan, tetapi juga menyusun kode dengan cara yang sangat logis. Anda dapat langsung melihat bahwa musuh akan bergerak ke kanan, lalu ke bawah, lalu ke kiri, lalu ke atas, dan kemudian ulangi. Jika Anda menerapkan metode ini sebagai state machine, aliran kontrol akan kurang jelas.
Mari kita tambahkan perancah yang diperlukan untuk membuat perilaku bekerja. Musuh perlu menyimpan perilaku mereka, jadi kita akan menambahkan variabel ke kelas Musuh
.
private List<IEnumerator<int>> behaviours = new List<IEnumerator<int>>();
Perhatikan bahwa perilaku memiliki tipe IEnumerator<int>
, bukan IEnumerable<int>
. Anda dapat menganggap IEnumerable
sebagai template untuk perilaku dan IEnumerator
sebagai contoh yang sedang berjalan. IEnumerator
ingat di mana kita berada dalam perilaku dan akan mengambil di mana ia tinggalkan ketika Anda memanggil metode MoveNext()
. Setiap frame kita akan melalui semua perilaku musuh dan memanggil MoveNext()
pada masing-masing. Jika MoveNext()
mengembalikan false, itu berarti perilaku telah selesai sehingga kita harus menghapusnya dari daftar.
Kita akan menambahkan metode berikut ke kelas Musuh
:
private void AddBehaviour(IEnumerable<int> behaviour) { behaviours.Add(behaviour.GetEnumerator()); } private void ApplyBehaviours() { for (int i = 0; i < behaviours.Count; i++) { if (!behaviours[i].MoveNext()) behaviours.RemoveAt(i--); } }
Dan kita akan memodifikasi metode Update()
untuk memanggil ApplyBehaviours()
:
if (timeUntilStart <= 0) ApplyBehaviours(); // ...
Sekarang kita bisa membuat metode statis untuk menciptakan musuh yang mencari. Yang harus kita lakukan adalah memilih gambar yang kita inginkan dan menambahkan perilaku FollowPlayer()
.
public static Enemy CreateSeeker(Vector2 position) { var enemy = new Enemy(Art.Seeker, position); enemy.AddBehaviour(enemy.FollowPlayer()); return enemy; }
Untuk membuat musuh yang bergerak secara acak, kita akan memilih arah dan kemudian melakukan sedikit penyesuaian acak ke arah itu. Namun, jika kita menyesuaikan arah setiap frame, gerakan akan gelisah, jadi kami hanya akan menyesuaikan arah secara berkala. Jika musuh berlari ke tepi layar, kita akan memilih arah acak baru yang menjauh dari dinding.
IEnumerable<int> MoveRandomly() { float direction = rand.NextFloat(0, MathHelper.TwoPi); while (true) { direction += rand.NextFloat(-0.1f, 0.1f); direction = MathHelper.WrapAngle(direction); for (int i = 0; i < 6; i++) { Velocity += MathUtil.FromPolar(direction, 0.4f); Orientation -= 0.05f; var bounds = GameRoot.Viewport.Bounds; bounds.Inflate(-image.Width, -image.Height); // if the enemy is outside the bounds, make it move away from the edge if (!bounds.Contains(Position.ToPoint())) direction = (GameRoot.ScreenSize / 2 - Position).ToAngle() + rand.NextFloat(-MathHelper.PiOver2, MathHelper.PiOver2); yield return 0; } } }
Kita sekarang dapat membuat metode pabrik untuk menciptakan musuh yang berkeliaran, seperti yang kita lakukan untuk para pencari:
public static Enemy CreateWanderer(Vector2 position) { var enemy = new Enemy(Art.Wanderer, position); enemy.AddBehaviour(enemy.MoveRandomly()); return enemy; }
Deteksi Tabrakan
Untuk deteksi tabrakan, kita akan memodelkan kapal pemain, musuh, dan peluru sebagai lingkaran. Deteksi tabrakan sirkular bagus karena sederhana, cepat, dan tidak berubah ketika objek berputar. Jika Anda ingat, kelas Entitas
memiliki radius dan posisi (posisi mengacu pada pusat entitas). Ini yang kita butuhkan untuk mendeteksi tabrakan melingkar.
Menguji setiap entitas terhadap semua entitas lain yang berpotensi bertabrakan bisa sangat lambat jika Anda memiliki sejumlah besar entitas. Ada banyak teknik yang dapat Anda gunakan untuk mempercepat deteksi tabrakan fase luas, seperti quadtrees, sweep dan prune, dan pohon BSP. Namun, untuk saat ini, kita hanya akan memiliki beberapa lusin entitas di layar pada satu waktu, jadi kita tidak akan khawatir tentang teknik yang lebih rumit ini. Kita selalu dapat menambahkannya nanti jika kita membutuhkannya.
Dalam Shape Blaster, tidak setiap entitas dapat bertabrakan dengan setiap jenis entitas lainnya. Peluru dan kapal pemain bisa bertabrakan hanya dengan musuh. Musuh juga dapat bertabrakan dengan musuh lainnya - ini akan mencegah mereka dari tumpang tindih.
Untuk menangani berbagai jenis tabrakan ini, kita akan menambahkan dua daftar baru ke EntityManager
untuk melacak peluru dan musuh. Setiap kali kita menambahkan entitas ke EntityManager
, kita ingin menambahkannya ke daftar yang sesuai, jadi kita akan membuat metode AddEntity()
pribadi untuk melakukannya. Kita juga akan menghapus entitas yang telah kedaluwarsa dari semua daftar setiap frame.
static List<Enemy> enemies = new List<Enemy>(); static List<Bullet> bullets = new List<Bullet>(); private static void AddEntity(Entity entity) { entities.Add(entity); if (entity is Bullet) bullets.Add(entity as Bullet); else if (entity is Enemy) enemies.Add(entity as Enemy); } // ... // in Update() bullets = bullets.Where(x => !x.IsExpired).ToList(); enemies = enemies.Where(x => !x.IsExpired).ToList();
Ganti panggilan ke entity.Add()
di EntityManager.Add()
dan EntityManager.Update()
dengan panggilan ke AddEntity()
.
Sekarang mari tambahkan metode yang akan menentukan apakah dua entitas bertabrakan:
private static bool IsColliding(Entity a, Entity b) { float radius = a.Radius + b.Radius; return !a.IsExpired && !b.IsExpired && Vector2.DistanceSquared(a.Position, b.Position) < radius * radius; }
Untuk menentukan apakah dua lingkaran tumpang tindih, cukup periksa apakah jarak antara mereka kurang dari jumlah jari-jari mereka. Metode kita mengoptimalkan ini sedikit dengan memeriksa apakah kuadrat jarak kurang dari kuadrat dari jumlah jari-jari. Ingat bahwa itu sedikit lebih cepat untuk menghitung jarak kuadrat dari jarak sebenarnya.
Hal yang berbeda akan terjadi tergantung pada dua benda yang bertabrakan. Jika dua musuh bertabrakan, kita ingin mereka saling mendorong. Jika peluru menghantam musuh, peluru dan musuh harus dihancurkan. Jika pemain menyentuh musuh, pemain harus mati dan levelnya harus disetel ulang.
Kita akan menambahkan metode HandleCollision()
ke kelas Musuh
untuk menangani tabrakan antar musuh:
public void HandleCollision(Enemy other) { var d = Position - other.Position; Velocity += 10 * d / (d.LengthSquared() + 1); }
Metode ini akan mendorong musuh saat ini menjauh dari musuh lainnya. Semakin dekat mereka, semakin sulit untuk didorong, karena besarnya (d / d.LengthSquared())
hanya satu dari kejauhan.
Merespons Pemain
Selanjutnya kita membutuhkan metode untuk menangani kapal pemain yang terbunuh. Ketika ini terjadi, kapal pemain akan menghilang untuk waktu yang singkat sebelum respawning.
Kita mulai dengan menambahkan dua anggota baru ke PlayerShip
.
int framesUntilRespawn = 0; public bool IsDead { get { return framesUntilRespawn > 0; } }
Pada awal PlayerShip.Update()
, tambahkan yang berikut:
if (IsDead) { framesUntilRespawn--; return; }
Dan kita menimpa Draw()
seperti yang ditunjukkan:
public override void Draw(SpriteBatch spriteBatch) { if (!IsDead) base.Draw(spriteBatch); }
Akhirnya, kita menambahkan metode Kill()
ke PlayerShip
.
public void Kill() { framesUntilRespawn = 60; }
Sekarang bahwa semua potongan berada di tempat, kita akan menambahkan sebuah metode untuk EntityManager
yang melewati semua entitas dan cek untuk tabrakan.
static void HandleCollisions() { // handle collisions between enemies for (int i = 0; i < enemies.Count; i++) for (int j = i + 1; j < enemies.Count; j++) { if (IsColliding(enemies[i], enemies[j])) { enemies[i].HandleCollision(enemies[j]); enemies[j].HandleCollision(enemies[i]); } } // handle collisions between bullets and enemies for (int i = 0; i < enemies.Count; i++) for (int j = 0; j < bullets.Count; j++) { if (IsColliding(enemies[i], bullets[j])) { enemies[i].WasShot(); bullets[j].IsExpired = true; } } // handle collisions between the player and enemies for (int i = 0; i < enemies.Count; i++) { if (enemies[i].IsActive && IsColliding(PlayerShip.Instance, enemies[i])) { PlayerShip.Instance.Kill(); enemies.ForEach(x => x.WasShot()); break; } } }
Panggil metode ini dari Update()
segera setelah pengaturan IsUpdating
ke true
.
Musuh Spawner
Hal terakhir yang harus dilakukan adalah membuat kelas EnemySpawner
, yang bertanggung jawab untuk menciptakan musuh. Kita ingin permainan dimulai dengan mudah dan menjadi lebih sulit, sehingga EnemySpawner
akan menciptakan musuh dengan laju yang semakin meningkat seiring berjalannya waktu. Ketika pemain itu mati, kita akan mereset EnemySpawner
ke kesulitan awalnya.
static class EnemySpawner { static Random rand = new Random(); static float inverseSpawnChance = 60; public static void Update() { if (!PlayerShip.Instance.IsDead && EntityManager.Count < 200) { if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateSeeker(GetSpawnPosition())); if (rand.Next((int)inverseSpawnChance) == 0) EntityManager.Add(Enemy.CreateWanderer(GetSpawnPosition())); } // slowly increase the spawn rate as time progresses if (inverseSpawnChance > 20) inverseSpawnChance -= 0.005f; } private static Vector2 GetSpawnPosition() { Vector2 pos; do { pos = new Vector2(rand.Next((int)GameRoot.ScreenSize.X), rand.Next((int)GameRoot.ScreenSize.Y)); } while (Vector2.DistanceSquared(pos, PlayerShip.Instance.Position) < 250 * 250); return pos; } public static void Reset() { inverseSpawnChance = 60; } }
Setiap frame, ada satu di inverseSpawnChance
menghasilkan masing-masing jenis musuh. Peluang pemijahan musuh secara bertahap meningkat sampai mencapai maksimum satu dari dua puluh. Musuh selalu dibuat setidaknya 250 piksel dari pemain.
Hati-hati tentang loop sementara di GetSpawnPosition()
. Ini akan bekerja secara efisien selama area di mana musuh dapat bertelur lebih besar dari area di mana mereka tidak dapat bertelur. Namun, jika Anda membuat area terlarang terlalu besar, Anda akan mendapatkan lingkaran tak terbatas.
Hubungi EnemySpawner.Update()
dari GameRoot.Update()
dan panggil EnemySpawner.Reset()
saat pemain terbunuh.
Skor dan Nyawa
Dalam Shape Blaster, Anda akan memulai dengan empat kehidupan, dan akan mendapatkan kehidupan tambahan setiap 2.000 poin. Anda menerima poin untuk menghancurkan musuh, dengan berbagai jenis musuh yang bernilai poin dalam jumlah yang berbeda. Setiap musuh yang hancur juga meningkatkan pengganda nilai Anda dengan satu. Jika Anda tidak membunuh musuh dalam waktu singkat, pengali Anda akan disetel ulang. Jumlah total poin yang diterima dari setiap musuh yang Anda hancurkan adalah jumlah poin musuh bernilai dikalikan dengan pengganda Anda saat ini. Jika Anda kehilangan seluruh hidup Anda, permainan berakhir dan Anda memulai permainan baru dengan skor Anda disetel ke nol.
Untuk menangani semua ini, kita akan membuat kelas statis yang disebut PlayerStatus
.
static class PlayerStatus { // amount of time it takes, in seconds, for a multiplier to expire. private const float multiplierExpiryTime = 0.8f; private const int maxMultiplier = 20; public static int Lives { get; private set; } public static int Score { get; private set; } public static int Multiplier { get; private set; } private static float multiplierTimeLeft; // time until the current multiplier expires private static int scoreForExtraLife; // score required to gain an extra life // Static constructor static PlayerStatus() { Reset(); } public static void Reset() { Score = 0; Multiplier = 1; Lives = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; } public static void Update() { if (Multiplier > 1) { // update the multiplier timer if ((multiplierTimeLeft -= (float)GameRoot.GameTime.ElapsedGameTime.TotalSeconds) <= 0) { multiplierTimeLeft = multiplierExpiryTime; ResetMultiplier(); } } } public static void AddPoints(int basePoints) { if (PlayerShip.Instance.IsDead) return; Score += basePoints * Multiplier; while (Score >= scoreForExtraLife) { scoreForExtraLife += 2000; Lives++; } } public static void IncreaseMultiplier() { if (PlayerShip.Instance.IsDead) return; multiplierTimeLeft = multiplierExpiryTime; if (Multiplier < maxMultiplier) Multiplier++; } public static void ResetMultiplier() { Multiplier = 1; } public static void RemoveLife() { Lives--; } }
Panggil PlayerStatus.Update()
dari GameRoot.Update()
saat game tidak dijeda.
Selanjutnya kita ingin menampilkan skor Anda, kehidupan, dan pengganda di layar. Untuk melakukan ini, kita perlu menambahkan SpriteFont
di proyek Konten
dan variabel yang sesuai di kelas Seni
, yang akan kita beri nama Font
. Muat font di Art.Load()
seperti yang kita lakukan dengan tekstur.
Ubah akhir GameRoot.Draw()
di mana kursor digambar seperti yang ditunjukkan di bawah ini.
spriteBatch.Begin(0, BlendState.Additive); spriteBatch.DrawString(Art.Font, "Lives: " + PlayerStatus.Lives, new Vector2(5), Color.White); DrawRightAlignedString("Score: " + PlayerStatus.Score, 5); DrawRightAlignedString("Multiplier: " + PlayerStatus.Multiplier, 35); // draw the custom mouse cursor spriteBatch.Draw(Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End();
DrawRightAlignedString()
adalah metode pembantu untuk menggambar teks sejajar di sisi kanan layar. Tambahkan ke GameRoot
dengan menambahkan kode di bawah ini.
private void DrawRightAlignedString(string text, float y) { var textWidth = Art.Font.MeasureString(text).X; spriteBatch.DrawString(Art.Font, text, new Vector2(ScreenSize.X - textWidth - 5, y), Color.White); }
Sekarang hidup Anda, skor, dan pengganda akan ditampilkan di layar. Namun, kita masih perlu memodifikasi nilai-nilai ini sebagai tanggapan terhadap acara game. Tambahkan properti yang disebut PointValue
ke kelas Musuh
.
public int PointValue { get; private set; }
Atur nilai poin untuk musuh yang berbeda untuk sesuatu yang Anda rasa sesuai. Saya membuat musuh yang mengembara bernilai satu poin, dan musuh yang mencari bernilai dua poin.
Selanjutnya, tambahkan dua baris berikut ke Enemy.WasShot()
untuk meningkatkan skor dan pengganda pemain:
PlayerStatus.AddPoints(PointValue); PlayerStatus.IncreaseMultiplier();
Panggil PlayerStatus.RemoveLife()
di PlayerShip.Kill()
. Jika pemain kehilangan seluruh hidup mereka, panggil PlayerStatus.Reset()
untuk mengatur ulang skor mereka dan tinggal di awal permainan baru.
Skor tinggi
Mari tambahkan kemampuan game untuk melacak skor terbaik Anda. Kita ingin skor ini bertahan sepanjang permainan jadi kita akan menyimpannya ke file. Kita akan membuatnya sangat sederhana dan menyimpan skor tinggi sebagai nomor teks-tunggal dalam file di direktori kerja saat ini (ini akan menjadi direktori yang sama yang berisi file .exe
permainan).
Tambahkan metode berikut ke PlayerStatus
:
private const string highScoreFilename = "highscore.txt"; private static int LoadHighScore() { // return the saved high score if possible and return 0 otherwise int score; return File.Exists(highScoreFilename) && int.TryParse(File.ReadAllText(highScoreFilename), out score) ? score : 0; } private static void SaveHighScore(int score) { File.WriteAllText(highScoreFilename, score.ToString()); }
Metode LoadHighScore()
pertama-tama memeriksa apakah file skor tinggi ada, dan kemudian memeriksa apakah file tersebut berisi bilangan bulat yang valid. Pemeriksaan kedua kemungkinan besar tidak akan pernah gagal kecuali pengguna secara manual mengedit file skor tinggi ke sesuatu yang tidak valid, tetapi ada baiknya untuk berhati-hati.
Kita ingin memuat skor tinggi saat pertandingan dimulai, dan menyimpannya ketika pemain mendapat skor tinggi baru. Kita akan memodifikasi konstruktor statis dan Reset()
metode di PlayerStatus
untuk melakukannya. Kami juga akan menambahkan properti pembantu, IsGameOver
yang akan kami gunakan sebentar lagi.
public static bool IsGameOver { get { return Lives == 0; } } static PlayerStatus() { HighScore = LoadHighScore(); Reset(); } public static void Reset() { if (Score > HighScore) SaveHighScore(HighScore = Score); Score = 0; Multiplier = 1; Lives = 4; scoreForExtraLife = 2000; multiplierTimeLeft = 0; }
Itu membutuhkan pelacakan skor tinggi. Sekarang kita perlu menampilkannya. Tambahkan kode berikut ke GameRoot.Draw()
di blok SpriteBatch
yang sama di mana teks lain diambil:
if (PlayerStatus.IsGameOver) { string text = "Game Over\n" + "Your Score: " + PlayerStatus.Score + "\n" + "High Score: " + PlayerStatus.HighScore; Vector2 textSize = Art.Font.MeasureString(text); spriteBatch.DrawString(Art.Font, text, ScreenSize / 2 - textSize / 2, Color.White); }
Ini akan membuatnya menampilkan skor Anda dan skor tinggi di gim, berpusat di layar.
Sebagai penyesuaian akhir, kita akan meningkatkan waktu sebelum kapal bereaksi pada game untuk memberi waktu pemain untuk melihat skor mereka. Ubah PlayerShip.Kill()
dengan mengatur waktu respawn menjadi 300 frame (lima detik) jika pemain sudah tidak aktif.
// in PlayerShip.Kill() PlayerStatus.RemoveLife(); framesUntilRespawn = PlayerStatus.IsGameOver ? 300 : 120;
Game ini sekarang siap dimainkan. Mungkin tidak terlihat banyak, tetapi memiliki semua mekanisme dasar yang diterapkan. Di tutorial masa depan kita akan menambahkan filter mekar dan efek partikel untuk membumbui. Tapi sekarang, mari kita cepat menambahkan beberapa suara dan musik untuk membuatnya lebih menarik.
Suara dan Musik
Memutar suara dan musik sangatlah mudah di XNA. Pertama, kami menambahkan efek suara dan musik ke saluran konten. Di panel Properties, pastikan prosesor konten diatur ke Song
untuk musik dan Efek Suara
untuk bunyi.
Selanjutnya, kita membuat kelas pembantu statis untuk bunyi.
static class Sound { public static Song Music { get; private set; } private static readonly Random rand = new Random(); private static SoundEffect[] explosions; // return a random explosion sound public static SoundEffect Explosion { get { return explosions[rand.Next(explosions.Length)]; } } private static SoundEffect[] shots; public static SoundEffect Shot { get { return shots[rand.Next(shots.Length)]; } } private static SoundEffect[] spawns; public static SoundEffect Spawn { get { return spawns[rand.Next(spawns.Length)]; } } public static void Load(ContentManager content) { Music = content.Load<Song>("Sound/Music"); // These linq expressions are just a fancy way loading all sounds of each category into an array. explosions = Enumerable.Range(1, 8).Select(x => content.Load<SoundEffect>("Sound/explosion-0" + x)).ToArray(); shots = Enumerable.Range(1, 4).Select(x => content.Load<SoundEffect>("Sound/shoot-0" + x)).ToArray(); spawns = Enumerable.Range(1, 8).Select(x => content.Load<SoundEffect>("Sound/spawn-0" + x)).ToArray(); } }
Karena kita memiliki beberapa variasi dari setiap suara, properti Explosion
, Shot
, dan Spawn
akan memilih suara secara acak di antara varian.
Call Sound.Load()
di GameRoot.LoadContent()
. Untuk memutar musik, tambahkan dua baris berikut di akhir GameRoot.Initialize()
.
MediaPlayer.IsRepeating = true; MediaPlayer.Play(Sound.Music);
Untuk memutar suara di XNA, Anda cukup memanggil metode Play()
pada SoundEffect
. Metode ini juga memberikan beban berlebih yang memungkinkan Anda mengatur volume, pitch, dan pan suara. Trik untuk membuat suara kita lebih bervariasi adalah menyesuaikan jumlah ini pada setiap permainan.
Untuk memicu efek suara untuk pemotretan, tambahkan baris berikut di PlayerShip.Update()
, di dalam pernyataan if di mana peluru dibuat. Perhatikan bahwa kita menggeser pitch ke atas atau ke bawah secara acak, hingga seperlima oktaf, untuk membuat suara kurang berulang.
Sound.Shot.Play(0.2f, rand.NextFloat(-0.2f, 0.2f), 0);
Demikian juga, memicu efek suara ledakan setiap kali musuh dihancurkan dengan menambahkan yang berikut ke Enemy.WasShot ()
.
Sound.Explosion.Play(0.5f, rand.NextFloat(-0.2f, 0.2f), 0);
Anda sekarang memiliki suara dan musik dalam game Anda. Mudah, bukan?
Kesimpulan
Itu membungkus mekanika permainan dasar. Dalam tutorial berikutnya, kita akan menambahkan filter mekar untuk membuat lampu neon besinar.