Membuat Shooter Vektor Neon di XNA: The Warping Grid
Indonesian (Bahasa Indonesia) translation by Shylchemist (you can also view the original English article)
Dalam seri tutorial ini, saya akan menunjukkan cara membuat penembak tongkat neon 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
Dalam seri sejauh ini kami menciptakan gameplay, mekar, dan efek partikel. Pada bagian akhir ini, kita akan membuat grid latar belakang yang dinamis dan melengkung.
Salah satu efek paling keren dalam Geometri War adalah grid latar belakang warping. Kami akan memeriksa cara membuat efek serupa dalam Shape Blaster. Grid akan menampilkan peluru, lubang hitam, dan pemain yang sedang beristirahat. Tidak sulit paksa dan terlihat mengagumkan.
Kami akan membuat grid menggunakan simulasi pegas. Di setiap persimpangan dari grid, kami akan menempatkan beban kecil dan memasang pegas di setiap sisi. Mata air ini hanya akan menarik dan tidak pernah mendorong, seperti karet gelang. Untuk menjaga grid tetap pada posisinya, massa di perbatasan grid akan berlabuh di tempatnya. Di bawah ini adalah diagram tata letak.

Kami akan membuat kelas yang disebut Grid
untuk membuat efek ini. Namun, sebelum kita mengerjakan jaringan itu sendiri, kita perlu membuat dua kelas pembantu: Spring
dan PointMass
.
Kelas PointMass
Kelas PointMass
mewakili massa yang akan kita pasang mata air. Mata air tidak pernah terhubung langsung ke mata air lainnya. Sebaliknya, mereka menerapkan kekuatan kepada massa yang mereka hubungkan, yang pada gilirannya dapat meregangkan mata air lainnya.
private class PointMass { public Vector3 Position; public Vector3 Velocity; public float InverseMass; private Vector3 acceleration; private float damping = 0.98f; public PointMass(Vector3 position, float invMass) { Position = position; InverseMass = invMass; } public void ApplyForce(Vector3 force) { acceleration += force * InverseMass; } public void IncreaseDamping(float factor) { damping *= factor; } public void Update() { Velocity += acceleration; Position += Velocity; acceleration = Vector3.Zero; if (Velocity.LengthSquared() < 0.001f * 0.001f) Velocity = Vector3.Zero; Velocity *= damping; damping = 0.98f; } }
Ada beberapa poin menarik tentang kelas ini. Pertama, perhatikan bahwa itu menyimpan kebalikan dari massa, 1 / mass
. Ini sering merupakan ide yang bagus dalam simulasi-simulasi fisika karena persamaan fisika cenderung menggunakan inversi massa lebih sering, dan karena itu memberi kita cara mudah untuk merepresentasikan benda-benda tak terhingga berat yang tak bergerak dengan mengatur massa inverse ke nol.
Kelas juga berisi damping variabel. Ini digunakan kira-kira sebagai gesekan atau hambatan udara. Secara bertahap memperlambat massa. Ini membantu membuat grid akhirnya berhenti dan juga meningkatkan stabilitas simulasi pegas.
Metode Update()
melakukan pekerjaan menggerakkan titik massa setiap frame. Ini dimulai dengan melakukan symplectic Euler integration, yang berarti kita menambahkan percepatan ke kecepatan dan kemudian menambahkan kecepatan yang diperbarui ke posisi. Ini berbeda dari integrasi Euler standar di mana kami akan memperbarui kecepatan setelah memperbarui posisi.
Tip: Euler symplectic lebih baik untuk simulasi musim semi karena menghemat energi. Jika Anda menggunakan integrasi Euler secara teratur dan membuat pegas tanpa peredam, mereka akan cenderung meregangkan lebih jauh dan lebih lanjut setiap pantulan saat mereka mendapatkan energi, akhirnya melanggar simulasi Anda.
Setelah memperbarui kecepatan dan posisi, kami memeriksa apakah kecepatannya sangat kecil, dan jika demikian kami mengaturnya menjadi nol. Ini bisa menjadi penting untuk kinerja karena sifat denormalized floating-point numbers.
(Ketika angka floating-point menjadi sangat kecil, mereka menggunakan representasi khusus yang disebut nomor denormal. Ini memiliki keuntungan memungkinkan float untuk mewakili angka yang lebih kecil, tetapi itu datang dengan harga. Kebanyakan chipset tidak dapat menggunakan operasi aritmatika standar mereka pada angka denormalized dan sebaliknya harus mengemulasikannya menggunakan serangkaian langkah. Ini bisa puluhan hingga ratusan kali lebih lambat daripada melakukan operasi pada angka floating-point yang dinormalisasi. Karena kita melipatgandakan kecepatan kita dengan faktor redaman kita setiap frame, akhirnya akan menjadi sangat kecil.Kami sebenarnya tidak peduli tentang kecepatan kecil seperti itu, jadi kami cukup mengaturnya ke nol.)
Metode IncreaseDamping()
digunakan untuk sementara meningkatkan jumlah redaman. Kami akan menggunakan ini nanti untuk efek tertentu.
Kelas Musim Semi
Pegas menghubungkan dua titik massa, dan, jika membentang melewati panjang alami, berlaku kekuatan yang menarik massa bersama. Mata air mengikuti versi modifikasi Hooke's Law dengan redaman:
\ [f = -kx - bv \]
- \(f\) adalah gaya yang dihasilkan oleh pegas.
- \(k\) adalah konstanta pegas, atau kekakuan pegas.
- \(x\) adalah jarak pegas merentang di luar panjang alami.
- \(b\) adalah faktor redaman.
- \(v\) adalah kecepatannya.
Kode untuk kelas Spring
adalah sebagai berikut.
private struct Spring { public PointMass End1; public PointMass End2; public float TargetLength; public float Stiffness; public float Damping; public Spring(PointMass end1, PointMass end2, float stiffness, float damping) { End1 = end1; End2 = end2; Stiffness = stiffness; Damping = damping; TargetLength = Vector3.Distance(end1.Position, end2.Position) * 0.95f; } public void Update() { var x = End1.Position - End2.Position; float length = x.Length(); // these springs can only pull, not push if (length <= TargetLength) return; x = (x / length) * (length - TargetLength); var dv = End2.Velocity - End1.Velocity; var force = Stiffness * x - dv * Damping; End1.ApplyForce(-force); End2.ApplyForce(force); } }
Ketika kita membuat pegas, kita mengatur panjang alami pegas menjadi sedikit lebih rendah dari jarak antara dua titik akhir. Ini menjaga kisi-kisi tetap kencang meskipun saat istirahat dan memperbaiki penampilan.
Metode Update()
pertama kali memeriksa apakah pegas merentang melampaui panjang alami. Jika tidak diregangkan, tidak ada yang terjadi. Jika ya, kami menggunakan Hukum Hooke yang dimodifikasi untuk menemukan gaya dari pegas dan menerapkannya pada dua massa yang terhubung.
Membuat Grid
Sekarang kita memiliki kelas bertingkat yang diperlukan, kita siap untuk membuat grid. Kami mulai dengan membuat objek PointMass
di setiap persimpangan di grid. Kami juga membuat beberapa objek PointMass
anchor yang tidak dapat dipindahkan untuk menahan grid di tempatnya.Kami kemudian menghubungkan massa dengan mata air.
Spring[] springs; PointMass[,] points; public Grid(Rectangle size, Vector2 spacing) { var springList = new List(); int numColumns = (int)(size.Width / spacing.X) + 1; int numRows = (int)(size.Height / spacing.Y) + 1; points = new PointMass[numColumns, numRows]; // these fixed points will be used to anchor the grid to fixed positions on the screen PointMass[,] fixedPoints = new PointMass[numColumns, numRows]; // create the point masses int column = 0, row = 0; for (float y = size.Top; y <= size.Bottom; y += spacing.Y) { for (float x = size.Left; x <= size.Right; x += spacing.X) { points[column, row] = new PointMass(new Vector3(x, y, 0), 1); fixedPoints[column, row] = new PointMass(new Vector3(x, y, 0), 0); column++; } row++; column = 0; } // link the point masses with springs for (int y = 0; y < numRows; y++) for (int x = 0; x < numColumns; x++) { if (x == 0 || y == 0 || x == numColumns - 1 || y == numRows - 1) // anchor the border of the grid springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.1f, 0.1f)); else if (x % 3 == 0 && y % 3 == 0) // loosely anchor 1/9th of the point masses springList.Add(new Spring(fixedPoints[x, y], points[x, y], 0.002f, 0.02f)); const float stiffness = 0.28f; const float damping = 0.06f; if (x > 0) springList.Add(new Spring(points[x - 1, y], points[x, y], stiffness, damping)); if (y > 0) springList.Add(new Spring(points[x, y - 1], points[x, y], stiffness, damping)); } springs = springList.ToArray(); }
Yang pertama for
loop menciptakan baik massa biasa dan massa tak bergerak di setiap persimpangan grid. Kami tidak akan benar-benar menggunakan semua massa yang tak bergerak, dan massa yang tidak terpakai hanya akan menjadi sampah yang dikumpulkan beberapa saat setelah konstruktor berakhir. Kita bisa mengoptimalkan dengan menghindari membuat objek yang tidak perlu, tetapi karena grid biasanya hanya dibuat sekali, itu tidak akan membuat banyak perbedaan.
Selain menggunakan titik jangkar massa di sekitar perbatasan grid, kami juga akan menggunakan beberapa jangkar massa di dalam grid. Ini akan digunakan untuk dengan sangat lembut membantu menarik grid kembali ke posisi semula setelah mengalami deformasi.
Karena titik jangkar tidak pernah bergerak, mereka tidak perlu memperbarui setiap bingkai. Kami hanya dapat menghubungkan mereka ke mata air dan melupakannya. Oleh karena itu, kami tidak memiliki variabel anggota dalam kelas Grid
untuk massa ini.
Ada sejumlah nilai yang dapat Anda atur dalam kreasi kisi. Yang paling penting adalah kekakuan dan redaman mata air. Kekakuan dan redaman jangkar perbatasan dan jangkar interior diatur secara independen dari mata air utama. Nilai kekakuan yang lebih tinggi akan membuat pegas berosilasi lebih cepat, dan nilai peredaman yang lebih tinggi akan menyebabkan pegas melambat lebih cepat.
Memanipulasi Grid
Agar grid bisa bergerak, kita harus memperbaruinya setiap frame. Ini sangat sederhana karena kami sudah melakukan semua kerja keras di kelas PointMass
dan Spring
.
public void Update() { foreach (var spring in springs) spring.Update(); foreach (var mass in points) mass.Update(); }
Sekarang kita akan menambahkan beberapa metode yang memanipulasi grid. Anda dapat menambahkan metode untuk segala jenis manipulasi yang dapat Anda pikirkan. Kami akan mengimplementasikan tiga jenis manipulasi di sini: mendorong bagian grid dalam arah tertentu, mendorong grid keluar dari beberapa titik, dan menarik grid ke arah beberapa titik. Ketiganya akan mempengaruhi grid dalam radius tertentu dari beberapa titik target. Di bawah ini adalah beberapa gambar manipulasi ini dalam aksi.









public void ApplyDirectedForce(Vector3 force, Vector3 position, float radius) { foreach (var mass in points) if (Vector3.DistanceSquared(position, mass.Position) < radius * radius) mass.ApplyForce(10 * force / (10 + Vector3.Distance(position, mass.Position))); } public void ApplyImplosiveForce(float force, Vector3 position, float radius) { foreach (var mass in points) { float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) { mass.ApplyForce(10 * force * (position - mass.Position) / (100 + dist2)); mass.IncreaseDamping(0.6f); } } } public void ApplyExplosiveForce(float force, Vector3 position, float radius) { foreach (var mass in points) { float dist2 = Vector3.DistanceSquared(position, mass.Position); if (dist2 < radius * radius) { mass.ApplyForce(100 * force * (mass.Position - position) / (10000 + dist2)); mass.IncreaseDamping(0.6f); } } }
Kami akan menggunakan ketiga metode ini dalam Shape Blaster untuk efek yang berbeda.
Rendering Grid
Kami akan menggambar grid dengan menggambar segmen garis antara setiap pasangan titik yang berdekatan. Pertama, kita akan membuat metode perluasan pada SpriteBatch
yang memungkinkan kita untuk menggambar segmen garis dengan mengambil tekstur dari satu piksel dan merentangkannya menjadi sebuah garis.
Buka kelas Art
dan deklarasikan tekstur untuk piksel.
public static Texture2D Pixel { get; private set; }
Anda dapat mengatur tekstur piksel dengan cara yang sama kami mengatur gambar lainnya, atau Anda dapat menambahkan dua baris berikut ke metode Art.Load()
.
Pixel = new Texture2D(Player.GraphicsDevice, 1, 1); Pixel.SetData(new[] { Color.White });
Ini hanya menciptakan tekstur 1x1px baru dan menetapkan satu-satunya piksel menjadi putih. Sekarang tambahkan metode berikut di kelas Extensions
.
public static void DrawLine(this SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, float thickness = 2f) { Vector2 delta = end - start; spriteBatch.Draw(Art.Pixel, start, null, color, delta.ToAngle(), new Vector2(0, 0.5f), new Vector2(delta.Length(), thickness), SpriteEffects.None, 0f); }
Metode ini membentang, memutar, dan mewarnai tekstur piksel untuk menghasilkan garis yang kita inginkan.
Selanjutnya, kita perlu metode untuk memproyeksikan titik grid 3D ke layar 2D kami. Biasanya ini mungkin dilakukan dengan menggunakan matriks, tetapi di sini kita akan mengubah koordinat secara manual.
Tambahkan yang berikut ke kelas Grid
.
public Vector2 ToVec2(Vector3 v) { // do a perspective projection float factor = (v.Z + 2000) / 2000; return (new Vector2(v.X, v.Y) - screenSize / 2f) * factor + screenSize / 2; }
Transformasi ini akan memberikan grid pandangan perspektif di mana titik jauh muncul lebih dekat bersama di layar. Sekarang kita dapat menggambar grid dengan mengulang melalui baris dan kolom dan menggambar garis di antara mereka.
public void Draw(SpriteBatch spriteBatch) { int width = points.GetLength(0); int height = points.GetLength(1); Color color = new Color(30, 30, 139, 85); // dark blue for (int y = 1; y < height; y++) { for (int x = 1; x < width; x++) { Vector2 left = new Vector2(), up = new Vector2(); Vector2 p = ToVec2(points[x, y].Position); if (x > 1) { left = ToVec2(points[x - 1, y].Position); float thickness = y % 3 == 1 ? 3f : 1f; spriteBatch.DrawLine(left, p, color, thickness); } if (y > 1) { up = ToVec2(points[x, y - 1].Position); float thickness = x % 3 == 1 ? 3f : 1f; spriteBatch.DrawLine(up, p, color, thickness); } } } }
Dalam kode di atas, p
adalah titik kita saat ini di grid, kiriadalah titik langsung ke kiri
dan ke atas
adalah titik tepat di atasnya. Kami menggambar setiap baris ketiga lebih tebal secara horizontal dan vertikal untuk efek visual.
Interpolasi
Kita dapat mengoptimalkan grid dengan meningkatkan kualitas visual untuk sejumlah mata air tertentu tanpa meningkatkan biaya kinerja secara signifikan. Kami akan melakukan dua optimasi seperti itu.
Kami akan membuat jaringan lebih padat dengan menambahkan segmen garis di dalam sel-sel grid yang ada. Kami melakukannya dengan menggambar garis dari titik tengah satu sisi sel ke titik tengah sisi yang berlawanan. Gambar di bawah ini menunjukkan garis interpolasi baru berwarna merah.



Menggambar garis interpolasi sangat mudah. Jika Anda memiliki dua poin, a
dan b
, titik tengahnya adalah (a + b) / 2
.Jadi, untuk menggambar garis interpolasi, kami menambahkan kode berikut di dalam for
loops metode Draw()
kami .
if (x > 1 && y > 1) { Vector2 upLeft = ToVec2(points[x - 1, y - 1].Position); spriteBatch.DrawLine(0.5f * (upLeft + up), 0.5f * (left + p), color, 1f); // vertical line spriteBatch.DrawLine(0.5f * (upLeft + left), 0.5f * (up + p), color, 1f); // horizontal line }
Perbaikan kedua adalah melakukan interpolasi pada segmen garis lurus kami untuk membuatnya menjadi kurva yang lebih halus. XNA menyediakan metode Vector2.CatmullRom()
berguna yang melakukan Catmull-Rom interpolation. Anda melewatkan metode empat titik berurutan pada garis lengkung, dan akan mengembalikan titik-titik sepanjang kurva halus antara titik kedua dan ketiga yang Anda berikan.
Argumen kelima untuk Vector2.CatmullRom()
adalah faktor pembobotan yang menentukan titik mana pada kurva interpolasi yang dihasilkannya. Faktor pembobot 0
atau 1
masing-masing akan mengembalikan titik kedua atau ketiga yang Anda berikan, dan faktor pembobotan 0.5
akan mengembalikan titik pada kurva interpolasi di antara dua titik. Dengan secara berangsur-angsur menggerakkan faktor pembobotan dari nol ke satu dan menggambar garis di antara titik-titik yang dikembalikan, kita dapat menghasilkan kurva yang sangat mulus. Namun, untuk menjaga biaya kinerja rendah, kami hanya akan mengambil satu titik interpolasi menjadi pertimbangan, pada faktor pembobotan 0.5
. Kami kemudian mengganti garis lurus asli di grid dengan dua garis yang bertemu pada titik interpolasi.
Diagram di bawah ini menunjukkan efek interpolasi ini.

Karena segmen garis dalam grid sudah kecil, menggunakan lebih dari satu titik interpolasi umumnya tidak membuat perbedaan yang nyata.
Seringkali, garis-garis di grid kita akan sangat lurus dan tidak akan memerlukan smoothing. Kita dapat memeriksa ini dan menghindari menggambar dua garis, bukan satu. Kami memeriksa apakah jarak antara titik interpolasi dan titik tengah garis lurus lebih besar dari satu piksel. Jika ya, kita asumsikan garis melengkung dan kita menggambar dua segmen garis. Modifikasi metode Draw()
kami untuk menambahkan interpolasi Catmull-Rom untuk garis horizontal ditunjukkan di bawah ini.
left = ToVec2(points[x - 1, y].Position); float thickness = y % 3 == 1 ? 3f : 1f; // use Catmull-Rom interpolation to help smooth bends in the grid int clampedX = Math.Min(x + 1, width - 1); Vector2 mid = Vector2.CatmullRom(ToVec2(points[x - 2, y].Position), left, p, ToVec2(points[clampedX, y].Position), 0.5f); // If the grid is very straight here, draw a single straight line. Otherwise, draw lines to our // new interpolated midpoint if (Vector2.DistanceSquared(mid, (left + p) / 2) > 1) { spriteBatch.DrawLine(left, mid, color, thickness); spriteBatch.DrawLine(mid, p, color, thickness); } else spriteBatch.DrawLine(left, p, color, thickness);
Gambar di bawah ini menunjukkan efek smoothing. Titik hijau digambar pada setiap titik interpolasi untuk mengilustrasikan dengan lebih baik di mana garis-garis diperhalus.



Menggunakan Grid dalam Shape Blaster
Sekarang saatnya menggunakan grid di game kami. Kita mulai dengan mendeklarasikan variabel Grid
publik statis di GameRoot
dan membuat grid dalam method GameRoot.Initialize()
. Kami akan membuat grid dengan kira-kira 1600 poin seperti itu.
const int maxGridPoints = 1600; Vector2 gridSpacing = new Vector2((float)Math.Sqrt(Viewport.Width * Viewport.Height / maxGridPoints)); Grid = new Grid(Viewport.Bounds, gridSpacing);
Kemudian kita memanggil Grid.Update()
dan Grid.Draw()
dari metode Update()
dan Draw()
di GameRoot
. Ini akan memungkinkan kita melihat grid ketika kita menjalankan game. Namun, kita masih perlu membuat berbagai objek game yang berinteraksi dengan grid.
Peluru akan mengusir grid. Kami sudah membuat metode untuk melakukan ini yang disebut ApplyExplosiveForce()
. Tambahkan baris berikut ke method Bullet.Update()
.
GameRoot.Grid.ApplyExplosiveForce(0.5f * Velocity.Length(), Position, 80);
Ini akan membuat peluru mengusir grid secara proporsional sesuai kecepatannya. Itu sangat mudah.
Sekarang mari kita bekerja pada lubang hitam. Tambahkan baris ini ke BlackHole.Update()
.
GameRoot.Grid.ApplyImplosiveForce((float)Math.Sin(sprayAngle / 2) * 10 + 20, Position, 200);
Hal ini membuat lubang hitam menyedot grid dengan jumlah kekuatan yang bervariasi. Saya menggunakan kembali variabel sprayAngle
, yang akan menyebabkan gaya pada grid untuk berdenyut sinkron dengan sudut yang disemprotkan partikel (meskipun pada setengah frekuensi karena pembagian oleh dua). Gaya yang diteruskan akan bervariasi secara sinusoidal antara 10 dan 30.
Akhirnya, kita akan menciptakan gelombang kejut di grid ketika kapal pemain bereaksi setelah kematian. Kami akan melakukannya dengan menarik grid sepanjang sumbu z dan kemudian memungkinkan kekuatan untuk merambat dan memantul melalui pegas. Sekali lagi, ini hanya membutuhkan modifikasi kecil untuk PlayerShip.Update()
.
if (IsDead) { if (--framesUntilRespawn == 0) GameRoot.Grid.ApplyDirectedForce(new Vector3(0, 0, 5000), new Vector3(Position, 0), 50); return; }
Apa berikutnya?
Kami memiliki gameplay dan efek dasar yang diterapkan.Terserah Anda untuk mengubahnya menjadi permainan yang lengkap dan dipoles dengan cita rasa Anda sendiri. Coba tambahkan beberapa mekanika baru yang menarik, beberapa efek baru yang keren, atau kisah unik. Jika Anda tidak yakin harus mulai dari mana, berikut beberapa saran.
- Buat tipe musuh baru seperti ular atau musuh yang meledak.
- Buat jenis senjata baru seperti mencari rudal atau lightning gun.
- Tambahkan layar judul dan menu utama.
- Tambahkan tabel skor tinggi.
- Tambahkan beberapa powerup seperti perisai atau bom. Untuk poin bonus, berkreasi dengan kekuatan Anda. Anda dapat membuat powerups yang memanipulasi gravitasi, mengubah waktu, atau tumbuh seperti organisme. Anda dapat memasang bola raksasa yang berbasiskan fisika ke kapal untuk menghancurkan musuh. Bereksperimen untuk menemukan powerup yang menyenangkan dan membantu game Anda menonjol.
- Buat beberapa level. Tingkat yang lebih keras dapat memperkenalkan musuh yang lebih keras dan senjata dan powerup yang lebih canggih.
- Izinkan pemain kedua untuk bergabung dengan gamepad.
- Biarkan arena bergulir sehingga mungkin lebih besar dari jendela permainan.
- Tambahkan bahaya lingkungan seperti laser.
- Tambahkan sistem toko atau leveling dan izinkan pemain untuk mendapatkan peningkatan.
Terima kasih sudah membaca!