增加蛇的身体结点增长逻辑 + 增加蛇的键盘输入操作 + 增加蛇的移动逻辑 + 美化蛇身体结点

This commit is contained in:
flykhan 2023-02-14 17:02:19 +08:00
parent 622fe376fc
commit 341410178d
No known key found for this signature in database
4 changed files with 187 additions and 5 deletions

View File

@ -10,7 +10,7 @@ export class AcGameObject {
// 每创建一个,就 push 一个,先创建先 push,后创建后 push
// 先创建的先执行 update ,后创建的会把先创建的给覆盖掉
AC_GAME_OBJECTS.push(this);
// 帧与帧执行的时间间隔
// 帧与帧执行的时间间隔,单位:秒
this.timedelta=0;
// 是否执行过 start 函数
this.has_called_start = false;

View File

@ -132,6 +132,29 @@ export class GameMap extends AcGameObject{
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(){
// 开始时调用一次创建墙的函数
@ -139,6 +162,9 @@ export class GameMap extends AcGameObject{
for(let i = 0; i < 1000; i ++)
if(this.create_wall())
break;
// 开始时启动监听方法
this.add_listening_events();
}
// 每一帧都更新一下小正方格的边长
@ -151,9 +177,32 @@ export class GameMap extends AcGameObject{
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();
}
}
update(){
this.update_size();
// 当两条蛇都准备好进入下一回合后
if (this.check_ready()) {
this.next_step();
}
// 每次更新都重新执行渲染
this.render();
}

View File

@ -17,13 +17,129 @@ export class Snake extends AcGameObject {
// 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置
// cells[] 存放蛇的身体, cells[0] 存放蛇头
this.cells = [new Cell(info.r, info.c)]
this.next_cell = null; // 下一步的目标位置
this.speed = 5; // 蛇的速度:每秒走五个格子
// 定义蛇下一步的指令相关属性
// -1 表示没有指令, 0、 1、 2、 3 表示上右下左方向
this.direction = -1;
// idle 表示静止, move 表示移动, die 表示死亡(状态判断的逻辑在蛇群公共部分 GameMap.js 中定义)
this.status = "idle";
// 4方向行方向偏移量
this.dr = [-1, 0, 1, 0];
// 4方向列方向偏移量
this.dc = [0, 1, 0, -1];
// 当前的回合数:前 10 回合,每次蛇身节数都会 +1, 后面每隔 3 回合蛇身节数 +1
this.step = 0;
// 定义允许的误差: 0.01 ,当误差为 0.01 以内时,就认为两个点已经重合
this.eps = 1e-2;
}
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.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]));
}
}
update_move(){
// 计算目标方向 dx , dy : 使用目标点的坐标减去当前蛇头的坐标
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 清空
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 次
update() {
// 当蛇处于 move 移动状态时, 使用 update_move 方法更新蛇的移动
if (this.status === "move") {
// 每一帧调用 this.update_move()
this.update_move();
}
// 每次更新蛇类对象都重新调用一次渲染
this.render();
}
@ -43,9 +159,26 @@ export class Snake extends AcGameObject {
// 画成圆形
ctx.beginPath();
ctx.arc(cell.x * L, cell.y * L, L * 0.5, 0, 2*Math.PI);
ctx.arc(cell.x * L, cell.y * L, L * 0.5 * 0.8, 0, 2*Math.PI);
// 填充颜色
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.35) * L, Math.min(a.y, b.y) * L, L * 0.7, Math.abs(a.y - b.y) * L);
}
// 横方向的画法
else {
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - 0.35) * L, Math.abs(a.x - b.x) * L, L * 0.7);
}
}
}
}

View File

@ -1,9 +1,9 @@
//
//
<template>
<!-- ref="parent" 用于将 return 返回的 parent 指向 div -->
<div ref="parent" class="gamemap">
<!-- canvas 画布 -->
<canvas ref="canvas"></canvas>
<!-- canvas 画布, tabindex="0" 属性用于接受用户键盘操作 -->
<canvas ref="canvas" tabindex="0"></canvas>
</div>
</template>