手把手教你完成第一个JS项目:用简单到极致的贪吃蛇游戏熟悉JS语法

大家好,我是ChinaManor,直译过来就是中国码农的意思,我希望自己能成为国家复兴道路的铺路人,大数据领域的耕耘者,平凡但不甘于平庸的人。

前言

贪吃蛇被业内视为真正意义上的第一款手机游戏,玩法简单到极致,随着诺基亚手机的流行风靡一时!在本次课程中我们采用Pixelbox.js这个框架进行开发,其核心是数据模型及渲染。通过贪吃蛇的开发,我们将对JS的语法更加的熟悉,同时学习如何把一个需求给分解成具体的开发步骤,培养你做项目的思维。

我们先来看一下开发完后的效果:
在这里插入图片描述

是不是很有复古的气息!!!

思路分析:

在这里插入图片描述

第一关 Pixelbox.js的下载和使用

1.Pixelbox.js的下载

下载页面:https://pixwlk.itch.io/pixelbox
github:https://github.com/cstoquer/pixelbox
Pixelbox的下载是免费的的,下载完毕后我们会得到一个安装包,把压缩包解压到你喜欢的位置上就会得到安装文件。
在这里插入图片描述

双击它进行安装,它会自动安装,并不需要我们的操作就能完成,安装完毕后我们打开看一下,你会看到一个很简洁的页面,只有新建和打开两个按钮。

在这里插入图片描述
2.Pixelbox项目创建和基本配置
在下载安装成功之后,我们来新建一个Pixelbox的项目。点击New后,我们输入好项目名,然后点击Location选择创建项目所在的路径,填写完毕我们点击Create project创建项目。
在这里插入图片描述
此时,你可以看到Pixelbox跳转到了项目的界面。如下图所示,整个界面包括了菜单栏、assets、map等在内的6个区域,我们可以分别了解一下它们的作用。
在这里插入图片描述
先来看看我们在这个贪吃蛇小游戏中会用到的几个功能:
(1)assets。assets面板是项目资源的文件夹目录,游戏中用到的图片、地图等资源都是放在这个文件夹中。可以看到项目自带了palette.png和tilesheet.png两个图片的资源文件。

在这里插入图片描述
(2)palette。palette面板对应的是assets文件夹中的palette.png文件。可以看到这个面板中有16个颜色,并且有对应的编号,这个我们会用到。
在这里插入图片描述
(3)菜单栏。View菜单内可以控制界面中各面板的显示。Project菜单用来设置项目的各项配置。Debug菜单用来调试程序。Run用来运行项目。
来,和我一起看看Project菜单
在这里插入图片描述
点开Project内的Settings。Settings的screen面板用来配置游戏窗口和tile、文字的像素大小,我们不用改这里的配置,但是要记住游戏窗口的宽和高都是128像素,我们在开发中会用到这个值。
在这里插入图片描述
controls面板keyboard用来设置操作游戏的按键,默认是上下左右箭头、空格和X键,我们可以修改默认按键。
在这里插入图片描述
keyboard分了三列,第一列是别名,这个名字是我们在编程中用到的,比如说如果按↑键时要执行一些操作,我们就要通过btn.up来得到这个输入,点击后可以修改。第二列是按键对应的keycode。第三列就是按键名,我们可以点击点击其中一个来更换按键,在本课中我们要把Space键换成R键。

在这里插入图片描述
3.Pixelbox项目的目录结构
在前面的内容中,我们已经完成了基本配置,现在可以开发了。不过开发我们就几乎用不到这个软件了,我们还是要使用visual code来编辑代码。用vscode打开这个项目,看一下目录结构:
在这里插入图片描述
assets用来放置游戏中用到的资源,这个我们已经知道了。
audio用来放置游戏中用到的声音文件。
build用来放置编译好的游戏文件。
node_modules,node.js标准的模块文件夹。
src,源码目录。
tools,工具,其中的settings.json可以修改Pixelbox软件的各项配置。

4.调试代码
我们现在打开src中的main.js文件。里面有一个函数。

exports.update = function () {

};

这个函数会被Pixelbox在游戏的运行中不停的、持续的调用,也就是说我们所有的游戏代码都是在update函数内来执行的。

