网站改版新闻稿,建设银行企业网站首页,asp 网站开发实例教程,推荐一些能打开的网站笔记内容转载自 AcWing 的 SpringBoot 框架课讲义#xff0c;课程链接#xff1a;AcWing SpringBoot 框架课。 CONTENTS 1. 地图优化改进2. 绘制玩家的起始位置3. 实现玩家移动4. 优化蛇的身体效果5. 碰撞检测实现 本节实现两名玩家即两条蛇的绘制与人工操作移动功能。
1. 地…笔记内容转载自 AcWing 的 SpringBoot 框架课讲义课程链接AcWing SpringBoot 框架课。 CONTENTS 1. 地图优化改进2. 绘制玩家的起始位置3. 实现玩家移动4. 优化蛇的身体效果5. 碰撞检测实现 本节实现两名玩家即两条蛇的绘制与人工操作移动功能。
1. 地图优化改进
之前我们设计的地图尺寸是13×13两名玩家的起点横纵坐标之和均为偶数因此可能在同一时刻走到同一个格子上为了避免这种情况可以将地图改为13×14的大小即将 GameMap.js 中的 this.cols 改为14这样两名玩家就不会在同一时刻走到同一个格子上。这样修改完之后就不能用轴对称了需要改为中心对称
// 添加地图内部的随机障碍物需要有对称性因此枚举一半即可另一半对称生成
for (let i 0; i this.inner_walls_count / 2; i) {for (let j 0; j 10000; j) {let r parseInt(Math.random() * this.rows);let c parseInt(Math.random() * this.cols);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) continue; // 判断是否覆盖到出生地g[r][c] g[this.rows - 1 - r][this.cols - 1 - c] true;break;}
}2. 绘制玩家的起始位置
刚开始玩家占一个格子我们可以规定一下前十步的每一步将蛇的长度加一之后改为每三步长度加一。每条蛇其实就是一堆格子的序列我们可以将一个格子定义成一个 Cell 对象在 scripts 目录下创建 Cell.js 记录格子的坐标。
我们在每个格子中绘制的是一个圆若格子的左上角坐标为 (x, y) 则圆的圆心坐标应该是 (x 0.5, y 0.5)Cell.js 如下
export class Cell {constructor(r, c) {this.r r;this.c c;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();this.id info.id; // 每条蛇有唯一的id进行区分this.color info.color; // 颜色this.gamemap gamemap;this.cells [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头}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 中创建两条蛇
import { AcGameObject } from ./AcGameObject;
import { Wall } from ./Wall;
import { Snake } from ./Snake;export class GameMap extends AcGameObject {constructor(ctx, parent) { // ctx表示画布parent表示画布的父元素super();this.ctx ctx;this.parent parent;this.L 0; // 一个单位的绝对长度this.rows 13; // 地图的行数this.cols 14; // 地图的列数this.inner_walls_count 20; // 地图内部的随机障碍物数量需要是偶数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) { // 用flood fill算法判断两名玩家是否连通...}create_walls() {...}start() {...}update_size() { // 每一帧更新地图大小...}update() {...}render() {...}
}3. 实现玩家移动
为了实现蛇移动的连续性我们不对每个格子进行更新只更新头部和尾部头部创建一个新的点往前动尾部直接往前动。首先在 Snake 对象中设置一些移动的属性
import { AcGameObject } from ./AcGameObject;
import { Cell } from ./Cell;export class Snake extends AcGameObject {constructor(info, gamemap) {super();this.id info.id; // 每条蛇有唯一的id进行区分this.color info.color; // 颜色this.gamemap gamemap;this.cells [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头this.speed 2; // 蛇每秒走2个格子this.direction -1; // 下一步移动的指令-1表示没有指令0、1、2、3分别表示上、右、下、左this.status idle; // 蛇的状态idle表示静止move表示正在移动die表示死亡this.next_cell null; // 下一步的目标位置this.step 0; // 回合数this.dr [-1, 0, 1, 0];this.dc [0, 1, 0, -1];}start() {}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;}set_direction(d) { // 由于未来不一定只会从键盘获取输入因此实现一个接口修改directionthis.direction d;}update_move() {}update() {if (this.status move) {this.update_move();}this.render();}render() {...}
}由于游戏是回合制的因此移动的判定条件应该是获取到了两名玩家的指示后才能移动一次且该指令既可以由键盘输入也可以由 AI 代码输入判定两条蛇是否准备好执行下一步不能各自判断需要由上层也就是 GameMap 判定判定条件是两条蛇都处于静止状态且都已经获取到了下一步指令
import { AcGameObject } from ./AcGameObject;
import { Wall } from ./Wall;
import { Snake } from ./Snake;export class GameMap extends AcGameObject {constructor(ctx, parent) { // ctx表示画布parent表示画布的父元素...}check_connectivity(g, sx, sy, tx, ty) { // 用flood fill算法判断两名玩家是否连通...}create_walls() {...}start() {...}update_size() { // 每一帧更新地图大小...}check_ready() { // 判断两条蛇是否都准备好下一回合了for (const snake of this.snakes) {if (snake.status ! idle || snake.direction -1) return false;}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();}render() {...}
}现在我们只能从前端获得用户的操作即获取用户的键盘输入。为了能够让 Canvas 获取键盘输入需要添加一个 tabindex 属性在 GameMap.vue 中进行修改
templatediv refparent classgamemapcanvas refcanvas tabindex0/canvas/div
/templatescript
...
/scriptstyle scoped
...
/style这样我们就能够在 GameMap.js 中绑定键盘的监听事件
import { AcGameObject } from ./AcGameObject;
import { Wall } from ./Wall;
import { Snake } from ./Snake;export class GameMap extends AcGameObject {...add_listening_events() {this.ctx.canvas.focus(); // 使Canvas聚焦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);});}start() {for (let i 0; i 10000; i) { // 暴力枚举直至生成合法的地图if (this.create_walls())break;}this.add_listening_events();}...
}现在我们即可在 Snake.js 中实现蛇的移动
import { AcGameObject } from ./AcGameObject;
import { Cell } from ./Cell;export class Snake extends AcGameObject {constructor(info, gamemap) {super();this.id info.id; // 每条蛇有唯一的id进行区分this.color info.color; // 颜色this.gamemap gamemap;this.cells [new Cell(info.r, info.c)]; // 初始化时只有一个点即cells[0]表示蛇头this.speed 2; // 蛇每秒走2个格子this.direction -1; // 下一步移动的指令-1表示没有指令0、1、2、3分别表示上、右、下、左this.status idle; // 蛇的状态idle表示静止move表示正在移动die表示死亡this.next_cell null; // 下一步的目标位置this.step 0; // 回合数this.dr [-1, 0, 1, 0];this.dc [0, 1, 0, -1];this.eps 0.01; // 误差}start() {}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])); // 注意要深层复制一份还有一个细节是JS的数组越界不会出错}}set_direction(d) { // 由于未来不一定只会从键盘获取输入因此实现一个接口修改directionthis.direction d;}update_move() { // 将头节点cells[0]向目标节点next_cell移动const dx this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量const dy this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量const distance Math.sqrt(dx * dx dy * dy); // 与目的地的距离if (distance this.eps) { // 已经走到目标点this.status idle; // 状态变为静止this.cells[0] this.next_cell; // 将头部更新为目标点this.next_cell null;} else {const move_length this.speed * this.timedelta / 1000; // 每一帧移动的距离const cos_theta dx / distance; // cos值const sin_theta dy / distance; // sin值this.cells[0].x move_length * cos_theta;this.cells[0].y move_length * sin_theta;}}update() {if (this.status move) { // 只有移动状态才执行update_move函数this.update_move();}this.render();}render() {...}
}接着我们还需要实现蛇尾的移动如果蛇的长度增加了一个单位那么尾部不用动即可否则尾部需要向前一个节点移动且当移动完成后需要将尾部节点对象删去
import { AcGameObject } from ./AcGameObject;
import { Cell } from ./Cell;export class Snake extends AcGameObject {...check_tail_increasing() { // 检测当前回合蛇的长度是否增加if (this.step 7) return true; // 前7回合每一回合长度都增加if (this.step % 3 1) return true; // 之后每3回合增加一次长度return false;}update_move() { // 将头节点cells[0]向目标节点next_cell移动const dx this.next_cell.x - this.cells[0].x; // 在x方向上与目的地的偏移量const dy this.next_cell.y - this.cells[0].y; // 在y方向上与目的地的偏移量const distance Math.sqrt(dx * dx dy * dy); // 与目的地的距离if (distance this.eps) { // 已经走到目标点this.status idle; // 状态变为静止this.cells[0] this.next_cell; // 将头部更新为目标点this.next_cell null;if (!this.check_tail_increasing()) { // 尾部没有变长则移动完成后要删去尾部this.cells.pop();}} else {const move_length this.speed * this.timedelta / 1000; // 每一帧移动的距离const cos_theta dx / distance; // cos值const sin_theta dy / distance; // sin值this.cells[0].x move_length * cos_theta;this.cells[0].y move_length * sin_theta;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_length * tail_dx / distance; // 此处就不分开计算cos和sin了tail.y move_length * tail_dy / distance;}}}update() {if (this.status move) { // 只有移动状态才执行update_move函数this.update_move();}this.render();}...
}4. 优化蛇的身体效果
现在我们蛇的身体还是分开的若干个圆球没有连续感。我们可以在两个相邻的圆球中间绘制一个矩形覆盖一遍即可。然后我们这边再做个小优化将蛇的半径缩小一点不然贴在一起时就会融合在一起不好看
import { AcGameObject } from ./AcGameObject;
import { Cell } from ./Cell;export class Snake extends AcGameObject {constructor(info, gamemap) {...this.radius 0.4; // 蛇中每个节点的半径...}...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 * this.radius, 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值为两者的最小值因为越往上y越小ctx.fillRect((a.x - this.radius) * L, Math.min(a.y, b.y) * L, 2 * this.radius * L, Math.abs(a.y - b.y) * L);} else { // 左右排列画法同理ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - this.radius) * L, Math.abs(a.x - b.x) * L, 2 * this.radius * L);}}}
}5. 碰撞检测实现