Saya baru terpapar kanvas selama lebih dari sebulan. Ini adalah pertama kalinya menerapkan proses permainan sepenuhnya, dan keuntungannya cukup besar.
Tangkapan layar permainan menembak
Buka demonya dulu: https://littleyljy.github.io/demo/shootgame/
aturan permainanPemain diharuskan mengendalikan pesawat untuk menembakkan peluru dan menghancurkan monster yang bergerak. Jika semua monster hancur, permainan berhasil. Jika monster bergerak ke bawah, permainan gagal.
Permainan ini dibagi menjadi beberapa adegan:
Untuk mencapai peralihan adegan, Anda sebenarnya harus terlebih dahulu menampilkan: tidak ada untuk semua adegan, lalu mengontrol status data melalui js untuk memulai, memutar, gagal, sukses, semua sukses, dan berhenti untuk mengimplementasikan tampilan adegan: blok yang sesuai.
HTML dan CSSnya adalah sebagai berikut:
<div id=game data-status=start> <div class=game-panel> <section class=game-intro game-ui> <h1 class=section-title>Game menembak</h1> <p class=game- desc>Ini adalah game menembak yang membuat ketagihan. Gunakan ← dan → untuk mengoperasikan pesawat Anda, gunakan ruang untuk menembak, dan gunakan enter untuk menjeda permainan. Ayo hancurkan monster luar angkasa bersama-sama! </p> <p class=game-level>Level Saat Ini: 1</p> <button class=js-play button>Mulai permainan</button> </section> <section class=game-failed game-ui> <h1 class=section-title>Permainan berakhir</h1> <p class=game-info-text>Skor akhir: <span class=score></span></p> <button class=js-replay button> Mulai lagi</tombol> </section> <section class=game-success game-ui> <h1 class=section-title>Game sukses</h1> <p class=game-next-level game-info-text></p> <button class=js-next button>Lanjutkan permainan</button> </section> <section class=game-all-success game-ui> <h1 class=section-title>Berhasil</h1> <p class=game- tingkat berikutnya game-info-text>Anda telah berhasil bertahan melawan semua serangan monster. </p> <button class=js-replay button>Mainkan lagi</button> </section> <section class=game-stop game-ui> <h1 class=section-title>Jeda permainan</h1> < tombol class=js-stop button>Permainan berlanjut</button> </section> </div> <div class=game-info game-ui> <span class=title>Skor:</span> <span class=score > </span> </div> <canvas id=canvas width=700 height=600> <!-- Papan gambar animasi --> </canvas> </div>
#game{ lebar: 700 piksel; tinggi: 600 piksel; posisi: relatif; kiri: 50%; atas: 40 piksel; margin: 0 0 0 -350 piksel; );}.game-ui{ tampilan: tidak ada; padding: 55 piksel; kotak perbatasan; tinggi: 100%;}[status data=mulai] .game-intro { tampilan: blok; padding-top: 180px; latar belakang: url(./img/bg.png) tanpa pengulangan 430px 180px; ukuran latar belakang: 200px;}[status data=bermain] .game-info { tampilan: blok posisi: absolut; kiri:0; padding:20px;}[status-data=gagal] .game-gagal,[status-data=sukses] .sukses-game,[status-data=semua-sukses] .game-semua-sukses,[ data-status=stop] .game-stop{ tampilan: blok; padding-top: 180px latar belakang: url(./img/bg-end.png) tanpa pengulangan 380px 190px; ukuran latar: 250px;}berorientasi objek
Keseluruhan permainan dapat memperlakukan monster (Musuh), pesawat (Plane), dan peluru (Bullets) sebagai objek, serta objek konfigurasi (CONFIG) dan objek permainan (GAME) yang mengontrol logika permainan.
Konfigurasi terkait game/** * Konfigurasi terkait game* @type {Object} */var CONFIG = { status: 'start', // Game dimulai secara default sebagai level awal: 1, // Level default game totalLevel: 6, // Total 6 Off numPerLine: 7, // Jumlah monster default game per baris CanvasPadding: 30, // Interval kanvas default bulletSize: 10, // Panjang peluru default bulletSpeed: 10, // Kecepatan pergerakan peluru default Kecepatan musuh: 2, // Jarak pergerakan musuh default Ukuran musuh: 50, // Ukuran musuh default Celah musuh: 10, // Jarak default antar musuh ikon musuh: './img/enemy.png', // Gambar musuh monsterBoomIcon: './img/boom.png', // Gambar musuh kematian monsterArah: 'kanan', // Musuh default bergerak ke kanan di awal planeSpeed: 5, // Jarak default pesawat bergerak di setiap langkah planeSize: { width: 60, height: 100 }, // Ukuran default pesawat, planeIcon: '. /img/pesawat.png' };Tentukan kelas induk
Karena monster (Musuh), pesawat (Plane), dan peluru (Bullet) semuanya memiliki atribut x, y, ukuran, kecepatan, dan metode move() yang sama, Anda dapat mendefinisikan Elemen kelas induk dan mengimplementasikannya dengan mewarisi kelas induk dari subkelas tersebut.
/*Kelas induk: Berisi xy speed move() draw()*/var Elemen = fungsi (opts) { this.opts = opts || {} //Tetapkan koordinat, ukuran, kecepatan this.x = opts.x; this.y = opts.y; this.size = opts.size; this.speed = opts.speed;};Elemen.prototype.move = fungsi (x, y) { var addX = x || var addY = y ||.0; this.x += addX; this.y += addY;};//Fungsi yang mewarisi fungsi prototipe mewarisiPrototype(subType, superType) { var proto = Object.create(superType.prototype); proto .konstruktor = subJenis; subJenis.prototipe = proto;}
Metode move(x, y) menumpuk dirinya sendiri berdasarkan nilai (x, y) yang diteruskan.
Definisikan monsterMonster menyertakan atribut unik: status monster, gambar, boomCount yang mengontrol durasi status ledakan, dan metode draw(), down(), Direction(), dan booming().
/*Enemy*/var Enemy = function (opts) { this.opts = opts || {}; //Panggil atribut kelas induk Element.call(this, opts); //status dan gambar atribut khusus this.status = 'normal';//normal, booming, disebut this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon; 0;};//Mewarisi metode Elemen mewarisiPrototipe(Musuh, Elemen);//Metode: Menggambar Musuh musuh.prototipe.draw = function () { if (this.enemyIcon && this.enemyBoomIcon) { switch (this.status ) { kasus 'normal': var musuhIcon = Gambar baru(); musuhIcon.src = ini.enemyIcon; ini.x, ini.y, ini.ukuran, ini.ukuran); istirahat; kasus 'booming': var musuhBoomIcon = Gambar baru(); musuhBoomIcon.src = ini.enemyBoomIcon; ini.y, ini.ukuran, ini.ukuran); istirahat; kasus 'booming': ctx.clearRect(ini.x, ini.y, ini.ukuran, this.size); default: break; } } kembalikan ini;};//Metode: turunkan Musuh.prototype.down = function () { this.move(0, this.size); ;//Metode: Bergerak ke kiri atau ke kanan Musuh.prototipe.arah = fungsi (arah) { if (arah === 'kanan') { this.move(ini.kecepatan, 0); this.move(-this.speed, 0); } kembalikan ini;};//Metode: Musuh meledak Musuh.prototype.booming = function () { this.status = 'booming'; (this.boomCount > 4) { this.status = 'boom'; } kembalikan ini;}
Poin memiliki metode fly() dan draw().
/*Bullet*/var Bullet = function (opts) { this.opts = opts ||. {}; Element.call(this, opts);};inheritPrototype(Bullet, Element);//Metode: Biarkan peluru terbang Peluru .prototype.fly = function () { this.move(0, -this.speed); kembalikan ini;};//Metode: Gambarkan sebuah peluru Bullet.prototype.draw = function () { ctx.beginPath(); ctx.strokeStyle = '#fff'; ctx.moveTo(ini.x, ini.y); ctx.lineTo(ini.x, ini.y - CONFIG.bulletSize()); ; ctx.stroke(); kembalikan ini;};
Objek pesawat berisi atribut unik: status, lebar dan tinggi, gambar, nilai absis maksimum dan minimum, serta metode hasHit(), draw(), Direction(), shoot(), dan drawBullets().
/*Plane*/var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //status dan gambar atribut unik this.status = 'normal'; = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; //Bullet terkait this.bullet = []; this.bulletSpeed = opts.bulletSpeed ||. CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || (Pesawat, Elemen) ;//Metode: Peluru mengenai sasaran Plane.prototype.hasHit = function (musuh) { var bullets = ini.peluru; untuk (var i = peluru.panjang - 1; i >= 0; i--) { var peluru = peluru[i]; var isHitPosX = (musuh.x < peluru.x) && (peluru.x < (musuh.x + musuh.ukuran)); var isHitPosY = (musuh.y < peluru.y) && (peluru.y < (musuh.y + musuh.ukuran)); (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } return false;};//Metode: Gambarlah bidang Plane.prototype.draw = function () { this.drawBullets(); var planeIcon = Gambar baru(); planeIcon.src = ini.planeIcon; ctx.drawImage(planeIcon, this.x, this.y, this.width, this.height); kembalikan ini;};//Metode: Arah bidang Bidang.prototipe.arah = fungsi (arah) { var speed = this.speed; ) { planeSpeed = this.x < this.minX 0 : -speed; } else { planeSpeed = this.x > this.maxX ? console.log('planeSpeed:', planeSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX console.log('ini .maxX:', this.maxX); this.move(planeSpeed, 0); kembalikan ini;//Panggilan berantai yang nyaman};//Metode: meluncurkan peluru Plane.prototype.shoot = function () { var bulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x: bulletPosX, y: this.y, ukuran: this.bulletSize, kecepatan: this.bulletSpeed })); this; };//Metode: menggambar peluru Plane.prototype.drawBullets = function() { var bullets = this.bullets; var i = bullets.length; (i--) { var peluru = peluru[i]; peluru.terbang(); if (peluru.y <= 0) { peluru.splice(i, 1 } peluru.draw();
Peristiwa keyboard memiliki status berikut:
Karena pesawat harus tetap bergerak ketika tombol kiri (keyCode=37) dan tombol kanan (keyCode=39) ditekan (keydown), dan keyup tidak bergerak saat dilepas. Ketika spasi (keyCode=32) atau tombol panah atas (keyCode=38) ditekan (keydown), peluru ditembakkan, dan ketika dilepaskan, keyup berhenti menembak. Tekan juga tombol Enter (keyCode=13) untuk menjeda permainan. Oleh karena itu, Anda perlu mendefinisikan objek KeyBoard untuk memantau apakah onkeydown dan onkeyup menekan atau melepaskan tombol.
Karena tombol kiri dan kanan saling bertentangan, untuk amannya, Anda perlu menyetel kunci kanan ke false saat menekan tombol kiri. Hal yang sama berlaku untuk klik kanan.
//Acara keyboard var KeyBoard = function () { document.onkeydown = this.keydown.bind(ini); document.onkeyup = this.keyup.bind(ini);}; salah, ditekanKanan: salah, ditekanUp: salah, ditahanKiri: salah, ditahanKanan: salah, ditekanSpace: salah, ditekanEnter: salah, keydown: fungsi (e) { var key = e.keyCode; switch (kunci) { case 32://Space - menembakkan peluru this.pressedSpace = true; case 37://Kiri panah key this.pressedLeft = true; false; this.heldRight = false; break; case 38://Tombol panah atas - meluncurkan poin this.pressedUp = true; break; = false; this.pressedRight = true; this.heldRight = true; break; case 13://Enter key - jeda permainan this.pressedEnter = true; .keyCode; saklar (kunci) { kasus 32: this.pressedSpace = salah; kasus 37: this.heldLeft = salah; this.pressedUp = salah; kasus 39: this.heldRight = salah;Logika permainan
Objek permainan (GAME) berisi logika keseluruhan permainan, termasuk init (inisialisasi), bindEvent (tombol pengikatan), setStatus (memperbarui status permainan), play (dalam permainan), stop (jeda), end (akhir), dll. ., dalam hal ini tidak memperluas uraiannya. Ini juga mencakup fungsi seperti menghasilkan monster dan menggambar elemen permainan.
//Seluruh objek game var GAME = { //Serangkaian fungsi logika // Fungsi elemen game}1. Inisialisasi
Fungsi inisialisasi terutama menentukan koordinat awal pesawat, rentang pergerakan pesawat, rentang pergerakan monster, menginisialisasi skor, susunan monster, membuat objek KeyBoard, dan mengeksekusinya hanya sekali.
/** * Fungsi inisialisasi, fungsi ini dijalankan hanya sekali * @param {object} opts * @return {[type]} [deskripsi] */init: function (opts) { //Set opts var opts = Object.assign ( {}, opts, CONFIG);//Gabungkan semua parameter this.opts = opts; this.status = 'start'; //Hitung koordinat awal objek pesawat this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; //Batas bidang mengoordinasikan this.planeMinX = opts.planeMaxX = canvasWidth - opts.canvasPadding - opts.planeSize; lebar; //Hitung area pergerakan musuh this.enemyMinX = opts.canvasPadding; canvasWidth - opts.canvasPadding - opts.enemySize; //Skor disetel ke 0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); );2. Acara tombol ikat
Karena beberapa adegan permainan menyertakan tombol untuk memulai permainan (playBtn), memulai kembali (replayBtn), level permainan berikutnya (nextBtn), dan menjeda permainan untuk melanjutkan (stopBtn). Kita perlu melakukan event berbeda untuk tombol berbeda.
Alasan pertama untuk mendefinisikan var self = this; Dalam fungsi bindEvent, ini menunjuk ke objek GAME, dan dalam playBtn.onclick = function () {}; ini menunjuk ke playBtn. Ini jelas bukan yang kita inginkan, karena playBtn tidak memiliki acara play(), hanya Objek GAME memilikinya. Oleh karena itu, objek GAME perlu ditetapkan ke variabel self, dan kemudian event play() dapat dipanggil di playBtn.onclick = function() {};.
Perlu dicatat bahwa tombol replayBtn muncul dalam skenario kegagalan dan izin, jadi yang diperoleh adalah kumpulan semua .js-replay. Kemudian forEach mengulangi setiap tombol replayBtn, mengatur ulang level dan skor, dan memanggil acara play().
bindEvent: fungsi () { var self = ini; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); js-berikutnya'); var stopBtn = dokumen.querySelector('.js-stop'); Pengikatan tombol mulai permainan playBtn.onclick = function () { self.play() }; //Mulai ulang pengikatan tombol permainan replayBtn.forEach(function (e) { e.onclick = function () { self.opts.level = 1 ; self.play(); self.skor = 0; totalScoreText.innerText = diri.skor }; Pengikatan tombol permainan tingkat berikutnya nextBtn.onclick = function () { self.opts.level += 1; self.play() }; // Jeda permainan dan lanjutkan pengikatan tombol stopBtn.onclick = function () { self ('bermain'); mandiri.updateElement();3. Menghasilkan pesawat
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, lebar: opts.planeSize.width, tinggi: opts.planeSize.height, minX : this.planeMinX, kecepatan: opts.planeSpeed, maxX: this.planeMaxX, planeIcon: opts.planeIcon });}4. Hasilkan sekelompok monster
Karena monster muncul berkelompok, dan jumlah monster di setiap level juga berbeda, maka fungsi dari kedua loop for tersebut adalah untuk menghasilkan deretan monster, dan menambah deretan level monster sesuai dengan jumlah levelnya. Atau tingkatkan kecepatan monster (kecepatan: kecepatan + i,) untuk meningkatkan kesulitan setiap level, dll.
//Hasilkan musuh createEnemy: function (enemyType) { var opts = this.opts; var level = opts.level; .enemyGap; var size = opts.enemySize; var speed = opts.enemySpeed; //Tambahkan baris untuk setiap level level musuh for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //Parameter elemen komprehensif var initOpt = { x : padding + j * (ukuran + celah), y: padding + i * (ukuran + celah), ukuran: ukuran, kecepatan: kecepatan, status: tipe musuh, ikon musuh: opts.enemyIcon, musuhBoomIcon: opts.enemyBoomIcon }; musuh.push(Musuh baru(initOpt));5. Perbarui monster
Dapatkan nilai x dari array monster dan tentukan apakah mencapai batas kanvas. Jika mencapai batas, monster akan bergerak ke bawah. Pada saat yang sama, status monster juga harus dipantau, apakah monster dalam status normal telah terkena, monster dalam status ledakan, dan monster yang menghilang harus dikeluarkan dari array dan diberi skor pada saat yang bersamaan.
//Perbarui status musuh updateEnemeis: function() { var opts = this.opts; var plane = this.plane; var musuh = this.enemies; var musuhX = getHorizontalBoundary(musuh); if (musuhX.minX < ini.musuhMinX || musuhX.maxX >= ini.musuhMaxX) { console.log('enemiesX.minX', musuhX.minX); console.log('enemiesX.maxX', musuhX.maxX); opts.enemyDirection = opts.enemyDirection === 'kanan' : 'kanan '; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true; } //Perulangan perbarui musuh sementara (i--) { var musuh = musuh[i]; if (isFall) { musuh.turun(); } musuh.arah(opts.enemyDirection); saklar (musuh.status) { kasus 'normal': if (pesawat.hasHit(musuh)) { musuh .booming(); } istirahat; kasus 'booming': musuh.booming(); istirahat; kasus 'booming': musuh.splice(i, 1); bawaan: rusak;
Fungsi dari fungsi getHorizontalBoundary adalah menelusuri nilai x setiap elemen array, memfilter nilai yang lebih besar atau lebih kecil, dan mendapatkan nilai x maksimum dan minimum dari array.
//Dapatkan fungsi batas horizontal dari array getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { jika (barang.x < min) { min = barang.x; } if (barang.x > maks) { maks = barang.x; } } }); maks }}6. Perbarui panel keyboard
Tekan tombol Enter untuk menjalankan fungsi stop(), tekan tombol kiri untuk menggerakkan pesawat ke kiri, tekan tombol kanan untuk menggerakkan pesawat ke kanan, dan tekan tombol spasi untuk menjalankan pesawat yang menembakkan peluru untuk mencegah peluru dari menghubungkan dalam garis lurus, atur keyBoard di sini.
updatePanel: function() { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); arah('kiri'); } jika (keyBoard.pressedRight || keyBoard.heldRight) { pesawat.arah('kanan'); (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = salah; keyBoard.pressedSpace = salah;7. Gambar semua elemen
seri: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (musuh) {//console.log('draw:this.enemy',enemy); musuh. menggambar(); }); },8. Perbarui semua elemen
Pertama, tentukan apakah panjang array monster adalah 0. Jika 0 dan levelnya sama dengan totalLevel, berarti level tersebut dilewati. Jika tidak, layar persiapan game untuk level berikutnya akan ditampilkan jika koordinat y Jika susunan monster lebih besar dari koordinat y pesawat ditambah tinggi monster, maka permainan telah gagal.
Prinsip animasi kanvas adalah menggambar, memperbarui, dan membersihkan kanvas secara terus-menerus.
Prinsip jeda permainan adalah untuk mencegah fungsi requestAnimationFrame() dijalankan, tetapi tidak untuk mereset elemen. Oleh karena itu, ketika status status dinilai berhenti, fungsi tersebut akan dilompati.
//Perbarui status semua elemen updateElement: function () { var self = this; var opts = this.opts; var musuh = this.enemies; if (enemies.length === 0) { if (opts.level = == opts.totalLevel) { this.end('all-success'); } else { this.end('success' } kembali; - 1].y >= this.planePosY - opts.enemySize) { this.end('failed'); //Hapus kanvas ctx.clearRect(0, 0, canvasWidth, canvasHeight); kanvas ini .draw(); //Perbarui status elemen this.updatePanel(); if(self.status === 'stop'){ kembali; }else{ self.updateElement();tulis di akhir
Melalui langkah-langkah di atas, fungsi dasar permainan diselesaikan. Kontrol proses permainan lainnya, termasuk awal, akhir, penghitungan skor, dll., tidak akan dijelaskan di sini.
Apa yang bisa dioptimalkan: Saat menahan spasi, peluru bisa ditembakkan terus menerus. Namun, ketika saya menekan tombol arah lagi, saya menemukan bahwa saya tidak dapat lagi menembakkan peluru. Yang terbaik adalah bisa terus menembakkan peluru saat Anda bisa bergerak.
Cukup menarik untuk memainkan game dengan kanvas. Selain itu, game ini dapat diperluas dan diubah menjadi versi seluler. Ukuran kanvas ditentukan dengan memperoleh lebar dan tinggi layar , touchend). Penampilan monster juga dapat diubah. Ubah menjadi jatuh secara acak dari atas layar, dan monster tersebut akan meningkatkan kesehatannya (misalnya, akan menghilang setelah menembak 4 kali), dll.
Alamat unduhan: https://github.com/littleyljy/shoot
Di atas adalah keseluruhan isi artikel ini, saya harap dapat bermanfaat untuk pembelajaran semua orang. Saya juga berharap semua orang mendukung VeVb Wulin Network.