我们先在update函数中输入以下代码,来看看效果。

exports.update = function () {
  cls();
  paper(15);
    rectf(0, 0, 10, 10);
  paper(0);
};

你现在还不用明白代码的意思,我们在后面的内容中会详细讲解的~。保存好代码,然后点击Pixelbox的Run键。
在这里插入图片描述
然后游戏窗口就出现了,如果没错的话,窗口中的左上角有一个蓝色的方块。
在这里插入图片描述
现在我们再把这个方块改成黄色。

exports.update = function () {
  cls();
  paper(11);//!!!!!!!改了这里
    rectf(0, 0, 10, 10);
  paper(0);
};

(1)使用开发工具进行调试
然后关闭游戏窗口,再点击Run打开……是不是很麻烦,每次修改代码都要关闭游戏窗口再打开。这时,Debug菜单就有用武之地了。我们点开Debug菜单,然后选中Devtool。
在这里插入图片描述
选中后再点击Run来运行游戏。
在这里插入图片描述
此时可以看到,打开游戏窗口的同时还打开了一个Devtool,是不是很眼熟?它就是chrome里面的F12,打开调试工具后我们再修改代码,就可以选中调试工具窗口,然后按F5来刷新游戏窗口啦。

(2)使用浏览器调试
眼尖的同学估计还发现了一个更简单地调试方法。就是复制游戏的地址。
在这里插入图片描述

点击这一项后,我们就会复制游戏所在的本地服务器地址,然后把地址粘贴进浏览器,然后通过浏览器进行调试了。
在这里插入图片描述

在本节中我们介绍了Pixelbox的基本内容,虽然不全面,但都是我们这次课中会用到的。

第二关 孵一条小蛇-蛇的创建

1.思路分析
表面上,游戏中我们是操作一条蛇在吃蛋,那么游戏里的蛇是什么呢?它是一个个的小方块组成的长条状物体!没错,不过这个只是表面现象,其实我们操作的是一个数据!而我们看到的蛇,就是根据这个数据而渲染出来的。
这就好比我们打开电商网站看到的商品一样,为什么商品列表中是你看到的这个图片?为什么这个商品就是这个价格?这个问题虽然看起来有点白痴,但表达的意思就是服务器把这个商品的数据给我们传过来了,然后前端根据这个数据渲染出来的商品列表,所以你看到的其实是数据的一种可视化的表现形式。

回到我们的贪吃蛇中。根据上面这个想法,我们可以把游戏分成两块,第一,游戏的数据,第二,根据数据渲染成我们看到的游戏画面。
OK,那我们就先研究一下蛇到底是个什么数据。如下图所示,看看下面这条蛇,我们可以用什么数据来表示呢?

在这里插入图片描述

像下图这样呢?我们把它切成一小块儿一小块儿的,你是不是已经想出来啦?
在这里插入图片描述

对,我们就用数组这个数据结构来表示蛇。

第一个问题解决了,蛇用数组表示。那么数组里放什么呢?好问题,再看看下面这张图:
在这里插入图片描述

首先,我们通过上一节课的Settings知道游戏窗口的宽和高是128px,经过我的测试,我发现把蛇的每一块设为4px是效果最好的,所以,我们按照4px的宽高把整个游戏窗口打上网格,横向为x轴,纵向为y轴。这样,蛇的每一部分正好可以填满一个网格,我们在给x和y轴标上序号,从0开始,这样,每一个网格就就独一无二的坐标了,以(x, y)的形式来表现,比如图片中的蛇所在的方块就是(4, 5),(5, 5),(6, 5),(7, 5),(8, 5)。是不是明白了?我们用数组来表示蛇,而数组中的元素就是坐标。

OK,关于蛇的思路我们已经了解了,下面我们来写代码。

2.蛇的数据结构

let snake = [{ x: 4, y: 5 }{ x: 5, y: 5 }{ x: 6, y: 5 }];
//我们用数组来表示蛇,数组里面是坐标

经过前面思路的整理,我们知道了用数组来表示蛇,而数组的内容就是坐标。坐标有x和y两个值,那么坐标就可以使用对象来表示。不过因为在后续的开发中我们不知道会不会对坐标进行拓展,而用对象字面量(也就是{key:value})的形式是不易于拓展的,所以我们要把坐标封装成一个类,然后通过类来创建坐标,这样就很易于拓展了。和我一起来实现一下:

首先,在src文件夹中创建Point.js文件。
在这里插入图片描述
然后,编写Point类。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}
//编写Point类,类中目前只有两个属性,x和y,在构造函数中赋值

export default Point;
//别忘了导出

最后,回到main.js中,把数组中的内容换成Point对象。

import Point from './Point';
//引入Point类
let snake = [new Point(4, 5), new Point(5, 5), new Point(6, 5)];

这样,我们就已经用合理的数据来表示蛇了,下面我们就根据这个数据结构把蛇在游戏窗口中画出来。

3.蛇的渲染
大家都知道动画片的原理,就是一张张快速的切换,从而达到动起来的效果。我们开发游戏也是如此,在上一节中我们说过,我们的主要程序代码最终要在update函数执行,所以画图的代码要写在update函数中,而update函数会在游戏程序运行的过程中不停的被调用,所以,画好一个画面就要清空,然后再画一个画面,如此反复循环就构成了动态游戏换面。
简单来说,整个渲染中其实只有两个操作,一个清空画面,一个绘制画面。Pixelbox给我们提供了cls函数来清空画面。

exports.update = function() {
    cls();
}

如何清空我们已经会了,下面我们学习怎么画。
因为我们的贪吃蛇游戏除了文字都是方块,所以我们就画一个方块。Pixelbox给我们提供了rect和rectf两个函数来画方块。它们的区别在于:rect是画的方框,是空心的;而rectf是方块,中心是填满颜色的。我们试一试(要注意看代码注释哦~)。

/* x,横坐标位置,最大值为Settings设置的Width减1
 * y,纵坐标位置,最大值为Settings设置的Height减1
 * w,方块的的宽
 * h,放款的高
 * 以上单位均为像素px
 * rect(x, y, w, h)
 * rectf(x, y, w, h)
 * 这俩调用的参数全一样...
 **/
exports.update = function () {
    cls();
      //每次画画前要先清空之前的画面
    rect(0, 0, 4, 4);
      //然后我们在画面左上角0,0的位置画个宽高为4px方框
    rectf(10, 10, 4, 4);
      //在10,10的位置画宽高为4px的实心方块
};

你可以看到每个颜色都有一个数字编号,那就是colorId!为了方便开发,我们先决定好所有会使用的颜色,我把蛇设为7绿色,蛋设为10橙色,背景设为0黑色,分数文字设为11黄色,当然你也可以设为自己喜欢的颜色。

/*    pen(colorId)
 *    paper(colorId)
 **/
const BACKGROUND_COLOR = 0,
      SNAKE_COLOR = 7,
      EGG_COLOR = 10;
//这里把颜色设为常量的原因就是方便修改,如果哪天想把蛇变个颜色我们只要改这里就好,
//而不用去代码中寻找颜色的代码了

exports.update = function() {
    cls();
  //先清空画面
  rect(0, 0, 4, 4);
  //画个框
  paper(SNAKE_COLOR);
  //先把paper color换成蛇的颜色,也就是绿色
  rectf(10, 10, 4, 4);
  //然后画出来的方块就是绿色的
  paper(BACKGROUND_COLOR);
  //最后把paper color设置成黑色,这样背景色就是黑的了
}

现在,你已经可以画出方块了,这回我们可以根据蛇的数据来渲染画面了(要仔细看代码注释哦)。

import Point from './Point';
//引入Point类

const BACKGROUND_COLOR = 0,
      SNAKE_COLOR = 7,
      EGG_COLOR = 10;

