Membuat Shooter Vektor Neon di XNA: Basic Gameplay
Indonesian (Bahasa Indonesia) translation by Sap (you can also view the original English article)
Dalam seri tutorial ini, saya akan menunjukkan cara membuat tongkat shooter neon twin seperti Geometri Wars, yang akan kita sebut Shape Blaster, 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.
Saya menganjurkan Anda untuk memperluas dan bereksperimen dengan kode yang diberikan dalam tutorial ini. Kita akan membahas topik ini di seluruh seri:
- Siapkan basic gameplay, membuat player's ship dan menangani input, suara, dan musik.
- Selesaikan penerapan mekanika permainan dengan menambahkan musuh, menangani deteksi tabrakan, dan melacak skor pemain dan nyawa.
- Tambahkan filter bloom, yang merupakan efek untuk memberikan grafis cahaya neon.
- Tambahkan efek partikel over-the-top yang gila.
- Tambahkan grid background warping.
Inilah yang akan kita punya pada akhir seri:
Dan inilah yang akan kita punya di akhir bagian pertama:
Musik dan efek suara yang dapat Anda dengar dalam video ini dibuat oleh RetroModular, dan Anda dapat membaca tentang bagaimana ia melakukannya di Audiotuts+.
Sprite oleh Jacob Zinman-Jeanes, penghuni desainer Tuts+. Semua karya seni dapat ditemukan di zip file source download.



