Indonesian (Bahasa Indonesia) translation by Keti Pritania (you can also view the original English article)
Dalam seri ini tutorial (bagian gratis, Bagian premi) kami akan membuat kinerja tinggi 2D shoot-em-up menggunakan baru hardware-accelerated Stage3D mesin render. Kami akan mengambil keuntungan dari beberapa hardcore optimasi teknik untuk mencapai kinerja render besar 2D sprite. Dalam bagian ini, kita akan membangun demo kinerja tinggi yang menarik ratusan bergerak sprite layar sekaligus.
Hasil akhir Tinjauan
Mari kita lihat pada hasil akhir yang kita akan bekerja ke arah: demo kinerja tinggi 2D sprite yang menggunakan Stage3D dengan optimasi yang mencakup spritesheet dan objek penggabungan.
Pendahuluan: Flash 11 Stage3D
Jika Anda berharap untuk mengambil Flash permainan ke tingkat berikutnya dan mencari banyak mata-permen dan menakjubkan framerate, Stage3D akan menjadi teman terbaik Anda yang baru.
Kecepatan yang luar biasa baru Flash 11 hardware accelerated Stage3D API hanya memohon untuk digunakan untuk permainan 2D. Daripada menggunakan sprite Flash kuno pada DisplayList atau terakhir-gen blitting teknik seperti yang dipopulerkan oleh mesin pencari seperti FlashPunk dan Flixel, generasi baru permainan 2D menggunakan kekuatan kartu video Anda GPU untuk api melalui tugas-tugas render di hingga 1000 x kecepatan apapun Flash 10 bisa mengelola.
Meskipun memiliki 3D dalam namanya, API baru ini juga bagus untuk permainan 2D. Kita dapat membuat sederhana geometri dalam bentuk 2D kotak (disebut quads) dan menarik mereka di pesawat datar. Ini akan memungkinkan kita untuk membuat ton sprite pada layar halus-halus 60fps.
Kami akan membuat penembak gulir samping yang terinspirasi oleh judul arcade retro seperti R-Type atau Gradius di ActionScript menggunakan API Stage3D Flash 11. Ini tidak setengah sulit seperti yang dikatakan beberapa orang, dan Anda tidak perlu belajar opcode AGAL bahasa assembly.
Dalam seri tutorial 6-bagian ini, kita akan program shoot-'em-up 2D sederhana yang memberikan kinerja render bertiup. Kita akan membangun menggunakan AS3 murni, disusun dalam FlashDevelop (Baca lebih lanjut di sini). FlashDevelop besar karena itu adalah 100% freeware - tidak perlu membeli alat yang mahal untuk mendapatkan IDE AS3 terbaik sekitar.
Langkah 1: Buat proyek baru
Jika Anda belum memiliki itu, pastikan untuk men-download dan menginstal FlashDevelop. Setelah Anda sudah siap (dan Anda telah diperbolehkan untuk menginstal versi terbaru dari kompilator Flex secara otomatis), api itu dan mulai baru "proyek AS3."



FlashDevelop akan menciptakan sebuah proyek template kosong untuk Anda. Kita akan mengisi kekosongan, sepotong-demi-sepotong, sampai kami telah menciptakan permainan layak.
Langkah 2: Target Flash 11
Pergi ke menu proyek dan mengubah beberapa pilihan:
- Target Flash 11.1
- Mengubah ukuran untuk 600x400px
- Mengubah warna latar belakang hitam
- Perubahan FPD 60
- Mengubah nama file SWF untuk nama yang Anda pilih



