乐趣区

关于前端:用-Vue3Canvas-开发了个塔防小游戏感兴趣可以玩一玩

LegendTD

我的项目地址: http://codeape.site:16666

源码: https://github.com/ApeWhoLovesCode/LegendTD

根本介绍

开发技术: Vue3 + Canvas + Ts

这是一款反对 pc 端 挪动端 的网页塔防小游戏。

其余性能:

  • 抉择关卡
  • 抉择塔防
  • 排行榜

要是整个我的项目都放到这篇文章来解说的话会比较复杂,我这里简略复现几个小 demo 来作为展现。

具体看源码可能会更清晰。

实现技术分享

圆形滚动组件

这个组件我也是依据之前实现的 react 版本改写成 vue 版本的,并依据我的项目须要进行了一些欠缺。从我的项目中也能够看到我用在了好几个中央。

因为篇幅问题,具体能够看源码: https://github.com/ApeWhoLovesCode/LegendTD/tree/master/src/components/scrollCircle

感兴趣的话也能够看这篇文章:https://juejin.cn/post/7174959812084498488

悬浮球

这个也是依据之前的一个 react 的版本写成 vue 的版本的。而后这个组件我持续沿用了 jsx 的写法。就当练习一下 vue3 中的 jsx 写法了。

因为篇幅问题,具体能够看源码: https://github.com/ApeWhoLovesCode/LegendTD/tree/master/src/components/floating-ball

感兴趣的话也能够看这篇文章:https://juejin.cn/post/7186658143563644985

用 canvas 简略模仿一个塔防小游戏性能

jcode

requestAnimationFrame 绘制

借助 requestAnimationFrame 即可一直绘制,成果相似于 setInterval,不过不同点是前者大抵能达到每秒 60 帧的刷新,而且是稳固的(具体成果还是不同机型会不太雷同)。这里就不细说,具体能够看其余专门解说的文章。

const draw = () => {if(timer) cancelAnimationFrame(timer);
  (function go() {startDraw()
    timer = requestAnimationFrame(go)
  })()}
draw()
绘画主函数

每一次的绘画都须要先清空之前的画布内容

function startDraw() {ctx.clearRect(0, 0, w, h)
  drawTower()
  drawEnemy()
  moveEnemy()
  shootFun()
  moveBullet()}
定义变量

定义好敌人,塔防和子弹的存储对象和数组。

const enemy = {
  x: 50,
  y: 50,
  // xy 示意: 1: 左 2: 下 3: 右 4: 上
  xy: 3,
  // 速度
  speed: 2,
}
const tower = {
  x: 200,
  y: 200,
  // 子弹速度
  bulletSpeed: 8,
}
const bulletArr = []

用来管制敌人转弯的

const xyArr = [{x: 350, y: 350},
  {x: 50, y: 350},
  {x: 50, y: 50},
  {x: 350,y: 50},
]
绘制塔防和敌人

这里为了简便,间接绘画一个文字作为代表了。

function drawTower() {
  ctx.font = '50px 宋体'
  ctx.fillText('塔', tower.x, tower.y)
}
function drawEnemy() {
  ctx.font = '50px 宋体'
  ctx.fillText('敌', enemy.x, enemy.y)
}
使敌人挪动

这里就是每次触发都判断敌人以后的方向,对 xy 进行增减即可。

function moveEnemy() {const {speed, xy, x, y} = enemy
  for(let i = 0; i < xyArr.length; i++) {if(x >= xyArr[i].x && x <= xyArr[i].x + speed && y >= xyArr[i].y && y <= xyArr[i].y + speed) {if(i + 1 !== enemy.xy) {
        enemy.xy = i + 1
        break
      }
    }
  }
  switch (enemy.xy) {
    case 1: enemy.x -= speed; break;
    case 2: enemy.y -= speed; break;
    case 3: enemy.x += speed; break;
    case 4: enemy.y += speed; break;
  }
}

这时就能产生大抵如下的成果

发射子弹
  • 触发子弹射击的防抖函数
const shootFun = throttle(() => {shootBullet()
})
function throttle(fn) {
  let timer = null;
  return () => {if(timer) return
    timer = setTimeout(() => {fn()
      clearTimeout(timer)
      timer = null
    }, 500);
  }
}
  • 发射子弹的函数

依据敌人和塔防的核心,而后计算间隔,并得出接下来子弹 xy 应该减少和缩小的值即可。

function shootBullet() {
  const size = 50
  // 敌人核心
  const ex = enemy.x + size / 2, ey = enemy.y - size / 2
  // 塔防核心,也是子弹初始坐标
  const begin = {x: tower.x + size / 2, y: tower.y - size / 2}
  const diff = {x: ex - begin.x, y: ey - begin.y}
  // 子弹和敌人的间隔
  const distance = powAndSqrt(diff.x, diff.y)
  const addX = tower.bulletSpeed * diff.x / distance
  const addY = tower.bulletSpeed * diff.y / distance
  bulletArr.push({x: begin.x, y: begin.y, addX, addY, xy: 0, distance})
}
  • 挪动子弹