Mari kita mulai.
Overview
Dalam tutorial ini kita akan membuat penembak kembar-tongkat; pemain akan mengontrol kapal dengan keyboard, keyboard dan mouse, atau dua thumbstick dari gamepad.
Kita menggunakan sejumlah kelas untuk mencapai ini:
-
Entity
: Kelas dasar untuk musuh, peluru, dan kapal pemain. Entitas dapat bergerak dan ditarik. -
Bullet
danPlayerShip
. -
EntityManager
: Melacak semua entitas dalam game dan melakukan deteksi tabrakan. -
Input
: Membantu mengelola input dari keyboard, mouse, dan gamepad. -
Art
: Memuat dan menyimpan referensi ke tekstur yang dibutuhkan untuk game. -
Sound
: Memuat dan menyimpan referensi ke suara dan musik. -
MathUtil
danExtensions
: Berisi beberapa metode statis dan metode ekstensi yang membantu. -
GameRoot
: Mengontrol putaran utama game. Merupakangame1
kelas XNA yang secara otomatis menghasilkan, diganti namanya.
Kode dalam tutorial ini bertujuan untuk menjadi sederhana dan mudah dimengerti. Tidak akan memiliki setiap fitur atau arsitektur rumit yang dirancang untuk mendukung setiap kebutuhan yang mungkin. Sebaliknya, ia hanya akan melakukan apa yang perlu dilakukan. Menjaga agar tetap sederhana akan mempermudah Anda untuk memahami konsep, dan kemudian memodifikasi dan memperluasnya ke dalam permainan unik Anda sendiri.
Entities dan Player's Ship
Buat proyek XNA baru. Ubah nama kelas Game1
menjadi sesuatu yang lebih cocok. Saya menyebutnya GameRoot
.
Sekarang mari kita mulai dengan membuat base class untuk entitas permainannya.
abstract class Entity { protected Texture2D image; // The tint of the image. This will also allow us to change the transparency. protected Color color = Color.White; public Vector2 Position, Velocity; public float Orientation; public float Radius = 20; // used for circular collision detection public bool IsExpired; // true if the entity was destroyed and should be deleted. public Vector2 Size { get { return image == null ? Vector2.Zero : new Vector2(image.Width, image.Height); } } public abstract void Update(); public virtual void Draw(SpriteBatch spriteBatch) { spriteBatch.Draw(image, Position, null, color, Orientation, Size / 2f, 1f, 0, 0); } }
Semua entitas kita (musuh, bullets dan player's ship) memiliki beberapa properti dasar seperti gambar dan posisi. IsExpired
akan digunakan untuk menunjukkan bahwa entitas telah dihancurkan dan harus dihapus dari daftar yang memegang referensi untuk itu.
Selanjutnya kita membuat EntityManager
untuk melacak entitas kita dan untuk memperbarui dan menarik mereka.
static class EntityManager { static List<Entity> entities = new List<Entity>(); static bool isUpdating; static List<Entity> addedEntities = new List<Entity>(); public static int Count { get { return entities.Count; } } public static void Add(Entity entity) { if (!isUpdating) entities.Add(entity); else addedEntities.Add(entity); } public static void Update() { isUpdating = true; foreach (var entity in entities) entity.Update(); isUpdating = false; foreach (var entity in addedEntities) entities.Add(entity); addedEntities.Clear(); // remove any expired entities. entities = entities.Where(x => !x.IsExpired).ToList(); } public static void Draw(SpriteBatch spriteBatch) { foreach (var entity in entities) entity.Draw(spriteBatch); } }
Ingat, jika Anda mengubah daftar saat melakukan iterasi di atasnya, Anda akan mendapatkan pengecualian. Kode di atas menangani ini dengan mengantre setiap entitas yang ditambahkan selama memperbarui dalam daftar terpisah, dan menambahkannya setelah selesai memperbarui entitas yang ada.
Membuat Mereka Terlihat
Kita perlu memuat beberapa tekstur jika ingin menggambar apa pun. Kita akan membuat kelas statis untuk menyimpan referensi ke semua tekstur.
static class Art { public static Texture2D Player { get; private set; } public static Texture2D Seeker { get; private set; } public static Texture2D Wanderer { get; private set; } public static Texture2D Bullet { get; private set; } public static Texture2D Pointer { get; private set; } public static void Load(ContentManager content) { Player = content.Load<Texture2D>("Player"); Seeker = content.Load<Texture2D>("Seeker"); Wanderer = content.Load<Texture2D>("Wanderer"); Bullet = content.Load<Texture2D>("Bullet"); Pointer = content.Load<Texture2D>("Pointer"); } }
Muat seni dengan memanggil Art.Load(Conten)
di GameRoot.LoadContent()
. Selain itu, sejumlah kelas perlu mengetahui dimensi layar, jadi tambahkan properti berikut ke GameRoot
:
public static GameRoot Instance { get; private set; } public static Viewport Viewport { get { return Instance.GraphicsDevice.Viewport; } } public static Vector2 ScreenSize { get { return new Vector2(Viewport.Width, Viewport.Height); } }
Dan dalam konstruktor GameRoot
, tambahkan:
Instance = this;
Sekarang kita akan mulai menulis kelas PlayerShip
.
class PlayerShip : Entity { private static PlayerShip instance; public static PlayerShip Instance { get { if (instance == null) instance = new PlayerShip(); return instance; } } private PlayerShip() { image = Art.Player; Position = GameRoot.ScreenSize / 2; Radius = 10; } public override void Update() { // ship logic goes here } }
Kita membuat PlayerShip
menjadi tunggal, mengatur citranya, dan menempatkannya di tengah layar.
Akhirnya, mari tambahkan kapal pemain ke EntityManager
dan perbarui dan gambarkan. Tambahkan kode berikut di GameRoot
:
// in Initialize(), after the call to base.Initialize() EntityManager.Add(PlayerShip.Instance); // in Update() EntityManager.Update(); // in Draw() GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw(spriteBatch); spriteBatch.End();
Kita menggambar sprite dengan aditif blending, yang merupakan bagian dari apa yang akan memberi mereka tampilan neon mereka. Jika Anda menjalankan permainan pada titik ini Anda harus melihat kapal Anda di tengah layar. Namun, itu belum menanggapi masukan. Mari perbaiki itu.
Input
Untuk gerakan, pemain dapat menggunakan WASD pada keyboard, atau thumbstick kiri pada gamepad. Untuk membidik, mereka dapat menggunakan tombol panah, thumbstick kanan, atau mouse. Kita tidak akan mengharuskan pemain untuk menahan tombol mouse untuk menembak karena tidak nyaman untuk terus menekan tombol. Ini memberi kita sedikit masalah: bagaimana kita tahu apakah pemain bertujuan dengan mouse, keyboard, atau gamepad?
Kita akan menggunakan sistem berikut: kita akan menambahkan input keyboard dan gamepad bersama. Jika pemain menggerakkan mouse, kita beralih ke tujuan mouse. Jika pemain menekan tombol panah atau menggunakan thumbstick kanan, kita matikan mouse.
Satu hal yang perlu diperhatikan: mendorong maju thumbstick akan mengembalikan nilai y positif. Dalam koordinat layar, nilai y meningkat ke bawah. Kita ingin membalikkan sumbu y pada pengontrol sehingga mendorong thumbstick ke atas akan mengarahkan atau memindahkan kita ke bagian atas layar.
Kita akan membuat kelas statis untuk melacak berbagai perangkat input dan berhati-hati berpindah antar berbagai jenis penargetan.
static class Input { private static KeyboardState keyboardState, lastKeyboardState; private static MouseState mouseState, lastMouseState; private static GamePadState gamepadState, lastGamepadState; private static bool isAimingWithMouse = false; public static Vector2 MousePosition { get { return new Vector2(mouseState.X, mouseState.Y); } } public static void Update() { lastKeyboardState = keyboardState; lastMouseState = mouseState; lastGamepadState = gamepadState; keyboardState = Keyboard.GetState(); mouseState = Mouse.GetState(); gamepadState = GamePad.GetState(PlayerIndex.One); // If the player pressed one of the arrow keys or is using a gamepad to aim, we want to disable mouse aiming. Otherwise, // if the player moves the mouse, enable mouse aiming. if (new[] { Keys.Left, Keys.Right, Keys.Up, Keys.Down }.Any(x => keyboardState.IsKeyDown(x)) || gamepadState.ThumbSticks.Right != Vector2.Zero) isAimingWithMouse = false; else if (MousePosition != new Vector2(lastMouseState.X, lastMouseState.Y)) isAimingWithMouse = true; } // Checks if a key was just pressed down public static bool WasKeyPressed(Keys key) { return lastKeyboardState.IsKeyUp(key) && keyboardState.IsKeyDown(key); } public static bool WasButtonPressed(Buttons button) { return lastGamepadState.IsButtonUp(button) && gamepadState.IsButtonDown(button); } public static Vector2 GetMovementDirection() { Vector2 direction = gamepadState.ThumbSticks.Left; direction.Y *= -1; // invert the y-axis if (keyboardState.IsKeyDown(Keys.A)) direction.X -= 1; if (keyboardState.IsKeyDown(Keys.D)) direction.X += 1; if (keyboardState.IsKeyDown(Keys.W)) direction.Y -= 1; if (keyboardState.IsKeyDown(Keys.S)) direction.Y += 1; // Clamp the length of the vector to a maximum of 1. if (direction.LengthSquared() > 1) direction.Normalize(); return direction; } public static Vector2 GetAimDirection() { if (isAimingWithMouse) return GetMouseAimDirection(); Vector2 direction = gamepadState.ThumbSticks.Right; direction.Y *= -1; if (keyboardState.IsKeyDown(Keys.Left)) direction.X -= 1; if (keyboardState.IsKeyDown(Keys.Right)) direction.X += 1; if (keyboardState.IsKeyDown(Keys.Up)) direction.Y -= 1; if (keyboardState.IsKeyDown(Keys.Down)) direction.Y += 1; // If there's no aim input, return zero. Otherwise normalize the direction to have a length of 1. if (direction == Vector2.Zero) return Vector2.Zero; else return Vector2.Normalize(direction); } private static Vector2 GetMouseAimDirection() { Vector2 direction = MousePosition - PlayerShip.Instance.Position; if (direction == Vector2.Zero) return Vector2.Zero; else return Vector2.Normalize(direction); } public static bool WasBombButtonPressed() { return WasButtonPressed(Buttons.LeftTrigger) || WasButtonPressed(Buttons.RightTrigger) || WasKeyPressed(Keys.Space); } }
Panggilan Input.Update()
di awal GameRoot.Update()
agar kelas input bekerja.
Tip: Anda mungkin memperhatikan saya memasukkan metode untuk bom. Kita tidak akan menerapkan bom sekarang tetapi metode itu ada untuk digunakan di masa depan.
Anda mungkin juga memperhatikan di GetMovementDirection()
saya menulis direction.LenghtSquared () > 1
. Menggunakan LengthSquared()
merupakan optimalisasi kinerja kecil; menghitung kuadrat panjangnya sedikit lebih cepat daripada menghitung panjangnya sendiri karena menghindari operasi akar kuadrat yang relatif lambat. Anda akan melihat kode menggunakan kuadrat panjang atau jarak di seluruh program. Dalam kasus khusus ini, perbedaan kinerja dapat diabaikan, tetapi optimalisasi ini dapat membuat perbedaan ketika digunakan dalam loop yang ketat.
Gerakan
Kita sekarang siap untuk membuat kapal bergerak. Tambahkan kode ini ke metode PlayerShip.Update()
:
const float speed = 8; Velocity = speed * Input.GetMovementDirection(); Position += Velocity; Position = Vector2.Clamp(Position, Size / 2, GameRoot.ScreenSize - Size / 2); if (Velocity.LengthSquared() > 0) Orientation = Velocity.ToAngle();
Ini akan membuat kapal bergerak dengan kecepatan hingga delapan piksel per frame, menjepit posisinya sehingga tidak bisa keluar layar, dan memutar kapal untuk menghadap ke arah pergerakannya.
ToAngle()
adalah metode ekstensi sederhana yang didefinisikan dalam kelas Ekstensions
seperti:
public static float ToAngle(this Vector2 vector) { return (float)Math.Atan2(vector.Y, vector.X); }
Menembak
Jika Anda menjalankan permainan sekarang, Anda harus dapat menerbangkan kapal. Sekarang mari kita buat itu menembak.
Pertama, kita butuh kelas untuk peluru.
class Bullet : Entity { public Bullet(Vector2 position, Vector2 velocity) { image = Art.Bullet; Position = position; Velocity = velocity; Orientation = Velocity.ToAngle(); Radius = 8; } public override void Update() { if (Velocity.LengthSquared() > 0) Orientation = Velocity.ToAngle(); Position += Velocity; // delete bullets that go off-screen if (!GameRoot.Viewport.Bounds.Contains(Position.ToPoint())) IsExpired = true; } }
Kita ingin periode cooldown singkat antara peluru, jadi tambahkan bidang berikut ke kelas PlayerShip
.
const int cooldownFrames = 6; int cooldownRemaining = 0; static Random rand = new Random();
Juga, tambahkan kode berikut untuk PlayerShip.Update()
.
var aim = Input.GetAimDirection(); if (aim.LengthSquared() > 0 && cooldownRemaining <= 0) { cooldownRemaining = cooldownFrames; float aimAngle = aim.ToAngle(); Quaternion aimQuat = Quaternion.CreateFromYawPitchRoll(0, 0, aimAngle); float randomSpread = rand.NextFloat(-0.04f, 0.04f) + rand.NextFloat(-0.04f, 0.04f); Vector2 vel = MathUtil.FromPolar(aimAngle + randomSpread, 11f); Vector2 offset = Vector2.Transform(new Vector2(25, -8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); offset = Vector2.Transform(new Vector2(25, 8), aimQuat); EntityManager.Add(new Bullet(Position + offset, vel)); } if (cooldownRemaining > 0) cooldownRemaining--;
Kode ini menciptakan dua peluru yang berjalan sejajar satu sama lain. Ini menambah sedikit keacakan ke arah. Ini membuat bidikan menyebar sedikit seperti senapan mesin. Kita menambahkan dua nomor acak bersama-sama karena ini membuat jumlah mereka lebih mungkin untuk berpusat (sekitar nol) dan cenderung mengirim peluru jauh. Kita menggunakan quaternion untuk memutar posisi awal peluru ke arah yang mereka lalui.
Kita juga menggunakan dua metode pembantu baru:
-
Random.NextFloat()
mengembalikan float antara nilai minimum dan maksimum. -
MathUtil.FromPolar()
menciptakanVector2
dari sudut dan besarnya.
// in Extensions public static float NextFloat(this Random rand, float minValue, float maxValue) { return (float)rand.NextDouble() * (maxValue - minValue) + minValue; } // in MathUtil public static Vector2 FromPolar(float angle, float magnitude) { return magnitude * new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); }
Kursor Kustom
Ada satu hal lagi yang harus kita lakukan sekarang karena kita memiliki kelas Input
. Mari menggambar kursor mouse khusus untuk mempermudah melihat ke mana tujuan kapal. Di GameRoot.Draw
, cukup gambar Art.Pointer
pada posisi mouse.
spriteBatch.Begin(SpriteSortMode.Texture, BlendState.Additive); EntityManager.Draw(spriteBatch); // draw the custom mouse cursor spriteBatch.Draw(Art.Pointer, Input.MousePosition, Color.White); spriteBatch.End();
Kesimpulan
Jika Anda menguji gim ini sekarang, Anda akan dapat memindahkan kapal dengan tombol WASD atau atau thumbstick kiri, dan mengarahkan aliran peluru terus menerus dengan tombol panah, mouse, atau thumbstick kanan.
Di bagian selanjutnya, kita akan menyelesaikan permainan dengan menambahkan musuh dan skor.