蛇的移动和键盘操作

master
barney 2 years ago
parent 03d1906bea
commit 997e5e6532
  1. 2
      web/src/assets/scripts/AcGameObject.js
  2. 9
      web/src/assets/scripts/Cell.js
  3. 101
      web/src/assets/scripts/GameMap.js
  4. 173
      web/src/assets/scripts/Snake.js
  5. 3
      web/src/components/GameMap.vue

@ -3,7 +3,7 @@ const AC_GAME_OBJECTS = [];
export class AcGameObject { export class AcGameObject {
constructor() { constructor() {
AC_GAME_OBJECTS.push(this); // 数组添加元素 AC_GAME_OBJECTS.push(this); // 数组添加元素
this.timedelta = 0; this.timedelta = 0; // 注意单位是毫秒,转化为秒需要除于1000
this.has_called_start = false; this.has_called_start = false;
} }

@ -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;
}
}

@ -1,4 +1,5 @@
import { AcGameObject } from "./AcGameObject"; // 导入js的export class import { AcGameObject } from "./AcGameObject"; // 导入js的export class
import { Snake } from "./Snake";
import { Wall } from "./Wall"; import { Wall } from "./Wall";
export class GameMap extends AcGameObject { export class GameMap extends AcGameObject {
@ -10,10 +11,17 @@ export class GameMap extends AcGameObject {
this.L = 0; this.L = 0;
this.rows = 13; this.rows = 13;
this.cols = 13; this.cols = 14;
this.inner_walls_count = 20; // 障碍物的数量(最大建议80) this.inner_walls_count = 20; // 障碍物的数量(最大建议80)
this.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),
];
} }
check_connectivity(g, sx, sy, tx, ty) { // 判断生成的地图是否可以连通 check_connectivity(g, sx, sy, tx, ty) { // 判断生成的地图是否可以连通
@ -21,7 +29,7 @@ export class GameMap extends AcGameObject {
g[sx][sy] = true; g[sx][sy] = true;
let dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1]; 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]; let x = sx + dx[i], y = sy + dy[i];
if (!g[x][y] && this.check_connectivity(g, x, y, tx, ty)) if (!g[x][y] && this.check_connectivity(g, x, y, tx, ty))
return true; return true;
@ -32,33 +40,34 @@ export class GameMap extends AcGameObject {
create_walls() { // 判断是否生成有效的地图 create_walls() { // 判断是否生成有效的地图
const g = []; const g = [];
for (let r = 0; r < this.rows; r ++ ) { for (let r = 0; r < this.rows; r++) {
g[r] = []; g[r] = [];
for (let c = 0; c < this.cols; c ++ ) { for (let c = 0; c < this.cols; c++) {
g[r][c] = false; 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; 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; g[0][c] = g[this.rows - 1][c] = true;
} }
// 创建随机障碍物 // 创建随机障碍物
for (let i = 0; i < this.inner_walls_count / 2; i ++ ) { for (let i = 0; i < this.inner_walls_count / 2; i++) {
for (let j = 0; j < 1000; j ++ ) { for (let j = 0; j < 1000; j++) {
let r = parseInt(Math.random() * this.rows); let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols); 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) if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2)
continue; continue;
// 每次将障碍物生成到对称的两个位置 // 每次将障碍物生成到对称的两个位置
g[r][c] = g[c][r] = true; g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
break; break;
} }
} }
@ -69,8 +78,8 @@ export class GameMap extends AcGameObject {
if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2)) if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2))
return false; return false;
for (let r = 0; r < this.rows; r ++ ) { for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c ++ ) { for (let c = 0; c < this.cols; c++) {
if (g[r][c]) { if (g[r][c]) {
// 每次添加一个障碍物 // 每次添加一个障碍物
this.walls.push(new Wall(r, c, this)); this.walls.push(new Wall(r, c, this));
@ -81,11 +90,31 @@ export class GameMap extends AcGameObject {
return true; 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() { start() {
// 尝试1000次,直到找到符合条件的地图为止 // 尝试1000次,直到找到符合条件的地图为止
for (let i = 0; i < 1000; i ++ ) for (let i = 0; i < 1000; i++)
if (this.create_walls()) if (this.create_walls())
break; break;
this.add_listening_events();
} }
update_size() { // 每过一帧重新生成新的地图尺寸 update_size() { // 每过一帧重新生成新的地图尺寸
@ -97,13 +126,53 @@ export class GameMap extends AcGameObject {
update() { // 更新地图:每隔一帧都要重新渲染 update() { // 更新地图:每隔一帧都要重新渲染
this.update_size(); // 更新地图大小 this.update_size(); // 更新地图大小
if (this.check_ready()) {
this.next_step(); // 进入下一回合
}
this.render(); // 重新渲染 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() { render() {
const color_even = "#AAD751", color_odd = "#A2D149"; const color_even = "#AAD751", color_odd = "#A2D149";
for (let r = 0; r < this.rows; r ++ ) { for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c ++ ) { for (let c = 0; c < this.cols; c++) {
if ((r + c) % 2 == 0) { if ((r + c) % 2 == 0) {
this.ctx.fillStyle = color_even; this.ctx.fillStyle = color_even;
} else { } else {

@ -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();
}
}
}

@ -1,6 +1,7 @@
<template> <template>
<div ref="parent" class="gamemap"> <!--使用下面返回的parent--> <div ref="parent" class="gamemap"> <!--使用下面返回的parent-->
<canvas ref="canvas"></canvas> <!--使用下面返回的canvas--> <!--tabindex=0获取用户的输入操作-->
<canvas ref="canvas" tabindex="0"></canvas> <!--使用下面返回的canvas-->
</div> </div>
</template> </template>

Loading…
Cancel
Save