遍历子弹数组,如果子弹达到了该达到的间隔就革除该子弹,否则持续向前挪动。(想进一步欠缺的话,能够在遍历的时候,从新计算子弹 xy 应该挪动的值)

function moveBullet() {for(let i = bulletArr.length - 1; i >= 0; i--) {const {addX, addY, distance} = bulletArr[i]
    if(bulletArr[i].xy >= distance) {bulletArr.splice(i, 1)
    } else {bulletArr[i].x += addX
      bulletArr[i].y += addY
      bulletArr[i].xy += tower.bulletSpeed
      drawBullet(bulletArr[i])
    }
  }
}
  • 画子弹

简略画一个圆

function drawBullet(bullet) {ctx.save()
  ctx.beginPath()
  ctx.arc(bullet.x, bullet.y, 5, 0, 2 * Math.PI, false)
  ctx.fillStyle = 'skyblue'
  ctx.fill()
  ctx.restore()}

最初就实现了大抵如下成果了。

塔防局部子弹成果

缩放子弹 旋转子弹 继续变粗的火焰柱

具体代码放到码上掘金了,有须要能够自提

缩放子弹

jcode

旋转子弹

jcode

继续变粗的火焰柱

jcode

pc 端和挪动端的兼容解决

pinia 全局保留一个状态代表以后是 pc 端还是挪动端。

// 判断是挪动端还是 pc 端的办法
function isMobile() {return navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
}

我这里的 canvas 在 pc 端定义了一个 size50px 的一个基准。当来到了挪动端我这里是依据之前定义好的一个 canvas 宽高和手机的宽高来转化成一个我须要的 size 大小。

// canvas 的默认宽高 {w: 1050, h: 600}
const {w, h} = gameConfigState.defaultCanvas
const wp = document.documentElement.clientWidth / (h + 80)
const hp = document.documentElement.clientHeight / (w + 80)
const p = Math.floor(Math.min(wp, hp) * 10) / 10
// 将 50px 进行比例转化
gameConfigState.size *= p

再通过 style 将这个变量传递到 css 中即可应用了。

<template>
    <div class="game-wrap" :style="{'--size': size +'px'}"></div>
</template>
<style lang='less' scoped>
.game-wrap {@size: var(--size); // 这个就是 50px 的一个变量了
    .title {width: calc(@size * 0.5); // 应用
    }
}
</style>

挪动端下将游戏区域横屏解决,旋转 90 度 canvas 画板即可。

@media screen and (orientation: portrait) {
  .game-wrap {-webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }
}

我的项目领会和播种

因为 vue2 之前就把握了,而公司的技术栈是 react;始终没什么机会接触到 vue3 方面的技术,而后就想着做个 vue3 我的项目。而当初的这个我的项目也是依据之前写的 vue2 简陋版本,来进行改写成 vue3 的,同时也做了很多欠缺。

之前的简陋 vue2 版本:https://juejin.cn/post/7087764218673365023

整个我的项目下来,在游戏实现方面其实波及 vue3 的货色不多,次要是 js 的解决;次要是组件的封装和其余一些性能对 vue3 波及较多。顺带说一句我将之前的 js 改写成 ts 后感觉是真的香,之前 js 写起来和改起来都很麻烦。还有就是 vitepinia 应用起来是真的香。

总体来说,整个我的项目开发下来,对 vue3 的一些应用根本理解把握,canvas 的应用相熟了不少,js 基本功也晋升不少。

从 react 到 vue3 的应用体验。

这不是什么比照文章,这里就简略说下我在这个我的项目中的开发领会。

  1. vue3jsx 组件对 ts 的反对不太敌对。不论是 props 属性的类型定义,还是 emits 事件的定义。
  2. 还有就是 props 不能用解构写法,因为 vue 中不是每次 render 都能从新触发 props 的解构的,所以就失落了响应式。

不过也可能是我写得不熟吧。为此我也看了 element-plus的源码,它们也是采纳 jsx 写法,我改写起来感觉确实没 react 优雅。

不过 vue 中也有应用起来令我感觉比 react 难受的中央

比方就是 state 的批改,间接批改就完了,不必搞什么 setState,而后这个 state 扭转后,也不必搞什么 useEffect。这点在我我的项目中应用起来我感觉尤为要害。

阐明

第一次在思否这边发文,这是我在掘金写的一篇文章,有不分明的中央能够看原文

https://juejin.cn/post/7214517573584601144

退出移动版