3.实现蛇和基本游戏界面

1. 整体框架

逻辑框架图


2. 修改地图


思想

如果两条蛇,同时走到相同的格子,会造成平局,这种情况会对优势者不利!
需要把地图变为 偶数奇数

修改代码:

x = rows - 1 - x;

y = cols - 1 - y;

实现方式 :

包含以下位置需要修改:

修改地图1


地图2


成功界面:

地图修改sucess


3. 实现蛇的头部


1. 思想

如何画蛇 -> 本质上蛇是由一堆格子组成的序列。

2. 实现

  1. 新建 Cell.js 存储一堆格子的序列 --> 存储蛇。
export class Cell {
    constructor(r, c) {
        this.r = r;
        this.c = c;
        // 转换为 canvas 的坐标
        this.x = c + 0.5;  
        this.y = r + 0.5;
    }
}

蛇头1


  1. 新建 Snake.js 对象,方便我们进行操作。
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";


export class Snake extends AcGameObject {
    constructor(info, gamemap) {
        super();
        
        // 取出基本的id
        this.id = info.id;
        this.color = info.color;
        this.gamemap = gamemap; // 方便调用函数和参数
        
        //存放蛇的身体;
        this.cells = [new Cell(info.r, info.c)];
       
    }
    
    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();
        }
    }
}

蛇头2


  1. GameMap.js 中创建两条蛇的对象。
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),
}

蛇头3


  1. 成功界面

蛇头sucess


4. 实现蛇的移动

思想
移动应该是连贯的。

身体有多个部分,如何让保持连贯?

中间保持不动,头和尾动,在头部创建一个新的节点,朝着目的地移动。尾巴朝着目的地动

蛇什么时候可以动?

同时获取两个人 / 两个机器 的操作才能动。

4.1 基本的移动

  1. Snake.js 中添加代码,实现蛇头的向右移动。
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
    constructor(info, gamemap) {
        super(); // 继承AcGameObject的方法

        this.id = info.id;
        this.color = info.color;
        this.gamemap = gamemap;

        this.cells = [new Cell(info.r, info.c)]; // 存放蛇的身体, cell[0] 存放蛇头
		// new add
        this.speed = 5;
    }


    update_move() {
        // 向右移动
        this.cells[0].x += this.speed * this.timedelta / 1000;
        //向上移动
        //this.cells[0].y -= this.speed * this.timedelta / 1000;
    }

    update() {
        this.update_move();
        this.render();
    }

在这里插入图片描述

  1. 大致效果:
    蓝色的球一直往右移动,红色的已经移动到看不见的地方了。

向右移动sucess


4.2 连贯的移动

  1. 由于可能会产生一些问题, 也就是中间某个状态,没有完全移出去,蛇的身子会出现问题。
  2. 中间不动,首尾动!创建的虚拟节点朝着目的地移动。只有两个点动。
  3. 考虑蛇什么时候动? 回合制游戏,两个人都有输入的时候,才可以移动。
  1. 修改 Snake.js
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)];
        
        this.speed = 5; // 蛇每秒走5格
        // new add
        this.direction = -1; // -1表示没有指令 0 1 2 3 
    	this.status = "idle"; // 静止, move 移动 die 死亡
    }  
}

基本移动


  1. 需要有一个裁判来判断两条蛇的移动,我们放在 GameMap.js
check_ready() { // 判断两条蛇是否准备下一回合了
   for (const snake of this.snakes) {
      if (snake.status !== "idle") return false;
      if (snake.direction === -1) return false;
     }
  return true;  
}

裁判逻辑


  1. Snake.js 中更新一下蛇的状态:
this.next_cell = null; //下一步的目标位置

this.dr = [-1, 0, 1, 0]; // 行
this.dc = [0, 1, 0, -1]; //列

this.step = 0;

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

在这里插入图片描述


  1. GameMap.js 中更新
next_step() {
    for (const snake of this.snake) {
        snake.next_step();
    }
}

