规格化 js 文件
This commit is contained in:
parent
0badc0f83d
commit
f1101db237
|
@ -4,80 +4,71 @@ const AC_GAME_OBJECTS = [];
|
||||||
|
|
||||||
// 导出类
|
// 导出类
|
||||||
export class AcGameObject {
|
export class AcGameObject {
|
||||||
// 构造函数
|
// 构造函数
|
||||||
constructor(){
|
constructor() {
|
||||||
// push(this) 是将当前对象存下来的意思
|
// push(this) 是将当前对象存下来的意思
|
||||||
// 每创建一个,就 push 一个,先创建先 push,后创建后 push
|
// 每创建一个,就 push 一个,先创建先 push,后创建后 push
|
||||||
// 先创建的先执行 update ,后创建的会把先创建的给覆盖掉
|
// 先创建的先执行 update ,后创建的会把先创建的给覆盖掉
|
||||||
AC_GAME_OBJECTS.push(this);
|
AC_GAME_OBJECTS.push(this);
|
||||||
// 帧与帧执行的时间间隔,单位:秒
|
// 帧与帧执行的时间间隔,单位:秒
|
||||||
this.timedelta=0;
|
this.timedelta = 0;
|
||||||
// 是否执行过 start 函数
|
// 是否执行过 start 函数
|
||||||
this.has_called_start = false;
|
this.has_called_start = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// start 函数只执行一次
|
// start 函数只执行一次
|
||||||
start(){
|
start() {}
|
||||||
|
|
||||||
}
|
// 除第一帧之外,每一帧执行一遍
|
||||||
|
update() {}
|
||||||
// 除第一帧之外,每一帧执行一遍
|
|
||||||
update(){
|
// 删除之前执行
|
||||||
|
on_destroy() {}
|
||||||
}
|
|
||||||
|
// 删除
|
||||||
// 删除之前执行
|
destroy() {
|
||||||
on_destroy(){
|
// 删除之前调用 on_destroy 函数
|
||||||
|
this.on_destroy();
|
||||||
}
|
|
||||||
|
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
|
||||||
// 删除
|
for (let i in AC_GAME_OBJECTS) {
|
||||||
destroy(){
|
const obj = AC_GAME_OBJECTS[i];
|
||||||
// 删除之前调用 on_destroy 函数
|
// 如果 obj 等于当前对象,则删除该对象
|
||||||
this.on_destroy();
|
if (obj === this) {
|
||||||
|
// 使用 splice 删除数组里的对象
|
||||||
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
|
AC_GAME_OBJECTS.splice(i);
|
||||||
for(let i in AC_GAME_OBJECTS){
|
break;
|
||||||
const obj = AC_GAME_OBJECTS[i];
|
}
|
||||||
// 如果 obj 等于当前对象,则删除该对象
|
|
||||||
if(obj === this){
|
|
||||||
// 使用 splice 删除数组里的对象
|
|
||||||
AC_GAME_OBJECTS.splice(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 上一帧执行的时刻
|
// 上一帧执行的时刻
|
||||||
let last_timestamp;
|
let last_timestamp;
|
||||||
// step 函数需要传入当前帧执行的时刻 timestamp
|
// step 函数需要传入当前帧执行的时刻 timestamp
|
||||||
const step = (timestamp) => {
|
const step = (timestamp) => {
|
||||||
// 遍历所有的物品
|
// 遍历所有的物品
|
||||||
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
|
// 在 js 里,使用 of 遍历的是数组里的值;使用 in 遍历的是数组的下标。
|
||||||
for(let obj of AC_GAME_OBJECTS){
|
for (let obj of AC_GAME_OBJECTS) {
|
||||||
// 如果当前物品没有执行 start 函数,则该物品执行一次 start 函数
|
// 如果当前物品没有执行 start 函数,则该物品执行一次 start 函数
|
||||||
if(!obj.has_called_start){
|
if (!obj.has_called_start) {
|
||||||
// 将该物品的 has_called_start 赋值为 true,表示其已经执行过了
|
// 将该物品的 has_called_start 赋值为 true,表示其已经执行过了
|
||||||
obj.has_called_start = true;
|
obj.has_called_start = true;
|
||||||
obj.start();
|
obj.start();
|
||||||
}
|
|
||||||
// 如果执行过 start ,则接下来应该执行 update 函数
|
|
||||||
else{
|
|
||||||
// 当前帧与上一帧的时间间隔:当前帧执行时刻减去上一帧执行时刻
|
|
||||||
obj.timedelta = timestamp - last_timestamp;
|
|
||||||
obj.update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 如果执行过 start ,则接下来应该执行 update 函数
|
||||||
|
else {
|
||||||
|
// 当前帧与上一帧的时间间隔:当前帧执行时刻减去上一帧执行时刻
|
||||||
|
obj.timedelta = timestamp - last_timestamp;
|
||||||
|
obj.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新 last_timestamp ,作为下一次更新的“上一帧执行的时刻”
|
// 更新 last_timestamp ,作为下一次更新的“上一帧执行的时刻”
|
||||||
last_timestamp = timestamp;
|
last_timestamp = timestamp;
|
||||||
// 递归调用
|
// 递归调用
|
||||||
requestAnimationFrame(step)
|
requestAnimationFrame(step);
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 定义需要的刷新次数,传入的函数step会在下一帧浏览器渲染之前执行一遍。
|
// 定义需要的刷新次数,传入的函数step会在下一帧浏览器渲染之前执行一遍。
|
||||||
requestAnimationFrame(step)
|
requestAnimationFrame(step);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// 定义一个格子
|
// 定义一个格子
|
||||||
export class Cell {
|
export class Cell {
|
||||||
// 参数是格子的行数和列数
|
// 参数是格子的行数和列数
|
||||||
constructor(r,c){
|
constructor(r, c) {
|
||||||
this.r = r;
|
this.r = r;
|
||||||
this.c = c;
|
this.c = c;
|
||||||
// 坐标转换,用于确定蛇的每节身体所在格子中心点的坐标
|
// 坐标转换,用于确定蛇的每节身体所在格子中心点的坐标
|
||||||
this.x = c + 0.5;
|
this.x = c + 0.5;
|
||||||
this.y = r + 0.5;
|
this.y = r + 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,255 +5,260 @@ import { Snake } from "./Snake";
|
||||||
import { Wall } from "./Wall";
|
import { Wall } from "./Wall";
|
||||||
|
|
||||||
// 导出定义的 GameMap 游戏地图类
|
// 导出定义的 GameMap 游戏地图类
|
||||||
export class GameMap extends AcGameObject{
|
export class GameMap extends AcGameObject {
|
||||||
// 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
|
// 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
|
||||||
constructor(ctx,parent){
|
constructor(ctx, parent) {
|
||||||
// super() 用于先执行基类的构造函数
|
// super() 用于先执行基类的构造函数
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// 存下 ctx 和 parent
|
// 存下 ctx 和 parent
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
||||||
// 存下每个格子的绝对距离
|
// 存下每个格子的绝对距离
|
||||||
this.L = 0;
|
this.L = 0;
|
||||||
|
|
||||||
// 定义棋盘格的行数和列数
|
// 定义棋盘格的行数和列数
|
||||||
// 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平
|
// 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平
|
||||||
this.rows = 13;
|
this.rows = 13;
|
||||||
this.cols = 14;
|
this.cols = 14;
|
||||||
|
|
||||||
// 绘制棋盘内部区域的障碍物(墙)的数量
|
// 绘制棋盘内部区域的障碍物(墙)的数量
|
||||||
this.inner_walls_count = 20;
|
this.inner_walls_count = 20;
|
||||||
|
|
||||||
// 存储所有的墙
|
// 存储所有的墙
|
||||||
// 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
|
// 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
|
||||||
this.walls = [];
|
this.walls = [];
|
||||||
|
|
||||||
// 创建蛇对象数组
|
// 创建蛇对象数组
|
||||||
this.snakes = [
|
this.snakes = [
|
||||||
// 注意这里的对象生成方式和传参方式
|
// 注意这里的对象生成方式和传参方式
|
||||||
new Snake({id: 0, color: "#4876ec", r: this.rows-2, c: 1}, this),
|
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),
|
new Snake({ id: 1, color: "#f94848", r: 1, c: this.cols - 2 }, this),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标
|
// 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标
|
||||||
check_connectivity(g, sx, sy, tx, ty){
|
check_connectivity(g, sx, sy, tx, ty) {
|
||||||
// 当起点坐标和中点坐标一致时,判断联通,直接返回
|
// 当起点坐标和中点坐标一致时,判断联通,直接返回
|
||||||
if(sx == tx && sy == ty) return true;
|
if (sx == tx && sy == ty) return true;
|
||||||
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++){
|
// 枚举上下左右四个方向,求当前点下一个相邻点的坐标
|
||||||
let x = sx + dx[i], y = sy + dy[i];
|
for (let i = 0; i < 4; i++) {
|
||||||
// 判断是否撞墙,如果没有撞墙,且可以搜到终点的话,返回 true ,否则返回 false
|
let x = sx + dx[i],
|
||||||
if(!g[x][y] == true && this.check_connectivity(g, x, y, tx, ty))
|
y = sy + dy[i];
|
||||||
return true;
|
// 判断是否撞墙,如果没有撞墙,且可以搜到终点的话,返回 true ,否则返回 false
|
||||||
}
|
if (!g[x][y] == true && this.check_connectivity(g, x, y, tx, ty))
|
||||||
// 搜不到终点,返回 false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建墙函数
|
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
// 搜不到终点,返回 false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
|
// 创建墙函数
|
||||||
add_listening_events () {
|
create_wall() {
|
||||||
// 聚焦到获取输入的画布页面
|
// 创建一个墙格进行测试
|
||||||
this.ctx.canvas.focus();
|
// new Wall(0,0,this);
|
||||||
|
|
||||||
// 先取出两条蛇对象
|
// 开一个布尔数组,有墙为 true
|
||||||
const [snake0, snake1] = this.snakes;
|
// 一开始先将所有墙初始化为 false
|
||||||
|
const g = [];
|
||||||
// 获取用户信息:绑定 keydown 事件
|
for (let r = 0; r < this.rows; r++) {
|
||||||
this.ctx.canvas.addEventListener("keydown", e => {
|
g[r] = [];
|
||||||
// 定义 snake0 的键盘绑定事件
|
for (let c = 0; c < this.cols; c++) {
|
||||||
if (e.key === 'w') snake0.set_direction(0);
|
g[r][c] = false;
|
||||||
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(){
|
// 给左右加上墙
|
||||||
// 开始时调用一次创建墙的函数
|
for (let r = 0; r < this.rows; r++) {
|
||||||
// 循环 1000 次,如果成功创建则 break ,否则继续循环创建
|
g[r][0] = g[r][this.cols - 1] = true;
|
||||||
for(let i = 0; i < 1000; i ++)
|
|
||||||
if(this.create_wall())
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 开始时启动监听方法
|
|
||||||
this.add_listening_events();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每一帧都更新一下小正方格的边长
|
// 给上下加上墙
|
||||||
update_size(){
|
for (let c = 0; c < this.cols; c++) {
|
||||||
// 计算当前帧每个格子的宽度, parseInt 取整是为了避免渲染出的格子之间出现小空隙
|
g[0][c] = g[this.rows - 1][c] = true;
|
||||||
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() {
|
// 因为每次计算都会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2
|
||||||
for (const snake of this.snakes) {
|
for (let i = 0; i < this.inner_walls_count / 2; i++) {
|
||||||
// 判断蛇的状态,当状态不为静止,表示蛇上一步骤没有执行终止,返回 false 表示未准备好进入下一回合
|
// 避免位置重复:重复 1000 次,只要找到了就禁止随机
|
||||||
if (snake.status !== "idle") return false;
|
for (let j = 0; j < 1000; j++) {
|
||||||
// 判断蛇下一步指令的方向,如果为 -1 ,表示蛇目前没有获取到方向指令,返回 false 表示未准备好进入下一回合
|
let r = parseInt(Math.random() * this.rows);
|
||||||
if (snake.direction === -1) return false;
|
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));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 当上面的条件判断都满足进入下一回合的条件时,返回 true 表示准备好进入下一步
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 让两条蛇进入下一回合
|
// 绘制成功则 return turn
|
||||||
next_step() {
|
return true;
|
||||||
for (const snake of this.snakes) {
|
}
|
||||||
snake.next_step();
|
|
||||||
}
|
// 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
|
||||||
|
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_wall()) break;
|
||||||
|
|
||||||
|
// 开始时启动监听方法
|
||||||
|
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 表示准备好进入下一步
|
||||||
check_valid (cell) {
|
return true;
|
||||||
// 碰墙检测
|
}
|
||||||
for (const wall of this.walls){
|
|
||||||
if (wall.r === cell.r && wall.c === cell.c)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 蛇身碰撞检测
|
// 让两条蛇进入下一回合
|
||||||
for (const snake of this.snakes) {
|
next_step() {
|
||||||
// 先特判蛇尾碰撞情况
|
for (const snake of this.snakes) {
|
||||||
let k = snake.cells.length;
|
snake.next_step();
|
||||||
// 当蛇尾可以前进(蛇尾可以前进说明此回合蛇尾没有增加)的时候,蛇尾不要判断
|
}
|
||||||
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;
|
check_valid(cell) {
|
||||||
|
// 碰墙检测
|
||||||
|
for (const wall of this.walls) {
|
||||||
|
if (wall.r === cell.r && wall.c === cell.c) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(){
|
// 蛇身碰撞检测
|
||||||
this.update_size();
|
for (const snake of this.snakes) {
|
||||||
// 当两条蛇都准备好进入下一回合后
|
// 先特判蛇尾碰撞情况
|
||||||
if (this.check_ready()) {
|
let k = snake.cells.length;
|
||||||
this.next_step();
|
// 当蛇尾可以前进(蛇尾可以前进说明此回合蛇尾没有增加)的时候,蛇尾不要判断
|
||||||
}
|
if (!snake.check_tail_increasing()) {
|
||||||
// 每次更新都重新执行渲染
|
k--;
|
||||||
this.render();
|
}
|
||||||
|
// 判断蛇身现有结点是否碰撞
|
||||||
|
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(){
|
return true;
|
||||||
// b47226 棕色 aad751 浅绿 a2d048 深绿
|
}
|
||||||
// this.ctx.fillStyle = 'green';
|
|
||||||
// this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
||||||
|
|
||||||
// 定义偶数格even、奇数格odd的颜色
|
update() {
|
||||||
const color_even = "#aad751", color_odd = "#a2d048";
|
this.update_size();
|
||||||
|
// 当两条蛇都准备好进入下一回合后
|
||||||
for(let r = 0; r < this.rows; r++){
|
if (this.check_ready()) {
|
||||||
for(let c = 0; c < this.cols; c++){
|
this.next_step();
|
||||||
// 当列标加行标: 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 每次更新都重新执行渲染
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,233 +4,245 @@ import { Cell } from "./Cell";
|
||||||
|
|
||||||
// 需要继承自绘制基础类 AcGameObject ,用于蛇的绘制刷新
|
// 需要继承自绘制基础类 AcGameObject ,用于蛇的绘制刷新
|
||||||
export class Snake extends AcGameObject {
|
export class Snake extends AcGameObject {
|
||||||
// 参数:蛇的信息 info ,当前游戏地图 gamemap
|
// 参数:蛇的信息 info ,当前游戏地图 gamemap
|
||||||
constructor(info, gamemap) {
|
constructor(info, gamemap) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// 每条蛇的 id 用于区分每条蛇, color 用于定义蛇的颜色
|
// 每条蛇的 id 用于区分每条蛇, color 用于定义蛇的颜色
|
||||||
this.id = info.id;
|
this.id = info.id;
|
||||||
this.color = info.color;
|
this.color = info.color;
|
||||||
// 调用地图的引用获取一些地图参数,例如:每个格子的边长
|
// 调用地图的引用获取一些地图参数,例如:每个格子的边长
|
||||||
this.gamemap = gamemap;
|
this.gamemap = gamemap;
|
||||||
|
|
||||||
// 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置
|
// 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置
|
||||||
// cells[] 存放蛇的身体, cells[0] 存放蛇头
|
// cells[] 存放蛇的身体, cells[0] 存放蛇头
|
||||||
this.cells = [new Cell(info.r, info.c)]
|
this.cells = [new Cell(info.r, info.c)];
|
||||||
this.next_cell = null; // 下一步的目标位置
|
this.next_cell = null; // 下一步的目标位置
|
||||||
|
|
||||||
this.speed = 5; // 蛇的速度:每秒走五个格子
|
this.speed = 5; // 蛇的速度:每秒走五个格子
|
||||||
|
|
||||||
// 定义蛇下一步的指令相关属性
|
// 定义蛇下一步的指令相关属性
|
||||||
// -1 表示没有指令, 0、 1、 2、 3 表示上右下左方向
|
// -1 表示没有指令, 0、 1、 2、 3 表示上右下左方向
|
||||||
this.direction = -1;
|
this.direction = -1;
|
||||||
// idle 表示静止, move 表示移动, die 表示死亡(状态判断的逻辑在蛇群公共部分 GameMap.js 中定义)
|
// idle 表示静止, move 表示移动, die 表示死亡(状态判断的逻辑在蛇群公共部分 GameMap.js 中定义)
|
||||||
this.status = "idle";
|
this.status = "idle";
|
||||||
|
|
||||||
// 4方向行方向偏移量
|
// 4方向行方向偏移量
|
||||||
this.dr = [-1, 0, 1, 0];
|
this.dr = [-1, 0, 1, 0];
|
||||||
// 4方向列方向偏移量
|
// 4方向列方向偏移量
|
||||||
this.dc = [0, 1, 0, -1];
|
this.dc = [0, 1, 0, -1];
|
||||||
|
|
||||||
// 当前的回合数:前 10 回合,每次蛇身节数都会 +1, 后面每隔 3 回合蛇身节数 +1
|
// 当前的回合数:前 10 回合,每次蛇身节数都会 +1, 后面每隔 3 回合蛇身节数 +1
|
||||||
this.step = 0;
|
this.step = 0;
|
||||||
|
|
||||||
// 定义允许的误差: 0.01 ,当误差为 0.01 以内时,就认为两个点已经重合
|
// 定义允许的误差: 0.01 ,当误差为 0.01 以内时,就认为两个点已经重合
|
||||||
this.eps = 1e-2;
|
this.eps = 1e-2;
|
||||||
|
|
||||||
// 蛇头的眼睛方向:(默认)左下角蛇初始方向朝上
|
// 蛇头的眼睛方向:(默认)左下角蛇初始方向朝上
|
||||||
this.eye_direction = 0;
|
this.eye_direction = 0;
|
||||||
// 右上角蛇初始方向朝下
|
// 右上角蛇初始方向朝下
|
||||||
if (this.id === 1) this.eye_direction = 2;
|
if (this.id === 1) this.eye_direction = 2;
|
||||||
// 蛇眼睛四方向x偏移量
|
// 蛇眼睛四方向x偏移量
|
||||||
this.eye_dx = [
|
this.eye_dx = [
|
||||||
[-1, 1],
|
[-1, 1],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[1, -1],
|
[1, -1],
|
||||||
[-1, -1],
|
[-1, -1],
|
||||||
];
|
];
|
||||||
|
|
||||||
// 蛇眼睛四方向y偏移量
|
// 蛇眼睛四方向y偏移量
|
||||||
this.eye_dy = [
|
this.eye_dy = [
|
||||||
[-1, -1],
|
[-1, -1],
|
||||||
[-1, 1],
|
[-1, 1],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[1, -1],
|
[1, -1],
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.eye_direction = 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]));
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
// 如果下一步操作的目标位置碰撞检测不合法,则蛇直接去世
|
||||||
|
if (!this.gamemap.check_valid(this.next_cell)) {
|
||||||
|
this.status = "die";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 定义方向设置接口
|
update_move() {
|
||||||
set_direction(d) {
|
// 计算目标方向 dx , dy : 使用目标点的坐标减去当前蛇头的坐标
|
||||||
// 可以将当前方向 this.direction 变为 d , d 是通过键盘或者其他方式人为赋予的方向值
|
const dx = this.next_cell.x - this.cells[0].x;
|
||||||
this.direction = d;
|
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;
|
||||||
|
|
||||||
// 检测当前回合,蛇尾是否增加
|
// 更新蛇尾位置
|
||||||
check_tail_increasing() {
|
if (!this.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.eye_direction = d;
|
|
||||||
|
|
||||||
// 计算完坐标之后,清空方向
|
|
||||||
this.direction = -1;
|
|
||||||
// 将状态从静止变为移动
|
|
||||||
this.status = "move";
|
|
||||||
|
|
||||||
// 增加回合数
|
|
||||||
this.step ++;
|
|
||||||
|
|
||||||
// 计算新的蛇身体结点
|
|
||||||
const k = this.cells.length;
|
const k = this.cells.length;
|
||||||
for(let i = k; i > 0; i --) {
|
// 取出当前的蛇尾
|
||||||
// 每个身体结点都要往后移动一位,配合头部新生成的一位结点,共同组成一个新的蛇身
|
const tail = this.cells[k - 1];
|
||||||
// 这里需要使用 JSON 方法进行深度复制,以产生新的对象避免数据出错
|
// 当前蛇尾的下一个目标位置
|
||||||
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
|
const tail_target = this.cells[k - 2];
|
||||||
}
|
|
||||||
|
|
||||||
// 如果下一步操作的目标位置碰撞检测不合法,则蛇直接去世
|
// 把当前蛇尾移动到下一个蛇尾目标位置: 将 tail 移动到 tail_target 位置
|
||||||
if (!this.gamemap.check_valid(this.next_cell)){
|
// 求两个位置的横纵坐标差值
|
||||||
this.status = "die";
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
update_move(){
|
// render 不来自基类,需要本类自己实现
|
||||||
// 计算目标方向 dx , dy : 使用目标点的坐标减去当前蛇头的坐标
|
render() {
|
||||||
const dx = this.next_cell.x - this.cells[0].x;
|
// 取出单元格长度引用和画布
|
||||||
const dy = this.next_cell.y - this.cells[0].y;
|
const L = this.gamemap.L;
|
||||||
// 蛇头移动到下一步目标点之间,目前已经移动过的距离
|
const ctx = this.gamemap.ctx;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
// 计算当前身体结点是否走到终点(下一步的目标位置): 已经移动到终点,则停止移动;
|
// 定义填充颜色
|
||||||
if (distance < this.eps) {
|
ctx.fillStyle = this.color;
|
||||||
// 把目标点存下来作为新的头
|
// 当蛇已经去世了,将其颜色变成惨白色
|
||||||
this.cells[0] = this.next_cell;
|
if (this.status === "die") {
|
||||||
// 下一步之前,将当前的 this.next_cell 清空
|
ctx.fillStyle = "#ffffff";
|
||||||
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 次
|
// 蛇的身体不止一节,需要枚举画出所有的肢结。of 遍历 cells 值
|
||||||
update() {
|
for (const cell of this.cells) {
|
||||||
// 当蛇处于 move 移动状态时, 使用 update_move 方法更新蛇的移动
|
// 画成正方形
|
||||||
if (this.status === "move") {
|
// ctx.fillRect(cell.c * L, cell.r * L, L, L);
|
||||||
// 每一帧调用 this.update_move()
|
|
||||||
this.update_move();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每次更新蛇类对象都重新调用一次渲染
|
// 画成圆形
|
||||||
this.render();
|
ctx.beginPath();
|
||||||
|
ctx.arc(cell.x * L, cell.y * L, L * 0.5 * 0.8, 0, 2 * Math.PI);
|
||||||
|
// 填充颜色
|
||||||
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// render 不来自基类,需要本类自己实现
|
// 使得蛇身体丰满一点
|
||||||
render() {
|
for (let i = 1; i < this.cells.length; i++) {
|
||||||
// 取出单元格长度引用和画布
|
const a = this.cells[i - 1],
|
||||||
const L = this.gamemap.L;
|
b = this.cells[i];
|
||||||
const ctx = this.gamemap.ctx;
|
// 当两个目标点重合时,不用在绘制矩形填充
|
||||||
|
if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps)
|
||||||
|
continue;
|
||||||
|
|
||||||
// 定义填充颜色
|
// 定义蛇身结点宽度比例
|
||||||
ctx.fillStyle = this.color;
|
const snake_node_width = 0.7;
|
||||||
// 当蛇已经去世了,将其颜色变成惨白色
|
// 如果两个目标点在竖方向重合(横坐标一致,纵坐标不重合)时的画法
|
||||||
if (this.status === "die") {
|
if (Math.abs(a.x - b.x) < this.eps) {
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillRect(
|
||||||
}
|
(a.x - snake_node_width / 2) * L,
|
||||||
|
Math.min(a.y, b.y) * L,
|
||||||
// 蛇的身体不止一节,需要枚举画出所有的肢结。of 遍历 cells 值
|
L * snake_node_width,
|
||||||
for(const cell of this.cells){
|
Math.abs(a.y - b.y) * L
|
||||||
// 画成正方形
|
);
|
||||||
// ctx.fillRect(cell.c * L, cell.r * L, L, L);
|
}
|
||||||
|
// 横方向的画法
|
||||||
// 画成圆形
|
else {
|
||||||
ctx.beginPath();
|
ctx.fillRect(
|
||||||
ctx.arc(cell.x * L, cell.y * L, L * 0.5 * 0.8, 0, 2*Math.PI);
|
Math.min(a.x, b.x) * L,
|
||||||
// 填充颜色
|
(a.y - snake_node_width / 2) * L,
|
||||||
ctx.fill();
|
Math.abs(a.x - b.x) * L,
|
||||||
}
|
L * snake_node_width
|
||||||
|
);
|
||||||
// 使得蛇身体丰满一点
|
}
|
||||||
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;
|
|
||||||
|
|
||||||
// 定义蛇身结点宽度比例
|
|
||||||
const snake_node_width = 0.7;
|
|
||||||
// 如果两个目标点在竖方向重合(横坐标一致,纵坐标不重合)时的画法
|
|
||||||
if (Math.abs(a.x - b.x) < this.eps) {
|
|
||||||
ctx.fillRect((a.x - snake_node_width / 2) * L, Math.min(a.y, b.y) * L, L * snake_node_width, Math.abs(a.y - b.y) * L);
|
|
||||||
}
|
|
||||||
// 横方向的画法
|
|
||||||
else {
|
|
||||||
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - snake_node_width / 2) * L, Math.abs(a.x - b.x) * L, L * snake_node_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制蛇眼睛
|
|
||||||
// 定义眼睛颜色
|
|
||||||
ctx.fillStyle = "#fbff00";
|
|
||||||
// 定义蛇眼睛大小
|
|
||||||
const snake_eye_size = 0.07 * L;
|
|
||||||
// 画眼睛
|
|
||||||
for (let i = 0; i < 2; i ++) {
|
|
||||||
// 眼睛的横纵坐标(乘以 L 是绝对距离)
|
|
||||||
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, snake_eye_size, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绘制蛇眼睛
|
||||||
|
// 定义眼睛颜色
|
||||||
|
ctx.fillStyle = "#fbff00";
|
||||||
|
// 定义蛇眼睛大小
|
||||||
|
const snake_eye_size = 0.07 * L;
|
||||||
|
// 画眼睛
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
// 眼睛的横纵坐标(乘以 L 是绝对距离)
|
||||||
|
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, snake_eye_size, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,35 +2,34 @@
|
||||||
import { AcGameObject } from "./AcGameObject";
|
import { AcGameObject } from "./AcGameObject";
|
||||||
|
|
||||||
export class Wall extends AcGameObject {
|
export class Wall extends AcGameObject {
|
||||||
// 构造函数定义,参数为墙的坐标 r 行, c 列,gamemap 用于绘制
|
// 构造函数定义,参数为墙的坐标 r 行, c 列,gamemap 用于绘制
|
||||||
constructor(r,c,gamemap){
|
constructor(r, c, gamemap) {
|
||||||
// 先执行基类的构造函数
|
// 先执行基类的构造函数
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.r = r;
|
this.r = r;
|
||||||
this.c = c;
|
this.c = c;
|
||||||
this.gamemap = gamemap;
|
this.gamemap = gamemap;
|
||||||
this.color = "#b47226";
|
this.color = "#b47226";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 墙的更新
|
// 墙的更新
|
||||||
update(){
|
update() {
|
||||||
// 执行渲染
|
// 执行渲染
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 墙的渲染
|
// 墙的渲染
|
||||||
render(){
|
render() {
|
||||||
// 从 gamemap 对象中拿到小格(墙)的边长
|
// 从 gamemap 对象中拿到小格(墙)的边长
|
||||||
const L = this.gamemap.L;
|
const L = this.gamemap.L;
|
||||||
|
|
||||||
// 拿到ctx画布
|
// 拿到ctx画布
|
||||||
this.ctx = this.gamemap.ctx;
|
this.ctx = this.gamemap.ctx;
|
||||||
|
|
||||||
// 设置 ctx 画布填充色
|
// 设置 ctx 画布填充色
|
||||||
this.ctx.fillStyle = this.color;
|
this.ctx.fillStyle = this.color;
|
||||||
// 绘制矩形
|
// 绘制矩形
|
||||||
this.ctx.fillRect(this.c*L, this.r*L, L, L);
|
this.ctx.fillRect(this.c * L, this.r * L, L, L);
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,67 +1,105 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
// 导入所有 view 页面
|
// 导入所有 view 页面
|
||||||
import PkIndexView from '../views/pk/PkIndexView.vue'
|
import PkIndexView from "../views/pk/PkIndexView.vue";
|
||||||
import RanklistIndexView from '../views/ranklist/RanklistIndexView.vue'
|
import RanklistIndexView from "../views/ranklist/RanklistIndexView.vue";
|
||||||
import RecordIndexView from '../views/record/RecordIndexView.vue'
|
import RecordIndexView from "../views/record/RecordIndexView.vue";
|
||||||
import UserBotIndexView from '../views/user/bot/UserBotIndexView.vue'
|
import UserBotIndexView from "../views/user/bot/UserBotIndexView.vue";
|
||||||
import NotFound from '../views/error/NotFound.vue'
|
import NotFound from "../views/error/NotFound.vue";
|
||||||
import UserAccountLoginView from '@/views/user/account/UserAccountLoginView.vue'
|
import UserAccountLoginView from "@/views/user/account/UserAccountLoginView.vue";
|
||||||
import UserAccountRegisterView from '@/views/user/account/UserAccountRegisterView.vue'
|
import UserAccountRegisterView from "@/views/user/account/UserAccountRegisterView.vue";
|
||||||
|
// 读入 store 信息
|
||||||
|
import store from "../store/index";
|
||||||
|
|
||||||
// 定义所有页面的 URL 路由
|
// 定义所有页面的 URL 路由
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path:'/',
|
path: "/",
|
||||||
name:'home',
|
name: "home",
|
||||||
// 重定向:将 home 重定向到 pk 页面
|
// 重定向:将 home 重定向到 pk 页面
|
||||||
redirect:'/pk/'
|
redirect: "/pk/",
|
||||||
|
// meta 存其他信息
|
||||||
|
meta: {
|
||||||
|
// 页面是否需要授权
|
||||||
|
requestAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/pk/',
|
path: "/pk/",
|
||||||
name:'pk_index',
|
name: "pk_index",
|
||||||
component:PkIndexView
|
component: PkIndexView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/ranklist/',
|
path: "/ranklist/",
|
||||||
name:'ranklist_index',
|
name: "ranklist_index",
|
||||||
component:RanklistIndexView
|
component: RanklistIndexView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/record/',
|
path: "/record/",
|
||||||
name:'record_index',
|
name: "record_index",
|
||||||
component:RecordIndexView
|
component: RecordIndexView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/user/bot/',
|
path: "/user/bot/",
|
||||||
name:'user_bot_index',
|
name: "user_bot_index",
|
||||||
component:UserBotIndexView
|
component: UserBotIndexView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/user/account/login/',
|
path: "/user/account/login/",
|
||||||
name:'user_account_login',
|
name: "user_account_login",
|
||||||
component:UserAccountLoginView
|
component: UserAccountLoginView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/user/account/register/',
|
path: "/user/account/register/",
|
||||||
name:'user_account_register',
|
name: "user_account_register",
|
||||||
component:UserAccountRegisterView
|
component: UserAccountRegisterView,
|
||||||
|
meta: {
|
||||||
|
requestAuth: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path:'/404/',
|
path: "/404/",
|
||||||
name:'404',
|
name: "404",
|
||||||
component:NotFound
|
component: NotFound,
|
||||||
|
meta: {
|
||||||
|
requestAuth: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// 正则匹配所有其他非法页面到 404
|
// 正则匹配所有其他非法页面到 404
|
||||||
path:'/:catchAll(.*)',
|
path: "/:catchAll(.*)",
|
||||||
redirect:'/404/'
|
redirect: "/404/",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
// to 表示从那个页面跳转, from 表示从哪个页面跳转出去, next 表示页面要不要执行下一步操作
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 如果页面需要授权而且未登录,则跳转到用户登录页面
|
||||||
|
if (to.meta.requestAuth && !store.state.user.is_login) {
|
||||||
|
next({ name: "user_account_login" });
|
||||||
|
} else {
|
||||||
|
// next() 跳转到默认页面
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
import { createStore } from 'vuex'
|
import { createStore } from "vuex";
|
||||||
import ModuleUser from './user'
|
import ModuleUser from "./user";
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state: {
|
state: {},
|
||||||
},
|
getters: {},
|
||||||
getters: {
|
mutations: {},
|
||||||
},
|
actions: {},
|
||||||
mutations: {
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
},
|
|
||||||
modules: {
|
modules: {
|
||||||
user: ModuleUser,
|
user: ModuleUser,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,100 +1,93 @@
|
||||||
import $ from 'jquery'
|
import $ from "jquery";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
state: {
|
state: {
|
||||||
id: "",
|
id: "",
|
||||||
username: "",
|
username: "",
|
||||||
photo: "",
|
photo: "",
|
||||||
token: "",
|
token: "",
|
||||||
is_login: false,
|
is_login: false,
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
// 同步事件
|
||||||
|
mutations: {
|
||||||
|
// 更新用户信息
|
||||||
|
updateUser(state, user) {
|
||||||
|
state.id = user.id;
|
||||||
|
state.username = user.username;
|
||||||
|
state.photo = user.photo;
|
||||||
|
state.is_login = user.is_login;
|
||||||
},
|
},
|
||||||
getters: {
|
// 更新用户 Token
|
||||||
|
updateToken(state, token) {
|
||||||
|
state.token = token;
|
||||||
},
|
},
|
||||||
// 同步事件
|
// 退出登录
|
||||||
mutations: {
|
logout(state) {
|
||||||
// 更新用户信息
|
state.id = "";
|
||||||
updateUser(state, user){
|
state.username = "";
|
||||||
state.id = user.id;
|
state.photo = "";
|
||||||
state.username = user.username;
|
state.token = "";
|
||||||
state.photo = user.photo;
|
state.is_login = false;
|
||||||
state.is_login = user.is_login;
|
},
|
||||||
|
},
|
||||||
|
// 异步事件
|
||||||
|
actions: {
|
||||||
|
// 登录函数
|
||||||
|
login(context, data) {
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:3000/user/account/token/",
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
},
|
},
|
||||||
// 更新用户 Token
|
success(resp) {
|
||||||
updateToken(state, token){
|
// console.log(resp.token, "\n成功了\n", resp.error_message);
|
||||||
state.token = token;
|
if (resp.error_message === "success") {
|
||||||
},
|
/*
|
||||||
// 退出登录
|
|
||||||
logout(state){
|
|
||||||
state.id = "";
|
|
||||||
state.username = "";
|
|
||||||
state.photo = "";
|
|
||||||
state.token = "";
|
|
||||||
state.is_login = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 异步事件
|
|
||||||
actions: {
|
|
||||||
// 登录函数
|
|
||||||
login(context, data){
|
|
||||||
$.ajax({
|
|
||||||
url: "http://localhost:3000/user/account/token/",
|
|
||||||
type: "POST",
|
|
||||||
data: {
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
},
|
|
||||||
success(resp) {
|
|
||||||
// console.log(resp.token, "\n成功了\n", resp.error_message);
|
|
||||||
if(resp.error_message === "success"){
|
|
||||||
/*
|
|
||||||
登录成功则将获取到的 resp 里的 token 传给 mutations 里的
|
登录成功则将获取到的 resp 里的 token 传给 mutations 里的
|
||||||
updateToken 方法,对用户 token 信息进行更新
|
updateToken 方法,对用户 token 信息进行更新
|
||||||
*/
|
*/
|
||||||
context.commit("updateToken", resp.token);
|
context.commit("updateToken", resp.token);
|
||||||
data.success(resp);
|
data.success(resp);
|
||||||
} else {
|
} else {
|
||||||
data.error(resp);
|
data.error(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
|
||||||
error(resp) {
|
|
||||||
data.error(resp);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// 获取登录成功后的用户信息
|
error(resp) {
|
||||||
getinfo(context,data){
|
data.error(resp);
|
||||||
$.ajax({
|
|
||||||
url: "http://localhost:3000/user/account/info/",
|
|
||||||
type: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization:
|
|
||||||
"Bearer " + context.state.token,
|
|
||||||
},
|
|
||||||
success(resp) {
|
|
||||||
if(resp.error_message === "success"){
|
|
||||||
// 更新用户信息
|
|
||||||
context.commit("updateUser",{
|
|
||||||
...resp, // 解构 resp 中的内容
|
|
||||||
is_login: true, // 登录成功,将 is_login 置为 true
|
|
||||||
});
|
|
||||||
data.success(resp); // 调用回调函数
|
|
||||||
} else {
|
|
||||||
data.error(resp);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error(resp) {
|
|
||||||
data.error(resp);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
logout(context){
|
});
|
||||||
context.commit("logout");
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
modules: {
|
// 获取登录成功后的用户信息
|
||||||
|
getinfo(context, data) {
|
||||||
}
|
$.ajax({
|
||||||
}
|
url: "http://localhost:3000/user/account/info/",
|
||||||
|
type: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer " + context.state.token,
|
||||||
|
},
|
||||||
|
success(resp) {
|
||||||
|
if (resp.error_message === "success") {
|
||||||
|
// 更新用户信息
|
||||||
|
context.commit("updateUser", {
|
||||||
|
...resp, // 解构 resp 中的内容
|
||||||
|
is_login: true, // 登录成功,将 is_login 置为 true
|
||||||
|
});
|
||||||
|
data.success(resp); // 调用回调函数
|
||||||
|
} else {
|
||||||
|
data.error(resp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error(resp) {
|
||||||
|
data.error(resp);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logout(context) {
|
||||||
|
context.commit("logout");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modules: {},
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue