使用canvas怎么實(shí)現(xiàn)一個(gè)飛機(jī)打怪獸射擊小游戲?相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
成都創(chuàng)新互聯(lián)公司主要從事網(wǎng)站設(shè)計(jì)、網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)祁東,十年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):18980820575
游戲規(guī)則
要求玩家控制飛機(jī)發(fā)射子彈,消滅會(huì)移動(dòng)的怪獸,如果全部消滅了則游戲成功,如果怪獸移動(dòng)到底部則游戲失敗。
使用 ← 和 → 操作飛機(jī)
使用空格(space)進(jìn)行射擊
需有暫停功能
多關(guān)卡
場(chǎng)景切換
游戲分為幾個(gè)場(chǎng)景:
開始游戲(.game-intro)
游戲中(#canvas)
游戲失敗(.game-failed)
游戲成功(.game-success)
游戲通關(guān)(.game-all-success)
暫停(.game-stop)
實(shí)現(xiàn)場(chǎng)景切換,其實(shí)是先把所有場(chǎng)景 display: none , 然后通過 js 控制 data-status 分別為 start 、playing 、failed 、success 、all-success 、stop 來實(shí)現(xiàn)對(duì)應(yīng)場(chǎng)景 display: block 。
HTML 和 CSS 如下:
射擊游戲
這是一個(gè)令人欲罷不能的射擊游戲,使用 ← 和 → 操作你的飛機(jī),使用空格(space)進(jìn)行射擊,使用回車(enter)暫停游戲。一起來消滅宇宙怪獸吧!
當(dāng)前Level: 1
游戲結(jié)束
最終得分:
游戲成功
通關(guān)成功
你已經(jīng)成功地防御了怪獸的所有攻擊。
游戲暫停
分?jǐn)?shù):
#game{ width: 700px; height: 600px; position: relative; left: 50%; top: 40px; margin: 0 0 0 -350px; background: linear-gradient(-180deg, #040024 0%, #07165C 97%); } .game-ui{ display: none; padding: 55px; box-sizing: border-box; height: 100%; } [data-status="start"] .game-intro { display: block; padding-top: 180px; background: url(./img/bg.png) no-repeat 430px 180px; background-size: 200px; } [data-status="playing"] .game-info { display: block; position: absolute; top:0; left:0; padding:20px; } [data-status="failed"] .game-failed, [data-status="success"] .game-success, [data-status="all-success"] .game-all-success, [data-status="stop"] .game-stop{ display: block; padding-top: 180px; background: url(./img/bg-end.png) no-repeat 380px 190px; background-size: 250px; }
面向?qū)ο?/strong>
整個(gè)游戲可以把怪獸(Enemy)、飛機(jī)(Plane)、子彈(Bullet)都當(dāng)作對(duì)象,另外還有配置對(duì)象(CONFIG)和控制游戲邏輯的游戲?qū)ο螅℅AME)。
游戲相關(guān)配置
/** * 游戲相關(guān)配置 * @type {Object} */ var CONFIG = { status: 'start', // 游戲開始默認(rèn)為開始中 level: 1, // 游戲默認(rèn)等級(jí) totalLevel: 6, // 總共6關(guān) numPerLine: 7, // 游戲默認(rèn)每行多少個(gè)怪獸 canvasPadding: 30, // 默認(rèn)畫布的間隔 bulletSize: 10, // 默認(rèn)子彈長(zhǎng)度 bulletSpeed: 10, // 默認(rèn)子彈的移動(dòng)速度 enemySpeed: 2, // 默認(rèn)敵人移動(dòng)距離 enemySize: 50, // 默認(rèn)敵人的尺寸 enemyGap: 10, // 默認(rèn)敵人之間的間距 enemyIcon: './img/enemy.png', // 怪獸的圖像 enemyBoomIcon: './img/boom.png', // 怪獸死亡的圖像 enemyDirection: 'right', // 默認(rèn)敵人一開始往右移動(dòng) planeSpeed: 5, // 默認(rèn)飛機(jī)每一步移動(dòng)的距離 planeSize: { width: 60, height: 100 }, // 默認(rèn)飛機(jī)的尺寸, planeIcon: './img/plane.png' };
定義父類
因?yàn)楣肢F(Enemy)、飛機(jī)(Plane)、子彈(Bullet)都有相同的 x, y, size, speed 屬性和 move() 方法,所以可以定義一個(gè)父類 Element,通過子類繼承父類的方式實(shí)現(xiàn)。
/*父類:包含x y speed move() draw()*/ var Element = function (opts) { this.opts = opts || {}; //設(shè)置坐標(biāo)、尺寸、速度 this.x = opts.x; this.y = opts.y; this.size = opts.size; this.speed = opts.speed; }; Element.prototype.move = function (x, y) { var addX = x || 0; var addY = y || 0; this.x += addX; this.y += addY; }; //繼承原型的函數(shù) function inheritPrototype(subType, superType) { var proto = Object.create(superType.prototype); proto.constructor = subType; subType.prototype = proto; }
move(x, y) 方法根據(jù)傳入的 (x, y) 值自疊加。
定義怪獸
怪獸包含特有屬性:怪獸狀態(tài)、圖像、控制爆炸狀態(tài)持續(xù)的 boomCount ,和 draw()、down()、direction()、booming() 方法。
/*敵人*/ var Enemy = function (opts) { this.opts = opts || {}; //調(diào)用父類屬性 Element.call(this, opts); //特有屬性狀態(tài)和圖像 this.status = 'normal';//normal、booming、noomed this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon; this.boomCount = 0; }; //繼承Element方法 inheritPrototype(Enemy, Element); //方法:繪制敵人 Enemy.prototype.draw = function () { if (this.enemyIcon && this.enemyBoomIcon) { switch (this.status) { case 'normal': var enemyIcon = new Image(); enemyIcon.src = this.enemyIcon; ctx.drawImage(enemyIcon, this.x, this.y, this.size, this.size); break; case 'booming': var enemyBoomIcon = new Image(); enemyBoomIcon.src = this.enemyBoomIcon; ctx.drawImage(enemyBoomIcon, this.x, this.y, this.size, this.size); break; case 'boomed': ctx.clearRect(this.x, this.y, this.size, this.size); break; default: break; } } return this; }; //方法:down 向下移動(dòng) Enemy.prototype.down = function () { this.move(0, this.size); return this; }; //方法:左右移動(dòng) Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0); } else { this.move(-this.speed, 0); } return this; }; //方法:敵人爆炸 Enemy.prototype.booming = function () { this.status = 'booming'; this.boomCount += 1; if (this.boomCount > 4) { this.status = 'boomed'; } return this; }
draw() 主要是根據(jù)怪獸的狀態(tài)繪制不同的圖像。
down() 調(diào)用父類 move() 方法,傳入 y 值控制怪獸向下移動(dòng)。
direction() 根據(jù)傳入的方向值控制左/右移動(dòng)。
booming() 讓爆炸狀態(tài)持續(xù)4幀,4幀后再消失。
定義子彈
子彈有 fly() 、draw() 方法。
/*子彈*/ var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts); }; inheritPrototype(Bullet, Element); //方法:讓子彈飛 Bullet.prototype.fly = function () { this.move(0, -this.speed); return this; }; //方法:繪制子彈 Bullet.prototype.draw = function () { ctx.beginPath(); ctx.strokeStyle = '#fff'; ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y - CONFIG.bulletSize); ctx.closePath(); ctx.stroke(); return this; };
fly() 調(diào)用父類 move() 方法,傳入 y 值控制子彈向上移動(dòng)。
draw() 因?yàn)樽訌椘鋵?shí)就是一條長(zhǎng)度為 10 的直線,通過繪制路徑的方式畫出子彈。
定義飛機(jī)
飛機(jī)對(duì)象包含特有屬性:狀態(tài)、寬高、圖像、橫坐標(biāo)最大最小值,有 hasHit()、draw()、direction()、shoot()、drawBullets() 方法。
/*飛機(jī)*/ var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //特有屬性狀態(tài)和圖像 this.status = 'normal'; this.width = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; this.minX = opts.minX; this.maxX = opts.maxX; //子彈相關(guān) this.bullets = []; this.bulletSpeed = opts.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || CONFIG.bulletSize; }; //繼承Element方法 inheritPrototype(Plane, Element); //方法:子彈擊中目標(biāo) Plane.prototype.hasHit = function (enemy) { var bullets = this.bullets; for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; var isHitPosX = (enemy.x < bullet.x) && (bullet.x < (enemy.x + enemy.size)); var isHitPosY = (enemy.y < bullet.y) && (bullet.y < (enemy.y + enemy.size)); if (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } } return false; }; //方法:繪制飛機(jī) Plane.prototype.draw = function () { this.drawBullets(); var planeIcon = new Image(); planeIcon.src = this.planeIcon; ctx.drawImage(planeIcon, this.x, this.y, this.width, this.height); return this; }; //方法:飛機(jī)方向 Plane.prototype.direction = function (direction) { var speed = this.speed; var planeSpeed; if (direction === 'left') { planeSpeed = this.x < this.minX ? 0 : -speed; } else { planeSpeed = this.x > this.maxX ? 0 : speed; } console.log('planeSpeed:', planeSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX); console.log('this.maxX:', this.maxX); this.move(planeSpeed, 0); return this;//方便鏈?zhǔn)秸{(diào)用 }; //方法:發(fā)射子彈 Plane.prototype.shoot = function () { var bulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x: bulletPosX, y: this.y, size: this.bulletSize, speed: this.bulletSpeed })); return this; }; //方法:繪制子彈 Plane.prototype.drawBullets = function () { var bullets = this.bullets; var i = bullets.length; while (i--) { var bullet = bullets[i]; bullet.fly(); if (bullet.y <= 0) { bullets.splice(i, 1); } bullet.draw(); } };
hasHit() 判斷飛機(jī)發(fā)射的子彈是否擊中怪獸,主要是判斷子彈的橫坐標(biāo)是否在[怪獸橫坐標(biāo),怪獸橫坐標(biāo)+怪獸高度]范圍內(nèi),同時(shí)子彈的縱坐標(biāo)在[怪獸縱坐標(biāo),怪獸縱坐標(biāo)+怪獸寬度]范圍內(nèi),擊中返回 true,并移除該子彈。
draw() 繪制子彈和飛機(jī)。
direction() 因?yàn)轱w機(jī)移動(dòng)范圍有左右邊界,需要判斷飛機(jī)橫坐標(biāo)是否到達(dá)邊界,如果到達(dá)邊界 planeSpeed 為 0,不再移動(dòng)。
shoot() 創(chuàng)建子彈對(duì)象,保存到 bullets 數(shù)組,子彈橫坐標(biāo)為飛機(jī)橫坐標(biāo)加上飛機(jī)寬度的一半。
drawBullets() 繪制子彈,從數(shù)組最后往回遍歷子彈對(duì)象數(shù)組,調(diào)用子彈 fly() 方法,如果子彈向上飛出屏幕,則移除這顆子彈。
定義鍵盤事件
鍵盤事件有以下幾種狀態(tài):
keydown:用戶在鍵盤上按下某按鍵時(shí)發(fā)生。一直按著某按鍵則會(huì)不斷觸發(fā)(opera 瀏覽器除外)。
keypress:用戶按下一個(gè)按鍵,并產(chǎn)生一個(gè)字符時(shí)發(fā)生(也就是不管類似 shift、alt、ctrl 之類的鍵,就是說用戶按了一個(gè)能在屏幕上輸出字符的按鍵 keypress 事件才會(huì)觸發(fā))。一直按著某按鍵則會(huì)不斷觸發(fā)。
keyup:用戶釋放某一個(gè)按鍵是觸發(fā)。
因?yàn)轱w機(jī)需要按下左鍵(keyCode=37)右鍵(keyCode=39)時(shí)(keydown)一直移動(dòng),釋放時(shí) keyup 不移動(dòng)。按下空格(keyCode=32)或上方向鍵(keyCode=38)時(shí)(keydown)發(fā)射子彈,釋放時(shí) keyup 停止發(fā)射。另外按下回車鍵(keyCode=13)暫停游戲。所以,需要定義一個(gè) KeyBoard 對(duì)象監(jiān)聽 onkeydown 和 onkeyup 是否按下或釋放某個(gè)鍵。
因?yàn)樽笥益I是矛盾的,為保險(xiǎn)起見,按下左鍵時(shí)需要把右鍵 設(shè)為 false。右鍵同理。
//鍵盤事件 var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this); }; //KeyBoard對(duì)象 KeyBoard.prototype = { pressedLeft: false, pressedRight: false, pressedUp: false, heldLeft: false, heldRight: false, pressedSpace: false, pressedEnter: false, keydown: function (e) { var key = e.keyCode; switch (key) { case 32://空格-發(fā)射子彈 this.pressedSpace = true; break; case 37://左方向鍵 this.pressedLeft = true; this.heldLeft = true; this.pressedRight = false; this.heldRight = false; break; case 38://上方向鍵-發(fā)射子彈 this.pressedUp = true; break; case 39://右方向鍵 this.pressedLeft = false; this.heldLeft = false; this.pressedRight = true; this.heldRight = true; break; case 13://回車鍵-暫停游戲 this.pressedEnter = true; break; } }, keyup: function (e) { var key = e.keyCode; switch (key) { case 32: this.pressedSpace = false; break; case 37: this.heldLeft = false; this.pressedLeft = false; break; case 38: this.pressedUp = false; break; case 39: this.heldRight = false; this.pressedRight = false; break; case 13: this.pressedEnter = false; break; } } };
游戲邏輯
游戲?qū)ο螅℅AME)包含了整個(gè)游戲的邏輯,包括init(初始化)、bindEvent(綁定按鈕)、setStatus(更新游戲狀態(tài))、play(游戲中)、stop(暫停)、end(結(jié)束)等,在此不展開描述。也包含了生成怪獸、繪制游戲元素等函數(shù)。
// 整個(gè)游戲?qū)ο? var GAME = { //一系列邏輯函數(shù) //游戲元素函數(shù) }
1、初始化
初始化函數(shù)主要是定義飛機(jī)初始坐標(biāo)、飛機(jī)移動(dòng)范圍、怪獸移動(dòng)范圍,以及初始化分?jǐn)?shù)、怪獸數(shù)組,創(chuàng)建 KeyBoard 對(duì)象,只執(zhí)行一次。
/** * 初始化函數(shù),這個(gè)函數(shù)只執(zhí)行一次 * @param {object} opts * @return {[type]} [description] */ init: function (opts) { //設(shè)置opts var opts = Object.assign({}, opts, CONFIG);//合并所有參數(shù) this.opts = opts; this.status = 'start'; //計(jì)算飛機(jī)對(duì)象初始坐標(biāo) this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; //飛機(jī)極限坐標(biāo) this.planeMinX = opts.canvasPadding; this.planeMaxX = canvasWidth - opts.canvasPadding - opts.planeSize.width; //計(jì)算敵人移動(dòng)區(qū)域 this.enemyMinX = opts.canvasPadding; this.enemyMaxX = canvasWidth - opts.canvasPadding - opts.enemySize; //分?jǐn)?shù)設(shè)置為0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); this.bindEvent(); this.renderLevel(); },
2、綁定按鈕事件
因?yàn)閹讉€(gè)游戲場(chǎng)景中包含開始游戲(playBtn)、重新開始(replayBtn)、下一關(guān)游戲(nextBtn)、暫停游戲繼續(xù)(stopBtn)幾個(gè)按鈕。我們需要給不同按鈕執(zhí)行不同事件。
首先定義 var self = this; 的原因是 this 的用法。在 bindEvent 函數(shù)中, this 指向 GAME 對(duì)象,而在 playBtn.onclick = function () {}; 中 this 指向了 playBtn ,這顯然不是我們希望的,因?yàn)?playBtn 沒有 play() 事件,GAME 對(duì)象中才有。因此需要把GAME 對(duì)象賦值給一個(gè)變量 self ,然后才能在 playBtn.onclick = function () {}; 中調(diào)用 play() 事件。
需要注意的是 replayBtn 按鈕在闖關(guān)失敗和通關(guān)場(chǎng)景都有出現(xiàn),因此獲取的是所有 .js-replay 的集合。然后 forEach 遍歷每個(gè) replayBtn 按鈕,重置關(guān)卡和分?jǐn)?shù),調(diào)用 play() 事件。
bindEvent: function () { var self = this; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelector('.js-next'); var stopBtn = document.querySelector('.js-stop'); // 開始游戲按鈕綁定 playBtn.onclick = function () { self.play(); }; //重新開始游戲按鈕綁定 replayBtn.forEach(function (e) { e.onclick = function () { self.opts.level = 1; self.play(); self.score = 0; totalScoreText.innerText = self.score; }; }); // 下一關(guān)游戲按鈕綁定 nextBtn.onclick = function () { self.opts.level += 1; self.play(); }; // 暫停游戲繼續(xù)按鈕綁定 stopBtn.onclick = function () { self.setStatus('playing'); self.updateElement(); }; },
3、生成飛機(jī)
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, width: opts.planeSize.width, height: opts.planeSize.height, minX: this.planeMinX, speed: opts.planeSpeed, maxX: this.planeMaxX, planeIcon: opts.planeIcon }); }
4、生成一組怪獸
因?yàn)楣肢F都是成組出現(xiàn)的,每一關(guān)的怪獸數(shù)量也不同,兩個(gè) for 循環(huán)的作用就是生成一行怪獸,根據(jù)關(guān)數(shù)(level)增加 level 行怪獸?;蛘咴黾庸肢F的速度(speed: speed + i,)來提高每一關(guān)難度等。
//生成敵人 createEnemy: function (enemyType) { var opts = this.opts; var level = opts.level; var enemies = this.enemies; var numPerLine = opts.numPerLine; var padding = opts.canvasPadding; var gap = opts.enemyGap; var size = opts.enemySize; var speed = opts.enemySpeed; //每升級(jí)一關(guān)敵人增加一行 for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //綜合元素的參數(shù) var initOpt = { x: padding + j * (size + gap), y: padding + i * (size + gap), size: size, speed: speed, status: enemyType, enemyIcon: opts.enemyIcon, enemyBoomIcon: opts.enemyBoomIcon }; enemies.push(new Enemy(initOpt)); } } return enemies; },
5、更新怪獸
獲取怪獸數(shù)組的 x 值,判斷是否到達(dá)畫布邊界,如果到達(dá)邊界則怪獸向下移動(dòng)。同時(shí)也要監(jiān)聽怪獸狀態(tài),正常狀態(tài)下的怪獸是否被擊中,爆炸狀態(tài)下的怪獸,消失的怪獸要從數(shù)組剔除,同時(shí)得分。
//更新敵人狀態(tài) updateEnemeis: function () { var opts = this.opts; var plane = this.plane; var enemies = this.enemies; var i = enemies.length; var isFall = false;//敵人下落 var enemiesX = getHorizontalBoundary(enemies); if (enemiesX.minX < this.enemyMinX || enemiesX.maxX >= this.enemyMaxX) { console.log('enemiesX.minX', enemiesX.minX); console.log('enemiesX.maxX', enemiesX.maxX); opts.enemyDirection = opts.enemyDirection === 'right' ? 'left' : 'right'; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true; } //循環(huán)更新敵人 while (i--) { var enemy = enemies[i]; if (isFall) { enemy.down(); } enemy.direction(opts.enemyDirection); switch (enemy.status) { case 'normal': if (plane.hasHit(enemy)) { enemy.booming(); } break; case 'booming': enemy.booming(); break; case 'boomed': enemies.splice(i, 1); this.score += 1; break; default: break; } } },
getHorizontalBoundary 函數(shù)的作用是遍歷數(shù)組每個(gè)元素的 x 值,篩選出更大或更小的值,從而獲得數(shù)組最大和最小的 x 值。
//獲取數(shù)組橫向邊界 function getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x; } } }); return { minX: min, maxX: max } }
6、更新鍵盤面板
按下回車鍵執(zhí)行 stop() 函數(shù),按下左鍵執(zhí)行飛機(jī)左移,按下右鍵執(zhí)行飛機(jī)右移,按下空格執(zhí)行飛機(jī)發(fā)射子彈,為了不讓子彈連成一條直線,在這里設(shè)置 keyBoard.pressedUp 和 keyBoard.pressedSpace 為 false。
updatePanel: function () { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); return; } if (keyBoard.pressedLeft || keyBoard.heldLeft) { plane.direction('left'); } if (keyBoard.pressedRight || keyBoard.heldRight) { plane.direction('right'); } if (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = false; keyBoard.pressedSpace = false; plane.shoot(); } },
7、繪制所有元素
draw: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemy) { //console.log('draw:this.enemy',enemy); enemy.draw(); }); },
8、更新所有元素
首先判斷怪獸數(shù)組長(zhǎng)度是否為 0 ,為 0 且 level 等于 totalLevel 說明通關(guān),否則顯示下一關(guān)游戲準(zhǔn)備畫面;如果怪獸數(shù)組 y 坐標(biāo)大于飛機(jī) y 坐標(biāo)加怪獸高度,顯示游戲失敗。
canvas 動(dòng)畫的原理就是不斷繪制、更新、清除畫布。
游戲暫停的原理就是阻止 requestAnimationFrame() 函數(shù)執(zhí)行,但不重置元素。因此判斷 status 的狀態(tài)為 stop 時(shí)跳出函數(shù)。
//更新所有元素狀態(tài) updateElement: function () { var self = this; var opts = this.opts; var enemies = this.enemies; if (enemies.length === 0) { if (opts.level === opts.totalLevel) { this.end('all-success'); } else { this.end('success'); } return; } if (enemies[enemies.length - 1].y >= this.planePosY - opts.enemySize) { this.end('failed'); return; } //清理畫布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); //繪制畫布 this.draw(); //更新元素狀態(tài) this.updatePanel(); this.updateEnemeis(); //不斷循環(huán)updateElement requestAnimationFrame(function () { if(self.status === 'stop'){ return; }else{ self.updateElement(); } }); }
看完上述內(nèi)容,你們掌握使用canvas怎么實(shí)現(xiàn)一個(gè)飛機(jī)打怪獸射擊小游戲的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!