Langkah 3: impor
Sekarang bahwa proyek kami kosong diatur, mari kita menyelam dalam dan melakukan coding. Untuk mulai dengan, kita akan perlu mengimpor semua fungsi Stage3D diperlukan. Tambahkan baris berikut ke bagian paling atas dari Main.as file.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
// Created for active.tutsplus.com
|
5 |
|
6 |
package
|
7 |
{
|
8 |
[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] |
9 |
|
10 |
import flash.display3D.*; |
11 |
import flash.display.Sprite; |
12 |
import flash.display.StageAlign; |
13 |
import flash.display.StageQuality; |
14 |
import flash.display.StageScaleMode; |
15 |
import flash.events.Event; |
16 |
import flash.events.ErrorEvent; |
17 |
import flash.geom.Rectangle; |
18 |
import flash.utils.getTimer; |
Langkah 4: Menginisialisasi Stage3D
Langkah berikutnya adalah untuk menunggu permainan kami muncul di panggung Flash. Melakukan hal-hal dengan cara ini memungkinkan untuk penggunaan masa depan preloader. Untuk kesederhanaan, kami akan melakukan sebagian besar permainan kami dalam satu kelas kecil yang mewarisi dari kelas Flash Sprite sebagai berikut.
1 |
|
2 |
public class Main extends Sprite |
3 |
{
|
4 |
private var _entities : EntityManager; |
5 |
private var _spriteStage : LiteSpriteStage; |
6 |
private var _gui : GameGUI; |
7 |
private var _width : Number = 600; |
8 |
private var _height : Number = 400; |
9 |
public var context3D : Context3D; |
10 |
|
11 |
// constructor function for our game
|
12 |
public function Main():void |
13 |
{
|
14 |
if (stage) init(); |
15 |
else addEventListener(Event.ADDED_TO_STAGE, init); |
16 |
}
|
17 |
|
18 |
// called once Flash is ready
|
19 |
private function init(e:Event = null):void |
20 |
{
|
21 |
removeEventListener(Event.ADDED_TO_STAGE, init); |
22 |
stage.quality = StageQuality.LOW; |
23 |
stage.align = StageAlign.TOP_LEFT; |
24 |
stage.scaleMode = StageScaleMode.NO_SCALE; |
25 |
stage.addEventListener(Event.RESIZE, onResizeEvent); |
26 |
trace("Init Stage3D..."); |
27 |
_gui = new GameGUI("Simple Stage3D Sprite Demo v1"); |
28 |
addChild(_gui); |
29 |
stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); |
30 |
stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); |
31 |
stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); |
32 |
trace("Stage3D requested..."); |
33 |
}
|
Setelah mengatur beberapa properti tahap khusus, kami meminta konteks Stage3D. Ini dapat berlangsung lama (sepersekian detik) sebagai kartu video Anda dikonfigurasi untuk hardware render, jadi kita perlu menunggu acara onContext3DCreate.
Kami juga ingin untuk mendeteksi setiap kesalahan yang mungkin terjadi, terutama karena Stage3D konten tidak berjalan jika HTML menempelkan kode yang memuat SWF Anda tidak menyertakan parameter "wmode = langsung". Kesalahan ini bisa juga terjadi jika pengguna menjalankan versi lama flash atau jika mereka tidak memiliki kartu video mampu menangani pixel shader 2.0.
Langkah 5: Menangani peristiwa apapun
Tambahkan fungsi berikut yang mendeteksi setiap peristiwa yang mungkin memicu sebagaimana ditentukan di atas. Dalam kasus kesalahan karena berjalan lama Flash plugin, di masa depan versi dari permainan ini kita mungkin ingin untuk output pesan dan mengingatkan pengguna untuk meng-upgrade, tapi untuk saat kesalahan ini hanya diabaikan.
Untuk pengguna dengan kartu video lama (atau driver) yang tidak mendukung shader model 2.0, Kabar baiknya adalah bahwa Flash 11 cukup cerdas untuk menyediakan sebuah software renderer. Ini tidak berjalan sangat cepat tapi setidaknya setiap orang akan dapat bermain game. Mereka yang layak gaming rig akan mendapatkan framerate fantastis seperti youve pernah terlihat di Flash permainan sebelum.
1 |
|
2 |
// this is called when the 3d card has been set up
|
3 |
// and is ready for rendering using stage3d
|
4 |
private function onContext3DCreate(e:Event):void |
5 |
{
|
6 |
trace("Stage3D context created! Init sprite engine..."); |
7 |
context3D = stage.stage3Ds[0].context3D; |
8 |
initSpriteEngine(); |
9 |
}
|
10 |
|
11 |
// this can be called when using an old version of Flash
|
12 |
// or if the html does not include wmode=direct
|
13 |
private function errorHandler(e:ErrorEvent):void |
14 |
{
|
15 |
trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); |
16 |
}
|
17 |
|
18 |
protected function onResizeEvent(event:Event) : void |
19 |
{
|
20 |
trace("resize event..."); |
21 |
|
22 |
// Set correct dimensions if we resize
|
23 |
_width = stage.stageWidth; |
24 |
_height = stage.stageHeight; |
25 |
|
26 |
// Resize Stage3D to continue to fit screen
|
27 |
var view:Rectangle = new Rectangle(0, 0, _width, _height); |
28 |
if ( _spriteStage != null ) { |
29 |
_spriteStage.position = view; |
30 |
}
|
31 |
if(_entities != null) { |
32 |
_entities.setPosition(view); |
33 |
}
|
34 |
}
|
Acara penanganan kode di atas mendeteksi ketika Stage3D siap untuk render hardware dan menetapkan variabel context3D untuk penggunaan masa depan. Kesalahan akan diabaikan untuk sekarang. Peristiwa yang mengubah ukuran hanya update ukuran panggung dan batch render sistem dimensi.
Langkah 6: Init mesin Sprite
Setelah context3D
diterima, kami siap untuk memulai permainan. Melanjutkan dengan Main.as
, tambahkan berikut ini.
1 |
|
2 |
private function initSpriteEngine():void |
3 |
{
|
4 |
// init a gpu sprite system
|
5 |
var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); |
6 |
_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); |
7 |
_spriteStage.configureBackBuffer(_width,_height); |
8 |
|
9 |
// create a single rendering batch
|
10 |
// which will draw all sprites in one pass
|
11 |
var view:Rectangle = new Rectangle(0,0,_width,_height) |
12 |
_entities = new EntityManager(stageRect); |
13 |
_entities.createBatch(context3D); |
14 |
_spriteStage.addBatch(_entities._batch); |
15 |
// add the first entity right now
|
16 |
_entities.addEntity(); |
17 |
|
18 |
// tell the gui where to grab statistics from
|
19 |
_gui.statsTarget = _entities; |
20 |
|
21 |
// start the render loop
|
22 |
stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); |
23 |
}
|
Fungsi ini menciptakan mesin rendering sprite (untuk diimplementasikan di bawah) di panggung, siap untuk menggunakan ukuran penuh file flash Anda. Kami kemudian tambahkan manajer entitas dan batched geometri sistem (yang kita akan membahas di bawah). Kami sekarang dapat memberikan rujukan kepada manajer entitas kelas GUI Statistik kami sehingga ia bisa menampilkan beberapa angka pada layar mengenai berapa banyak sprite telah dibuat atau kembali. Terakhir, kita mulai mendengarkan acara ENTER_FRAME, yang akan mulai menembak dengan kecepatan hingga 60 kali per detik.
Langkah 7: Mulai Render Loop
Sekarang bahwa segala sesuatu telah diinisialisasi, kami siap untuk bermain! Fungsi berikut akan dijalankan setiap satu frame. Untuk keperluan demo teknologi yang pertama ini, kita akan menambahkan satu baru sprite di panggung setiap frame. Karena kami akan menerapkan kolam objek (yang Anda dapat membaca lebih lanjut tentang dalam tutorial ini) daripada inifinitely membuat objek baru sampai kita kehabisan RAM, kita akan mampu untuk menggunakan kembali lama entitas yang telah pindah dari layar.
Setelah hal ikan bertelur sprite lain, kita jelas daerah stage3D layar (pengaturan untuk hitam murni). Selanjutnya kami memperbarui semua entitas yang dikendalikan oleh Manajer badan kami. Ini akan memindahkan mereka sedikit lebih setiap frame. Setelah semua sprite telah diperbarui, kita mengatakan sistem batched geometri untuk mengumpulkan mereka semua ke dalam satu besar vertex buffer dan kulit kayu mereka di layar dalam satu menarik panggilan, untuk efisiensi. Akhirnya, kami memberitahukan context3D untuk memperbarui layar dengan render akhir kami.
1 |
|
2 |
// this function draws the scene every frame
|
3 |
private function onEnterFrame(e:Event):void |
4 |
{
|
5 |
try
|
6 |
{
|
7 |
// keep adding more sprites - FOREVER!
|
8 |
// this is a test of the entity manager's
|
9 |
// object reuse "pool"
|
10 |
_entities.addEntity(); |
11 |
|
12 |
// erase the previous frame
|
13 |
context3D.clear(0, 0, 0, 1); |
14 |
|
15 |
// move/animate all entities
|
16 |
_entities.update(getTimer()); |
17 |
|
18 |
// draw all entities
|
19 |
_spriteStage.render(); |
20 |
|
21 |
// update the screen
|
22 |
context3D.present(); |
23 |
}
|
24 |
catch (e:Error) |
25 |
{
|
26 |
// this can happen if the computer goes to sleep and
|
27 |
// then re-awakens, requiring reinitialization of stage3D
|
28 |
// (the onContext3DCreate will fire again)
|
29 |
}
|
30 |
}
|
31 |
} // end class |
32 |
} // end package |
That's it untuk kawasan! Sesederhana kedengarannya, kami telah sekarang menciptakan proyek template yang siap untuk ledakan keluar jumlah yang gila sprite. Kami tidak akan menggunakan seni vektor. Kami tidak akan menempatkan sprite Flash kuno di atas panggung selain dari jendela Stage3D dan beberapa overlay GUI. Semua pekerjaan render grafis dalam permainan kami akan ditangani oleh Stage3D, sehingga kita dapat menikmati peningkatan kinerja.
Pergi lebih dalam: Mengapa adalah Stage3D begitu cepat?
Dua alasan:
- Menggunakan akselerasi hardware, berarti bahwa semua perintah menggambar dikirim ke GPU 3D pada kartu video Anda dalam cara yang sama bahwa permainan XBOX360 dan PlayStation3 mendapatkan diterjemahkan.
- Perintah render ini diproses secara paralel ke seluruh kode ActionScript. Ini berarti bahwa setelah perintah dikirim ke kartu video Anda, semua terjemahan dilakukan pada waktu yang sama seperti kode lain dalam permainan Anda menjalankan - Flash tidak harus menunggu mereka untuk diselesaikan. Ketika piksel diledakkan ke layar Anda, Flash melakukan hal-hal lain seperti menangani input pemain, memutar suara, dan memperbarui posisi musuh.
- objek penggabungan
- spritesheet (atlas tekstur)
- batched geometri
Yang mengatakan, banyak mesin Stage3D tampaknya macet oleh beberapa ratus sprite. Ini karena mereka telah diprogram tanpa memperhatikan overhead yang ditambahkan oleh setiap perintah draw. Ketika Stage3D pertama kali keluar, beberapa mesin 2D pertama akan menggambar masing-masing dan setiap sprite secara individu dalam satu lingkaran raksasa (lambat dan tidak efisien). Karena artikel ini adalah tentang optimasi ekstrim untuk game 2D generasi berikutnya dengan framerate yang luar biasa, kami akan menerapkan sistem rendering yang sangat efisien yang menyatukan semua geometri menjadi satu batch besar sehingga kami dapat menggambar semuanya hanya dalam satu atau dua perintah.
Cara Menjadi Hardcore: Optimalkan!
Gamedev hardcore menyukai optimasi. Untuk meledakkan sprite terbanyak di layar dengan perubahan status paling sedikit (seperti beralih tekstur, memilih buffer vertex baru, atau harus memperbarui transformasi sekali untuk setiap sprite di layar), kita akan mengambil keuntungan dari tiga optimasi kinerja berikut:
Ini trik hardcore gamedev tiga adalah kunci untuk mendapatkan FPS yang mengagumkan dalam permainan Anda. Mari kita menerapkan mereka sekarang. Sebelum kita lakukan, kita perlu membuat beberapa kecil kelas yang teknik ini akan membuat menggunakan.
Langkah 8: Tampilan statistik
Jika kita akan melakukan banyak optimasi dan menggunakan Stage3D dalam upaya untuk mencapai kinerja rendering sangat cepat, kita perlu cara untuk melacak statistik. Beberapa benchmark kecil dapat pergi jauh untuk membuktikan bahwa apa yang kita lakukan adalah memiliki efek positif pada framerate. Sebelum kita melangkah lebih jauh, membuat sebuah class baru yang disebut GameGUI.as dan menerapkan FPS super sederhana dan statistik menampilkan sebagai berikut.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// GameGUI.as
|
6 |
// A typical simplistic framerate display for benchmarking performance,
|
7 |
// plus a way to track rendering statistics from the entity manager.
|
8 |
|
9 |
package
|
10 |
{
|
11 |
import flash.events.Event; |
12 |
import flash.events.TimerEvent; |
13 |
import flash.text.TextField; |
14 |
import flash.text.TextFormat; |
15 |
import flash.utils.getTimer; |
16 |
|
17 |
public class GameGUI extends TextField |
18 |
{
|
19 |
public var titleText : String = ""; |
20 |
public var statsText : String = ""; |
21 |
public var statsTarget : EntityManager; |
22 |
private var frameCount:int = 0; |
23 |
private var timer:int; |
24 |
private var ms_prev:int; |
25 |
private var lastfps : Number = 60; |
26 |
|
27 |
public function GameGUI(title:String = "", inX:Number=8, inY:Number=8, inCol:int = 0xFFFFFF) |
28 |
{
|
29 |
super(); |
30 |
titleText = title; |
31 |
x = inX; |
32 |
y = inY; |
33 |
width = 500; |
34 |
selectable = false; |
35 |
defaultTextFormat = new TextFormat("_sans", 9, 0, true); |
36 |
text = ""; |
37 |
textColor = inCol; |
38 |
this.addEventListener(Event.ADDED_TO_STAGE, onAddedHandler); |
39 |
|
40 |
}
|
41 |
public function onAddedHandler(e:Event):void { |
42 |
stage.addEventListener(Event.ENTER_FRAME, onEnterFrame); |
43 |
}
|
44 |
|
45 |
private function onEnterFrame(evt:Event):void |
46 |
{
|
47 |
timer = getTimer(); |
48 |
|
49 |
if( timer - 1000 > ms_prev ) |
50 |
{
|
51 |
lastfps = Math.round(frameCount/(timer-ms_prev)*1000); |
52 |
ms_prev = timer; |
53 |
|
54 |
// grab the stats from the entity manager
|
55 |
if (statsTarget) |
56 |
{
|
57 |
statsText = |
58 |
statsTarget.numCreated + ' created ' + |
59 |
statsTarget.numReused + ' reused'; |
60 |
}
|
61 |
|
62 |
text = titleText + ' - ' + statsText + " - FPS: " + lastfps; |
63 |
frameCount = 0; |
64 |
}
|
65 |
|
66 |
// count each frame to determine the framerate
|
67 |
frameCount++; |
68 |
|
69 |
}
|
70 |
} // end class |
71 |
} // end package |
Langkah 9: Kelas entitas
Kami akan menerapkan kelas manajer entitas yang akan "objek renang" seperti dijelaskan di atas. Pertama-tama kita perlu membuat kelas sederhana untuk setiap entitas individu di game kita. Kelas ini akan digunakan untuk semua objek dalam game, mulai dari pesawat ruang angkasa hingga peluru.
Buat sebuah file baru yang disebut Entity.as dan menambahkan beberapa Getter dan setter sekarang. Untuk tech demo ini pertama, kelas ini adalah semata-mata pengganti kosong tanpa banyak fungsionalitas, tetapi dalam kemudian tutorial ini adalah dimana kami akan mengimplementasikan jauh dari permainan.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// Entity.as
|
6 |
// The Entity class will eventually hold all game-specific entity logic
|
7 |
// for the spaceships, bullets and effects in our game. For now,
|
8 |
// it simply holds a reference to a gpu sprite and a few demo properties.
|
9 |
// This is where you would add hit points, weapons, ability scores, etc.
|
10 |
|
11 |
package
|
12 |
{
|
13 |
public class Entity |
14 |
{
|
15 |
private var _speedX : Number; |
16 |
private var _speedY : Number; |
17 |
private var _sprite : LiteSprite; |
18 |
public var active : Boolean = true; |
19 |
|
20 |
public function Entity(gs:LiteSprite = null) |
21 |
{
|
22 |
_sprite = gs; |
23 |
_speedX = 0.0; |
24 |
_speedY = 0.0; |
25 |
}
|
26 |
public function die() : void |
27 |
{
|
28 |
// allow this entity to be reused by the entitymanager
|
29 |
active = false; |
30 |
// skip all drawing and updating
|
31 |
sprite.visible = false; |
32 |
}
|
33 |
public function get speedX() : Number |
34 |
{
|
35 |
return _speedX; |
36 |
}
|
37 |
public function set speedX(sx:Number) : void |
38 |
{
|
39 |
_speedX = sx; |
40 |
}
|
41 |
public function get speedY() : Number |
42 |
{
|
43 |
return _speedY; |
44 |
}
|
45 |
public function set speedY(sy:Number) : void |
46 |
{
|
47 |
_speedY = sy; |
48 |
}
|
49 |
public function get sprite():LiteSprite |
50 |
{
|
51 |
return _sprite; |
52 |
}
|
53 |
public function set sprite(gs:LiteSprite):void |
54 |
{
|
55 |
_sprite = gs; |
56 |
}
|
57 |
} // end class |
58 |
} // end package |
Langkah 10: Buat Spritesheet
Teknik optimasi penting yang kita akan gunakan adalah penggunaan spritesheet - kadang-kadang disebut sebagai Atlas tekstur. Bukan upload puluhan atau ratusan gambar individu untuk RAM video untuk digunakan selama rendering, kita akan membuat sebuah file citra yang memegang semua sprite dalam permainan kami. Dengan cara ini, kita dapat menggunakan tekstur tunggal untuk menarik banyak musuh atau medan yang berbeda.
Menggunakan spritesheet dianggap praktik terbaik oleh gamedev veteran yang perlu memastikan permainan mereka berjalan secepat mungkin. Alasannya mempercepat banyak hal adalah sama dengan alasan mengapa kita akan menggunakan geometri batching: daripada harus memberitahu kartu video berulang-ulang untuk menggunakan tekstur tertentu untuk menggambar sprite tertentu, kita dapat dengan mudah memberi tahu untuk selalu menggunakan tekstur yang sama untuk semua panggilan draw.
Ini mengurangi 'perubahan negara' yang sangat mahal dalam hal waktu. Kita tidak perlu lagi mengatakan 'kartu video, mulai menggunakan tekstur 24 ... sekarang menggambar sprite 14' dan seterusnya. Kami hanya mengatakan "menggambar semuanya menggunakan tekstur ini satu" dalam satu berlalu. Ini dapat meningkatkan kinerja dengan urutan besarnya.
Kami permainan contoh kami akan menggunakan koleksi hukum menggunakan freeware gambar oleh DanC berbakat, yang dapat Anda peroleh di sini. Ingat bahwa jika Anda menggunakan gambar-gambar ini Anda harus kredit mereka dalam permainan Anda sebagai berikut: seni "Seni koleksi judul" oleh Daniel Cook (Lostgarden.com).
Menggunakan Photoshop (atau GIMP, atau apa pun editor gambar Anda suka), memotong dan menyisipkan sprite permainan Anda akan perlu ke dalam satu file PNG yang memiliki latar belakang transparan. Tempat setiap sprite pada grid jarak merata dengan beberapa piksel ruang kosong antara masing-masing. Buffer kecil ini diperlukan untuk menghindari 'pendarahan' piksel tepi dari sprite yang berdekatan yang dapat terjadi karena penyaringan tekstur bilinear yang terjadi pada GPU. Jika setiap sprite menyentuh yang berikutnya, sprite dalam gim Anda mungkin memiliki tepi yang tidak diinginkan di mana sprite tersebut benar-benar transparan.
Untuk alasan optimasi, GPU bekerja dengan baik dengan gambar (disebut tekstur) persegi dan dimensi yang sama dengan kekuatan dua dan secara merata dibagi oleh delapan. Mengapa? Karena cara bahwa pixel data diakses, angka ajaib ini terjadi untuk menyelaraskan dengan VRAM dalam cara yang tepat untuk menjadi tercepat untuk akses, karena data sering dibaca dalam potongan.
Oleh karena itu, pastikan bahwa Anda spritesheet adalah 64 x 64, 128 x 128, 256 x 256, 512 x 512 atau 1024 x 1024. Seperti yang Anda duga, semakin kecil semakin baik - bukan hanya dalam hal kinerja tetapi karena tekstur yang lebih kecil akan secara alami menyimpan permainan Anda akhir SWF lebih kecil.
Ini adalah spritesheet yang akan kita gunakan sebagai contoh. Seni 'Tyrian' oleh Daniel Cook (Lostgarden.com).