let snake = [new Point(4, 5), new Point(5, 5), new Point(6, 5)];
//定义蛇
exports.update = function () {
    cls();
    paper(SNAKE_COLOR);

      //蛇是一个数组,需要循环
    for (let i = 0, len = snake.length; i < len; i++) {
          //我们曾说过,游戏窗口的宽高为128px,蛇的每一块都是4px,我们把蛇的块当做基本单位,
          //这样整个游戏的画面就是宽高32个单位,而(4, 5)的坐标值乘以4就可以转换成实际的像素值
        rectf(snake[i].x * 4, snake[i].y * 4, 4, 4);
          //根据蛇的每一块坐标乘以4就是实际的像素值,宽高也是4px
    }

    paper(BACKGROUND_COLOR);

4.代码的封装

import Point from './Point';

const BACKGROUND_COLOR = 0,
      SNAKE_COLOR = 7,
      EGG_COLOR = 10;

class Game {
    constructor() {
        this.snake = [new Point(4, 5), new Point(5, 5), new Point(6, 5)];
          //把snake数组从变量变成对象的属性
    }

    updateData() {
        //由于我们现在还没有让蛇动起来,所以这里先空着
    }

    draw() {
          //把画图的代码放进这里
        cls();
        paper(SNAKE_COLOR);
        for (let i = 0, len = this.snake.length; i < len; i++) {
            rectf(this.snake[i].x * 4, this.snake[i].y * 4, 4, 4);
        }
        paper(BACKGROUND_COLOR);
    }
}

export default Game;

再打开main.js进行修改。

//main.js
import Game from './Game';
//引入Game类

const game = new Game();
//创建game对象

exports.update = function () {
      //调用game对象的更新数据方法和画画方法
    game.updateData();
    game.draw();
};

这样代码看起来就舒服多了。我们已经有了蛇的数据结构,并且根据这个数据结构在游戏窗口中画出了蛇。

第三关 让小蛇动起来-移动

1.蛇的移动
说起移动,我们首先想到的就是方向,只要是移动就一定有一个方向,在贪吃蛇这个游戏中蛇的移动方向只有四个,上、下、左、右,而且这四个方向是固定的,所以我们先定义好这四个方向的常量。那么问题来了,方向应该是什么数据呢?仔细看下图:
在这里插入图片描述
坐标(4, 5)为蛇,可以看出如果蛇向左走,那么x坐标-1,y坐标不变;向右走x+1,y坐标不变;向上走y-1,x坐标不变;向下走y+1,x坐标不变。也就是说,移动就是当蛇只有一个点时(也可以理解为蛇的头部),蛇的坐标(x,y)加另一个坐标。举个例子:坐标为(4,5)的点向左移动,那么就是(4 ,5) + (-1, 0),移动后的坐标为(3,5);向右就是(4, 5) + (1, 0),移动后坐标为(5,5)。所以现在你明白了,方向应该也是坐标。

打开Game.js文件

//Game.js
import Point from './Point';

const UP = new Point(0, -1),
    DOWN = new Point(0, 1),
    LEFT = new Point(-1, 0),
    RIGHT = new Point(1, 0);

//...省略下方

四个方向定义好了之后,我们还要定义一个蛇当前的移动方向,我先默认蛇的移动方向是右边。

import Point from './Point';

const UP = new Point(0, -1),
    DOWN = new Point(0, 1),
    LEFT = new Point(-1, 0),
    RIGHT = new Point(1, 0);

class Game {
    constructor() {
        this.snake = [new Point(4, 5)];
          //先把蛇设为只有一个点
        this.direction = RIGHT;
          //定义了一个蛇的当前移动方向的属性,默认向右走
    }
      //...省略下方
}

蛇的移动方向有,我们该让它动起来了。那么我们该什么时候让它移动呢?游戏开始到结束的时候!也就是说,如果我们没有写蛇的死亡判定时,它会一直走下去。换句话说,蛇的移动方法应该在update函数中一直被调用,因为我们封装了代码,所以要在updateData函数中调用行走的方法。

class Game {
    //...省略
  move() {

  }

  updateData() {
      this.move();
  }
  //...省略
}

我们可以改变蛇的坐标来让蛇移动,这个你已经知道了。那么换个思路,如果我们新建一个Point对象,这个新的Point对象的x和y是当前蛇的坐标和方向坐标相加的值,然后把这个新的Point对象变成蛇的头,然后再把蛇的最后一个Point对象删掉(因为蛇本质是一个数组,我们在数组前添加一个Point,再删除最后一个Point,让蛇的长度始终保持相同),是不是也可以让蛇移动起来?

接下来我们就用这种方法来实现蛇的移动。有的小伙伴可能会问:为什么要用这么麻烦的方法?因为我看过剧本,这样写最简单……一会你就明白了我们为什么要这么写!
下面我们就开始写移动的方法。
1

move {
    const oldHead = this.snake[0];
  //蛇移动前的头部
  let newHead = new Point(oldHead.x + this.direction.x, oldHead.y + this.direction.y);
  //创建一个新的头部,x和y是没移动前的头部的坐标值与方向坐标值相加
  this.snake.unshift(newHead);
  //把新头部添加进数组
  this.snake.pop();
  //再删除最后一位
}

我们写一下控制调用速度的代码。

constructor() {
  this.snake = [new Point(4, 5)];
  this.direction = RIGHT;
  this.count = 0;
  //新加一个count属性,用来控制调用速度
}

updateData() {
    this.count++;
  //每调用一次都count加1
  if(this.count === 3) {
  //如果count等于3了,才调用一次move函数,也就是说一秒钟只调用了20次
      this.count = 0;
    //count等于3后一定要归零
    this.move();
    //调用move函数
  }
}

在这里插入图片描述
2.控制移动
蛇不是随意移动的,它是在一定的规则下进行移动的。玩过这个游戏的同学应该知道,当蛇在向右移动的过程中不能将方向改成向左的,我们只可以将蛇的移动方向改变成上或下的,而蛇向上移动的过程中也无法改成向下移动,只能变成左或右。所以,当蛇在移动的过程中,不能将蛇的方向改变成它移动的相反方向,只能改变成除当前移动方向和当前移动的相反方向外的其它两个方向。

updateDirection() {
    //定义更新方向函数,当我们按下上下左右按键时,更改蛇的当前移动方向
  //通过判断btn.方向来监听上下左右键的输入指令
  if(btn.right) {
      if(this.direction !== LEFT) {
        //正在往左行动时,按右键无效
      this.direction = RIGHT;
    }
  } else if(btn.left) {
      if(!this.direction !== RIGHT) {
        //正在往右行动时,按左键无效
      this.direction = LEFT;
    }
  } else if(btn.up) {
      if(!this.direction !== DOWN) {
        //正在往下行动时,按上键无效
      this.direction = UP;
    }
  } else if(btn.down) {
      if(!this.direction !== UP) {
        //正在往上行动时,按下键无效
      this.direction = DOWN;
    }
  }
}

updateData() {
  this.count++;
  if (this.count === 3) {
    this.count = 0;
    this.updateDirection();
    //在调用move函数上方调用更新方向函数
    this.move();
  }
}

这样蛇已经可以移动起来了

老实说,现在这么一个独立的小块儿实在不像一条蛇!那么我们把蛇变长一些来看看效果。

constructor() {
  this.snakeInit();
  //为了方便测试,我们不在构造函数里直接创建蛇了,我们新写一个蛇的初始化函数
  this.direction = RIGHT;
  this.count = 0;
}

snakeInit() {
    this.snake = [];
  for(let i = 5; i >= 0; i--) {
      //因为我们默认是向右移动,所以要倒着循环,定义蛇的长度为5
    this.snake.push(new Point(i, 5));
    //x的值是变化的,y不变
  }
}

在这里插入图片描述

第四关 贪吃的小蛇-蛇吃蛋

1.蛋的生成
蛇要吃蛋,就得要先有蛋,所以我们要先做蛋的生成函数。相信不用说你也知道,蛋其实也是一个Point对象。关于蛋的生成规则有两个,第一,要在游戏窗口的范围内,第二,不能跟蛇重复,第三,蛋的生成是游戏开始时就会生成一个和当蛋被蛇吃掉时生成一个。

reset() {
    //别忘了,我们把构造函数中的内容搬到了reset函数中
  ...
  this.egg = null;
  //定义一个蛋的属性
  this.generateEgg();
  //在游戏里一开始就调用一次来生成一个蛋
}
//生成蛋的方法
generateEgg() {
  let stringArr = this.snake.map(e => e.toString());
  //先获取蛇数组转换成字符串后的数组
  let tempEgg = this.snake[0].toString();
  //设置一个临时变量,用来当做蛋坐标的字符串形式,默认跟蛇的头部相等
  while (stringArr.includes(tempEgg)) {
    //判断临时蛋的坐标是不是在跟蛇有重复,因为默认蛋跟蛇的头部相等,所以该循环至少会执行一次
    let x = random(32),
        //Pixelbox给我们提供了random函数来生成0-n但不包含n的随机数
        //之前说过我们把游戏窗口分成了32个单位,但实际值是0-31,所以random的参数是32
        y = random(32);
            //生成两个随机数
    tempEgg = `${x}${y}`;
    //以字符串形式赋值给临时蛋,用来判断是否进行下一次循环
    this.egg = new Point(x, y);
    //再用相同的值创建一个Point对象赋值给egg属性
  }
}

光生成蛋的数据不行,我们还要把蛋画出来。蛋的颜色用橙色10号表示。

snakeInit() {
  //把蛇设置的短一些,方便测试
  this.snake = [];
  for (let i = 0; i >= 0; i--) {
    this.snake.push(new Point(i, 5));
  }
}

draw() {
    cls();
  if (!this.isDeath) {
    paper(SNAKE_COLOR);
    for (let i = 0, len = this.snake.length; i < len; i++) {
      rectf(this.snake[i].x * 4, this.snake[i].y * 4, 4, 4);
    }
    //↓这里!!!!!!!!
    paper(EGG_COLOR);
    //设置蛋的颜色为橙色
    rectf(this.egg.x * 4, this.egg.y * 4, 4, 4);
    //根据蛋的坐标画在相应位置画出来
    //↑这里!!!!!!!!
    paper(BACKGROUND_COLOR);
  } else {
    ...
  }
}

2.蛋的碰撞检测

这个检测的时机很重要,大家可以先思考一下应该什么时候进行检测。最方便快捷的做法就是蛇每走一步都进行一次检测。检测如果蛇的头部坐标跟蛋相等了,那么就表示蛋被吃掉了,同时重新执行一次生成蛋的函数,不要忘了还要在蛇的末尾再添加一个Point对象,因为吃了一个蛋,蛇就会变长一节。
那么该如何给蛇的尾部添加呢?还记得我们写蛇的移动函数时的方法吗?根据蛇的移动方向,把蛇没移动前的坐标与方向坐标相加,然后放在数组的最前面,最后再把蛇数组的最后一位删除掉。那么在检测到蛇跟蛋碰撞后,我们只要把删除的尾部再放回末尾的位置就好了。

reset() {
  ...
    this.popPoint = null;
  //用来保存被删掉的Point对象
}

move() {
  ...
  this.popPoint = this.snake.pop();
  //删除最后一位,不过要把删掉的Point对象保存进oioPoint属性中
}

checkEgg() {
  if(this.snake[0].toString() === this.egg.toString()) {
    //判断蛇头部的坐标跟蛋的坐标是否相等,如果相等就表示碰到了
    this.snake.push(this.popPoint);
    //把删掉的尾部再放回来
    this.generateEgg();
    //重新执行生成蛋的函数
    this.score += 1;
    //别忘了吃一个蛋加1分
  }
}

3.显示分数
为了让玩家能实时的看到自己的分数,我们最后再把实时的分数显示在左上角。为了美观,我们再把文字的颜色改成黄色11号,因为所有的文字我们都改成黄色,并且不会再变,所以只在初始化时设置一次就好。

reset() {
    ...
  pen(11);
  //把文字设置成黄色
}

snakeInit() {
  this.snake = [];
  //给蛇的长度设为1
  for (let i = 0; i >= 0; i--) {
    this.snake.push(new Point(i, 5));
  }
}

draw() {
  cls();
  if (!this.isDeath) {
    ...
    print(`SCORE: ${this.score}`);
    //在这里加上一行print就行
    paper(BACKGROUND_COLOR);
  } else {
    ...
  }
}

再次再运行一下看看

在这里插入图片描述

第五关 蛇之死-死亡判定

这节课中我们来实现一下蛇的死亡判定的功能。
我们了解一下蛇被判定死亡的规则:1.当蛇的头部触碰到游戏窗口边界时判定死亡;2.蛇头触碰到自己的身体时会被判定为死亡。
我们应该在什么时候做死亡的判定呢?答案是蛇每移动一步都要判断。也就是说,当蛇的移动函数被调用后,随即调用死亡判定的函数。

那么蛇死亡后,游戏该有哪些变化呢?首先,updateData函数就不应该再被调用了,因为游戏已经结束了,我们不应该再浪费计算资源来调用移动等方法了。第二,游戏应该跳转到游戏结束界面。所以说,应该有一个属性来表示蛇当前是否死亡。如果死亡了,就不调用updateData内的函数了,并且游戏画面变成GAME OVER画面。

在这里插入图片描述
1.蛇触碰边界死亡

我们先来看第一种情况,当蛇的头部触碰到游戏窗口边界时判定死亡(注意看代码注释哦~)

constructor() {
  this.snakeInit();
  this.direction = RIGHT;
  this.count = 0;
  this.isDeath = false;
  //用来表示蛇是否死亡
}

checkDeath() {
    //有两种情况会让蛇死亡,1.触碰边界。2.触碰自己身体,我们先实现第一种情况
  const head = this.snake[0];
  if(
      head.x < 0 || //触碰左边界
    head.x > 31 ||//触碰右边界
    head.y < 0 || //触碰上边界
    head.y > 31   //触碰下边界
    //这里用31来表示边界,这是因为我们在之前把4x4px的方块定义为一个标准单位,游戏窗口为128px,
    //共有32个单位,但是我们是从0开始的,所以31是最大值,
    //也就是说第31个单位是边界
  ) {
      //如果满足这些条件,那就是触碰边界了,蛇死亡了
    this.isDeath = true;
  }
}

updateData() {
  if(this.isDeath) return;
  //如果蛇死亡了,就不继续向下执行了
  this.count++;
  if (this.count === 3) {
    this.count = 0;
    this.updateDirection();
    this.move();
    this.checkDeath();
    //每移动一步,就检查一下蛇是否死亡
  }
}

2.蛇触碰自己的身体死亡

我们接着写触碰自己死亡。那么蛇触碰自己这个操作,在数据中是怎样的一种表现呢?蛇触碰了自己,也就是说蛇的头部坐标跟身体中的某一坐标相等了,出现了重复的(x,y)。
在ES6中,新增了Set这种数据结构,Set的一个特性就是,不会出现重复的数据,如果有重复的数据,那么第二个数据就会被忽略。你是不是已经想明白了?我们说蛇的触碰到自己的身体,就是出现了重复的(x,y)所以我们可以创建一个Set来把这个数组中重复的值去掉,这样Set的长度和snake数组的长度就不相等了,也就是蛇碰到自己了。
注意:数组内是Point对象,即使x和y相同,但它们在内存中的地址不同,Set并不能去掉地址不同的对象但是值重复的元素,所以我们要把Point对象转变成String类型来使用Set去重。我们来丰富一下Point类的方法。

编辑Point.js。

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

  toString() {
    //重写toString方法,返回x和y拼接后的字符串
      return `${this.x}${this.y}`;
  }
}

