增加蛇的身体结点增长逻辑 + 增加蛇的键盘输入操作 + 增加蛇的移动逻辑 + 美化蛇身体结点
This commit is contained in:
parent
622fe376fc
commit
341410178d
|
@ -10,7 +10,7 @@ export class AcGameObject {
|
||||||
// 每创建一个,就 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;
|
||||||
|
|
|
@ -132,6 +132,29 @@ export class GameMap extends AcGameObject{
|
||||||
return true;
|
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(){
|
start(){
|
||||||
// 开始时调用一次创建墙的函数
|
// 开始时调用一次创建墙的函数
|
||||||
|
@ -139,6 +162,9 @@ export class GameMap extends AcGameObject{
|
||||||
for(let i = 0; i < 1000; i ++)
|
for(let i = 0; i < 1000; i ++)
|
||||||
if(this.create_wall())
|
if(this.create_wall())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// 开始时启动监听方法
|
||||||
|
this.add_listening_events();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每一帧都更新一下小正方格的边长
|
// 每一帧都更新一下小正方格的边长
|
||||||
|
@ -151,9 +177,32 @@ export class GameMap extends AcGameObject{
|
||||||
this.ctx.canvas.height = this.L * this.rows;
|
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(){
|
update(){
|
||||||
this.update_size();
|
this.update_size();
|
||||||
|
// 当两条蛇都准备好进入下一回合后
|
||||||
|
if (this.check_ready()) {
|
||||||
|
this.next_step();
|
||||||
|
}
|
||||||
// 每次更新都重新执行渲染
|
// 每次更新都重新执行渲染
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,129 @@ export class Snake extends AcGameObject {
|
||||||
// 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置
|
// 蛇初始只有一个点(蛇头),初始时只需要定义出蛇头即可。初始坐标为每条蛇的起始位置
|
||||||
// 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.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() {
|
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() {
|
update() {
|
||||||
|
// 当蛇处于 move 移动状态时, 使用 update_move 方法更新蛇的移动
|
||||||
|
if (this.status === "move") {
|
||||||
|
// 每一帧调用 this.update_move()
|
||||||
|
this.update_move();
|
||||||
|
}
|
||||||
|
|
||||||
// 每次更新蛇类对象都重新调用一次渲染
|
// 每次更新蛇类对象都重新调用一次渲染
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
@ -43,9 +159,26 @@ export class Snake extends AcGameObject {
|
||||||
|
|
||||||
// 画成圆形
|
// 画成圆形
|
||||||
ctx.beginPath();
|
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();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
// 定义计分板
|
// 定义游戏画布页面
|
||||||
<template>
|
<template>
|
||||||
<!-- ref="parent" 用于将 return 返回的 parent 指向 div -->
|
<!-- ref="parent" 用于将 return 返回的 parent 指向 div -->
|
||||||
<div ref="parent" class="gamemap">
|
<div ref="parent" class="gamemap">
|
||||||
<!-- canvas 画布 -->
|
<!-- canvas 画布, tabindex="0" 属性用于接受用户键盘操作 -->
|
||||||
<canvas ref="canvas"></canvas>
|
<canvas ref="canvas" tabindex="0"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue