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

Posted ChinaManor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你完成第一个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对象

以上是关于手把手教你完成第一个JS项目:用简单到极致的贪吃蛇游戏熟悉JS语法的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你使用 Python 制作贪吃蛇游戏

手把手教你使用 Python 制作贪吃蛇游戏

今天大佬手把手教你使用 Python 制作贪吃蛇游戏

手把手教你使用Python写贪吃蛇游戏(pygame)

GUI简单实战——贪吃蛇

python——一步步教你用pyganme完成贪吃蛇小游戏(简易初版)