export default Point;

这个操作首先要保证蛇有一定的长度,所以我们把蛇的长度设为16。

snakeInit() {
  this.snake = [];
  for (let i = 16; i >= 0; i--) {
    //从16开始循环
    this.snake.push(new Point(i, 5));
  }
}

getSetSize() {
  //新增获取去重后的set长度函数
  let stringArr = this.snake.map(e => e.toString());
  //获取转换为字符串类型后的数组
  let set = new Set(stringArr);
  //根据stringArr创建Set,如果stringArr有重复的元素,那么第二个元素将被去掉
  return set.size;
  //返回set的长度
    //return new Set(this.snake.map(e => e.toString())).size;
    //也可以一行实现功能
}

checkDeath() {
  //有两种情况会让蛇死亡,1.触碰边界。2.触碰自己身体,我们先实现第一种情况
  const head = this.snake[0];
  if (
    head.x < 0 || //触碰左边界
    head.x > 31 || //触碰右边界
    head.y < 0 || //触碰上边界
    head.y > 31 //触碰下边界
  ) {
    //如果满足这些条件,那就是触碰边界了,蛇死亡了
    this.isDeath = true;
  } else if(this.getSetSize() !== this.snake.length) {
      //如果两个长度不同了,那么就是触碰自己了
    this.isDeath = true;
  }
}

