<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
記得幾年前剛做前端開發的時候,跟著師傅用純 es5 實現了這款坦克大戰,可以說我入行前端是從 javaScript 小遊戲開始的,時間已匆匆過去了數年,前端發展日新月異,各種新框架、新概念層出不窮,很容易就迷失在對各種新技術的盲目學習和應用中,真正的程式設計是什麼呢?值得思考的問題。
我準備用 vue3 重新實現一下這款遊戲,順便回顧和梳理下自己的知識體系。
W/上 S/下 A/左 D/右 F/射擊
讓我們開始吧!
專案技術選型為 vue3、vite、less、pnpm、ts,按照vue3 官網檔案來新建專案,注意:雖然我用了 vue3 實際上只是強行嚐鮮,主體內容都是 js 用到的框架特性有限。
$ pnpm create vite <project-name> -- --template vue $ cd <project-name> $ pnpm install $ pnpm add -D less $ pnpm dev
遊戲的核心為 canvas 畫布和坦克元素,我們定義兩個建構函式
canvas 建構函式的定義引數、方法:dom、dimension 尺寸、renderTo 渲染函數、drawText 文字繪製函數、drawImageSlice 圖片繪製函數
canvas 圖層按照一般的遊戲設計優化理念,需要為靜態背景和動態元素單獨用不同的 canvas 圖層表示,每次更新時只需要重新繪製動態元素就好了,我抽象出一個渲染函數
// 渲染 this.renderTo = function renderTo(container_id) { if (!is_rendered) { let container = document.getElementById(container_id) //畫布起始座標 dom = document.createElement('canvas') // 創造canvas畫布 dom.setAttribute('class', 'canvas') ctx = dom.getContext('2d') dom.setAttribute('width', container.clientWidth) dom.setAttribute('height', container.clientHeight) // 畫布尺寸 dimension = { x: container.clientWidth, y: container.clientHeight, } container.insertBefore(dom, container.firstChild) // 插入cantainer容器 } }
想要知道畫布中的具體位置座標,可以定義一個函數,當滑鼠滑動時候執行來將當前位置座標繪製出來
this.drawText = function drawText(text, offset_left, offset_top, font) { ctx.font = font || '25px Calibri' ctx.fillStyle = '#fff' ctx.fillText(text, offset_left, offset_top) }
每次重繪前需要先擦掉整個畫布
this.clear = function clear() { ctx.clearRect(0, 0, dimension.x, dimension.y) }
坦克、子彈、建築等元素等繪製都是通過這個函數來完成的,實現遠離是利用來雪碧圖,通過座標抓取特定位置的圖片元素來獲取各種不同坦克等元素的UI;
通過 rotate 旋轉元素來實現坦克的轉向;
this.drawImageSlice = function drawImage(img_ele, sx, sy, sWidth, sHeight, x, y, rotatation) { ctx.save() ctx.translate((2 * x + sWidth) / 2, (2 * y + sHeight) / 2) // 改變起始點座標 ctx.rotate((Math.PI / 180) * rotatation) // 旋轉 x = x || 0 y = y || 0 ctx.drawImage(img_ele, sx, sy, sWidth, sHeight, -sWidth / 2, -sHeight / 2, sWidth, sHeight) ctx.restore() // 復原 }
BattleCity 建構函式定義坦克的各種設定資訊,和方法函數
let TankConfig = function (cfg) { this.explosion_count = cfg.explosion_count this.width = cfg.type.dimension[0] this.height = cfg.type.dimension[1] this.missle_type = cfg.missle_type || MISSILE_TYPE.NORMAL this.x = cfg.x || 0 this.y = cfg.y || 0 this.direction = cfg.direction || DIRECTION.UP this.is_player = cfg.is_player || 0 this.moving = cfg.moving || 0 this.alive = cfg.alive || 1 this.border_x = cfg.border_x || 0 this.border_y = cfg.border_y || 0 this.speed = cfg.speed || TANK_SPEED this.direction = cfg.direction || DIRECTION.UP this.type = cfg.type || TANK_TYPE.PLAYER0 }
用鍵盤的 W、S、A、D、來表示上下左右方向鍵,按下鍵盤則會觸發對應坦克範例的 move 函數,用於計算移動後的位置座標資訊,注意:對邊界條件的判斷,不可使其超出戰場邊界。
CanvasSprite.prototype.move = function (d, obstacle_sprites) { this.direction = d switch (d) { case DIRECTION.UP: if ((obstacle_sprites && !this.checkRangeOverlap(obstacle_sprites)) || !obstacle_sprites) { this.y -= this.speed if (this.y <= 5) { if (!this.out_of_border_die) { this.y = 0 } else { // this.alive = 0; this.explode() document.getElementById('steelhit').play() } } } break case DIRECTION.DOWN: if ((obstacle_sprites && !this.checkRangeOverlap(obstacle_sprites)) || !obstacle_sprites) { this.y += this.speed if (this.y + this.height >= this.border_y - 10) { if (!this.out_of_border_die) { this.y = this.border_y - this.height } else { // this.alive = 0; this.explode() document.getElementById('steelhit').play() } } } break case DIRECTION.LEFT: if ((obstacle_sprites && !this.checkRangeOverlap(obstacle_sprites)) || !obstacle_sprites) { this.x -= this.speed if (this.x <= 5) { if (!this.out_of_border_die) { this.x = 0 } else { // this.alive = 0; this.explode() document.getElementById('steelhit').play() } } } break case DIRECTION.RIGHT: if ((obstacle_sprites && !this.checkRangeOverlap(obstacle_sprites)) || !obstacle_sprites) { this.x += this.speed if (this.x + this.width >= this.border_x - 10) { if (!this.out_of_border_die) { this.x = this.border_x - this.width } else { // this.alive = 0; this.explode() document.getElementById('steelhit').play() } } } break } }
首先需要定義子彈的設定資訊以及建構函式;
let MissileConfig = function (cfg) { this.x = cfg.x this.y = cfg.y this.type = cfg.type || MISSILE_TYPE.NORMAL this.width = cfg.width || this.type.dimension[0] this.height = cfg.height || this.type.dimension[1] this.direction = cfg.direction || DIRECTION.UP this.is_from_player = cfg.is_from_player this.out_of_border_die = cfg.out_of_border_die || 1 // 判斷邊界型別 this.border_x = cfg.border_x || 0 this.border_y = cfg.border_y || 0 this.speed = cfg.speed || TANK_SPEED this.alive = cfg.alive || 1 } var Missile = function (MissileConfig) { var x = MissileConfig.x var y = MissileConfig.y var width = MissileConfig.width var height = MissileConfig.width var direction = MissileConfig.direction this.type = MissileConfig.type this.is_from_player = MissileConfig.is_from_player || 0 var explosion_count = 0 CanvasSprite.apply(this, [ { alive: 1, out_of_border_die: 1, border_y: HEIGHT, border_x: WIDTH, speed: MISSILE_SPEED, direction: direction, x: x, y: y, width: width, height: height, }, ]) this.isDestroied = function () { return explosion_count > 0 } this.explode = function () { if (explosion_count++ === 5) { this.alive = 0 } } this.getImg = function () { if (explosion_count > 0) { return { width: TANK_EXPLOSION_FRAME[explosion_count].dimension[0], height: TANK_EXPLOSION_FRAME[explosion_count].dimension[1], offset_x: TANK_EXPLOSION_FRAME[explosion_count].image_coordinates[0], offset_y: TANK_EXPLOSION_FRAME[explosion_count].image_coordinates[1], } } else { return { width: width, height: height, offset_x: this.type.image_coordinates[0], offset_y: this.type.image_coordinates[1], } } } this.getHeadCoordinates = function () { var h_x, h_y switch (this.direction) { case DIRECTION.UP: h_x = this.x + this.width / 2 - this.type.dimension[0] / 2 h_y = this.y - this.type.dimension[1] / 2 break case DIRECTION.DOWN: h_x = this.x + this.width / 2 - this.type.dimension[0] / 2 h_y = this.y + this.height - this.type.dimension[1] / 2 break case DIRECTION.LEFT: h_x = this.x h_y = this.y + this.width / 2 - this.type.dimension[0] / 2 break case DIRECTION.RIGHT: h_x = this.x + this.height h_y = this.y + this.width / 2 - this.type.dimension[0] / 2 } console.log({ x: h_x, y: h_y, }) return { x: h_x, y: h_y, } } this._generateId = function () { return uuidv4() } sprites[this._generateId()] = this }
然後再定義一個 fire 開發函數,當開火後,會使用 window.requestAnimationFrame() 來達到迴圈的效果,每次重繪最新的位置資訊
this.fire = function (boolean_type) { if (!this.missle || !this.missle.alive) { var coor = this.getCannonCoordinates() this.missle = new Missile( new MissileConfig({ x: coor.x, y: coor.y, direction: this.direction, type: this.miss_type, is_from_player: boolean_type, }) ) if (boolean_type) { document.getElementById('shoot').play() } } }
利用 requestAnimationFrame 來實現迴圈重新整理畫布,通過修改各元素位置座標值,在下一次畫布重繪時更新檢視,這是階段互動的基本邏輯;
到這裡已經實現了坦克移動和發射子彈的效果。
以上就是Vue3+Canvas實現坦克大戰遊戲的詳細內容,更多關於Vue3 Canvas坦克大戰的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45