目录
一、整体框架
二、修改地图为中心对称
三、 画蛇
四、实现蛇的移动
五、读取键盘的操作
六、美化蛇和检测非法逻辑
一、整体框架
二、修改地图为中心对称
如果两条蛇,同时走到相同的格子,会造成平局,这种情况会对优势者不利!
需要把地图变为 偶数 乘 奇数
修改代码:
x = rows - 1 - x;y = cols - 1 - y;
成功后的界面:
三、 画蛇
如何画蛇 -> 本质上蛇是由一堆格子组成的序列。
蛇里面的一个格子我们定义成Cell
中心的坐标r+0.5, c +0.5
先调用基类的构造函数,
再把蛇的信息取出来
新建Cell.js 存储一堆格子的序列 (蛇的身体):
export class Cell { constructor(r, c) { this.r = r; this.c = c; // 转换为 canvas 的坐标 this.x = c + 0.5; this.y = r + 0.5; }}
新建Snake.js 对象,方便我们进行操作~
import { AcGameObject } from "./AcGameObject";import { Cell } from "./Cell";export class Snake extends AcGameObject { constructor(info, gamemap) { super(); // 取出基本的id this.id = info.id; this.color = info.color; this.gamemap = gamemap; // 方便调用函数和参数 //存放蛇的身体; this.cells = [new Cell(info.r, info.c)]; } start() { } update() { this.render(); } render() { // 画出基本的蛇头 const L = this.gamemap.L; const ctx = this.gamemap.ctx; ctx.fillStyle = this.color; for (const cell of this.cells) { ctx.beginPath(); ctx.arc(cell.x * L, cell.y * L, L / 2, 0, Math.PI * 2); ctx.fill(); } }}
在GameMap.js 中创建两条蛇的对象:
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),}
成功之后的界面:
四、实现蛇的移动
移动应该是连贯的。
身体有多个部分,如何让保持连贯?
中间保持不动,头和尾动,在头部创建一个新的节点,朝着目的地移动。尾巴朝着目的地动
蛇只有在获取到 两个人 或 机器的指令后才能动~~
在 Snake.js 中添加代码,实现蛇头的移动:
import { AcGameObject } from "./AcGameObject";import { Cell } from "./Cell";export class Snake extends AcGameObject { constructor(info, gamemap) { super(); // 继承AcGameObject的方法 this.id = info.id; this.color = info.color; this.gamemap = gamemap; this.cells = [new Cell(info.r, info.c)]; // 存放蛇的身体, cell[0] 存放蛇头 // new add this.speed = 5; } update_move() { // 向右移动 this.cells[0].x += this.speed * this.timedelta / 1000; //向上移动 //this.cells[0].y -= this.speed * this.timedelta / 1000; } update() { this.update_move(); this.render(); }
蓝色的球会一直向右移动:
如何实现连贯的移动呢?
1、 由于可能会产生一些问题, 也就是中间某个状态,没有完全移出去,蛇的身子会出现问题。
2、中间不动,首尾动!创建的虚拟节点朝着目的地移动。只有两个点动。
3、考虑蛇什么时候动? 回合制游戏,两个人都有输入的时候,才可以移动。
修改 Snake.js:
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)]; this.speed = 5; // 蛇每秒走5格 // new add this.direction = -1; // -1表示没有指令 0 1 2 3 this.status = "idle"; // 静止, move 移动 die 死亡 } }
首先我们需要一个裁判来判断两条蛇的移动,我们抽取放在 GameMap.js 中
check_ready() { // 判断两条蛇是否准备下一回合了 for (const snake of this.snakes) { if (snake.status !== "idle") return false; if (snake.direction === -1) return false; } return true; }
在 Snake.js 中更新蛇的状态:
this.next_cell = null; //下一步的目标位置this.dr = [-1, 0, 1, 0]; // 行this.dc = [0, 1, 0, -1]; //列this.step = 0;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 ++ ;}
在 GameMap.js 中更新:
next_step() { for (const snake of this.snake) { snake.next_step(); }}update() { this.update_size(); if (this.check_ready()) { this.next_step(); } this.render();}
五、读取键盘的操作
从键盘获取操作 w a s d 和 ↑ ↓ ← → 来控制两条蛇。
在 GameMap.vue 中修改:
<canvas ref="canvas" tabindex="0"></canvas>
这样我们的游戏才拥有了聚焦功能(这里Edge会有一个Bug就是上下左右一次后它会聚焦导航栏,但不用担心,因为我们后期实施多人对战,会让每个用户用 w a s d 来操作~)
绑定事件:
在 Snake.js 中加入辅助函数,用来获取方向
//辅助函数set_direction(d) { this.direction = d;}
在 GameMap.js 中修改,添加事件:
add_listening_events() { this.ctx.canvas.focus(); const [snake0, snake1] = this.snakes; 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); }); }
在 Snake.js 中更新状态:
update() { // 每一帧执行一次 if (this.status === 'move') { this.uppdate_move() } this.render();}
此时我们就可以真正让蛇动起来了~
在 Snake.js 中修改:
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[0]存放蛇头 this.next_cell = null; // 下一步的目标位置 this.speed = 5; // 蛇每秒走5个格子 this.direction = -1; // -1表示没有指令,0、1、2、3表示上右下左 this.status = "idle"; // idle表示静止,move表示正在移动,die表示死亡 this.dr = [-1, 0, 1, 0]; // 4个方向行的偏移量 this.dc = [0, 1, 0, -1]; // 4个方向列的偏移量 this.step = 0; // 表示回合数 this.eps = 1e-2; // 允许的误差 } start() { } set_direction(d) { this.direction = d; } 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 -- ) { // 初始元素不变 每一个元素往后移动一位 this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); } } update_move() { 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"; // 走完了,停下来 } 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; } } update() { // 每一帧执行一次 if (this.status === 'move') { this.update_move(); } this.render(); } render() { const L = this.gamemap.L; const ctx = this.gamemap.ctx; ctx.fillStyle = this.color; for (const cell of this.cells) { ctx.beginPath(); ctx.arc(cell.x * L, cell.y * L, L / 2, 0, Math.PI * 2); ctx.fill(); } }}
实现蛇尾的移动 :
在 Snake.js 中添加代码,判断蛇尾是否增长
check_tail_increasing() { if (step <= 10) return true; if (step % 3 === 1) return true; return false;}
修改 Snake.js , 判断蛇尾是否在下一步是否增长
this.next_cell = null; //下一步的目标位置this.dr = [-1, 0, 1, 0]; // 行this.dc = [0, 1, 0, -1]; //列this.step = 0;this.eps = 1e-2 // 允许的误差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 -- ) { // 初始元素不变 每一个元素往后移动一位 this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); }} update_move() { 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; } } }
效果图:
六、美化蛇和检测非法逻辑
修改 Snake.js ,让蛇变得连贯、缩小一点。添加下列代码:
render() { const L = this.gamemap.L; const ctx = this.gamemap.ctx; ctx.fillStyle = this.color; 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) { 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); } } }
效果图:
检测非法逻辑:
1)此时目标位置是否合法(墙体、蛇身、障碍物)
2)蛇尾前进时,蛇尾要不要判断
在 GameMap.js 中更新:
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; }
在 Snake.js 中更新:
next_step() { if (!this.gamemap.check_valid(this.next_cell)) { this.status = "die"; }}render() { if (this.status === "die") { ctx.fillStyle = "white"; }}
效果图:
画蛇点睛!!!!
修改 Snake.js
this.eye_direction = 0;if (this.id === 1) this.eye_direction = 2;this.eye_dx = [ [-1, 1]; [1, 1]; [1, -1]; [-1, -1];];this.eye_dy = [ [-1, -1]; [-1, 1]; [1, 1]; [1, -1];];next_step() { this.eye_direction = d;}render() { ctx.fillStyle = "black"; for (let i = 0; i < 2; i ++ ) { const eye_x = (this.cells[0].x + this.eye_dx[this.eye_direction][i] * 0.15) * L; const eye_y = (this.cells[0].y + this.eye_dy[this.eye_direction][i] * 0.15) * L; ctx.beginPath(); ctx.arc(eye_x, eye_y, L * 0.05, 0, Math.PI * 2); ctx.fill(); }}
效果图:
最后记得用 git 维护哦~~