3.绘制游戏结束画面
Pixelbox给我们提供了print和println方法来显示文字(只能显示英文字母和半角符号)。

/*    text: 要显示的文字
 *    x:    文字所在x轴的位置
 *    y:    文字所在y轴的位置
 *    print(text, [x, y])  注:x,y不是数组,而是表示x或y值是可以省略的
 *    println(text)
 *    
 *  二者区别在于print方法默认把文字打印在光标处,也可以通过参数x,y改变位置,
 *  而println不能随意改变显示位置,但是每调用一次都会换行
 **/

清楚了需求和所需要的方法,我们来对draw函数进行编写。

constructor() {
    ...
  this.score = 0;
  //增加score属性,表示游戏的分数
  ...
}

draw() {
  cls();
  //无论是画什么画面,都要把画面清空
  if (!this.isDeath) {//判断蛇是否死亡
    //如果没死,就绘制游戏的画面,把之前的代码搬进来就好
    paper(7);
    for (let i = 0, len = this.snake.length; i < len; i++) {
      rectf(this.snake[i].x * 4, this.snake[i].y * 4, 4, 4);
    }
    paper(0);
  } else {
    //如果死了,就绘制结束页面
    const GAME_OVER = 'GAME OVER!',
          SCORE = `SCORE:${this.score}`,
          RESTART = 'PRESS (R)ESTART';
    print(GAME_OVER);
    print(SCORE);
    print(RESTART);
  }
}

