diff --git a/web/src/assets/scripts/AcGameObject.js b/web/src/assets/scripts/AcGameObject.js index 8887af0..614a0b1 100644 --- a/web/src/assets/scripts/AcGameObject.js +++ b/web/src/assets/scripts/AcGameObject.js @@ -3,7 +3,7 @@ const AC_GAME_OBJECTS = []; export class AcGameObject { constructor() { AC_GAME_OBJECTS.push(this); // 数组添加元素 - this.timedelta = 0; + this.timedelta = 0; // 注意单位是毫秒,转化为秒需要除于1000 this.has_called_start = false; } diff --git a/web/src/assets/scripts/Cell.js b/web/src/assets/scripts/Cell.js new file mode 100644 index 0000000..2def2d3 --- /dev/null +++ b/web/src/assets/scripts/Cell.js @@ -0,0 +1,9 @@ +export class Cell{ + constructor(r,c) { // 传入蛇的坐标(r,c) + this.r = r; + this.c = c; + this.x = c + 0.5; // 将蛇的左上角坐标(r,c)转化为画布上的圆心坐标(x,y) + this.y = r + 0.5; + } + +} \ No newline at end of file diff --git a/web/src/assets/scripts/GameMap.js b/web/src/assets/scripts/GameMap.js index 439fb1d..b3bb1fd 100644 --- a/web/src/assets/scripts/GameMap.js +++ b/web/src/assets/scripts/GameMap.js @@ -1,4 +1,5 @@ import { AcGameObject } from "./AcGameObject"; // 导入js的export class +import { Snake } from "./Snake"; import { Wall } from "./Wall"; export class GameMap extends AcGameObject { @@ -10,10 +11,17 @@ export class GameMap extends AcGameObject { this.L = 0; this.rows = 13; - this.cols = 13; - + this.cols = 14; + this.inner_walls_count = 20; // 障碍物的数量(最大建议80) this.walls = []; // 所有障碍物组成的数组 + + // 创建两条蛇 + + this.snakes = [ // 蓝蛇和红蛇 + new Snake({ id: 0, color: "#4876EC", r: this.rows - 2, c: 1 }, this), + new Snake({ id: 1, color: "#F94848", r: 1, c: this.cols - 2 }, this), + ]; } check_connectivity(g, sx, sy, tx, ty) { // 判断生成的地图是否可以连通 @@ -21,7 +29,7 @@ export class GameMap extends AcGameObject { g[sx][sy] = true; let dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1]; - for (let i = 0; i < 4; i ++ ) { + for (let i = 0; i < 4; i++) { let x = sx + dx[i], y = sy + dy[i]; if (!g[x][y] && this.check_connectivity(g, x, y, tx, ty)) return true; @@ -32,33 +40,34 @@ export class GameMap extends AcGameObject { create_walls() { // 判断是否生成有效的地图 const g = []; - for (let r = 0; r < this.rows; r ++ ) { + for (let r = 0; r < this.rows; r++) { g[r] = []; - for (let c = 0; c < this.cols; c ++ ) { + for (let c = 0; c < this.cols; c++) { g[r][c] = false; } } // 给四周加上障碍物 - for (let r = 0; r < this.rows; r ++ ) { + for (let r = 0; r < this.rows; r++) { g[r][0] = g[r][this.cols - 1] = true; } - for (let c = 0; c < this.cols; c ++ ) { + for (let c = 0; c < this.cols; c++) { g[0][c] = g[this.rows - 1][c] = true; } // 创建随机障碍物 - for (let i = 0; i < this.inner_walls_count / 2; i ++ ) { - for (let j = 0; j < 1000; j ++ ) { + for (let i = 0; i < this.inner_walls_count / 2; i++) { + for (let j = 0; j < 1000; j++) { let r = parseInt(Math.random() * this.rows); let c = parseInt(Math.random() * this.cols); - if (g[r][c] || g[c][r]) continue; + // 两个对称的位置都没有障碍物 + if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue; // 不能将障碍物生成到两条蛇的起始位置处(左下,右上) if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) continue; // 每次将障碍物生成到对称的两个位置 - g[r][c] = g[c][r] = true; + g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true; break; } } @@ -69,8 +78,8 @@ export class GameMap extends AcGameObject { if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2)) return false; - for (let r = 0; r < this.rows; r ++ ) { - for (let c = 0; c < this.cols; c ++ ) { + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { if (g[r][c]) { // 每次添加一个障碍物 this.walls.push(new Wall(r, c, this)); @@ -81,11 +90,31 @@ export class GameMap extends AcGameObject { return true; } + add_listening_events() { // 绑定监听事件 + this.ctx.canvas.focus(); // 聚焦 + const [snake0, snake1] = this.snakes; + + // 蓝方用wsad控制,红方用上下左右控制 + this.ctx.canvas.addEventListener("keydown", e => { + 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); + 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() { // 尝试1000次,直到找到符合条件的地图为止 - for (let i = 0; i < 1000; i ++ ) + for (let i = 0; i < 1000; i++) if (this.create_walls()) break; + this.add_listening_events(); } update_size() { // 每过一帧重新生成新的地图尺寸 @@ -97,13 +126,53 @@ export class GameMap extends AcGameObject { update() { // 更新地图:每隔一帧都要重新渲染 this.update_size(); // 更新地图大小 + if (this.check_ready()) { + this.next_step(); // 进入下一回合 + } this.render(); // 重新渲染 } + check_ready() { // 判断两条蛇是否准备好进行下一步 + for (const snake of this.snakes) { + if (snake.status !== "idle") return false; + if (snake.direction === -1) return false; + } + return true; + } + + next_step() { // 让两条蛇进入下一回合 + for (const snake of this.snakes) { + snake.next_step(); + } + } + + check_vaild(cell) { // 检测蛇的目标位置是否合法:是否撞到两条蛇的身体和障碍物 + for (const wall of this.walls) { + if (wall.r === cell.r && wall.c === cell.c) { // 撞到墙了 + return false; + } + } + + for (const snake of this.snakes) { + let k = snake.cells.length; + // 特殊处理!!! + if (!snake.check_tail_increasing()) { // 当蛇为会前进的时候,蛇尾不需要判断,因为蛇头可以占据蛇尾的位置 + k--; + } + for (let i = 0; i < k; i++) { // 依次判断是否会撞到两条蛇的身体 + if (snake.cells[i].r === cell.r && snake.cells[i].c === cell.c) { // 撞到蛇的身体了 + return false; + } + } + } + + return true; + } + render() { const color_even = "#AAD751", color_odd = "#A2D149"; - for (let r = 0; r < this.rows; r ++ ) { - for (let c = 0; c < this.cols; c ++ ) { + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { if ((r + c) % 2 == 0) { this.ctx.fillStyle = color_even; } else { diff --git a/web/src/assets/scripts/Snake.js b/web/src/assets/scripts/Snake.js new file mode 100644 index 0000000..1fe2e13 --- /dev/null +++ b/web/src/assets/scripts/Snake.js @@ -0,0 +1,173 @@ +import { AcGameObject } from "./AcGameObject"; +import { Cell } from "./Cell"; + +export class Snake extends AcGameObject { + constructor(info,gamemap) { // 传入蛇的信息和地图 + super(); + + this.id = info.id; // 哪条蛇 + this.color = info.color; // 蛇的颜色 + this.gamemap = gamemap; // 方便调用地图的有关参数 + this.cells = [new Cell(info.r,info.c)] // cells存放蛇的坐标,cells[0]存放蛇头 + + this.speed = 5; // 定义蛇的速度,每秒走5个格子 + this.direction = -1; // -1表示没有指令,0,1,2,3分别表示左右下左 + this.status = "idle"; // idle表示静止,move表示移动,die表示死亡 + this.next_cell = null; // 下一步的目标位置 + + // 0,1,2,3:上右下左 + this.dr = [-1,0,1,0]; // 行方向的偏移量 + this.dc = [0,1,0,-1]; // 列方向的偏移量 + + this.step = 0; // 蛇当前的回合数 + this.eps = 1e-2; // 允许误差为0.01 + + this.eye_direction = 0; // 左下角的蛇的眼睛朝上 + + if (this.id === 1) this.eye_direction = 2; // 右上角的蛇的眼睛朝下 + + // 定义蛇左右眼的x和y坐标的偏移量 + this.eyes_dx = [ + [-1,1], + [1,1], + [-1,1], + [-1,-1] + ]; + + this.eyes_dy = [ + [-1,-1], + [-1,1], + [1,1], + [-1,1] + ]; + + } + + start() { // 第一帧调用该函数 + + } + + update() { // 更新下一帧 + if (this.status === "move") { // 只有下一步的状态为移动才可以移动 + this.update_move(); // 蛇进行移动 + } + this.render(); + } + + check_tail_increasing() { // 前10步每步增加1,后面每三步增加1 + if (this.step <= 10) return true; + if (this.step % 3 === 1) return true; + 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.eye_direction = d; // 更新蛇的眼睛方向 + this.step++; // 回合数加一 + + const k = this.cells.length; + for (let i = k; i > 0; i--) { + // 每个数组元素先后移动一位,相当于复制了一份第一个元素 + // 1,2,3 => 1,1,2,3 + this.cells[i] = JSON.parse(JSON.stringify(this.cells[i-1])); + } + + if (!this.gamemap.check_vaild(this.next_cell)) { // 下一步不合法, 蛇直接去世 + this.status = "die"; + } + + } + + set_direction(d) { // 通过读取用户输入来设置方向 + this.direction = d; + } + + update_move() { // 蛇进行移动 + // // 蛇向右移动5个格子 + // this.cells[0].x += this.speed * this.timedelta / 1000; // 注意每一帧不一定是1s + + 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 = null; + this.status = "idle"; // 走完了,停下来 + if (!this.check_tail_increasing()) { // 蛇不变长,去掉蛇尾 + this.cells.pop(); // 弹出蛇尾 + } + } + else { + const move_distance = this.speed * this.timedelta / 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],tail_target = this.cells[k-2]; + 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; + } + } + + } + + + render() { + const L = this.gamemap.L; // 单位距离 + const ctx = this.gamemap.ctx; // 画布 + + ctx.fillStyle = this.color; // 填充颜色 + + if (this.status === "die") { + ctx.fillStyle = "white"; + } + + for (const cell of this.cells) { // 将蛇用圆表示 + ctx.beginPath(); + // 前两个参数时圆心坐标,后面的 + ctx.arc(cell.x * L,cell.y * L,L / 2 * 0.8, 0, Math.PI * 2); + 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) { + // 矩形起始点的 x轴坐标、y轴坐标、矩形width、矩形height + // 用Math.min()是因为这两个矩阵上下关系不知道 + ctx.fillRect((a.x - 0.4) * L,Math.min(a.y,b.y) * L,L*0.8,Math.abs(a.y-b.y) * L); + } + else { + ctx.fillRect(Math.min(a.x,b.x) * L,(a.y - 0.4) * L,Math.abs(a.x-b.x) * L,L*0.8); + } + } + + + // 画出蛇的眼睛 + + ctx.fillStyle = "black"; + + for (let i = 0; i < 2 ; i++) { + const eye_x = (this.cells[0].x + this.eyes_dx[this.eye_direction][i] * 0.2) * L; + const eye_y = (this.cells[0].y + this.eyes_dy[this.eye_direction][i] * 0.2) * L; + + // 画眼睛 + + ctx.beginPath(); + ctx.arc(eye_x,eye_y,L*0.05,0,Math.PI*2); + ctx.fill(); + } + + } +} \ No newline at end of file diff --git a/web/src/components/GameMap.vue b/web/src/components/GameMap.vue index 6799c4f..418d511 100644 --- a/web/src/components/GameMap.vue +++ b/web/src/components/GameMap.vue @@ -1,6 +1,7 @@