用 Vue3+Canvas 开发了个塔防小游戏,感兴趣可以玩一玩

用 Vue3+Canvas 开发了个塔防小游戏,感兴趣可以玩一玩使用LOL和植物大战僵尸的部分ui和音频,来作为塔防和敌人。使用技术:Vue3 + Canvas + ts。

LegendTD

项目地址: game.codeape.site

github: github.com/ApeWhoLoves…

基本介绍

开发技术: Vue3 + Canvas + Ts

这是一款支持 pc端移动端 的网页塔防小游戏。

其他功能:

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

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

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

实现技术分享

圆形滚动组件

td-scrollCircle-4.gif

这个组件我也是根据之前实现的 react 版本改写成 vue 版本的,并根据项目需要进行了一些完善。从项目中也可以看到我用在了好几个地方。

由于篇幅问题,具体可以看源码: github.com/ApeWhoLoves…

感兴趣的话也可以看这篇文章:用 Vue3+Canvas 开发了个塔防小游戏,感兴趣可以玩一玩

悬浮球

td-floating-ball-2.gif

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

由于篇幅问题,具体可以看源码: github.com/ApeWhoLoves…

感兴趣的话也可以看这篇文章:用 Vue3+Canvas 开发了个塔防小游戏,感兴趣可以玩一玩

用 canvas 简单模拟一个塔防小游戏功能

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;
  }
}

这时就能产生大致如下的效果

Kapture 2023-03-17 at 15.23.31.gif

发射子弹

  • 触发子弹射击的防抖函数
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()
}

最后就实现了大致如下效果了。

Kapture 2023-03-17 at 15.25.15.gif

塔防部分子弹效果

缩放子弹 旋转子弹 持续变粗的火焰柱
td-canvas-scale.gif td-canvas-rotate.gif td-canvas-fire2.gif

具体代码放到码上掘金了,有需要可以自提

缩放子弹

旋转子弹

持续变粗的火焰柱

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 版本:用 Vue3+Canvas 开发了个塔防小游戏,感兴趣可以玩一玩

整个项目下来,在游戏实现方面其实涉及 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://daima100.com/37176.html

(0)

相关推荐

  • Oracle Loop 循环的一个赋值坑

    Oracle Loop 循环的一个赋值坑PROCEDURE test IS comp_return_rec_ m_purchase_comp_return_tab%Rowtype; barcode_hist_rec_ m_barcode_…

    2022-12-20
    128
  • 使用gensim库进行自然语言处理

    使用gensim库进行自然语言处理随着自然语言处理的发展,gensim库正变得越来越受欢迎。它是一种用于处理大型文本语料库的Python库,可用于实现各种自然语言处理任务,例如主题建模和相似性分析。

    2024-07-26
    11
  • 云 会议_年终安全会议

    云 会议_年终安全会议说起云会议大家第一想起的就是疫情期间,上网课也好,网上办公也好等等。在疫情到来之前线上会议就已经有了雏形但是并不完善,但是疫情开始之后,线上会议蓬勃发展,各种云会议应用而生。云会议得到了空前的发展,那

    2023-05-12
    140
  • Python import路径简介

    Python import路径简介Python是一种高级动态语言,有着广泛的应用。如果你是一个Python编程爱好者或者正准备学习这门语言,那么本文将会介绍Python import路径的相关知识。在Python中,import语句非常重要。它让你从一个文件中引入到另一个文件,或者从一个模块中引入到另一个模块。但是,当你使用import语句时,如果路径不正确,那么程序就会出错。本文将会介绍如何正确地使用Python的import路径。

    2024-05-30
    82
  • 程序媛眼中的 PingCAP:无法抗拒的五大吸引力 | PingCAP 招聘季「建议收藏」

    程序媛眼中的 PingCAP:无法抗拒的五大吸引力 | PingCAP 招聘季「建议收藏」今早开电脑,看到老板深夜的留言瑟瑟发抖…… 老板:大妹子啊,最近忙不? (俺是左思右想搔头摸耳揣摩老板啥意思,想来老板是耿直 Boy, 这次居然没有直接说要干啥,自行脑补了 N 多场景,最后…

    2023-02-22
    130
  • mysql – 查看数据库

    mysql – 查看数据库
    在 MySQL 中,可使用 SHOW DATABASES 语句来查看或显示当前用户权限范围以内的数据库。查看数据库的语法格式为: SHOW DATABASE…

    2023-04-07
    136
  • redis笔记01[通俗易懂]

    redis笔记01[通俗易懂]起步 redis简介 redis是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. redis与其他key-value缓存产品有一下三个特点: redis支持数据的持久化,可…

    2023-02-06
    137
  • Redis入门(6) – Lua脚本

    Redis入门(6) – Lua脚本Lua基本语法 表类型 函数 Redis执行脚本 KEYS与ARGV 沙盒与随机数 脚本相关命令 原子性和执行时间 Lua是一种高效的轻量级脚本语言,能够方便地嵌入到其他语言中使用。在Redis中,借

    2023-03-06
    136

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注