From 341410178d8f2bb7ec88d81192ec5594476a790d Mon Sep 17 00:00:00 2001 From: flykhan Date: Tue, 14 Feb 2023 17:02:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=9B=87=E7=9A=84=E8=BA=AB?= =?UTF-8?q?=E4=BD=93=E7=BB=93=E7=82=B9=E5=A2=9E=E9=95=BF=E9=80=BB=E8=BE=91?= =?UTF-8?q?=20+=20=E5=A2=9E=E5=8A=A0=E8=9B=87=E7=9A=84=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=93=8D=E4=BD=9C=20+=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=9B=87=E7=9A=84=E7=A7=BB=E5=8A=A8=E9=80=BB=E8=BE=91=20+=20?= =?UTF-8?q?=E7=BE=8E=E5=8C=96=E8=9B=87=E8=BA=AB=E4=BD=93=E7=BB=93=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/assets/scripts/AcGameObject.js | 2 +- web/src/assets/scripts/GameMap.js | 49 +++++++++ web/src/assets/scripts/Snake.js | 135 ++++++++++++++++++++++++- web/src/components/GameMap.vue | 6 +- 4 files changed, 187 insertions(+), 5 deletions(-) diff --git a/web/src/assets/scripts/AcGameObject.js b/web/src/assets/scripts/AcGameObject.js index 764e489..31fa5b6 100644 --- a/web/src/assets/scripts/AcGameObject.js +++ b/web/src/assets/scripts/AcGameObject.js @@ -10,7 +10,7 @@ export class AcGameObject { // 每创建一个,就 push 一个,先创建先 push,后创建后 push // 先创建的先执行 update ,后创建的会把先创建的给覆盖掉 AC_GAME_OBJECTS.push(this); - // 帧与帧执行的时间间隔 + // 帧与帧执行的时间间隔,单位:秒 this.timedelta=0; // 是否执行过 start 函数 this.has_called_start = false; diff --git a/web/src/assets/scripts/GameMap.js b/web/src/assets/scripts/GameMap.js index 9752eea..87918d0 100644 --- a/web/src/assets/scripts/GameMap.js +++ b/web/src/assets/scripts/GameMap.js @@ -132,6 +132,29 @@ export class GameMap extends AcGameObject{ return true; } + // 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇 + add_listening_events () { + // 聚焦到获取输入的画布页面 + this.ctx.canvas.focus(); + + // 先取出两条蛇对象 + const [snake0, snake1] = this.snakes; + + // 获取用户信息:绑定 keydown 事件 + this.ctx.canvas.addEventListener("keydown", e => { + // 定义 snake0 的键盘绑定事件 + if (e.key === 'w') snake0.set_direction(0); + else if (e.key === 'd') snake0.set_direction(1); + else if (e.key === 's') snake0.set_direction(2); + else if (e.key === 'a') snake0.set_direction(3); + + // 定义 snake1 的键盘绑定事件 + else if (e.key === 'ArrowUp') snake1.set_direction(0); + else if (e.key === 'ArrowRight') snake1.set_direction(1); + else if (e.key === 'ArrowDown') snake1.set_direction(2); + else if (e.key === 'ArrowLeft') snake1.set_direction(3); + }); + } start(){ // 开始时调用一次创建墙的函数 @@ -139,6 +162,9 @@ export class GameMap extends AcGameObject{ for(let i = 0; i < 1000; i ++) if(this.create_wall()) break; + + // 开始时启动监听方法 + this.add_listening_events(); } // 每一帧都更新一下小正方格的边长 @@ -151,9 +177,32 @@ export class GameMap extends AcGameObject{ this.ctx.canvas.height = this.L * this.rows; } + // 判断两条蛇是否准保好进入下一回合 + check_ready() { + for (const snake of this.snakes) { + // 判断蛇的状态,当状态不为静止,表示蛇上一步骤没有执行终止,返回 false 表示未准备好进入下一回合 + if (snake.status !== "idle") return false; + // 判断蛇下一步指令的方向,如果为 -1 ,表示蛇目前没有获取到方向指令,返回 false 表示未准备好进入下一回合 + if (snake.direction === -1) return false; + } + + // 当上面的条件判断都满足进入下一回合的条件时,返回 true 表示准备好进入下一步 + return true; + } + + // 让两条蛇进入下一回合 + next_step() { + for (const snake of this.snakes) { + snake.next_step(); + } + } update(){ this.update_size(); + // 当两条蛇都准备好进入下一回合后 + if (this.check_ready()) { + this.next_step(); + } // 每次更新都重新执行渲染 this.render(); } diff --git a/web/src/assets/scripts/Snake.js b/web/src/assets/scripts/Snake.js index 12c3458..74c09ef 100644 --- a/web/src/assets/scripts/Snake.js +++ b/web/src/assets/scripts/Snake.js @@ -17,13 +17,129 @@ export class Snake extends AcGameObject { // 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置 // cells[] 存放蛇的身体, cells[0] 存放蛇头 this.cells = [new Cell(info.r, info.c)] + this.next_cell = null; // 下一步的目标位置 + + this.speed = 5; // 蛇的速度:每秒走五个格子 + + // 定义蛇下一步的指令相关属性 + // -1 表示没有指令, 0、 1、 2、 3 表示上右下左方向 + this.direction = -1; + // idle 表示静止, move 表示移动, die 表示死亡(状态判断的逻辑在蛇群公共部分 GameMap.js 中定义) + this.status = "idle"; + + // 4方向行方向偏移量 + this.dr = [-1, 0, 1, 0]; + // 4方向列方向偏移量 + this.dc = [0, 1, 0, -1]; + + // 当前的回合数:前 10 回合,每次蛇身节数都会 +1, 后面每隔 3 回合蛇身节数 +1 + this.step = 0; + + // 定义允许的误差: 0.01 ,当误差为 0.01 以内时,就认为两个点已经重合 + this.eps = 1e-2; } start() { } + // 定义方向设置接口 + set_direction(d) { + // 可以将当前方向 this.direction 变为 d , d 是通过键盘或者其他方式人为赋予的方向值 + this.direction = d; + } + + // 检测当前回合,蛇尾是否增加 + check_tail_increasing() { + // 前 10 回合每次都增加,后面每 3 回合增加一节蛇尾 + if(this.step <= 10) return true; + if(this.step % 3 === 1) return true; + // 否则 return false + return false; + } + + // 更新当前状态,将蛇的状态变为走下一步 + next_step() { + // 当前的蛇头方向 + const d = this.direction; + // 下一节蛇身体的坐标计算 + this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]); + + // 计算完坐标之后,清空方向 + this.direction = -1; + // 将状态从静止变为移动 + this.status = "move"; + + // 增加回合数 + this.step ++; + + // 计算新的蛇身体结点 + const k = this.cells.length; + for(let i = k; i > 0; i --) { + // 每个身体结点都要往后移动一位,配合头部新生成的一位结点,共同组成一个新的蛇身 + // 这里需要使用 JSON 方法进行深度复制,以产生新的对象避免数据出错 + this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); + } + } + + + update_move(){ + // 计算目标方向 dx , dy : 使用目标点的坐标减去当前蛇头的坐标 + const dx = this.next_cell.x - this.cells[0].x; + const dy = this.next_cell.y - this.cells[0].y; + // 蛇头移动到下一步目标点之间,目前已经移动过的距离 + const distance = Math.sqrt(dx * dx + dy * dy); + + // 计算当前身体结点是否走到终点(下一步的目标位置): 已经移动到终点,则停止移动; + if (distance < this.eps) { + // 把目标点存下来作为新的头 + this.cells[0] = this.next_cell; + // 下一步之前,将当前的 this.next_cell 清空 + this.next_cell = null; + + // 将状态改为 idle 表示当前停下的状态 + this.status = "idle"; + + // 如果蛇不边长,则每次移动时,增加头部的同时,把蛇尾砍掉 + if (!this.check_tail_increasing()) { + this.cells.pop(); + } + } + // 不重合表示尚未移动到下一步,还可以继续移动 + else { + // 按时间(second)定义移动距离: 每一帧走过的距离 = 速度 * 两帧时间间隔 / 1000(ms) + const move_distance = this.speed * this.timedelta / 1000; // 处以 1000 ,将毫秒单位转换成秒单位 + this.cells[0].x += move_distance * dx / distance; + this.cells[0].y += move_distance * dy / distance; + + // 更新蛇尾位置 + if(!this.check_tail_increasing()) { + const k = this.cells.length; + // 取出当前的蛇尾 + const tail = this.cells[k - 1]; + // 当前蛇尾的下一个目标位置 + const tail_target = this.cells[k - 2]; + + // 把当前蛇尾移动到下一个蛇尾目标位置: 将 tail 移动到 tail_target 位置 + // 求两个位置的横纵坐标差值 + const tail_dx = tail_target.x - tail.x; + const tail_dy = tail_target.y - tail.y; + + // 移动蛇尾 + tail.x += move_distance * tail_dx / distance; + tail.y += move_distance * tail_dy / distance; + } + } + } + + // update 方法每一帧执行一次,每秒钟执行 60 次 update() { + // 当蛇处于 move 移动状态时, 使用 update_move 方法更新蛇的移动 + if (this.status === "move") { + // 每一帧调用 this.update_move() + this.update_move(); + } + // 每次更新蛇类对象都重新调用一次渲染 this.render(); } @@ -43,9 +159,26 @@ export class Snake extends AcGameObject { // 画成圆形 ctx.beginPath(); - ctx.arc(cell.x * L, cell.y * L, L * 0.5, 0, 2*Math.PI); + ctx.arc(cell.x * L, cell.y * L, L * 0.5 * 0.8, 0, 2*Math.PI); // 填充颜色 ctx.fill(); } + + // 使得蛇身体丰满一点 + for (let i = 1; i < this.cells.length; i++) { + const a = this.cells[i - 1], b = this.cells[i]; + // 当两个目标点重合时,不用在绘制矩形填充 + if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps) + continue; + + // 如果两个目标点在竖方向重合(横坐标一致,纵坐标不重合)时的画法 + if (Math.abs(a.x - b.x) < this.eps) { + ctx.fillRect((a.x - 0.35) * L, Math.min(a.y, b.y) * L, L * 0.7, Math.abs(a.y - b.y) * L); + } + // 横方向的画法 + else { + ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - 0.35) * L, Math.abs(a.x - b.x) * L, L * 0.7); + } + } } } \ No newline at end of file diff --git a/web/src/components/GameMap.vue b/web/src/components/GameMap.vue index 644c568..dbc04c1 100644 --- a/web/src/components/GameMap.vue +++ b/web/src/components/GameMap.vue @@ -1,9 +1,9 @@ -// 定义计分板 +// 定义游戏画布页面