// 在 AcGameObject.js 里使用的是 export class ,因此这里需要使用 {} 括起来引用;如果是 export default 则不需要用括号括起来 import { AcGameObject } from "./AcGameObject"; import { Snake } from "./Snake"; // 导入墙组件 import { Wall } from "./Wall"; // 导出定义的 GameMap 游戏地图类 export class GameMap extends AcGameObject { // 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽 constructor(ctx, parent, store) { // super() 用于先执行基类的构造函数 super(); // 存下 ctx 和 parent this.ctx = ctx; this.parent = parent; this.store = store; // 存下每个格子的绝对距离 this.L = 0; /* // 定义棋盘格的行数和列数(前端生成地图时使用) // 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平 this.rows = 13; this.cols = 14; // 绘制棋盘内部区域的障碍物(墙)的数量(前端生成地图时使用) this.inner_walls_count = 20; */ this.rows = this.store.state.pk.rows; this.cols = this.store.state.pk.cols; this.inner_walls_count = this.store.state.pk.inner_walls_count; // 存储所有的墙 // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制 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), ]; } /* // 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标(前端生成地图时使用) check_connectivity(g, sx, sy, tx, ty) { // 当起点坐标和中点坐标一致时,判断联通,直接返回 if (sx == tx && sy == ty) return true; g[sx][sy] = true; // 定义四方向偏移量 let dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1]; // 枚举上下左右四个方向,求当前点下一个相邻点的坐标 for (let i = 0; i < 4; i++) { let x = sx + dx[i], y = sy + dy[i]; // 判断是否撞墙,如果没有撞墙,且可以搜到终点的话,返回 true ,否则返回 false if (!g[x][y] == true && this.check_connectivity(g, x, y, tx, ty)) return true; } // 搜不到终点,返回 false return false; } */ // 创建障碍物(后端生成地图版本) create_walls() { // 取出后端生成后传到前端 store 中的 game_map const g = this.store.state.pk.game_map; // 枚举数组,将 g[r][c] == true 的部分绘制出来 // 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作 for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (g[r][c]) { // 将每个新生成的 Wall 对象 push 存入 walls 数组中 this.walls.push(new Wall(r, c, this)); } } } // 绘制成功则 return turn return true; } /* // 创建障碍物(前端计算地图版本) create_wall() { // 创建一个墙格进行测试 // new Wall(0,0,this); // 开一个布尔数组,有墙为 true // 一开始先将所有墙初始化为 false const g = []; for (let r = 0; r < this.rows; r++) { g[r] = []; for (let c = 0; c < this.cols; c++) { g[r][c] = false; } } // 给左右加上墙 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++) { g[0][c] = g[this.rows - 1][c] = true; } // 创建内部随机障碍物 // 因为每次计算都会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2 for (let i = 0; i < this.inner_walls_count / 2; i++) { // 避免位置重复:重复 1000 次,只要找到了就禁止随机 for (let j = 0; j < 1000; j++) { let r = parseInt(Math.random() * this.rows); let c = parseInt(Math.random() * this.cols); // 主对角线对称 g[r][c] 和 g[c][r] 完成两种联合判断 // 当此位置已经有障碍物了,则重新计算下一个位置 // if(g[r][c] || g[c][r]) continue; // 解决中心对称问题,需要将注释代码修改为下一行 if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue; // 将计算求得的随机障碍物的位置置为 true ,以对该位置进行绘制 // g[r][c] 和 g[c][r] 的坐标在对角线位置会重合,会被绘制为一个障碍物 // g[r][c] || g[c][r] = true; // 解决中心对称问题,需要将注释代码修改为下一行 g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true; // 1000 次中,规定数量的内部障碍物已经够了之后就 break 掉 break; } } // 避免内部障碍物覆盖掉左下角和右上角的角色出发点 g[this.rows - 2][1] = g[1][this.cols - 2] = false; // 保证两个对角角色的运动区域是联通的 // 检测联通需要把 g[][] 传过去给 check_connectivity() 函数进行判断,传过去之前需要把当前 g[][] 状态复制一份,避免当前数据被修改掉 // 深度复制方法:先转换数据为 JSON ,再把 JSON 解析出来 const copy_g = JSON.parse(JSON.stringify(g)); // 检测到不连通,则直接在生成对象之前 return false 退出函数 if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2)) return false; // 枚举数组,将 g[r][c] == true 的部分绘制出来 // 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作 for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (g[r][c]) { // 将每个新生成的 Wall 对象 push 存入 walls 数组中 this.walls.push(new Wall(r, c, this)); } } } // 绘制成功则 return turn 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() { // 开始时调用一次创建墙的函数 // 循环 1000 次,如果成功创建则 break ,否则继续循环创建(前端生成地图时使用这一条) // for (let i = 0; i < 1000; i++) if (this.create_walls()) break; // 使用后端生成地图时,这里只需要调用一次就好 this.create_walls(); // 开始时启动监听方法 this.add_listening_events(); } // 每一帧都更新一下小正方格的边长 update_size() { // 计算当前帧每个格子的宽度, parseInt 取整是为了避免渲染出的格子之间出现小空隙 this.L = parseInt( Math.min( this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows ) ); // 计算当前画布的宽度 this.ctx.canvas.width = this.L * this.cols; // 计算当前画布的高度 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(); } } // 增加碰撞检测:检测目标位置是否合法:没有撞到蛇的身体和墙 check_valid(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 return true; } update() { this.update_size(); // 当两条蛇都准备好进入下一回合后 if (this.check_ready()) { this.next_step(); } // 每次更新都重新执行渲染 this.render(); } // 渲染函数,把当前的游戏对象绘制到地图上 render() { // b47226 棕色 aad751 浅绿 a2d048 深绿 // this.ctx.fillStyle = 'green'; // this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); // 定义偶数格even、奇数格odd的颜色 const color_even = "#aad751", color_odd = "#a2d048"; for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { // 当列标加行标: r + c 是偶数时,选取偶数颜色,否则选取奇数颜色。 if ((r + c) % 2 == 0) { this.ctx.fillStyle = color_even; } else { this.ctx.fillStyle = color_odd; } // 绘制小方格:起始坐标x,起始坐标y,水平边长,竖直边长 this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L); } } } }