之前已经说过,文字共有三行,横向和纵向均居中显示,并且已知游戏窗口宽高为128px,文字为4px。知道这些就很容易了编写了,我们再进行一下修改。

draw() {
  cls();
  //无论是画什么画面,都要把画面清空
  if (!this.isDeath) {//判断蛇是否死亡
    //如果没死,就绘制游戏的画面,把之前的代码搬进来就好
    paper(7);
    for (let i = 0, len = this.snake.length; i < len; i++) {
      rectf(this.snake[i].x * 4, this.snake[i].y * 4, 4, 4);
    }
    paper(0);
  } else {
    //如果死了,就绘制结束页面
    const GAME_OVER = 'GAME OVER!',
          SCORE = `SCORE:${this.score}`,
          RESTART = 'PRESS (R)ESTART';
      print(GAME_OVER, 128 / 2 - GAME_OVER.length * 4 / 2, 12 * 4);
      //游戏窗口宽为128px,中间就是64px,用64再减去文字的一半宽度就是让文字居中的位置。
      print(SCORE, 128 / 2 - SCORE.length * 4 / 2, 14 * 4);
      print(RESTART, 128 / 2 - RESTART.length * 4 / 2, 16 * 4);
  }
}

调整过后再来运行看看,画面已经跳转了吧。

在这里插入图片描述
4.【R】键重新开始游戏

constructor(){
    this.reset();
}

reset() {
  this.snakeInit();
  this.direction = RIGHT;
  this.count = 0;
  this.isDeath = false;
  this.score = 0;
}

draw() {
  cls();
  if (!this.isDeath) {
    ...
  } else {
    ...
    if(btn.R) {
      this.reset();
    }
    //在游戏里结束画面,按下R键就重新开始
  }
}

试试成功了没有。

在这里插入图片描述
(由于动态图片无法检测键盘按键,所以我用了鼠标点击来表示游戏开始……)

课程总结

恭喜你完成了使用Pixelbox.js开发贪吃蛇的课程!!!

感谢回车课堂,回车yyds,感谢牛老师

要下的软件和代码,我都打包好了

点下关注之后,私信我免费获取!
为了涨粉也是拼了~
ps:资料已同步更新到 微信公众号:大数据智能ai
在这里插入图片描述
在这里插入图片描述

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AIMaynor

觉得有用,要个免费的三连可有?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值