Klik kanan untuk men-download
Langkah 11: Manajer entitas
Teknik optimasi pertama kita akan mengambil keuntungan dari untuk mencapai kinerja yang menyala adalah penggunaan "objek kolam-kolam". Daripada terus-menerus mengalokasikan lebih ram untuk benda-benda seperti peluru atau musuh, kita akan membuat kembali kolam yang tidak terpakai sprite mendaur ulang lagi dan lagi.
Teknik ini menjamin bahwa penggunaan RAM tetap sangat rendah dan cegukan GC (pengumpulan sampah) jarang terjadi. Hasilnya adalah bahwa framerate akan lebih tinggi dan permainan Anda akan berjalan lancar tidak peduli berapa lama Anda bermain.
Membuat sebuah class baru dalam proyek Anda disebut EntityManager.as dan mengimplementasikan mekanisme daur ulang-on-demand sederhana sebagai berikut.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// EntityManager.as
|
6 |
// The entity manager handles a list of all known game entities.
|
7 |
// This object pool will allow for reuse (respawning) of
|
8 |
// sprites: for example, when enemy ships are destroyed,
|
9 |
// they will be re-spawned when needed as an optimization
|
10 |
// that increases fps and decreases ram use.
|
11 |
// This is where you would add all in-game simulation steps,
|
12 |
// such as gravity, movement, collision detection and more.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.display.Bitmap; |
17 |
import flash.display3D.*; |
18 |
import flash.geom.Point; |
19 |
import flash.geom.Rectangle; |
20 |
|
21 |
public class EntityManager |
22 |
{
|
23 |
// the sprite sheet image
|
24 |
private var _spriteSheet : LiteSpriteSheet; |
25 |
private const SpritesPerRow:int = 8; |
26 |
private const SpritesPerCol:int = 8; |
27 |
[Embed(source="../assets/sprites.png")] |
28 |
private var SourceImage : Class; |
29 |
|
30 |
// a reusable pool of entities
|
31 |
private var _entityPool : Vector.<Entity>; |
32 |
|
33 |
// all the polygons that make up the scene
|
34 |
public var _batch : LiteSpriteBatch; |
35 |
|
36 |
// for statistics
|
37 |
public var numCreated : int = 0; |
38 |
public var numReused : int = 0; |
39 |
|
40 |
private var maxX:int; |
41 |
private var minX:int; |
42 |
private var maxY:int; |
43 |
private var minY:int; |
44 |
|
45 |
public function EntityManager(view:Rectangle) |
46 |
{
|
47 |
_entityPool = new Vector.<Entity>(); |
48 |
setPosition(view); |
49 |
}
|
Langkah 12: Tetapkan Batas
Manajer entitas kami akan mendaur ulang entitas ketika mereka bergerak ke tepi kiri layar. Fungsi di bawah ini disebut selama kawasan atau ketika mengubah ukuran acara ditembakkan. Kami menambahkan beberapa tambahan piksel tepi sehingga sprite tiba-tiba tidak pop dalam atau keluar dari keberadaan.
1 |
|
2 |
public function setPosition(view:Rectangle):void |
3 |
{
|
4 |
// allow moving fully offscreen before looping around
|
5 |
maxX = view.width + 32; |
6 |
minX = view.x - 32; |
7 |
maxY = view.height; |
8 |
minY = view.y; |
9 |
}
|
Langkah 13: Mengatur sprite
Manajer entitas menjalankan fungsi ini sekali saat startup. Ini menciptakan kumpulan geometri baru yang menggunakan spritesheet gambar yang telah tertanam dalam kode kita di atas. Ia akan mengirimkan bitmapData ke konstruktor kelas spritesheet, yang akan digunakan untuk menghasilkan tekstur yang memiliki semua gambar sprite tersedia di atasnya dalam grid. Kami memberitahu kami spritesheet bahwa kita akan menggunakan sprite berbeda 64 (8 oleh 8) pada satu tekstur. Spritesheet ini akan digunakan oleh batch geometri renderer.
Jika kita ingin, kita bisa menggunakan lebih dari satu spritesheet, oleh inisialisasi tambahan gambar dan batch yang diperlukan. Di masa depan, ini mungkin mana Anda membuat batch kedua untuk semua ubin Medan yang pergi di bawah sprite pesawat ruang angkasa Anda. Anda bahkan dapat menerapkan batch ketiga yang berlapis di atas segalanya untuk efek partikel mewah dan permen mata. Untuk saat ini, demo teknologi sederhana ini hanya membutuhkan satu tekstur spritesheet dan kumpulan geometri.
1 |
|
2 |
|
3 |
public function createBatch(context3D:Context3D) : LiteSpriteBatch |
4 |
{
|
5 |
var sourceBitmap:Bitmap = new SourceImage(); |
6 |
|
7 |
// create a spritesheet with 8x8 (64) sprites on it
|
8 |
_spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, 8, 8); |
9 |
|
10 |
// Create new render batch
|
11 |
_batch = new LiteSpriteBatch(context3D, _spriteSheet); |
12 |
|
13 |
return _batch; |
14 |
}
|
Langkah 14: Objek Kolam Renang
Ini adalah dimana entitas manajer meningkatkan kinerja. Optimalisasi satu (objek reuse kolam) akan memungkinkan kita untuk hanya membuat entitas baru pada permintaan (ketika tidak ada apapun yang tidak aktif yang dapat digunakan kembali). Perhatikan bagaimana kami menggunakan kembali setiap sprite yang saat ini ditandai sebagai tidak aktif, kecuali mereka semua saat ini sedang digunakan, dalam hal mana kami bertelur yang baru. Dengan cara ini, objek kolam hanya memegang setiap sprite sebanyak seperti bahkan terlihat pada saat yang sama. Setelah beberapa detik pertama yang permainan kami telah berjalan, Kolam entitas akan tetap konstan - jarang akan sebuah entitas baru perlu dibuat setelah ada cukup untuk menangani apa yang terjadi pada layar.
Lanjutkan menambahkan EntityManager.as sebagai berikut:
1 |
|
2 |
// search the entity pool for unused entities and reuse one
|
3 |
// if they are all in use, create a brand new one
|
4 |
public function respawn(sprID:uint=0):Entity |
5 |
{
|
6 |
var currentEntityCount:int = _entityPool.length; |
7 |
var anEntity:Entity; |
8 |
var i:int = 0; |
9 |
// search for an inactive entity
|
10 |
for (i = 0; i < currentEntityCount; i++ ) |
11 |
{
|
12 |
anEntity = _entityPool[i]; |
13 |
if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) |
14 |
{
|
15 |
//trace('Reusing Entity #' + i);
|
16 |
anEntity.active = true; |
17 |
anEntity.sprite.visible = true; |
18 |
numReused++; |
19 |
return anEntity; |
20 |
}
|
21 |
}
|
22 |
// none were found so we need to make a new one
|
23 |
//trace('Need to create a new Entity #' + i);
|
24 |
var sprite:LiteSprite; |
25 |
sprite = _batch.createChild(sprID); |
26 |
anEntity = new Entity(sprite); |
27 |
_entityPool.push(anEntity); |
28 |
numCreated++; |
29 |
return anEntity; |
30 |
}
|
31 |
|
32 |
// for this test, create random entities that move
|
33 |
// from right to left with random speeds and scales
|
34 |
public function addEntity():void |
35 |
{
|
36 |
var anEntity:Entity; |
37 |
var randomSpriteID:uint = Math.floor(Math.random() * 64); |
38 |
// try to reuse an inactive entity (or create a new one)
|
39 |
anEntity = respawn(randomSpriteID); |
40 |
// give it a new position and velocity
|
41 |
anEntity.sprite.position.x = maxX; |
42 |
anEntity.sprite.position.y = Math.random() * maxY; |
43 |
anEntity.speedX = (-1 * Math.random() * 10) - 2; |
44 |
anEntity.speedY = (Math.random() * 5) - 2.5; |
45 |
anEntity.sprite.scaleX = 0.5 + Math.random() * 1.5; |
46 |
anEntity.sprite.scaleY = anEntity.sprite.scaleX; |
47 |
anEntity.sprite.rotation = 15 - Math.random() * 30; |
48 |
}
|
Fungsi di atas dijalankan setiap kali sebuah sprite yang baru perlu ditambahkan pada layar. Manajer entitas scan kolam renang entitas untuk satu yang sedang tidak digunakan dan kembali ketika mungkin. Jika daftar penuh aktif entitas, yang baru perlu dibuat.
Langkah 15: Simulasikan!
Fungsi akhir yang merupakan tanggung jawab manajer entitas kami adalah salah satu yang akan dipanggil setiap bingkai. Hal ini digunakan untuk melakukan simulasi, AI, deteksi tabrakan, fisika atau animasi seperti yang diperlukan. Untuk demo sederhana teknologi saat ini, itu hanya loop melalui daftar aktif entitas di kolam dan update posisi mereka berdasarkan kecepatan. Setiap entitas akan dipindahkan sesuai dengan kecepatan mereka saat ini. Hanya untuk bersenang-senang, mereka diatur untuk berputar sedikit setiap frame juga.
Entitas yang melewati sisi kiri layar adalah "membunuh" dan ditandai sebagai tidak aktif dan tidak terlihat, siap untuk digunakan kembali dalam fungsi di atas. Jika sebuah entitas menyentuh tepi layar tiga lainnya, kecepatan dibalik sehingga itu akan "terpental" bahwa tepi. Lanjutkan menambahkan EntityManager.as sebagai berikut:
1 |
|
2 |
// called every frame: used to update the simulation
|
3 |
// this is where you would perform AI, physics, etc.
|
4 |
public function update(currentTime:Number) : void |
5 |
{
|
6 |
var anEntity:Entity; |
7 |
for(var i:int=0; i<_entityPool.length;i++) |
8 |
{
|
9 |
anEntity = _entityPool[i]; |
10 |
if (anEntity.active) |
11 |
{
|
12 |
anEntity.sprite.position.x += anEntity.speedX; |
13 |
anEntity.sprite.position.y += anEntity.speedY; |
14 |
anEntity.sprite.rotation += 0.1; |
15 |
|
16 |
if (anEntity.sprite.position.x > maxX) |
17 |
{
|
18 |
anEntity.speedX *= -1; |
19 |
anEntity.sprite.position.x = maxX; |
20 |
}
|
21 |
else if (anEntity.sprite.position.x < minX) |
22 |
{
|
23 |
// if we go past the left edge, become inactive
|
24 |
// so the sprite can be respawned
|
25 |
anEntity.die(); |
26 |
}
|
27 |
if (anEntity.sprite.position.y > maxY) |
28 |
{
|
29 |
anEntity.speedY *= -1; |
30 |
anEntity.sprite.position.y = maxY; |
31 |
}
|
32 |
else if (anEntity.sprite.position.y < minY) |
33 |
{
|
34 |
anEntity.speedY *= -1; |
35 |
anEntity.sprite.position.y = minY; |
36 |
}
|
37 |
}
|
38 |
}
|
39 |
}
|
40 |
} // end class |
41 |
} // end package |
Langkah 16: Kelas Sprite
Langkah terakhir untuk mendapatkan semuanya dan berjalan adalah untuk menerapkan empat kelas yang membuat sistem "mesin rendering" kami. Karena kata Sprite sudah digunakan dalam Flash, beberapa kelas berikutnya akan menggunakan istilah LiteSprite, yang tidak hanya nama yang mudah diingat tapi menyiratkan sifat ringan dan sederhana mesin ini.
Untuk memulai, kita akan membuat kelas sprite 2D sederhana yang mengacu pada kelas entitas kita di atas. Akan ada banyak sprite dalam game kami, yang masing-masing dikumpulkan ke dalam sejumlah besar poligon dan diberikan dalam satu lintasan tunggal.
Buat sebuah file baru dalam proyek Anda disebut LiteSprite.as dan menerapkan beberapa Getter dan setter sebagai berikut. Mungkin kita bisa lolos dengan hanya menggunakan variabel publik, tetapi di masa depan versi mengubah beberapa nilai-nilai ini akan memerlukan menjalankan beberapa kode pertama, sehingga teknik ini akan terbukti sangat berharga.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSprite.as
|
6 |
// A 2d sprite that is rendered by Stage3D as a textured quad
|
7 |
// (two triangles) to take advantage of hardware acceleration.
|
8 |
// Based on example code by Chris Nuuja which is a port
|
9 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
10 |
// which is itself a port of Iain Lobb's original work.
|
11 |
// Also includes code from the Starling framework.
|
12 |
// Grateful acknowledgements to all involved.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.geom.Point; |
17 |
import flash.geom.Rectangle; |
18 |
|
19 |
public class LiteSprite |
20 |
{
|
21 |
internal var _parent : LiteSpriteBatch; |
22 |
internal var _spriteId : uint; |
23 |
internal var _childId : uint; |
24 |
private var _pos : Point; |
25 |
private var _visible : Boolean; |
26 |
private var _scaleX : Number; |
27 |
private var _scaleY : Number; |
28 |
private var _rotation : Number; |
29 |
private var _alpha : Number; |
30 |
|
31 |
public function get visible() : Boolean |
32 |
{
|
33 |
return _visible; |
34 |
}
|
35 |
public function set visible(isVisible:Boolean) : void |
36 |
{
|
37 |
_visible = isVisible; |
38 |
}
|
39 |
public function get alpha() : Number |
40 |
{
|
41 |
return _alpha; |
42 |
}
|
43 |
public function set alpha(a:Number) : void |
44 |
{
|
45 |
_alpha = a; |
46 |
}
|
47 |
public function get position() : Point |
48 |
{
|
49 |
return _pos; |
50 |
}
|
51 |
public function set position(pt:Point) : void |
52 |
{
|
53 |
_pos = pt; |
54 |
}
|
55 |
public function get scaleX() : Number |
56 |
{
|
57 |
return _scaleX; |
58 |
}
|
59 |
public function set scaleX(val:Number) : void |
60 |
{
|
61 |
_scaleX = val; |
62 |
}
|
63 |
public function get scaleY() : Number |
64 |
{
|
65 |
return _scaleY; |
66 |
}
|
67 |
public function set scaleY(val:Number) : void |
68 |
{
|
69 |
_scaleY = val; |
70 |
}
|
71 |
public function get rotation() : Number |
72 |
{
|
73 |
return _rotation; |
74 |
}
|
75 |
public function set rotation(val:Number) : void |
76 |
{
|
77 |
_rotation = val; |
78 |
}
|
79 |
public function get rect() : Rectangle |
80 |
{
|
81 |
return _parent._sprites.getRect(_spriteId); |
82 |
}
|
83 |
public function get parent() : LiteSpriteBatch |
84 |
{
|
85 |
return _parent; |
86 |
}
|
87 |
public function get spriteId() : uint |
88 |
{
|
89 |
return _spriteId; |
90 |
}
|
91 |
public function set spriteId(num : uint) : void |
92 |
{
|
93 |
_spriteId = num; |
94 |
}
|
95 |
public function get childId() : uint |
96 |
{
|
97 |
return _childId; |
98 |
}
|
99 |
|
100 |
// LiteSprites are typically constructed by calling LiteSpriteBatch.createChild()
|
101 |
public function LiteSprite() |
102 |
{
|
103 |
_parent = null; |
104 |
_spriteId = 0; |
105 |
_childId = 0; |
106 |
_pos = new Point(); |
107 |
_scaleX = 1.0; |
108 |
_scaleY = 1.0; |
109 |
_rotation = 0; |
110 |
_alpha = 1.0; |
111 |
_visible = true; |
112 |
}
|
113 |
} // end class |
114 |
} // end package |
Sprite masing-masing dapat sekarang melacak di mana itu ada di layar, serta bagaimana besar itu adalah, bagaimana transparan, dan sudut apa itu menghadapi. Properti spriteID adalah nomor yang digunakan selama rendering untuk mencari kebutuhan koordinat UV (tekstur) yang digunakan sebagai sumber persegi untuk pixel dari spritesheet gambar menggunakan.
Langkah 17: Kelas Spritesheet
Kita sekarang perlu untuk mengimplementasikan mekanisme untuk memproses gambar spritesheet yang kami tertanam di atas dan menggunakan bagian-bagian itu pada semua geometri diberikan kami. Buat sebuah file baru dalam proyek Anda disebut LiteSpriteSheet.as dan mulai dengan mengimpor fungsionalitas yang diperlukan, menentukan beberapa variabel kelas dan fungsi konstruktor.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteSheet.as
|
6 |
// An optimization used to improve performance, all sprites used
|
7 |
// in the game are packed onto a single texture so that
|
8 |
// they can be rendered in a single pass rather than individually.
|
9 |
// This also avoids the performance penalty of 3d stage changes.
|
10 |
// Based on example code by Chris Nuuja which is a port
|
11 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
12 |
// which is itself a port of Iain Lobb's original work.
|
13 |
// Also includes code from the Starling framework.
|
14 |
// Grateful acknowledgements to all involved.
|
15 |
|
16 |
package
|
17 |
{
|
18 |
import flash.display.Bitmap; |
19 |
import flash.display.BitmapData; |
20 |
import flash.display.Stage; |
21 |
import flash.display3D.Context3D; |
22 |
import flash.display3D.Context3DTextureFormat; |
23 |
import flash.display3D.IndexBuffer3D; |
24 |
import flash.display3D.textures.Texture; |
25 |
import flash.geom.Point; |
26 |
import flash.geom.Rectangle; |
27 |
import flash.geom.Matrix; |
28 |
|
29 |
public class LiteSpriteSheet |
30 |
{
|
31 |
internal var _texture : Texture; |
32 |
|
33 |
protected var _spriteSheet : BitmapData; |
34 |
protected var _uvCoords : Vector.<Number>; |
35 |
protected var _rects : Vector.<Rectangle>; |
36 |
|
37 |
public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8) |
38 |
{
|
39 |
_uvCoords = new Vector.<Number>(); |
40 |
_rects = new Vector.<Rectangle>(); |
41 |
_spriteSheet = SpriteSheetBitmapData; |
42 |
createUVs(numSpritesW, numSpritesH); |
43 |
}
|
Konstruktor kelas di atas diberikan BitmapData
untuk spritesheet kami serta jumlah sprite yang ada di dalamnya (dalam demo ini, 64).
Langkah 18: Memotongnya
Karena kita menggunakan tekstur yang satu untuk menyimpan semua gambar sprite, kita perlu membagi foto menjadi beberapa bagian (satu untuk masing-masing sprite di atasnya) ketika me-render. Kita melakukan ini dengan menetapkan koordinat yang berbeda untuk setiap vertex (sudut) mesh setiap quad digunakan untuk menggambar sebuah sprite.
Koordinat ini disebut UVs, dan masing-masing pergi dari 0 ke 1 dan mewakili mana pada tekstur stage3D harus mulai sampling piksel ketika me-render. UV koordinat dan piksel persegi yang disimpan dalam array untuk kemudian menggunakan selama rendering sehingga kita tidak harus menghitung mereka setiap bingkai. Kami juga menyimpan ukuran dan bentuk masing-masing sprite (yang dalam demo ini semuanya identik) sehingga ketika kami memutar sprite kita tahu jari-jarinya (yang digunakan untuk menjaga pivot di tengah-tengah sprite).
1 |
|
2 |
|
3 |
// generate a list of uv coordinates for a grid of sprites
|
4 |
// on the spritesheet texture for later reference by ID number
|
5 |
// sprite ID numbers go from left to right then down
|
6 |
public function createUVs(numSpritesW:int, numSpritesH:int) : void |
7 |
{
|
8 |
trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ |
9 |
' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); |
10 |
|
11 |
var destRect : Rectangle; |
12 |
|
13 |
for (var y:int = 0; y < numSpritesH; y++) |
14 |
{
|
15 |
for (var x:int = 0; x < numSpritesW; x++) |
16 |
{
|
17 |
_uvCoords.push( |
18 |
// bl, tl, tr, br
|
19 |
x / numSpritesW, (y+1) / numSpritesH, |
20 |
x / numSpritesW, y / numSpritesH, |
21 |
(x+1) / numSpritesW, y / numSpritesH, |
22 |
(x + 1) / numSpritesW, (y + 1) / numSpritesH); |
23 |
|
24 |
destRect = new Rectangle(); |
25 |
destRect.left = 0; |
26 |
destRect.top = 0; |
27 |
destRect.right = _spriteSheet.width / numSpritesW; |
28 |
destRect.bottom = _spriteSheet.height / numSpritesH; |
29 |
_rects.push(destRect); |
30 |
}
|
31 |
}
|
32 |
}
|
33 |
|
34 |
public function removeSprite(spriteId:uint) : void |
35 |
{
|
36 |
if ( spriteId < _uvCoords.length ) { |
37 |
_uvCoords = _uvCoords.splice(spriteId * 8, 8); |
38 |
_rects.splice(spriteId, 1); |
39 |
}
|
40 |
}
|
41 |
|
42 |
public function get numSprites() : uint |
43 |
{
|
44 |
return _rects.length; |
45 |
}
|
46 |
|
47 |
public function getRect(spriteId:uint) : Rectangle |
48 |
{
|
49 |
return _rects[spriteId]; |
50 |
}
|
51 |
|
52 |
public function getUVCoords(spriteId:uint) : Vector.<Number> |
53 |
{
|
54 |
var startIdx:uint = spriteId * 8; |
55 |
return _uvCoords.slice(startIdx, startIdx + 8); |
56 |
}
|
Langkah 19: Menghasilkan Mipmaps
Sekarang kita perlu untuk memproses gambar ini selama init. Kami akan meng-upload untuk digunakan sebagai tekstur dengan GPU Anda. Ketika kita melakukannya, kita akan membuat salinan yang lebih kecil yang disebut "mipmaps". MIP-mapping digunakan oleh keras 3d untuk lebih mempercepat render dengan menggunakan versi yang lebih kecil tekstur sama setiap kali hal ini terlihat dari jauh (diperkecil) atau, dalam game 3D yang benar, ketika ia sedang dilihat pada sudut miring. Ini menghindari efek "moiree" (Ghost) daripada yang dapat terjadi jika mipmapping tidak digunakan. Mipmap masing-masing adalah setengah lebar dan tinggi seperti sebelumnya.
Melanjutkan dengan LiteSpriteSheet.as, mari kita melaksanakan rutinitas kita perlu yang akan menghasilkan mipmaps dan meng-upload mereka semua untuk GPU pada kartu video Anda.
1 |
|
2 |
public function uploadTexture(context3D:Context3D) : void |
3 |
{
|
4 |
if ( _texture == null ) { |
5 |
_texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); |
6 |
}
|
7 |
|
8 |
_texture.uploadFromBitmapData(_spriteSheet); |
9 |
|
10 |
// generate mipmaps
|
11 |
var currentWidth:int = _spriteSheet.width >> 1; |
12 |
var currentHeight:int = _spriteSheet.height >> 1; |
13 |
var level:int = 1; |
14 |
var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); |
15 |
var transform:Matrix = new Matrix(.5, 0, 0, .5); |
16 |
while ( currentWidth >= 1 || currentHeight >= 1 ) { |
17 |
canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); |
18 |
canvas.draw(_spriteSheet, transform, null, null, null, true); |
19 |
_texture.uploadFromBitmapData(canvas, level++); |
20 |
transform.scale(0.5, 0.5); |
21 |
currentWidth = currentWidth >> 1; |
22 |
currentHeight = currentHeight >> 1; |
23 |
}
|
24 |
}
|
25 |
} // end class |
26 |
} // end package |
Langkah 20: Geometri Batched
Optimalisasi hardcore akhir yang akan kami terapkan adalah sistem rendering geometri batch. Teknik 'batched geometry' ini sering digunakan dalam sistem partikel. Kita akan menggunakannya untuk segalanya. Dengan cara ini, kita dapat memberitahu Anda GPU menggambar semuanya dalam satu pergi daripada naif mengirim ratusan menarik perintah (satu untuk masing-masing sprite pada layar).
Untuk meminimalkan jumlah panggilan menarik dan rendering semuanya dalam satu pergi, kami akan menjadi batching sprite permainan semua ke dalam daftar panjang (x, y) koordinat. Pada dasarnya, kumpulan geometri diperlakukan oleh perangkat keras video sebagai mesh 3D tunggal. Kemudian, setelah setiap bingkai, kami akan meng-upload seluruh buffer untuk Stage3D dalam panggilan fungsi tunggal. Melakukan hal-hal dengan cara ini adalah jauh lebih cepat daripada meng-upload koordinat individu sprite masing-masing secara terpisah.
Buat file baru di proyek Anda bernama LiteSpriteBatch.as
dan mulailah dengan memasukkan semua impor untuk fungsionalitas yang diperlukan, variabel kelas yang akan digunakan, dan konstruktor sebagai berikut:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteBatch.as
|
6 |
// An optimization used to increase performance that renders multiple
|
7 |
// sprites in a single pass by grouping all polygons together,
|
8 |
// allowing stage3D to treat it as a single mesh that can be
|
9 |
// rendered in a single drawTriangles call.
|
10 |
// Each frame, the positions of each
|
11 |
// vertex is updated and re-uploaded to video ram.
|
12 |
// Based on example code by Chris Nuuja which is a port
|
13 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
14 |
// which is itself a port of Iain Lobb's original work.
|
15 |
// Also includes code from the Starling framework.
|
16 |
// Grateful acknowledgements to all involved.
|
17 |
|
18 |
package
|
19 |
{
|
20 |
import com.adobe.utils.AGALMiniAssembler; |
21 |
|
22 |
import flash.display.BitmapData; |
23 |
import flash.display3D.Context3D; |
24 |
import flash.display3D.Context3DBlendFactor; |
25 |
import flash.display3D.Context3DCompareMode; |
26 |
import flash.display3D.Context3DProgramType; |
27 |
import flash.display3D.Context3DTextureFormat; |
28 |
import flash.display3D.Context3DVertexBufferFormat; |
29 |
import flash.display3D.IndexBuffer3D; |
30 |
import flash.display3D.Program3D; |
31 |
import flash.display3D.VertexBuffer3D; |
32 |
import flash.display3D.textures.Texture; |
33 |
import flash.geom.Matrix; |
34 |
import flash.geom.Matrix3D; |
35 |
import flash.geom.Point; |
36 |
import flash.geom.Rectangle; |
37 |
|
38 |
public class LiteSpriteBatch |
39 |
{
|
40 |
internal var _sprites : LiteSpriteSheet; |
41 |
internal var _verteces : Vector.<Number>; |
42 |
internal var _indeces : Vector.<uint>; |
43 |
internal var _uvs : Vector.<Number>; |
44 |
|
45 |
protected var _context3D : Context3D; |
46 |
protected var _parent : LiteSpriteStage; |
47 |
protected var _children : Vector.<LiteSprite>; |
48 |
|
49 |
protected var _indexBuffer : IndexBuffer3D; |
50 |
protected var _vertexBuffer : VertexBuffer3D; |
51 |
protected var _uvBuffer : VertexBuffer3D; |
52 |
protected var _shader : Program3D; |
53 |
protected var _updateVBOs : Boolean; |
54 |
|
55 |
|
56 |
public function LiteSpriteBatch(context3D:Context3D, spriteSheet:LiteSpriteSheet) |
57 |
{
|
58 |
_context3D = context3D; |
59 |
_sprites = spriteSheet; |
60 |
|
61 |
_verteces = new Vector.<Number>(); |
62 |
_indeces = new Vector.<uint>(); |
63 |
_uvs = new Vector.<Number>(); |
64 |
|
65 |
_children = new Vector.<LiteSprite>; |
66 |
_updateVBOs = true; |
67 |
setupShaders(); |
68 |
updateTexture(); |
69 |
}
|
Langkah 21: Batch orangtua dan anak-anak
Lanjutkan dengan menerapkan Getter dan setter dan fungsi untuk menangani penambahan apapun sprite yang baru untuk batch. Orangtua mengacu pada objek tahap sprite digunakan oleh mesin permainan kami, sementara anak-anak semua sprite dalam batch render yang satu ini. Ketika kita menambahkan sebuah sprite anak, kami menambahkan lebih banyak data ke daftar verteces (yang memasok lokasi di layar sprite yang tertentu) serta UV Koordinat (lokasi di spritesheet tekstur yang sprite tertentu ini disimpan di). Ketika sebuah sprite anak ditambahkan atau dihapus dari batch, kami menetapkan variabel boolean untuk memberitahu kami sistem batch yang buffer perlu kembali upload sekarang bahwa mereka telah berubah.
1 |
|
2 |
public function get parent() : LiteSpriteStage |
3 |
{
|
4 |
return _parent; |
5 |
}
|
6 |
|
7 |
public function set parent(parentStage:LiteSpriteStage) : void |
8 |
{
|
9 |
_parent = parentStage; |
10 |
}
|
11 |
|
12 |
public function get numChildren() : uint |
13 |
{
|
14 |
return _children.length; |
15 |
}
|
16 |
|
17 |
// Constructs a new child sprite and attaches it to the batch
|
18 |
public function createChild(spriteId:uint) : LiteSprite |
19 |
{
|
20 |
var sprite : LiteSprite = new LiteSprite(); |
21 |
addChild(sprite, spriteId); |
22 |
return sprite; |
23 |
}
|
24 |
|
25 |
public function addChild(sprite:LiteSprite, spriteId:uint) : void |
26 |
{
|
27 |
sprite._parent = this; |
28 |
sprite._spriteId = spriteId; |
29 |
|
30 |
// Add to list of children
|
31 |
sprite._childId = _children.length; |
32 |
_children.push(sprite); |
33 |
|
34 |
// Add vertex data required to draw child
|
35 |
var childVertexFirstIndex:uint = (sprite._childId * 12) / 3; |
36 |
_verteces.push(0, 0, 1, 0, 0,1, 0, 0,1, 0, 0,1); // placeholders |
37 |
_indeces.push(childVertexFirstIndex, childVertexFirstIndex+1, childVertexFirstIndex+2, |
38 |
childVertexFirstIndex, childVertexFirstIndex+2, childVertexFirstIndex+3); |
39 |
|
40 |
var childUVCoords:Vector.<Number> = _sprites.getUVCoords(spriteId); |
41 |
_uvs.push( |
42 |
childUVCoords[0], childUVCoords[1], |
43 |
childUVCoords[2], childUVCoords[3], |
44 |
childUVCoords[4], childUVCoords[5], |
45 |
childUVCoords[6], childUVCoords[7]); |
46 |
|
47 |
_updateVBOs = true; |
48 |
}
|
49 |
|
50 |
public function removeChild(child:LiteSprite) : void |
51 |
{
|
52 |
var childId:uint = child._childId; |
53 |
if ( (child._parent == this) && childId < _children.length ) { |
54 |
child._parent = null; |
55 |
_children.splice(childId, 1); |
56 |
|
57 |
// Update child id (index into array of children) for remaining children
|
58 |
var idx:uint; |
59 |
for ( idx = childId; idx < _children.length; idx++ ) { |
60 |
_children[idx]._childId = idx; |
61 |
}
|
62 |
|
63 |
// Realign vertex data with updated list of children
|
64 |
var vertexIdx:uint = childId * 12; |
65 |
var indexIdx:uint= childId * 6; |
66 |
_verteces.splice(vertexIdx, 12); |
67 |
_indeces.splice(indexIdx, 6); |
68 |
_uvs.splice(vertexIdx, 8); |
69 |
|
70 |
_updateVBOs = true; |
71 |
}
|
72 |
}
|
Langkah 22: Mengatur Shader
Shader adalah seperangkat perintah yang di-upload langsung ke kartu video Anda untuk render yang sangat cepat. Di Flash 11 Stage3D, Anda menulis mereka dalam semacam bahasa assembly yang disebut AGAL. Shader ini membutuhkan hanya dibuat sekali, pada saat startup. Anda tidak perlu mengerti bahasa assembly opcodes untuk tutorial ini. Sebaliknya, hanya menerapkan penciptaan program vertex (yang menghitung lokasi Anda sprite pada layar) dan program fragmen (yang menghitung warna setiap piksel) sebagai berikut.
1 |
|
2 |
protected function setupShaders() : void |
3 |
{
|
4 |
var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); |
5 |
vertexShaderAssembler.assemble( Context3DProgramType.VERTEX, |
6 |
"dp4 op.x, va0, vc0 \n"+ // transform from stream 0 to output clipspace |
7 |
"dp4 op.y, va0, vc1 \n"+ // do the same for the y coordinate |
8 |
"mov op.z, vc2.z \n"+ // we don't need to change the z coordinate |
9 |
"mov op.w, vc3.w \n"+ // unused, but we need to output all data |
10 |
"mov v0, va1.xy \n"+ // copy UV coords from stream 1 to fragment program |
11 |
"mov v0.z, va0.z \n" // copy alpha from stream 0 to fragment program |
12 |
); |
13 |
|
14 |
var fragmentShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); |
15 |
fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT, |
16 |
"tex ft0, v0, fs0 <2d,clamp,linear,mipnearest> \n"+ // sample the texture |
17 |
"mul ft0, ft0, v0.zzzz\n" + // multiply by the alpha transparency |
18 |
"mov oc, ft0 \n" // output the final pixel color |
19 |
); |
20 |
|
21 |
_shader = _context3D.createProgram(); |
22 |
_shader.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode ); |
23 |
}
|
24 |
|
25 |
protected function updateTexture() : void |
26 |
{
|
27 |
_sprites.uploadTexture(_context3D); |
28 |
}
|
Langkah 23: Pindahkan Sprite Sekitar
Tepat sebelum diberikan, sprite setiap vertex koordinat di layar akan memiliki kemungkinan berubah seperti sprite bergerak di sekitar atau berputar. Fungsi berikut menghitung mana setiap node (sudut geometri) perlu. Karena setiap quad (Alun-alun yang membentuk satu sprite) memiliki empat simpul, dan vertex setiap kebutuhan x, y dan z koordinat, ada nilai-nilai dua belas untuk memperbarui. Sebagai sedikit optimasi, jika sprite tidak terlihat kita hanya menulis angka nol ke buffer vertex kami untuk menghindari melakukan perhitungan yang tidak perlu.
1 |
|
2 |
protected function updateChildVertexData(sprite:LiteSprite) : void |
3 |
{
|
4 |
var childVertexIdx:uint = sprite._childId * 12; |
5 |
|
6 |
if ( sprite.visible ) { |
7 |
var x:Number = sprite.position.x; |
8 |
var y:Number = sprite.position.y; |
9 |
var rect:Rectangle = sprite.rect; |
10 |
var sinT:Number = Math.sin(sprite.rotation); |
11 |
var cosT:Number = Math.cos(sprite.rotation); |
12 |
var alpha:Number = sprite.alpha; |
13 |
|
14 |
var scaledWidth:Number = rect.width * sprite.scaleX; |
15 |
var scaledHeight:Number = rect.height * sprite.scaleY; |
16 |
var centerX:Number = scaledWidth * 0.5; |
17 |
var centerY:Number = scaledHeight * 0.5; |
18 |
|
19 |
_verteces[childVertexIdx] = x - (cosT * centerX) - (sinT * (scaledHeight - centerY)); |
20 |
_verteces[childVertexIdx+1] = y - (sinT * centerX) + (cosT * (scaledHeight - centerY)); |
21 |
_verteces[childVertexIdx+2] = alpha; |
22 |
|
23 |
_verteces[childVertexIdx+3] = x - (cosT * centerX) + (sinT * centerY); |
24 |
_verteces[childVertexIdx+4] = y - (sinT * centerX) - (cosT * centerY); |
25 |
_verteces[childVertexIdx+5] = alpha; |
26 |
|
27 |
_verteces[childVertexIdx+6] = x + (cosT * (scaledWidth - centerX)) + (sinT * centerY); |
28 |
_verteces[childVertexIdx+7] = y + (sinT * (scaledWidth - centerX)) - (cosT * centerY); |
29 |
_verteces[childVertexIdx+8] = alpha; |
30 |
|
31 |
_verteces[childVertexIdx+9] = x + (cosT * (scaledWidth - centerX)) - (sinT * (scaledHeight - centerY)); |
32 |
_verteces[childVertexIdx+10] = y + (sinT * (scaledWidth - centerX)) + (cosT * (scaledHeight - centerY)); |
33 |
_verteces[childVertexIdx+11] = alpha; |
34 |
|
35 |
}
|
36 |
else { |
37 |
for (var i:uint = 0; i < 12; i++ ) { |
38 |
_verteces[childVertexIdx+i] = 0; |
39 |
}
|
40 |
}
|
41 |
}
|
Langkah 24: Gambarkan Geometri
Akhirnya, terus menambahkan kelas LiteSpriteBatch.as dengan menerapkan fungsi gambar. Ini adalah dimana kami memberitahukan stage3D untuk membuat semua sprite dalam satu berlalu. Pertama, kita loop melalui semua dikenal anak (sprite individu) dan update posisi verterx berdasarkan mana mereka berada pada layar. Kami kemudian memberitahu stage3D tekstur dan shader yang menggunakan, serta menetapkan faktor campuran render.
Apakah faktor campuran? Itu akan menentukan apakah atau tidak kita harus menggunakan transparansi, dan bagaimana untuk menangani transparan piksel pada tekstur kami. Anda bisa mengubah pilihan dalam panggilan setBlendFactors menggunakan aditif blanding, misalnya, yang tampak bagus untuk efek partikel seperti ledakan, karena piksel akan meningkatkan kecerahan di layar seperti mereka tumpang tindih. Dalam kasus sprite reguler, yang kami inginkan adalah menggambarnya dengan warna yang tepat seperti yang disimpan dalam tekstur spritesheet kami dan memungkinkan wilayah transparan.
Langkah terakhir dalam fungsi menarik kami adalah untuk memperbarui UV dan indeks buffer jika batch telah mengubah ukuran, dan untuk selalu meng-upload vertex data karena sprite kami exected terus-menerus bergerak. Kita mengatakan stage3D yang buffer untuk menggunakan dan akhirnya membuat seluruh daftar raksasa geometri seolah-olah satu 3D mesh, sehingga ini akan ditarik menggunakan satu, cepat, drawTriangles panggilan.
1 |
|
2 |
|
3 |
public function draw() : void |
4 |
{
|
5 |
var nChildren:uint = _children.length; |
6 |
if ( nChildren == 0 ) return; |
7 |
|
8 |
// Update vertex data with current position of children
|
9 |
for ( var i:uint = 0; i < nChildren; i++ ) { |
10 |
updateChildVertexData(_children[i]); |
11 |
}
|
12 |
|
13 |
_context3D.setProgram(_shader); |
14 |
_context3D.setBlendFactors(Context3DBlendFactor.ONE, |
15 |
Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA); |
16 |
_context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, |
17 |
0, _parent.modelViewMatrix, true); |
18 |
_context3D.setTextureAt(0, _sprites._texture); |
19 |
|
20 |
if ( _updateVBOs ) { |
21 |
_vertexBuffer = _context3D.createVertexBuffer(_verteces.length/3, 3); |
22 |
_indexBuffer = _context3D.createIndexBuffer(_indeces.length); |
23 |
_uvBuffer = _context3D.createVertexBuffer(_uvs.length/2, 2); |
24 |
_indexBuffer.uploadFromVector(_indeces, 0, _indeces.length); // indices won't change |
25 |
_uvBuffer.uploadFromVector(_uvs, 0, _uvs.length / 2); // child UVs won't change |
26 |
_updateVBOs = false; |
27 |
}
|
28 |
|
29 |
// we want to upload the vertex data every frame
|
30 |
_vertexBuffer.uploadFromVector(_verteces, 0, _verteces.length / 3); |
31 |
_context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); |
32 |
_context3D.setVertexBufferAt(1, _uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); |
33 |
|
34 |
_context3D.drawTriangles(_indexBuffer, 0, nChildren * 2); |
35 |
}
|
36 |
} // end class |
37 |
} // end package |
Langkah 25: Sprite tahap kelas
Kelas akhir yang diperlukan oleh mesin rendering mewah (dan cepat) hardware-accelerated sprite kami adalah kelas tahap sprite. Tahap ini, seperti tahap Flash tradisional, memegang daftar semua kumpulan yang digunakan untuk permainan Anda. Dalam demo ini pertama, tahap kami akan hanya menggunakan satu batch sprite, yang sendiri hanya menggunakan spritesheet tunggal.
Membuat satu file terakhir dalam proyek Anda disebut LiteSpriteStage.as dan mulai dengan menciptakan kelas sebagai berikut:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 1
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteStage.as
|
6 |
// The stage3D renderer of any number of batched geometry
|
7 |
// meshes of multiple sprites. Handles stage3D inits, etc.
|
8 |
// Based on example code by Chris Nuuja which is a port
|
9 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
10 |
// which is itself a port of Iain Lobb's original work.
|
11 |
// Also includes code from the Starling framework.
|
12 |
// Grateful acknowledgements to all involved.
|
13 |
|
14 |
package
|
15 |
{
|
16 |
import flash.display.Stage3D; |
17 |
import flash.display3D.Context3D; |
18 |
import flash.geom.Matrix3D; |
19 |
import flash.geom.Rectangle; |
20 |
|
21 |
public class LiteSpriteStage |
22 |
{
|
23 |
protected var _stage3D : Stage3D; |
24 |
protected var _context3D : Context3D; |
25 |
protected var _rect : Rectangle; |
26 |
protected var _batches : Vector.<LiteSpriteBatch>; |
27 |
protected var _modelViewMatrix : Matrix3D; |
28 |
|
29 |
public function LiteSpriteStage(stage3D:Stage3D, context3D:Context3D, rect:Rectangle) |
30 |
{
|
31 |
_stage3D = stage3D; |
32 |
_context3D = context3D; |
33 |
_batches = new Vector.<LiteSpriteBatch>; |
34 |
|
35 |
this.position = rect; |
36 |
}
|
Langkah 26: Matriks kamera
Untuk mengetahui dengan tepat ke mana pada layar setiap sprite harus pergi, kami akan melacak lokasi dan ukuran jendela render. Selama inisialisasi game kami (atau jika itu berubah) kami membuat matriks tampilan model yang digunakan oleh Stage3D untuk mengubah koordinat 3D internal dari kumpulan geometri kami ke lokasi di layar yang tepat.
1 |
|
2 |
|
3 |
public function get position() : Rectangle |
4 |
{
|
5 |
return _rect; |
6 |
}
|
7 |
|
8 |
public function set position(rect:Rectangle) : void |
9 |
{
|
10 |
_rect = rect; |
11 |
_stage3D.x = rect.x; |
12 |
_stage3D.y = rect.y; |
13 |
configureBackBuffer(rect.width, rect.height); |
14 |
|
15 |
_modelViewMatrix = new Matrix3D(); |
16 |
_modelViewMatrix.appendTranslation(-rect.width/2, -rect.height/2, 0); |
17 |
_modelViewMatrix.appendScale(2.0/rect.width, -2.0/rect.height, 1); |
18 |
}
|
19 |
|
20 |
internal function get modelViewMatrix() : Matrix3D |
21 |
{
|
22 |
return _modelViewMatrix; |
23 |
}
|
24 |
|
25 |
public function configureBackBuffer(width:uint, height:uint) : void |
26 |
{
|
27 |
_context3D.configureBackBuffer(width, height, 0, false); |
28 |
}
|
Langkah 27: Tangani Batch
Langkah terakhir dalam pembuatan demo game Stage3D kami adalah untuk menangani penambahan dan penghapusan batch geometri serta loop yang memanggil fungsi draw pada setiap batch. Dengan cara ini, ketika acara utama ENTER_FRAME
permainan kami dipecat, itu akan memindahkan sprite di layar melalui manajer entitas dan kemudian memberitahu sistem tahap sprite untuk menggambar dirinya sendiri, yang pada gilirannya memberitahu semua batch yang dikenal untuk menggambar.
Karena ini adalah demo berat dioptimalkan, hanya akan ada satu batch digunakan, tetapi ini akan berubah di masa depan tutorial seperti kita menambahkan lebih banyak mata permen.
1 |
|
2 |
public function addBatch(batch:LiteSpriteBatch) : void |
3 |
{
|
4 |
batch.parent = this; |
5 |
_batches.push(batch); |
6 |
}
|
7 |
|
8 |
public function removeBatch(batch:LiteSpriteBatch) : void |
9 |
{
|
10 |
for ( var i:uint = 0; i < _batches.length; i++ ) { |
11 |
if ( _batches[i] == batch ) { |
12 |
batch.parent = null; |
13 |
_batches.splice(i, 1); |
14 |
}
|
15 |
}
|
16 |
}
|
17 |
|
18 |
// loop through all batches
|
19 |
// (this demo uses only one)
|
20 |
// and tell them to draw themselves
|
21 |
public function render() : void |
22 |
{
|
23 |
for ( var i:uint = 0; i < _batches.length; i++ ) { |
24 |
_batches[i].draw(); |
25 |
}
|
26 |
}
|
27 |
} // end class |
28 |
} // end package |
Langkah 28: Kompilasi dan jalankan!
Kita hampir selesai! Mengkompilasi SWF Anda, memperbaiki kesalahan ketik, dan memeriksa kebaikan grafis. Anda harus memiliki demo yang terlihat seperti ini:



Jika Anda mengalami kesulitan kompilasi, dicatat bahwa proyek ini kelas yang dibuat oleh Adobe yang menangani kompilasi AGAL shaders, yang termasuk dalam download file .zip kode sumber.
Hanya untuk referensi, dan untuk memastikan bahwa Anda telah menggunakan nama file benar dan lokasi untuk segala sesuatu, di sini adalah apa yang proyek FlashDevelop Anda akan terlihat seperti:

Tutorial lengkap: Anda Are Awesome
That's it untuk tutorial dalam seri ini! Menyetel minggu depan untuk menonton pertandingan perlahan-lahan berevolusi menjadi besar-Cari, halus mulus 60 FPS shoot-em-up. Dalam bagian selanjutnya, kami akan menerapkan kontrol pemain (menggunakan keyboard untuk bergerak di sekitar) dan menambahkan beberapa gerakan, suara dan musik untuk permainan.
Saya akan senang mendengar dari Anda mengenai tutorial ini. Saya menyambut hangat semua pembaca untuk menghubungi saya melalui kericau: @mcfunkypants atau mcfunkypants.com blog saya atau di Google + setiap saat. Saya selalu mencari topik untuk menulis tutorial masa depan, sehingga merasa bebas untuk meminta satu. Akhirnya, saya akan senang melihat permainan Anda membuat menggunakan kode ini!
Terima kasih untuk membaca. Melihat Anda minggu depan. Good luck dan HAVE FUN!