LégendeTD
Adresse du projet : codeape.site:16666
github : github.com/ApeWhoLoves…
introduction de base
Technologie de développement : Vue3
+ Canvas
+Ts
Il s'agit pc端
d'un 移动端
petit jeu de défense de tour Web qui prend en charge et .
Autres fonctions :
- Choisir le niveau
- choisir la tour de défense
- classement
Si tout le projet est expliqué dans cet article, ce sera plus compliqué, ici je reproduis simplement quelques petites démos en guise de démonstration.
Cela peut être plus clair si vous regardez le code source.
Réaliser le partage de technologie
assemblage de volutes rondes
J'ai également réécrit ce composant dans une version basée sur la react
version vue
, et apporté quelques améliorations en fonction des besoins du projet. Il ressort également du projet que je l'ai utilisé à plusieurs endroits.
En raison de problèmes d'espace, vous pouvez consulter le code source pour plus de détails : github.com/ApeWhoLoves…
Si cela vous intéresse, vous pouvez également lire cet article : juejin.cn/post/717495…
balle en lévitation
Ceci est également écrit sur la base d'une version react
précédente vue
. Ensuite, j'ai continué à utiliser la méthode jsx
d'écriture . vue3
Entraînez-vous simplement jsx
à écrire en .
En raison de problèmes d'espace, vous pouvez consulter le code source pour plus de détails : github.com/ApeWhoLoves…
Si cela vous intéresse, vous pouvez également lire cet article : juejin.cn/post/718665…
Utilisez la toile pour simuler simplement la fonction d'un petit jeu de défense de tour
requestAnimationFrame dessine
借助 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
中即可使用了。
<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
版本:juejin.cn/post/708776…
整个项目下来,在游戏实现方面其实涉及 vue3
的东西不多,主要是 js
的处理;主要是组件的封装和其他一些功能对 vue3
涉及较多。顺带说一句我将之前的 js
改写成 ts
后感觉是真的香,之前 js
写起来和改起来都很麻烦。还有就是 vite
和 pinia
使用起来是真的香。
总体来说,整个项目开发下来,对 vue3
的一些使用基本了解掌握, canvas
的使用熟悉了不少,js
基本功也提升不少。
从 react 到 vue3 的使用体验。
这不是什么对比文章,这里就简单说下我在这个项目中的开发体会。
-
vue3
中jsx
组件对ts
的支持不太友好。不管是props
属性的类型定义,还是emits
事件的定义。 -
还有就是
props
不能用解构写法,因为vue
中不是每次render
都能重新触发props
的解构的,所以就丢失了响应式。
Mais peut-être que je ne le connais pas. Pour cette raison, j'ai également lu element-plus
le code source, ils utilisent également jsx
la méthode de formulation, et je pense qu'il n'est pas react
élégant .
Cependant vue
, il y a aussi react
des endroits dans lesquels je me sens plus à l'aise d'utiliser.
Par exemple, state
c'est la modification de , il suffit de le modifier directement, pas besoin de faire quoi que ce soit setState
, et après ce state
changement , il n'y a plus rien à faire useEffect
. Je pense que cela est particulièrement essentiel à utiliser dans mon projet.