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 简单模拟一个塔防小游戏功能
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)
}
使敌人移动
这里就是每次触发都判断敌人当前的方向,对 x
或 y
进行增减即可。
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);
}
}
- 发射子弹的函数
根据敌人和塔防的中心,然后计算距离,并得出接下来子弹 x
和 y
应该增加和减少的值即可。
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
})
}
- 移动子弹
遍历子弹数组,如果子弹到达了该到达的距离就清除该子弹,否则继续向前移动。(想进一步完善的话,可以在遍历的时候,重新计算子弹 x
和 y
应该移动的值)
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()
}
最后就实现了大致如下效果了。
塔防部分子弹效果
缩放子弹 | 旋转子弹 | 持续变粗的火焰柱 |
---|---|---|
具体代码放到码上掘金了,有需要可以自提
缩放子弹
旋转子弹
持续变粗的火焰柱
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端定义了一个 size
为 50px
的一个基准。当来到了移动端我这里是根据之前定义好的一个 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
中即可使用了。
移动端下将游戏区域横屏处理,旋转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
写起来和改起来都很麻烦。还有就是 vite
和 pinia
使用起来是真的香。
总体来说,整个项目开发下来,对 vue3
的一些使用基本了解掌握, canvas
的使用熟悉了不少,js
基本功也提升不少。
从 react 到 vue3 的使用体验。
这不是什么对比文章,这里就简单说下我在这个项目中的开发体会。
vue3
中jsx
组件对ts
的支持不太友好。不管是props
属性的类型定义,还是emits
事件的定义。- 还有就是
props
不能用解构写法,因为vue
中不是每次render
都能重新触发props
的解构的,所以就丢失了响应式。
不过也可能是我写得不熟吧。为此我也看了 element-plus
的源码,它们也是采用 jsx
写法,我改写起来感觉的确没 react
优雅。
不过 vue
中也有使用起来令我觉得比 react
舒服的地方
比如就是 state
的修改,直接修改就完了,不用搞什么 setState
,然后这个 state
改变后,也不用搞什么 useEffect
。 这点在我项目中使用起来我感觉尤为关键。
说明
第一次在思否这边发文,这是我在掘金写的一篇文章,有不清楚的地方可以看原文