update() {
    this.update_size();
    if (this.check_ready()) {
        this.next_step();
    }
    this.render();
}

基本移动4


4.3 读取键盘的操作:

从键盘获取操作 w a s d 来控制两条蛇。

  1. GameMap.vue中修改
<canvas ref="canvas" tabindex="0"></canvas>

在这里插入图片描述


  1. 绑定事件。

Snake.js 中加入一个辅助函数,用来获取方向。

//辅助函数
set_direction(d) {
    this.direction = d;
}

在这里插入图片描述


GameMap.js 中修改,添加事件。

add_listening_events() {
        this.ctx.canvas.focus();

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

在这里插入图片描述


  1. Snake.js 中更新状态:
update() { // 每一帧执行一次
    if (this.status === 'move') {
        this.uppdate_move()
    }

    this.render();
}

在这里插入图片描述


4.4 实现真正的移动

snake.js 中修改 :

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[0]存放蛇头
        this.next_cell = null;  // 下一步的目标位置

        this.speed = 5;  // 蛇每秒走5个格子
        this.direction = -1;  // -1表示没有指令,0、1、2、3表示上右下左
        this.status = "idle";  // idle表示静止,move表示正在移动,die表示死亡

        this.dr = [-1, 0, 1, 0];  // 4个方向行的偏移量
        this.dc = [0, 1, 0, -1];  // 4个方向列的偏移量

        this.step = 0;  // 表示回合数
        this.eps = 1e-2;  // 允许的误差

    }


    start() {
        
    }

    set_direction(d) {
        this.direction = d;
    }

    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]));
        }

    }

    update_move() {
        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";  // 走完了,停下来

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

    update() {  // 每一帧执行一次
        if (this.status === 'move') {
            this.update_move();
        }

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

4.5 蛇尾移动

  1. Snake.js 中添加代码,判断蛇尾是否增长。
check_tail_increasing() {
    if (step <= 10) return true;
    if (step % 3 === 1) return true;
    return false;
}

在这里插入图片描述


  1. 修改 Snake.js , 判断蛇尾是否在下一步是否增长
this.next_cell = null; //下一步的目标位置

this.dr = [-1, 0, 1, 0]; // 行
this.dc = [0, 1, 0, -1]; //列

this.step = 0;

this.eps = 1e-2 // 允许的误差

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]));
    }
    
}

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

在这里插入图片描述


  1. 基本效果

在这里插入图片描述


4.6 美化蛇

修改 Snake.js ,让蛇变得连贯、缩小一点。添加下列代码:

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.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) {
                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);
            }
        }
    }
  1. 基本效果:

在这里插入图片描述


4.7 检测非法逻辑

  1. GameMap.js中更新
check_valid(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;
    }

在这里插入图片描述


  1. snake.js中更新
next_step() {
        if (!this.gamemap.check_valid(this.next_cell)) {
        this.status = "die";
    }
}


render() {
    if (this.status === "die") {
        ctx.fillStyle = "white";
    }
}

在这里插入图片描述


在这里插入图片描述


  1. 最终效果,红色的蛇撞墙死去:

在这里插入图片描述


4.8 实现眼睛

  1. 修改snake.js
this.eye_direction = 0;
if (this.id === 1) this.eye_direction = 2;

this.eye_dx = [
    [-1, 1];
    [1, 1];
	[1, -1];
	[-1, -1];
];
this.eye_dy = [
    [-1, -1];
    [-1, 1];
	[1, 1];
	[1, -1];
];


next_step() {
    this.eye_direction = d;
    
}

render() {
    ctx.fillStyle = "black";
        for (let i = 0; i < 2; i ++ ) {
            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, L * 0.05, 0, Math.PI * 2);
            ctx.fill();
        }
}

在这里插入图片描述


在这里插入图片描述


效果:


在这里插入图片描述


至此,我们实现了最基本的地图和两条蛇的移动。

5 代码地址

https://git.acwing.com/syy/kob/-/commit/2ee3e35f352dbc104e69491db86856fe5d510ad6

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值