Membungum Stage3D Shoot-Em-Up:Medan, Musuh Al, dan Level Data
Indonesian (Bahasa Indonesia) translation by Suci Rohini (you can also view the original English article)
Kita sedang membuat shoot-em-up 2D berkinerja tinggi menggunakan mesin rendering Stage3D
percepatan perangkat keras Flash yang baru. Di bagian seri ini kita menambahkan mode pergerakan musuh baru, musuh yang menembak balik, dan level kerajinan tangan dengan medan background.
Juga tersedia dalam seri ini:
- Membangun Stage3D Shoot-’Em-Up: Tes Sprite
- Buat Stage3D Shoot-’Em-Up: Interaksi
- Membangun Stage3D Shoot-’Em-Up: Explosions, Parallax, dan Collisions
- Membangun Stage3D Shoot-'Em-Up: Terrain, Enemy AI, dan Level Data
- Membangun Stage3D Shoot-’Em-Up: Score, Health, Lives, HUD dan Transitions
- Membangun Stage3D Shoot-’Em-Up: Battles Boss Layar Penuh dan Polish
Pratinjau Hasil Akhir
Mari kita lihat hasil akhir yang akan kita upayakan: demo tembak-em-up yang dipercepat perangkat keras yang mencakup semuanya dari bagian satu hingga tiga dari seri ini, ditambah mode pergerakan musuh baru, musuh yang menembak balik, dan hand- tingkat buatan yang mencakup medan background.
Pendahuluan: Selamat Datang di Level Empat!
Mari kita terus membuat side-scrolling shooter yang terinspirasi oleh judul arcade retro seperti R-Type atau Gradius di AS3.
Pada bagian pertama dari seri ini, kami menerapkan mesin sprite 2D dasar yang mencapai kinerja luar biasa melalui penggunaan perenderan hardware Stage3D dan beberapa optimasi.
Pada bagian pertama, kita menerapkan layar judul, menu utama, suara dan musik, dan sistem input sehingga pemain dapat mengontrol pesawat ruang angkasa mereka menggunakan keyboard.
Dan di bagian ketiga, kita menambahkan semua eye-candy: sistem partikel lengkap dengan bunga api, puing-puing terbang, gelombang kejut, jalur api mesin dan berton-ton ledakan.
Pada bagian ini, kita akan meningkatkan beberapa komponen utama dari mesin permainan kita. Pertama, kita akan menambahkan A.I. (Kecerdasan buatan) untuk musuh kita dengan menciptakan beberapa perilaku dan gaya gerakan yang berbeda. Mereka akhirnya akan mulai menembak balik, dan tidak akan lagi hanya bergerak dalam garis lurus. Beberapa bahkan tidak akan bergerak sama sekali, tetapi akan menunjuk pada pemain: sempurna untuk senjata penjaga.
Kedua, kita akan menerapkan mekanisme parsing data level yang akan memberdayakan Anda untuk merancang dunia game yang luas menggunakan editor level.
Ketiga, kita akan membuat kumpulan spritesheet dan rendering baru untuk set sprite terain non-interaktif yang akan digunakan sebagai bagian dari background. Dengan cara ini, kita akan terbang di atas stasiun ruang angkasa dan asteroid terperinci, bukan hanya ruang kosong.
Langkah 1: Buka Proyek Anda Yang Ada
Kita akan membangun kode sumber yang ditulis dalam tutorial sebelumnya, yang kebanyakan tidak akan berubah. Jika Anda belum memilikinya, pastikan untuk mengunduh kode sumber dari tutorial sebelumnya. Buka file proyek di FlashDevelop (info di sini) dan bersiaplah untuk meningkatkan game Anda! Kode sumber ini akan berfungsi di kompiler AS3 lainnya, dari CS6 ke Flash Builder.
Langkah 2: Tingkatkan Kelas Entitas
Kita pertama-tama akan mengimplementasikan beberapa AI pergerakan baru ke kelas entitas. Untuk fungsionalitas ini, kita akan memerlukan beberapa data status lainnya untuk dilacak untuk setiap entitas. Khususnya kita akan memerlukan beberapa informasi jalur, dan berbagai timer yang akan memberi tahu kita seberapa sering musuh perlu menembaki pemain. Buka file Entity.as
yang ada dari terakhir kali dan tambahkan variabel kelas baru berikut sebagai berikut. Untuk menghindari kebingungan, seluruh bagian atas file disertakan di sini tetapi hanya variabel AI di atas yang baru.
// Stage3D Shoot-em-up Tutorial Part 4 // by Christer Kaitila - www.mcfunkypants.com // Entity.as // The Entity class will eventually hold all game-specific entity stats // for the spaceships, bullets and effects in our game. // It stores a reference to a gpu sprite and a few demo properties. // This is where you would add hit points, weapons, ability scores, etc. // This class handles any AI (artificial intelligence) for enemies as well. package { import flash.geom.Point; import flash.geom.Rectangle; public class Entity { // AI VARIABLES BEGIN // if this is set, custom behaviors are run public var aiFunction : Function; // the AI routines might want access to the entity manager public var gfx:EntityManager; // AI needs to have access to time passing (in seconds) public var age:Number = 0; public var fireTime:Number = 0; public var fireDelayMin:Number = 1; public var fireDelayMax:Number = 6; // an array of points defining a movement path for the AI public var aiPathWaypoints:Array; // how fast we travel from one spline node to the next in seconds public var pathNodeTime:Number = 1; // these offsets are added to the sprite location // so that ships move around but eventually scroll offscreen public var aiPathOffsetX:Number = 0; public var aiPathOffsetY:Number = 0; // how much big the path is (max) public var aiPathSize:Number = 128; // how many different nodes in the path public var aiPathWaypointCount:int = 8; // AI VARIABLES END private var _speedX : Number; private var _speedY : Number; private var _sprite : LiteSprite; public var active : Boolean = true; // collision detection public var isBullet:Boolean = false; // only these check collisions public var leavesTrail:Boolean = false; // creates particles as it moves public var collidemode:uint = 0; // 0=none, 1=sphere, 2=box, etc. public var collideradius:uint = 32; // used for sphere collision // box collision is not implemented (yet) public var collidebox:Rectangle = new Rectangle(-16, -16, 32, 32); public var collidepoints:uint = 25; // score earned if destroyed public var touching:Entity; // what entity just hit us? public var owner:Entity; // so your own bullets don't hit you public var orbiting:Entity; // entities can orbit (circle) others public var orbitingDistance:Number; // how far in px from the orbit center // used for particle animation (in units per second) public var fadeAnim:Number = 0; public var zoomAnim:Number = 0; public var rotationSpeed:Number = 0; // used to mark whether or not this entity was // freshly created or reused from an inactive one public var recycled:Boolean = false;
Langkah 3: Jangan Langsung Tembak
Kita akan melacak waktu yang telah berlalu sejak musuh menembak pemain sehingga orang jahat tidak terlalu sering menembak. Kita juga perlu musuh untuk mengingat agar tidak menembak langsung saat mereka pertama kali lahir, jadi kita perlu menambahkan sedikit waktu acak sebelum mereka mulai menembak.
Melanjutkan dengan Entity.as
, perbarui fungsi konstruktor kelas sebagai berikut, dan cukup catat fungsi pengambil dan penyetel yang tidak berubah serta kode deteksi tabrakan yang identik dari waktu lalu.
public function Entity(gs:LiteSprite, myManager:EntityManager) { _sprite = gs; _speedX = 0.0; _speedY = 0.0; // we need a reference to the entity manager gfx = myManager; // we don't want everyone shooting on the first frame fireTime = (gfx.fastRandom() * (fireDelayMax - fireDelayMin)) + fireDelayMin; } public function die() : void { // allow this entity to be reused by the entitymanager active = false; // skip all drawing and updating sprite.visible = false; // reset some things that might affect future reuses: leavesTrail = false; isBullet = false; touching = null; owner = null; age = 0; collidemode = 0; } public function get speedX() : Number { return _speedX; } public function set speedX(sx:Number) : void { _speedX = sx; } public function get speedY() : Number { return _speedY; } public function set speedY(sy:Number) : void { _speedY = sy; } public function get sprite():LiteSprite { return _sprite; } public function set sprite(gs:LiteSprite):void { _sprite = gs; } // used for collision callback performed in GameActorpool public function colliding(checkme:Entity):Entity { if (collidemode == 1) // sphere { if (isCollidingSphere(checkme)) return checkme; } return null; } // simple sphere to sphere collision public function isCollidingSphere(checkme:Entity):Boolean { // never collide with yourself if (this == checkme) return false; // only check if these shapes are collidable if (!collidemode || !checkme.collidemode) return false; // don't check your own bullets if (checkme.owner == this) return false; // don't check things on the same "team" if (checkme.owner == owner) return false; // don't check if no radius if (collideradius == 0 || checkme.collideradius == 0) return false; // this is the simpler way to do it, but it runs really slow // var dist:Number = Point.distance(sprite.position, checkme.sprite.position); // if (dist <= (collideradius+checkme.collideradius)) // this looks wierd but is 6x faster than the above // see: http://www.emanueleferonato.com/2010/10/13/as3-geom-point-vs-trigonometry/ if (((sprite.position.x - checkme.sprite.position.x) * (sprite.position.x - checkme.sprite.position.x) + (sprite.position.y - checkme.sprite.position.y) * (sprite.position.y - checkme.sprite.position.y)) <= (collideradius+checkme.collideradius)*(collideradius+checkme.collideradius)) { touching = checkme; // remember who hit us return true; } // default: too far away // trace("No collision. Dist = "+dist); return false; }
Langkah 4: Gerakan Kurva Spline
Salah satu mode AI baru akan menjadi lintasan gerakan acak yang melengkung. Algoritma hebat yang digunakan dalam banyak gim adalah kurva spline Catmull-Rom klasik, yang mengambil berbagai titik dan menyisipkan jalur yang mulus di antara semuanya. Jalur ini akan berputar di ujung jika perlu.
Rutin baru pertama yang dibutuhkan untuk kelas entitas kita adalah fungsi perhitungan spline. Dibutuhkan tiga poin dan "persentase" angka t
yang harus berubah dari nol menjadi satu dari waktu ke waktu. Saat t
mendekati 1, titik yang dikembalikan akan berada di bagian paling ujung dari kurva, dan sebaliknya jika nol, titik yang dikembalikan adalah posisi awal spline.
Untuk demo permainan saat ini, kita hanya akan menghasilkan banyak poin acak untuk setiap entitas baru yang menggunakan momen seperti ini, tetapi Anda dapat dengan mudah menambahkan jalur yang telah Anda tentukan sendiri untuk semua jenis pola pergerakan musuh yang menarik, dari sebuah gambar delapan ke pola zig-zag sederhana.
Anda dapat membaca lebih lanjut tentang kurva spline Catmull-Rom di AS3 dengan memeriksa demo ini dan tutorial ini.
// Calculates 2D cubic Catmull-Rom spline. // See http://www.mvps.org/directx/articles/catmull/ public function spline (p0:Point, p1:Point, p2:Point, p3:Point, t:Number):Point { return new Point ( 0.5 * ((2 * p1.x) + t * (( -p0.x + p2.x) + t * ((2 * p0.x -5 * p1.x +4 * p2.x -p3.x) + t * ( -p0.x +3 * p1.x -3 * p2.x +p3.x)))), 0.5 * ((2 * p1.y) + t * (( -p0.y + p2.y) + t * ((2 * p0.y -5 * p1.y +4 * p2.y -p3.y) + t * ( -p0.y +3 * p1.y -3 * p2.y +p3.y)))) ); } // generate a random path public function generatePath():void { trace("Generating AI path"); aiPathWaypoints = []; var N:int = aiPathWaypointCount; for (var i:int = 0; i < N; i++) { aiPathWaypoints.push (new Point (aiPathSize * Math.random (), aiPathSize * Math.random ())); } } // find the point on a spline at ratio (0 to 1) public function calculatePathPosition(ratio:Number = 0):Point { var i:int = int(ratio); var pointratio:Number = ratio - i; //trace(ratio + ' ratio = path point ' + i + ' segment ratio ' + pointratio); var p0:Point = aiPathWaypoints [(i -1 + aiPathWaypoints.length) % aiPathWaypoints.length]; var p1:Point = aiPathWaypoints [i % aiPathWaypoints.length]; var p2:Point = aiPathWaypoints [(i +1 + aiPathWaypoints.length) % aiPathWaypoints.length]; var p3:Point = aiPathWaypoints [(i +2 + aiPathWaypoints.length) % aiPathWaypoints.length]; // figure out current position var q:Point = spline (p0, p1, p2, p3, pointratio); return q; }
Langkah 5: Tentukan Kapan Memotret
Setiap tipe musuh perlu menembak pemain (kecuali untuk asteroid non-penembakan yang hanya akan berputar dan melayang di angkasa). Untuk saat ini, kita hanya akan melacak waktu yang telah berlalu sejak kita terakhir menembakkan senjata dan menambahkan jarak acak beberapa detik di antara tembakan.
// we could optionally implement many different // versions of this routine with different randomness public function maybeShoot(bulletNum:int = 1, delayMin:Number = NaN, delayMax:Number = NaN):void { // is it time to shoot a bullet? if (fireTime < age) { // if delay parameters were not set, use class defaults if (isNaN(delayMin)) delayMin = fireDelayMin; if (isNaN(delayMax)) delayMax = fireDelayMax; // shoot one from the current location gfx.shootBullet(bulletNum, this); // randly choose the next time to shoot fireTime = age + (gfx.fastRandom() * (delayMax - delayMin)) + delayMin; } }
Anda dapat menambahkan lebih banyak kecerdasan untuk rutin ini dengan hanya menembak ketika pemain berada dalam jarak tertentu, atau hanya menembak sekali dan merusak diri sendiri jika permainan Anda memerlukan semacam efek "bom waktu".
Langkah 6: AI #1: Bergerak dalam Garis Lurus
Fungsi "otak" AI pertama yang akan diimplementasikan adalah yang paling sederhana: gerakan langsung sepanjang garis seperti yang terlihat dalam tutorial minggu lalu. Semua yang kita lakukan di sini adalah titik ke arah saat ini dan bergerak di lintasan apa pun yang secara acak ditugaskan kepada kita ketika kita pertama kali melahirkan. Tidak ada apa-apa!
// moves forward and points at current destination based on speed public function straightAI(seconds:Number):void { age += seconds; maybeShoot(1); sprite.rotation = gfx.pointAtRad(speedX, speedY) - (90*gfx.DEGREES_TO_RADIANS); }
Langkah 7: AI #2: Getaran Sinusoidal
Salah satu pola gerakan yang paling umum dalam setiap shoot-em-up adalah gerakan "gelombang" sinusoidal. Kita akan menggunakan gelombang sinus yang akan bergetar naik dan turun seiring waktu ketika musuh bergerak dalam garis yang sebagian besar lurus ke arah pemain. Pola ini benar-benar terlihat bagus dan menambahkan beberapa gerakan menyenangkan bagi musuh-musuh Anda tanpa terlalu banyak kekacauan, yang membuat musuh-musuh seperti ini mudah diarahkan dan dihancurkan.
// a very simple up/down wobble movement public function wobbleAI(seconds:Number):void { age += seconds; maybeShoot(1); aiPathOffsetY = (Math.sin(age*2) / Math.PI) * 128; }
Langkah 8: AI #3: Sentry Guns
Gaya AI musuh lain yang sangat berguna dan umum yang digunakan oleh sebagian besar penembak adalah "penjaga senjata" yang tidak bergerak atau menara. Musuh semacam ini hanya akan duduk di sana dan membidik pemain. Ini bisa menakutkan bagi para pemain untuk melihat senjata penjaga mengikuti setiap gerakan mereka dan pasti akan mendapatkan beberapa manuver menghindar.
// simply point at the player: good for sentry guns public function sentryAI(seconds:Number):void { age += seconds; maybeShoot(3,3,6); if (gfx.thePlayer) sprite.rotation = gfx.pointAtRad( gfx.thePlayer.sprite.position.x - sprite.position.x, gfx.thePlayer.sprite.position.y - sprite.position.y) - (90*gfx.DEGREES_TO_RADIANS); }
Karena kita memerlukan akses ke posisi entitas pemain, kita melakukan pemeriksaan cepat untuk memastikannya ada karena fungsi AI mungkin dijalankan selama menu utama "attract mode" sebelum ada pemain yang membidik.
Langkah 9: AI #4: Jalur Spline
Gaya akhir pergerakan AI akan menggunakan rutin interpolasi kurva spline Catmull-Rom yang telah diprogramkan di atas. Musuh jenis ini akan goyah dan berputar-putar dengan cara yang choatic, frustasi. Yang terbaik adalah hanya menyertakan beberapa musuh jenis ini di gim Anda, kecuali jika Anda telah meningkatkan fungsi generatePath
untuk menggunakan array poin yang dimasukkan secara manual yang tidak begitu acak.
Agar semuanya terlihat lebih bagus, begitu kita mengetahui koordinat baru untuk musuh kita, kita mengorientasikan sprite untuk menghadapi arah pergerakannya, sehingga ia berputar saat ia berputar.
// move around on a random spline path // in future versions, you could upgrade this function // to (instead of random) follow a predefined array of points // that were designed by hand in code (or even in the level editor!) public function droneAI(seconds:Number):void { //trace('droneAI'); age += seconds; maybeShoot(1); // movement style inspired by Galaga, R-Type, Centipede // performed by easing through a catmull-rom spline curve // defined by an array of points if (aiPathWaypoints == null) generatePath(); // how many spline nodes have we passed? (loops around to beginning) var pathProgress:Number = age / pathNodeTime; var newPos:Point = calculatePathPosition(pathProgress); // point in the correct direction sprite.rotation = gfx.pointAtRad(newPos.x-aiPathOffsetX,newPos.y-aiPathOffsetY) - (90*gfx.DEGREES_TO_RADIANS); // change path offset location // this is added to the sprite scrolling location // so that ships eventually move offscreen // sprite.position.x = newPos.x; // sprite.position.y = newPos.y; aiPathOffsetX = newPos.x; aiPathOffsetY = newPos.x; } } // end class } // end package
Itu saja untuk kelas entitas kita yang baru saja ditingkatkan. Kita telah menambahkan beberapa kode pergerakan baru dan akhirnya armada besar kapal musuh mampu membalas pemain!
Langkah 10: Spritesheet Medan
Sebelum kita melanjutkan, kita akan membuat spritesheet baru untuk digunakan sebagai blok bangunan untuk grafik medan kita. Kita lagi akan menggunakan seni "Tyrian" yang indah, legal dan gratis oleh Daniel Cook (tersedia di Lostgarden.com). Sebelum melanjutkan, kita akan membuat spritesheet baru untuk digunakan sebagai blok bangunan untuk grafik medan kita. Kita lagi akan menggunakan seni "Tyrian" yang indah, legal dan gratis oleh Daniel Cook (tersedia di Lostgarden.com).



Langkah 11: Level Editor Waktu!
a Untuk melakukannya, kita akan membuat kelas sederhana yang mem-parsing output data oleh editor tingkat sumber terbuka populer yang disebut OGMO. Anda dapat membaca semua tentang OGMO di sini.
Anda tidak harus menggunakan OGMO: Anda dapat memodifikasi rutinitas ini untuk mem-parsing XML sebagai output oleh "Tile" atau "DAME", atau file .CSV sebagai output oleh Excel, atau bahkan .GIF atau .PNG sebagai output oleh Photoshop (dengan menggambar masing-masing piksel dan memunculkan berbagai jenis musuh tergantung pada warna masing-masing piksel).
Rutin parsing untuk semua jenis data level adalah sepele dibandingkan dengan fungsionalitas dalam game. Untuk kesederhanaan dan ukuran unduhan yang kecil, CSV (nilai yang dipisahkan koma) adalah alternatif yang bagus untuk XML yang kembung dan kompleks. Lebih penting lagi, sifat linear dari shoot-em-up kita memerlukan serangkaian data panjang yang meyakinkan untuk dapat mengakses kolom demi kolom dan baris demi baris, sebagai lawan dari "sup" entitas XML yang bisa dalam urutan apa pun. CSV membuat data mengalir dari kiri ke kanan, sama seperti game. Karena OGMO dapat menyimpan data dalam format ini, itu sangat cocok.
Unduh penginstal dan secara opsional kode sumber. Setelah Anda menginstalnya, buat dua proyek baru - satu untuk medan dan satu untuk sprite musuh. Pastikan bahwa data level Anda akan disimpan dalam format "CSV terpangkas" yang ringkas dan sederhana.



Pastikan untuk menyimpan dalam format CSV
Untuk proyek terrain, kita perlu membuat layer untuk terrain yang menggunakan spritesheet photoshopped kita yang baru di atas.



Sekarang, simpy gambar peta sesuka Anda. Anda dapat mengklik setiap sprite di spritesheet Anda di palet ubin dan mereka menggambar atau mengisi level Anda sesuai keinginan Anda. Klik kanan tingkat untuk menghapus ubin itu, dan tahan ruang sambil mengklik dan menarik untuk menggulir sekitar.



Sekarang lakukan hal yang sama untuk sprite musuh Anda. Buat layer yang menggunakan spritesheet yang dibuat di tutorial sebelumnya sebagai berikut:



Akhirnya, isi level Anda dengan segala macam skuadron kapal musuh yang menarik sesuai keinginan Anda. Untuk demo saat ini, saya mulai dengan hanya beberapa penjahat dan benar-benar mengisi setiap ruang dengan asteroid menjelang akhir level.



File sumber OMGO untuk level yang digunakan dalam game demo termasuk dalam folder /assets/
dari file zip kode sumber.
Langkah 10: Sematkan Data Level
Kita hanya akan menanamkan level ke SWF sehingga terus menjadi game mandiri yang tidak memerlukan file eksternal untuk diunduh. Anda dapat memiliki editor level terbuka saat memprogram game Anda. Setiap kali Anda mengubah level Anda, simpan saja dan klik tombol RUN FlashDevelop untuk melihat perubahan yang terjadi. Ini akan melihat cap waktu baru pada file level Anda dan mengkompilasi ulang SWF yang sesuai.
Bergantung pada level mana yang diminta, kita mengisi array dua dimensi dari nilai integer berdasarkan pada output level data oleh editor. Selama bermain game, manajer entitas kami akan secara berkala menelurkan kolom sprite medan atau musuh lain berdasarkan data ini. Buat file baru dalam proyek kode Anda yang disebut GameLevels.as
dan embed data level sebagai berikut.
// Stage3D Shoot-em-up Tutorial Part 4 // by Christer Kaitila - www.mcfunkypants.com // GameLevels.as // This class parses .CSV level data strings // that define the locations of tiles from a spritesheet // Example levels were created using the OGMO editor, // but could be designed by hand or any number of other // freeware game level editors that can output .csv // This can be a .txt, .csv, .oel, .etc file // - we will strip all xml/html tags (if any) // - we only care about raw csv data // Our game can access the current level with: // spriteId = myLevel.data[x][y]; package { import flash.display3D.Context3DProgramType; public class GameLevels { // the "demo" level seen during the title screen [Embed(source = '../assets/level0.oel', mimeType = 'application/octet-stream')] private static const LEVEL0:Class; private var level0data:String = new LEVEL0; // the "demo" level background TERRAIN [Embed(source = '../assets/terrain0.oel', mimeType = 'application/octet-stream')] private static const LEVEL0TERRAIN:Class; private var level0terrain:String = new LEVEL0TERRAIN; // the first level that the player actually experiences [Embed(source = '../assets/level1.oel', mimeType = 'application/octet-stream')] private static const LEVEL1:Class; private var level1data:String = new LEVEL1; // the first level background TERRAIN [Embed(source = '../assets/terrain1.oel', mimeType = 'application/octet-stream')] private static const LEVEL1TERRAIN:Class; private var level1terrain:String = new LEVEL1TERRAIN; // the currently loaded level data public var data:Array = []; public function GameLevels() { }
Langkah 11: Mengurai Data Level
Kelas parsing data level baru kita akan menjadi sangat sederhana: kita cukup menghapus semua XML yang berlebihan dan melahap data .CSV dengan memecah setiap baris dengan koma. Ini sudah cukup untuk tujuan kita.
Lanjutkan dengan GameLevels.as
dan terapkan fungsi parsing data level sebagai berikut:
private function stripTags(str:String):String { var pattern:RegExp = /<\/?[a-zA-Z0-9]+.*?>/gim; return str.replace(pattern, ""); } private function parseLevelData(lvl:String):Array { var levelString:String; var temps:Array; var nextValue:int; var output:Array = []; var nextrow:int; switch (lvl) { case "level0" : levelString = stripTags(level0data); break; case "terrain0" : levelString = stripTags(level0terrain); break; case "level1" : levelString = stripTags(level1data); break; case "terrain1" : levelString = stripTags(level1terrain); break; default: return output; } //trace("Level " + num + " data:\n" + levelString); var lines:Array = levelString.split(/\r\n|\n|\r/); for (var row:int = 0; row < lines.length; row++) { // split the string by comma temps = lines[row].split(","); if (temps.length > 1) { nextrow = output.push([]) - 1; // turn the string values into integers for (var col:int = 0; col < temps.length; col++) { if (temps[col] == "") temps[col] = "-1"; nextValue = parseInt(temps[col]); if (nextValue < 0) nextValue = -1; // we still need blanks trace('row '+ nextrow + ' nextValue=' + nextValue); output[nextrow].push(nextValue); } //trace('Level row '+nextrow+':\n' + String(output[nextrow])); } } //trace('Level output data:\n' + String(output)); return output; } public function loadLevel(lvl:String):void { trace("Loading level " + lvl); data = parseLevelData(lvl); } } // end class } // end package
Itu saja untuk kelas parsing level kita. Meskipun sederhana, ini membutuhkan ruang yang sangat sedikit di SWF kita, berjalan cukup cepat, dan memungkinkan untuk melakukan iterasi desain level dengan mudah dengan membuka FlashDevelop dan OGMO pada saat yang bersamaan. Hanya dua klik yang diperlukan untuk mencoba versi baru level Anda, yang berarti bahwa siklus desain-pengujian-pengulangan hanyalah beberapa detik.
Hanya untuk bersenang-senang, berikut adalah beberapa tangkapan layar dari gaya level yang akan dapat dimainkan di game:












Langkah 12: Tingkatkan Entity Manager
Kita perlu memanfaatkan mode pergerakan AI baru yang keren ini dan sistem medan baru yang mengagumkan yang baru saja dibuat. Ini akan membutuhkan beberapa perubahan kecil ke file EntityManager.as
dari terakhir kali. Untuk menghindari kebingungan, seluruh kelas disajikan di sini, tetapi hanya beberapa baris di sana-sini telah berubah.
Secara khusus, alih-alih memaksa manajer entitas untuk menggunakan gambar spritesheet tertentu, kami akan membiarkannya didefinisikan oleh Main.as
sehingga kita dapat memiliki lebih dari satu. Ini karena kita sekarang memiliki musuh dan manajer entitas medan yang berjalan bersamaan.
Perubahan kecil lainnya termasuk jarak pemusnahan yang lebih besar (tepi luar dunia game tempat sprite yang melampaui itu didaur ulang untuk digunakan kembali dalam kumpulan sprite), ditambah berbagai variabel kelas baru yang diperlukan untuk parsing data level secara rutin.
Mulailah dengan memutakhirkan semua variabel kelas di bagian atas file sebagai berikut:
// Stage3D Shoot-em-up Tutorial Part 4 // by Christer Kaitila - www.mcfunkypants.com // EntityManager.as // The entity manager handles a list of all known game entities. // This object pool will allow for reuse (respawning) of // sprites: for example, when enemy ships are destroyed, // they will be re-spawned when needed as an optimization // that increases fps and decreases ram use. package { import flash.display.Bitmap; import flash.display3D.*; import flash.geom.Point; import flash.geom.Rectangle; public class EntityManager { // the level data parser public var level:GameLevels; // the current level number public var levelNum:int = 0; // where in the level we are in pixels public var levelCurrentScrollX:Number = 0; // the last spawned column of level data public var levelPrevCol:int = -1; // pixels we need to scroll before spawning the next col public var levelTilesize:int = 48; // this is used to ensure all terrain tiles line up exactly public var lastTerrainEntity:Entity; // we need to allow at least enough space for ship movement // entities that move beyond the edges of the screen // plus this amount are recycled (destroyed for reuse) public var cullingDistance:Number = 200; // a particle system class that updates our sprites public var particles:GameParticles; // so that explosions can be played public var sfx:GameSound; // the sprite sheet image public var spriteSheet : LiteSpriteSheet; public var SpritesPerRow:int = 8; public var SpritesPerCol:int = 8; // we no longer force a particular spritesheet here //[Embed(source="../assets/sprites.png")] //private var SourceImage : Class; public var SourceImage : Class; // the general size of the player and enemies public var defaultScale:Number = 1; // how fast the default scroll (enemy flying) speed is public var defaultSpeed:Number = 128; // how fast player bullets go per second public var bulletSpeed:Number = 250; // for framerate-independent timings public var currentFrameSeconds:Number = 0; // sprite IDs (indexing the spritesheet) public const spritenumFireball:uint = 63; public const spritenumFireburst:uint = 62; public const spritenumShockwave:uint = 61; public const spritenumDebris:uint = 60; public const spritenumSpark:uint = 59; public const spritenumBullet3:uint = 58; public const spritenumBullet2:uint = 57; public const spritenumBullet1:uint = 56; public const spritenumPlayer:uint = 10; public const spritenumOrb:uint = 17; // reused for calculation speed public const DEGREES_TO_RADIANS:Number = Math.PI / 180; public const RADIANS_TO_DEGREES:Number = 180 / Math.PI; // the player entity - a special case public var thePlayer:Entity; // a "power orb" that orbits the player public var theOrb:Entity; // a reusable pool of entities // this contains every known Entity // including the contents of the lists below public var entityPool : Vector.<Entity>; // these pools contain only certain types // of entity as an optimization for smaller loops public var allBullets : Vector.<Entity>; public var allEnemies : Vector.<Entity>; // all the polygons that make up the scene public var batch : LiteSpriteBatch; // for statistics public var numCreated : int = 0; public var numReused : int = 0; public var maxX:int; public var minX:int; public var maxY:int; public var minY:int; public var midpoint:int;
Langkah 13: Tingkatkan Init
Kita perlu memutakhirkan konstruktor kelas manajer entitas untuk membuat turunan dari kelas parser data tingkat permainan yang ditulis di atas. Selain itu, kita ingin menyimpan titik tengah layar untuk digunakan sebagai posisi awal bermain dan memperluas lokasi sprite minimum dan maksimum setiap kali game diubah ukurannya. Melanjutkan dengan EntityManager.as
, tingkatkan yang berikut ini.
public function EntityManager(view:Rectangle) { entityPool = new Vector.<Entity>(); allBullets = new Vector.<Entity>(); allEnemies = new Vector.<Entity>(); particles = new GameParticles(this); setPosition(view); level = new GameLevels(); } public function setPosition(view:Rectangle):void { // allow moving fully offscreen before // automatically being culled (and reused) maxX = view.width + cullingDistance; minX = view.x - cullingDistance; maxY = view.height + cullingDistance; minY = view.y - cullingDistance; midpoint = view.height / 2; } // this XOR based fast random number generator runs 4x faster // than Math.random() and also returns a number from 0 to 1 // see http://www.calypso88.com/?cat=7 private const FASTRANDOMTOFLOAT:Number = 1 / uint.MAX_VALUE; private var fastrandomseed:uint = Math.random() * uint.MAX_VALUE; public function fastRandom():Number { fastrandomseed ^= (fastrandomseed << 21); fastrandomseed ^= (fastrandomseed >>> 35); fastrandomseed ^= (fastrandomseed << 4); return (fastrandomseed * FASTRANDOMTOFLOAT); }
Langkah 14: Akun untuk Padding UV
Ada satu penyesuaian khusus yang diperlukan untuk fungsi createBatch kita. Ternyata spritesheet medan, yang menggunakan sprite ubin yang bersebelahan satu sama lain dan tidak menyertakan ruang kosong di antara ubin, dapat menghasilkan gangguan visual pada game kita jika digunakan apa adanya. Ini karena cara GPU kartu video Anda mencicipi setiap tekstur dalam kumpulan sprite ketika membuat semua sprite. Berikut adalah contoh medan yang kami buat menggunakan rutinitas dari minggu lalu:



Apa yang terjadi dalam contoh #1 adalah bahwa piksel tepi ubin dapat "berdarah" ke ubin sebelah karena interpolasi bilinear dari nilai-nilai RGB. Untuk memperhitungkan hal ini, kita perlu mengizinkan nilai offset tekstur kecil (UV), yang akan "memperbesar" setiap ubin hanya dengan jumlah yang sangat kecil. Tanpa perubahan ini, game akan memiliki artefak seperti yang terlihat di atas.
public function createBatch(context3D:Context3D, uvPadding:Number=0) : LiteSpriteBatch { var sourceBitmap:Bitmap = new SourceImage(); // create a spritesheet with 8x8 (64) sprites on it spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, SpritesPerRow, SpritesPerCol, uvPadding); // Create new render batch batch = new LiteSpriteBatch(context3D, spriteSheet); return batch; }
Langkah 15: Rutinitas Pemijahan
Rutinitas berikut hampir identik dengan minggu lalu terlepas dari penggunaan nilai titik tengah dalam pemijahan pemain dan beberapa perbedaan ukuran. Mereka termasuk di sini untuk kelengkapan.
Akhirnya, fungsi addRandomEntity
seperti didefinisikan di bawah adalah apa yang di versi sebelumnya adalah fungsi addEntities
. Ini tidak digunakan dalam demo ini dan dapat dihapus, karena kita mengubah permainan kita untuk tidak lagi menggunakan musuh yang muncul secara acak dan alih-alih beralih ke level kerajinan tangan. Fungsi ini mungkin berguna dalam pengujian Anda untuk memperhitungkan waktu ketika tidak ada data level yang tersisa. Anda cukup menyalin dan menempelkan kode ini di atas rutinitas asli Anda dan melanjutkan tanpa melihat lebih dalam.
// search the entity pool for unused entities and reuse one // if they are all in use, create a brand new one public function respawn(sprID:uint=0):Entity { var currentEntityCount:int = entityPool.length; var anEntity:Entity; var i:int = 0; // search for an inactive entity for (i = 0; i < currentEntityCount; i++ ) { anEntity = entityPool[i]; if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) { //trace('Reusing Entity #' + i); anEntity.active = true; anEntity.sprite.visible = true; anEntity.recycled = true; numReused++; return anEntity; } } // none were found so we need to make a new one //trace('Need to create a new Entity #' + i); var sprite:LiteSprite; sprite = batch.createChild(sprID); anEntity = new Entity(sprite, this); entityPool.push(anEntity); numCreated++; return anEntity; } // this entity is the PLAYER public function addPlayer(playerController:Function):Entity { trace("Adding Player Entity"); thePlayer = respawn(spritenumPlayer); thePlayer.sprite.position.x = 64; thePlayer.sprite.position.y = midpoint; thePlayer.sprite.rotation = 180 * DEGREES_TO_RADIANS; thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = defaultScale; thePlayer.speedX = 0; thePlayer.speedY = 0; thePlayer.active = true; thePlayer.collidemode = 1; thePlayer.collideradius = 10; thePlayer.owner = thePlayer; // collisions require this thePlayer.aiFunction = playerController; // just for fun, spawn an orbiting "power orb" theOrb = respawn(spritenumOrb); theOrb.rotationSpeed = 720 * DEGREES_TO_RADIANS; theOrb.sprite.scaleX = theOrb.sprite.scaleY = defaultScale / 2; theOrb.leavesTrail = true; theOrb.collidemode = 1; theOrb.collideradius = 12; theOrb.isBullet = true; theOrb.owner = thePlayer; theOrb.orbiting = thePlayer; theOrb.orbitingDistance = 180; return thePlayer; } // shoot a bullet public function shootBullet(powa:uint=1, shooter:Entity = null):Entity { // just in case the AI is running during the main menu // and we've not yet created the player entity if (thePlayer == null) return null; var theBullet:Entity; // assume the player shot it // otherwise maybe an enemy did if (shooter == null) shooter = thePlayer; // three possible bullets, progressively larger if (powa == 1) theBullet = respawn(spritenumBullet1); else if (powa == 2) theBullet = respawn(spritenumBullet2); else theBullet = respawn(spritenumBullet3); theBullet.sprite.position.x = shooter.sprite.position.x + 8; theBullet.sprite.position.y = shooter.sprite.position.y + 2; theBullet.sprite.rotation = 180 * DEGREES_TO_RADIANS; theBullet.sprite.scaleX = theBullet.sprite.scaleY = 1; if (shooter == thePlayer) { theBullet.speedX = bulletSpeed; theBullet.speedY = 0; } else // enemy bullets move slower and towards the player { theBullet.sprite.rotation = pointAtRad(theBullet.sprite.position.x - thePlayer.sprite.position.x, theBullet.sprite.position.y-thePlayer.sprite.position.y) - (90*DEGREES_TO_RADIANS); // move in the direction we're facing theBullet.speedX = defaultSpeed*1.5*Math.cos(theBullet.sprite.rotation); theBullet.speedY = defaultSpeed*1.5*Math.sin(theBullet.sprite.rotation); // optionally, we could just fire straight ahead in the direction we're heading: // theBullet.speedX = shooter.speedX * 1.5; // theBullet.speedY = shooter.speedY * 1.5; // and we could point where we're going like this: // pointAtRad(theBullet.speedX,theBullet.speedY) - (90*DEGREES_TO_RADIANS); } theBullet.owner = shooter; theBullet.collideradius = 10; theBullet.collidemode = 1; theBullet.isBullet = true; if (!theBullet.recycled) allBullets.push(theBullet); return theBullet; } // Unused: this was "addEntities()" in the previous tutorials. // It spawns random enemies that move in a straight line public function addRandomEntity():void { var anEntity:Entity; var sprID:int; sprID = Math.floor(fastRandom() * 55); // try to reuse an inactive entity (or create a new one) anEntity = respawn(sprID); // give it a new position and velocity anEntity.sprite.position.x = maxX; anEntity.sprite.position.y = fastRandom() * maxY; anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); anEntity.sprite.scaleX = defaultScale; anEntity.sprite.scaleY = defaultScale; anEntity.sprite.rotation = pointAtRad(anEntity.speedX,anEntity.speedY) - (90*DEGREES_TO_RADIANS); anEntity.collidemode = 1; anEntity.collideradius = 16; if (!anEntity.recycled) allEnemies.push(anEntity); } // returns the angle in radians of two points public function pointAngle(point1:Point, point2:Point):Number { var dx:Number = point2.x - point1.x; var dy:Number = point2.y - point1.y; return -Math.atan2(dx,dy); } // returns the angle in degrees of 0,0 to x,y public function pointAtDeg(x:Number, y:Number):Number { return -Math.atan2(x,y) * RADIANS_TO_DEGREES; } // returns the angle in radians of 0,0 to x,y public function pointAtRad(x:Number, y:Number):Number { return -Math.atan2(x,y); } // as an optimization to saver millions of checks, only // the player's bullets check for collisions with all enemy ships // (enemy bullets only check to hit the player) public function checkCollisions(checkMe:Entity):Entity { var anEntity:Entity; var collided:Boolean = false; if (checkMe.owner != thePlayer) { // quick check ONLY to see if we have hit the player anEntity = thePlayer; if (checkMe.colliding(anEntity)) { trace("Player was HIT!"); collided = true; } } else // check all active enemies { for(var i:int=0; i< allEnemies.length;i++) { anEntity = allEnemies[i]; if (anEntity.active && anEntity.collidemode) { if (checkMe.colliding(anEntity)) { collided = true; break; } } } } if (collided) { //trace('Collision!'); if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); particles.addExplosion(checkMe.sprite.position); if ((checkMe != theOrb) && (checkMe != thePlayer)) checkMe.die(); // the bullet if ((anEntity != theOrb) && ((anEntity != thePlayer))) anEntity.die(); // the victim return anEntity; } return null; }
Langkah 16: Tingkatkan Lingkaran Render
Fungsi update()
dijalankan setiap frame tunggal, seperti sebelumnya. Beberapa modifikasi telah dibuat untuk memperhitungkan fungsionalitas AI musuh baru yang ditambahkan ke kelas entitas di atas, serta fungsi parsing data level baru kita.
Misalnya, minggu lalu kita hanya menjalankan langkah pembaruan simulasi entitas jika tidak ada aiFunction
yang didefinisikan. Sekarang, kita akan memanggil fungsi ini di hampir setiap entitas game yang bergerak, jadi kita menjalankannya dan kemudian melanjutkan dengan animasi standar dengan memeriksa kecepatan berbagai parameter entitas. Kita dulu hanya memeriksa tabrakan dengan peluru, tetapi sekarang kapal musuh dapat bertabrakan dengan pemain juga.
Melanjutkan dengan EntityManager.as
, terapkan perubahan ini sebagai berikut.
// called every frame: used to update the simulation // this is where you would perform AI, physics, etc. // in this version, currentTime is seconds since the previous frame public function update(currentTime:Number) : void { var anEntity:Entity; var i:int; var max:int; // what portion of a full second has passed since the previous update? currentFrameSeconds = currentTime / 1000; // handle all other entities max = entityPool.length; for (i = 0; i < max; i++) { anEntity = entityPool[i]; if (anEntity.active) { // subtract the previous aiPathOffset anEntity.sprite.position.x -= anEntity.aiPathOffsetX; anEntity.sprite.position.y -= anEntity.aiPathOffsetY; // calculate location on screen with scrolling anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds; anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds; // is a custom AI specified? if so, run it now if (anEntity.aiFunction != null) { anEntity.aiFunction(currentFrameSeconds); } // add the new aiPathOffset anEntity.sprite.position.x += anEntity.aiPathOffsetX; anEntity.sprite.position.y += anEntity.aiPathOffsetY; // collision detection if (anEntity.collidemode) { checkCollisions(anEntity); } // entities can orbit other entities // (uses their rotation as the position) if (anEntity.orbiting != null) { anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x + ((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y - ((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); } // entities can leave an engine emitter trail if (anEntity.leavesTrail) { // leave a trail of particles if (anEntity == theOrb) particles.addParticle(63, anEntity.sprite.position.x, anEntity.sprite.position.y, 0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1); else // other enemies particles.addParticle(63, anEntity.sprite.position.x + 12, anEntity.sprite.position.y + 2, 0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1); } if ((anEntity.sprite.position.x > maxX) || (anEntity.sprite.position.x < minX) || (anEntity.sprite.position.y > maxY) || (anEntity.sprite.position.y < minY)) { // if we go past any edge, become inactive // so the sprite can be respawned if ((anEntity != thePlayer) && (anEntity != theOrb)) anEntity.die(); } if (anEntity.rotationSpeed != 0) anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds; if (anEntity.fadeAnim != 0) { anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds; if (anEntity.sprite.alpha <= 0.001) { anEntity.die(); } else if (anEntity.sprite.alpha > 1) { anEntity.sprite.alpha = 1; } } if (anEntity.zoomAnim != 0) { anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds; anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds; if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0) anEntity.die(); } } } }
Langkah 17: Beralih Level
Fungsi yang tersisa di EntityManager.as
adalah merek baru. Kita membutuhkan mekanisme untuk secara instan menghancurkan semua entitas yang dikenal di seluruh dunia game. Ini akan terjadi setiap kali pemain naik ke level berikutnya. Ini juga terjadi segera ketika permainan meninggalkan mode tarik "menu utama" sehingga sprite yang ada di sana tidak mencemari dunia permainan pemain yang sebenarnya. Saat game dimulai, kita juga akan mengurai set data level berikutnya.
// kill (recycle) all known entities // this is run when we change levels public function killEmAll():void { //trace('Killing all entities...'); var anEntity:Entity; var i:int; var max:int; max = entityPool.length; for (i = 0; i < max; i++) { anEntity = entityPool[i]; if ((anEntity != thePlayer) && (anEntity != theOrb)) anEntity.die(); } } // load a new level for entity generation public function changeLevels(lvl:String):void { killEmAll(); level.loadLevel(lvl); levelCurrentScrollX = 0; levelPrevCol = -1; }
Langkah 18: Streaming Tingkat
Fungsi akhir yang perlu kita tambahkan ke manajer entitas kita adalah yang menumbuhkan entitas baru berdasarkan data level. Ini mengukur jarak yang telah kita tempuh, dan ketika set petak level berikutnya diharuskan, ia memunculkan kolom entitas lain seperti yang ditentukan oleh data level. Jika manajer entitas yang menjalankan rutin ini bertanggung jawab atas medan, tidak ada lagi yang perlu dilakukan, tetapi jika kita memunculkan kapal musuh, asteroid, dan senjata penjaga, kita perlu memutuskan jenis AI rutin apa yang diberikan untuk setiap entitas.
Salah satu pertimbangan penting berkaitan dengan gangguan medan seperti yang diilustrasikan dalam gambar di atas yang menunjukkan "jahitan" di antara ubin.



Dalam beberapa versi pertama dari fungsi ini, kita hanya mengukur jarak yang ditempuh berdasarkan waktu yang berlalu setiap frame dan menambah variabel penghitung, memunculkan baris ubin berikutnya jika diperlukan. Masalah dengan pendekatan ini adalah bahwa angka floating point (apa pun dengan titik desimal) tidak 100% akurat. Karena kita hanya dapat menyimpan begitu banyak informasi dalam jenis Number
, beberapa jumlah yang sangat kecil dibulatkan.
Ini tidak terlihat dalam sebagian besar situasi, tetapi seiring waktu sedikit perbedaan bertambah sampai akhirnya ubin medan dimatikan oleh piksel. Oleh karena itu, kita melacak petak medan kolom sebelumnya dan memaksa yang berikutnya tepat jarak yang tepat darinya. Kita tidak bisa berasumsi bahwa medan telah menggulir tepat 48 piksel sejak terakhir kali kita menghasilkan ubin. Mungkin telah memindahkan 48.00000000001 piksel.
Anda dapat membaca lebih lanjut tentang banyak masalah yang dapat diakumulasikan oleh floating-point di game dalam artikel yang sangat menarik ini.
// check to see if another row from the level data should be spawned public function streamLevelEntities(theseAreEnemies:Boolean = false):void { var anEntity:Entity; var sprID:int; // time-based with overflow remembering (increment and floor) levelCurrentScrollX += defaultSpeed * currentFrameSeconds; // is it time to spawn the next col from our level data? if (levelCurrentScrollX >= levelTilesize) { levelCurrentScrollX = 0; levelPrevCol++; // this prevents small "seams" due to floating point inaccuracies over time var currentLevelXCoord:Number; if (lastTerrainEntity && !theseAreEnemies) currentLevelXCoord = lastTerrainEntity.sprite.position.x + levelTilesize; else currentLevelXCoord = maxX; var rows:int = level.data.length; //trace('levelCurrentScrollX = ' + levelCurrentScrollX + //' - spawning next level column ' + levelPrevCol + ' row count: ' + rows); if (level.data && level.data.length) { for (var row:int = 0; row < rows; row++) { if (level.data[row].length > levelPrevCol) // data exists? NOP? { //trace('Next row data: ' + String(level.data[row])); sprID = level.data[row][levelPrevCol]; if (sprID > -1) // zero is a valid number, -1 means blank { anEntity = respawn(sprID); anEntity.sprite.position.x = currentLevelXCoord; anEntity.sprite.position.y = (row * levelTilesize) + (levelTilesize/2); trace('Spawning a level sprite ID ' + sprID + ' at ' + anEntity.sprite.position.x + ',' + anEntity.sprite.position.y); anEntity.speedX = -defaultSpeed; anEntity.speedY = 0; anEntity.sprite.scaleX = defaultScale; anEntity.sprite.scaleY = defaultScale;
Langkah 19: Berikan Otak Musuh
Melanjutkan fungsi streamLevelEntities
, kita hanya perlu memilih AI jenis apa yang akan digunakan untuk setiap musuh yang baru dilahirkan (jika ada). Untuk referensi, ini adalah spritesheet yang digunakan:

Spritesheet musuh telah dibagi menjadi baris. Baris pertama dari sprite hanya bergerak ke depan dalam garis lurus pada sudut acak. Baris kedua menggunakan gerakan baru dibuat sinusoidal "gelombang seperti" kita ponting yang dalam garis lurus. Kita menjelaskan dua ubin penjaga senjata dan tiga gambar asteroid sebagai kasus khusus. Baris berikutnya bergerak pada sudut acak dengan goyangan, dan akhirnya, semua sprite yang tersisa akan menggunakan gerakan kurva spline Catmull-Rom acak kita.
if (theseAreEnemies) { // which AI should we give this enemy? switch (sprID) { case 1: case 2: case 3: case 4: case 5: case 6: case 7: // move forward at a random angle anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); anEntity.aiFunction = anEntity.straightAI; break; case 8: case 9: case 10: case 11: case 12: case 13: case 14: case 15: // move straight with a wobble anEntity.aiFunction = anEntity.wobbleAI; break case 16: case 24: // sentry guns don't move and always look at the player anEntity.aiFunction = anEntity.sentryAI; anEntity.speedX = -90; // same speed as background break; case 17: case 18: case 19: case 20: case 21: case 22: case 23: // move at a random angle with a wobble anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); anEntity.aiFunction = anEntity.wobbleAI; break; case 32: case 40: case 48: // asteroids don't move or shoot but they do spin and drift anEntity.aiFunction = null; anEntity.rotationSpeed = fastRandom() * 8 - 4 anEntity.speedY = fastRandom() * 64 - 32; break; default: // follow a complex random spline curve path anEntity.aiFunction = anEntity.droneAI; break; } anEntity.sprite.rotation = pointAtRad(anEntity.speedX, anEntity.speedY) - (90*DEGREES_TO_RADIANS); anEntity.collidemode = 1; anEntity.collideradius = 16; if (!anEntity.recycled) allEnemies.push(anEntity); } // end if these were enemies }// end loop for level data rows } } } // remember the last created terrain entity // (might be null if the level data was blank for this column) // to avoid slight seams due to terrain scrolling speed over time if (!theseAreEnemies) lastTerrainEntity = anEntity; } } } // end class } // end package
Itu saja untuk kelas manajer entitas kita yang baru saja ditingkatkan. Sekarang mengambil keuntungan dari medan "streaming", memberikan musuh AI yang tepat, dan tidak lagi hanya memunculkan arus penjahat tak terbatas acak.
Langkah 20: Akun untuk Padding UV
Dalam peningkatan di atas, kita menghindari gangguan grafis kecil dengan memperhitungkan dua hal: ketidaksesuaian floating-point dan interpolasi sampling tekstur, yang menyebabkan pendarahan piksel tepi ke ubin medan yang berdekatan. Yang terakhir mengharuskan kita "memperbesar" ubin medan sedikit demi sedikit agar tepinya terlihat benar. Kita perlu meningkatkan kelas LiteSpriteSheet.as
yang ada untuk memperhitungkan offset kecil ini saat membuat semua koordinat tekstur UV untuk setiap sprite.
// Stage3D Shoot-em-up Tutorial Part 4 // by Christer Kaitila - www.mcfunkypants.com // LiteSpriteSheet.as // An optimization used to improve performance, all sprites used // in the game are packed onto a single texture so that // they can be rendered in a single pass rather than individually. // This also avoids the performance penalty of 3d stage changes. // Based on example code by Chris Nuuja which is a port // of the haXe+NME bunnymark demo by Philippe Elsass // which is itself a port of Iain Lobb's original work. // Also includes code from the Starling framework. // Grateful acknowledgements to all involved. package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Stage; import flash.display3D.Context3D; import flash.display3D.Context3DTextureFormat; import flash.display3D.IndexBuffer3D; import flash.display3D.textures.Texture; import flash.geom.Point; import flash.geom.Rectangle; import flash.geom.Matrix; public class LiteSpriteSheet { internal var _texture : Texture; protected var _spriteSheet : BitmapData; protected var _uvCoords : Vector.<Number>; protected var _rects : Vector.<Rectangle>; // because the edge pixels of some sprites are bleeding through, // we zoom in the texture just the slightest bit for terrain tiles public var uvPadding:Number = 0; // 0.01; public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8, uvPad:Number = 0) { _uvCoords = new Vector.<Number>(); _rects = new Vector.<Rectangle>(); _spriteSheet = SpriteSheetBitmapData; uvPadding = uvPad; createUVs(numSpritesW, numSpritesH); } // generate a list of uv coordinates for a grid of sprites // on the spritesheet texture for later reference by ID number // sprite ID numbers go from left to right then down public function createUVs(numSpritesW:int, numSpritesH:int) : void { trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ ' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); var destRect : Rectangle; for (var y:int = 0; y < numSpritesH; y++) { for (var x:int = 0; x < numSpritesW; x++) { _uvCoords.push( // bl, tl, tr, br (x / numSpritesW) + uvPadding, ((y+1) / numSpritesH) - uvPadding, (x / numSpritesW) + uvPadding, (y / numSpritesH) + uvPadding, ((x+1) / numSpritesW) - uvPadding, (y / numSpritesH) + uvPadding, ((x + 1) / numSpritesW) - uvPadding, ((y + 1) / numSpritesH) - uvPadding); destRect = new Rectangle(); destRect.left = 0; destRect.top = 0; destRect.right = _spriteSheet.width / numSpritesW; destRect.bottom = _spriteSheet.height / numSpritesH; _rects.push(destRect); } } } // when the automated grid isn't what we want // we can define any rectangle and return a new sprite ID public function defineSprite(x:uint, y:uint, w:uint, h:uint) : uint { var destRect:Rectangle = new Rectangle(); destRect.left = x; destRect.top = y; destRect.right = x + w; destRect.bottom = y + h; _rects.push(destRect); _uvCoords.push( destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height, destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height, destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height, destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height); return _rects.length - 1; } public function removeSprite(spriteId:uint) : void { if ( spriteId < _uvCoords.length ) { _uvCoords = _uvCoords.splice(spriteId * 8, 8); _rects.splice(spriteId, 1); } } public function get numSprites() : uint { return _rects.length; } public function getRect(spriteId:uint) : Rectangle { return _rects[spriteId]; } public function getUVCoords(spriteId:uint) : Vector.<Number> { var startIdx:uint = spriteId * 8; return _uvCoords.slice(startIdx, startIdx + 8); } public function uploadTexture(context3D:Context3D) : void { if ( _texture == null ) { _texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); } _texture.uploadFromBitmapData(_spriteSheet); // generate mipmaps var currentWidth:int = _spriteSheet.width >> 1; var currentHeight:int = _spriteSheet.height >> 1; var level:int = 1; var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); var transform:Matrix = new Matrix(.5, 0, 0, .5); while ( currentWidth >= 1 || currentHeight >= 1 ) { canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); canvas.draw(_spriteSheet, transform, null, null, null, true); _texture.uploadFromBitmapData(canvas, level++); transform.scale(0.5, 0.5); currentWidth = currentWidth >> 1; currentHeight = currentHeight >> 1; } } } // end class } // end package
Seperti yang Anda lihat, satu-satunya perubahan di atas terkait dengan parameter uvPadding
di konstruktor kelas untuk setiap spritesheet.
Langkah 21: Peningkatan Terakhir!
Kita hampir selesai! Yang perlu dilakukan sekarang adalah memutakhirkan Main.as
yang ada dalam proyek kita untuk memperhitungkan berbagai perubahan kecil pada cara kita membuat contoh baru dari manajer entitas kita dan spritesheets mereka. Perubahan pada file ini sepele, tetapi termasuk di sini secara penuh untuk menghindari kebingungan.
Perbedaan utama termasuk fakta bahwa kita sekarang menanamkan dua spritesheet utama di sini daripada di manajer entitas, penambahan lapisan medan, berurusan dengan dua kecepatan gulir yang berbeda (karena kita ingin medan bergerak lebih lambat daripada musuh di foreground, dan memicu level baru untuk dimuat.
// Stage3D Shoot-em-up Tutorial Part 4 // by Christer Kaitila - www.mcfunkypants.com // Created for active.tutsplus.com package { [SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] import flash.display3D.*; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageQuality; import flash.display.StageScaleMode; import flash.events.Event; import flash.events.ErrorEvent; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.utils.getTimer; public class Main extends Sprite { // the entity spritesheet (ships, particles) [Embed(source="../assets/sprites.png")] private var EntitySourceImage : Class; // the terrain spritesheet [Embed(source="../assets/terrain.png")] private var TerrainSourceImage : Class; // the keyboard control system private var _controls : GameControls; // don't update the menu too fast private var nothingPressedLastFrame:Boolean = false; // timestamp of the current frame public var currentTime:int; // for framerate independent speeds public var currentFrameMs:int; public var previousFrameTime:int; // player one's entity public var thePlayer:Entity; // movement speed in pixels per second public var playerSpeed:Number = 128; // timestamp when next shot can be fired private var nextFireTime:uint = 0; // how many ms between shots private var fireDelay:uint = 200; // main menu = 0 or current level number private var _state : int = 0; // the title screen batch private var _mainmenu : GameMenu; // the sound system private var _sfx : GameSound; // the background stars private var _bg : GameBackground; private var _terrain : EntityManager; private var _entities : EntityManager; private var _spriteStage : LiteSpriteStage; private var _gui : GameGUI; private var _width : Number = 600; private var _height : Number = 400; public var context3D : Context3D; // constructor function for our game public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } // called once flash is ready private function init(e:Event = null):void { _controls = new GameControls(stage); removeEventListener(Event.ADDED_TO_STAGE, init); stage.quality = StageQuality.LOW; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.addEventListener(Event.RESIZE, onResizeEvent); trace("Init Stage3D..."); _gui = new GameGUI("Stage3D Shoot-em-up Tutorial Part 3"); addChild(_gui); stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); trace("Stage3D requested..."); _sfx = new GameSound(); } // this is called when the 3d card has been set up // and is ready for rendering using stage3d private function onContext3DCreate(e:Event):void { trace("Stage3D context created! Init sprite engine..."); context3D = stage.stage3Ds[0].context3D; initSpriteEngine(); } // this can be called when using an old version of flash // or if the html does not include wmode=direct private function errorHandler(e:ErrorEvent):void { trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); } protected function onResizeEvent(event:Event) : void { trace("resize event..."); // Set correct dimensions if we resize _width = stage.stageWidth; _height = stage.stageHeight; // Resize Stage3D to continue to fit screen var view:Rectangle = new Rectangle(0, 0, _width, _height); if ( _spriteStage != null ) { _spriteStage.position = view; } if(_terrain != null) { _terrain.setPosition(view); } if(_entities != null) { _entities.setPosition(view); } if(_mainmenu != null) { _mainmenu.setPosition(view); } } private function initSpriteEngine():void { // init a gpu sprite system //var view:Rectangle = new Rectangle(0,0,_width,_height) var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); _spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); _spriteStage.configureBackBuffer(_width,_height); // create the background stars trace("Init background..."); _bg = new GameBackground(stageRect); _bg.createBatch(context3D); _spriteStage.addBatch(_bg.batch); _bg.initBackground(); // create the terrain spritesheet and batch trace("Init Terrain..."); _terrain = new EntityManager(stageRect); _terrain.SourceImage = TerrainSourceImage; _terrain.SpritesPerRow = 16; _terrain.SpritesPerCol = 16; _terrain.defaultSpeed = 90; _terrain.defaultScale = 1.5; _terrain.levelTilesize = 48; _terrain.createBatch(context3D, 0.001); // a little UV padding required _spriteStage.addBatch(_terrain.batch); _terrain.level.loadLevel('terrain0'); // demo level NOW // create a single rendering batch // which will draw all sprites in one pass trace("Init Entities..."); _entities = new EntityManager(stageRect); _entities.SourceImage = EntitySourceImage; _entities.defaultScale = 1.5; // 1 _entities.levelTilesize = 48; _entities.createBatch(context3D); _entities.sfx = _sfx; _spriteStage.addBatch(_entities.batch); _entities.level.loadLevel('level0'); // demo level NOW _entities.streamLevelEntities(true); // spawn first row of the level immediately // create the logo/titlescreen main menu _mainmenu = new GameMenu(stageRect); _mainmenu.createBatch(context3D); _spriteStage.addBatch(_mainmenu.batch); // tell the gui where to grab statistics from _gui.statsTarget = _entities; // start the render loop stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); // only used for the menu stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown); stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove); } public function playerLogic(seconds:Number):void { var me:Entity = _entities.thePlayer; me.speedY = me.speedX = 0; if (_controls.pressing.up) me.speedY = -playerSpeed; if (_controls.pressing.down) me.speedY = playerSpeed; if (_controls.pressing.left) me.speedX = -playerSpeed; if (_controls.pressing.right) me.speedX = playerSpeed; // keep on screen if (me.sprite.position.x < 0) me.sprite.position.x = 0; if (me.sprite.position.x > _width) me.sprite.position.x = _width; if (me.sprite.position.y < 0) me.sprite.position.y = 0; if (me.sprite.position.y > _height) me.sprite.position.y = _height; // // leave a trail of particles _entities.particles.addParticle(63, me.sprite.position.x - 12, me.sprite.position.y + 2, 0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5); } private function mouseDown(e:MouseEvent):void { trace('mouseDown at '+e.stageX+','+e.stageY); if (_state == 0) // are we at the main menu? { if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer())) { // if the above returns true we should start the game startGame(); } } } private function mouseMove(e:MouseEvent):void { if (_state == 0) // are we at the main menu? { // select menu items via mouse if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY); } } // handle any player input private function processInput():void { if (_state == 0) // are we at the main menu? { // select menu items via keyboard if (_controls.pressing.down || _controls.pressing.right) { if (nothingPressedLastFrame) { _sfx.playGun(1); _mainmenu.nextMenuItem(); nothingPressedLastFrame = false; } } else if (_controls.pressing.up || _controls.pressing.left) { if (nothingPressedLastFrame) { _sfx.playGun(1); _mainmenu.prevMenuItem(); nothingPressedLastFrame = false; } } else if (_controls.pressing.fire) { if (_mainmenu.activateCurrentMenuItem(getTimer())) { // if the above returns true we should start the game startGame(); } } else { // this ensures the menu doesn't change too fast nothingPressedLastFrame = true; } } else { // we are NOT at the main menu: we are actually playing the game // in future versions we will add projectile // spawning functinality here to fire bullets if (_controls.pressing.fire) { // is it time to fire again? if (currentTime >= nextFireTime) { //trace("Fire!"); nextFireTime = currentTime + fireDelay; _sfx.playGun(1); _entities.shootBullet(3); } } } } private function startGame():void { trace("Starting game!"); _state = 1; _spriteStage.removeBatch(_mainmenu.batch); _sfx.playMusic(); // add the player entity to the game! thePlayer = _entities.addPlayer(playerLogic); // load level one (and clear demo entities) _entities.changeLevels('level1'); _terrain.changeLevels('terrain1'); } // this function draws the scene every frame private function onEnterFrame(e:Event):void { try { // grab timestamp of current frame currentTime = getTimer(); currentFrameMs = currentTime - previousFrameTime; previousFrameTime = currentTime; // erase the previous frame context3D.clear(0, 0, 0, 1); // for debugging the input manager, update the gui _gui.titleText = _controls.textDescription(); // process any player input processInput(); // scroll the background if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height); _bg.update(currentTime); // update the main menu titlescreen if (_state == 0) _mainmenu.update(currentTime); // move/animate all entities _terrain.update(currentFrameMs); _entities.update(currentFrameMs); // keep adding more sprites - IF we need to _terrain.streamLevelEntities(false); _entities.streamLevelEntities(true); // draw all entities _spriteStage.render(); // update the screen context3D.present(); } catch (e:Error) { // this can happen if the computer goes to sleep and // then re-awakens, requiring reinitialization of stage3D // (the onContext3DCreate will fire again) } } } // end class } // end package
Dilakukan! Kompilasi proyek Anda, perbaiki salah ketik, dan jalankan game. Jika Anda mengalami masalah dengan kode yang Anda ketikkan atau hanya ingin kepuasan instan dari segala sesuatu di satu tempat, ingatlah bahwa Anda dapat mengunduh kode sumber lengkap di sini.
Berikut adalah beberapa tips untuk jika Anda mengalami masalah:
- Jika Anda menggunakan FlashBuilder, pastikan untuk memasukkan "
-default-frame-rate 60
" di opsi kompiler Anda untuk memastikan Anda mendapatkan kinerja terbaik. - Jika Anda menggunakan Linux atau Mac, Anda dapat mengompilasinya dari command-line (atau dalam makefile) menggunakan sesuatu yang mirip dengan
"mxmlc-load-config+=obj\shmup_tutorial_part4Config.xml-swf-version=13"
, tergantung pada lingkungan kerja Anda. - Ingatlah bahwa karena kita menggunakan Flash 11, Anda harus mengkompilasi menggunakan versi terbaru dari kompiler Flex dan
playerglobal.swc
. - Yang terpenting, ingat bahwa HTML embed Flash Anda harus menyertakan "
wmode=direct
" untuk mengaktifkan Stage3D.
Sumber ini hanya diuji menggunakan FlashDevelop di Windows, dan tips di atas telah diajukan oleh sesama pembaca Anda.
Setelah semuanya mengkompilasi dan berjalan dengan benar, Anda akan melihat sesuatu yang terlihat seperti ini: permainan tembak-menembak Stage3d aksi cepat lengkap dengan medan bergulir paralaks, berton-ton musuh untuk dihancurkan, suara, musik dan - yang tak kalah pentingnya - yang halus -mengisi 60 frame per framerate detik!



Bagian Empat Lengkap: Persiapkan untuk Level Lima!
Itu saja untuk tutorial nomor empat dalam seri ini. Kita sekarang dapat membanggakan dunia game yang terperinci, musuh dengan beberapa variasi, dan kemampuan untuk menggunakan editor level. Kita sedang dalam perjalanan menuju produk akhir!
Dalam tutorial berikutnya, kita akan meningkatkan kemampuan menembak baru yang mengagumkan ke akun untuk kesehatan dan skor pemain. Kita juga akan menambahkan konsep "live" sehingga Anda bisa mendapatkan kondisi "permainan berakhir" atau "kredit akhir". Kita akan menambahkan transisi untuk memberikan pemain jeda antar level, dan kita akan memungkinkan pengguna untuk melanjutkan di mana mereka tinggalkan. Alih-alih demo tanpa batas di mana Anda tidak bisa mati, proyek kita akhirnya akan menjadi permainan yang sepenuhnya dapat dimainkan yang benar-benar menimbulkan tantangan bagi pemain. Kita akan mencoba mengukur kesulitan dengan tepat sehingga mulai mudah dan semakin sulit saat Anda melanjutkan.
Setelah minggu depan, tutorial terakhir, nomor enam, adalah tentang menambahkan polesan akhir, ditambah pembuatan BOSS BATTLE epik! Ini akan menjadi menyenangkan!
Saya ingin sekali mendengar dari Anda tentang tutorial ini. Saya dengan hangat menyambut semua pembaca untuk menghubungi saya melalui twitter: @McFunkypants, blog saya mcfunkypants.com atau di google+ kapan saja. Secara khusus, saya ingin melihat permainan yang Anda buat menggunakan kode ini dan saya selalu mencari topik baru untuk menulis tutorial di masa depan. Hubungi saya kapan saja.
Jika Anda telah menikmati tutorial ini sejauh ini, mungkin Anda ingin mempelajari lebih lanjut tentang Stage3D? Jika demikian, mengapa tidak membeli buku Stage3d saya!
Semoga Berhasil dan BERSENANG-SENANGLAH!