2022-06-30

基于arduino制作的贪吃蛇游戏(抄开源作业)

因为前面做的一个8*8点阵屏沙漏对我来说想要短时间内完成太难了,涉及到没了解过的硬件和知识,无奈放弃就转头做一个能在Lcd1602上跑的贪吃蛇小游戏,相比较前者而言,对于后者所涉及到的概念早些时候有所了解,虽然从没做过,但也不至于太难上手。

准备工作

  • 一块 ardunio uno (祖国版)因为ardunio提供3.3V和5V,刚好在lcd1602和PS2摇杆的最佳工作电压范围内。详细参数请访问Arduino UNO数据手册(新手入门)

  • 一块LCD1602显示屏,这里用的是IIC(串行通讯总线)接口的

  • 一个五针摇杆模块,可以实现我们游戏的4方位的控制

  • 若干杜邦线和实验用面包板,以及热熔胶

单个硬件模块原理

LCD1602是大家耳熟能详的电子实验硬件了,这里不再过多赘述常用的库函数,可以根据下面提供的代码自行去搜索了解。我们使用的是IIC(I2C)接口的屏幕,对应的库文件可以直接在arduino的IDE去搜索关键字:“LiquidCrystal_I2C”就能找到。

LCD1602硬件说明

IIC「Inter-Integrated Circuit 集成电路总线」是一种串行通信总线,应用于板载低速设备间的通讯。其目的就是为了简化系统硬件设计,减少设备间的连线。IIC 串行总线有两根信号线,一根是双向的数字线SDA,另一根是时钟线SCL,每个IIC设备都有自己的地址,IIC总线上多个设备间通过设备地址进行区别。一把来说哪个硬件控制着时钟线SCL,哪个就是主设备,其余被连接的是从设备。我们的项目主设备就是UNO板,从设备就是LCD1602。

带 I2C 模块的 1602 屏幕背面如图所示。模块上有一颗可调电阻,用于调节显示的对比度。如果你新拿到一块屏幕无论怎么调试都不见显示,记得调节一下这里。

LCD1602的IIC默认地址为0x27,初始化代码如下

#include <LiquidCrystal_I2C.h> //导入库文件
#include <Wire.h>
#define width_LCD 16  //lcd宽度
#define height_LCD 2  //lcd高度
#define address_LCD 0x27  //lcd1602总线地址
LiquidCrystal_I2C  lcd(address_LCD, width_LCD, height_LCD);
void setup() {
 lcd.init();        //初始化lcd
 lcd.backlight();    //打开lcd背光
 /*******进入游戏前的显示界面*******/
 lcd.setCursor(2, 0);  //设置光标位置:第3列,第1行
 lcd.print("Snake_game");  //显示Snake_game
 lcd.setCursor(2, 1);  //设置光标位置:第3列,第2行
 lcd.print("By YouShuHeng");//显示By YSH
 delay(1500);          //显示1.5秒后退出显示,进入到游戏
 lcd.clear();
}

对于1602 的库函数在这里我们虽不做过多解释,但有两个函数需要先进行了解,方便后续理解我们的程序思路:

createChar(number,worddata);*//声明用户自定义字符*

write(byte(number));*//显示自定义字符*

PS2五针摇杆硬件说明

接口定义:

摇杆我们经常在一些游戏设备上有见到过,如XBOX、PS4上,我们今天使用的模块就是类似那些摇杆,但只需要一个就够了。为了后续的游戏,我们必须先知道该模块各个方向上的模拟信号数值,所以要先进行测试。测试的目的主要是每个人的模块可能数值并不一致,若只是照搬代码,可能会出现后续游戏操作功能出现异常。

void setup(){
 Serial.begin(9600);//打开串口调试,波特率9600(注意此处需与串口调试工具下的波特率一致)
 pinMode(A0,INPUT);//x接A0
 pinMode(A1,INPUT);//y接A1
 //z轴方向接口我们的游戏用不到,所以可以不接
}
void loop(){
 int x=analogRead(A0);
 int y=analogRead(A1);
 Serial.print("x:");
 Serial.print(x);
 Serial.print("  and  ");
 Serial.print("y:");
 Serial.println(y);  //打印完换行
}

记住测试的值,我们取差不多的中间值作为判断条件,不需要太准确,如:

left:0

Center:512

right:1024

那左转向我们就取300,右转向就取700;上和下也都同理。

硬件连接图

代码实现

游戏基本特征

用摇杆操控🐍去吃食物的二维平面游戏,利用二维数组创建游戏舞台(矩阵),以摇杆方位作为判断条件实现转向(x++ , x-- ; y++ , y--)。

游戏图像显示的问题解决方案

我们知道1602只有两行的显示单元,若是以此为单位来做我们的游戏,那就未免也太大了。那我们不禁思考,有没有什么办法是可以用一个像素点为单位去进行显示的办法呢?还记得上面对1602硬件描述时的提醒的函数吗:

createChar(number,worddata);*//声明用户自定义字符*

write(byte(number));*//显示自定义字符*

这两个函数是什么意思呢?首先是createChar(number,worddata),这个函数允许用户自己去创建字符,一个字符占用8个字节总共可以创建8个自定义字符。一个字符8个字节是怎么算的呢?我们来具体的看一下

1602的一个显示单元是5*8个像素,对应下面的 列0~4 和 行0~7

0 1 2 3 4 对应16进制 0 1 2 3 4
0 0x0 B 0 0 0 0 0
1 1 1 1 0xE0 B 1 1 1 0 0
2 1 1 0xA0 B 1 0 1 0 0
3 1 1 0xA0 B 1 0 1 0 0
4 1 1 0xA0 B 1 0 1 0 0
5 1 1 1 0xE0 B 1 1 1 0 0
6 0x0 B 0 0 0 0 0
7 0x0 B 0 0 0 0 0

上面是数字“0”的像素表示:

  1. 横向第1个16进制值为0x00,大小为1字节==8位二进制值:B 0 0 0 0 0

  2. 横向第2个16进制值为0xE0,大小为1字节==8位二进制值:B 1 1 1 0 0

  3. 横向第3个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0

  4. 横向第4个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0

  5. 横向第5个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0

  6. 横向第6个16进制值为0xE0,大小为1字节==8位二进制值:B 1 1 1 0 0

  7. 横向第7个16进制值为0x00,大小为1字节==8位二进制值:B 0 0 0 0 0

  8. 横向第8个16进制值为0x00,大小为1字节==8位二进制值:B 0 0 0 0 0

将对应的像素赋于“1”,则可点亮单个像素。创建好自定义字符后,我们就可以让LCD1602显示我们的自定义字符了。显示自定义字符需要使用write()函数实现:

lcd.write(byte(number));//显示自定义字符(number:要自定义字符的编号)

byte zero[8] = {  //创建自定义字符数据zero
 B00000,
 B11100,
 B10100,
 B10100,
 B11100,
 B00000,
 B00000,
};
LiquidCrystal_I2C  lcd(0x27, 16, 2);//初始化lcd显示屏,显示方式为双行每行16个字符
void setup()
{
 lcd.init();          //初始化lcd
 lcd.backlight();     //打开lcd背光
 lcd.createChar(zero,0);  //创建用户字符,字符数据为kj,自定义字符编号为0
 lcd.setCursor(0,0);  //在第一行,第一列的单元内显示像素
 lcd.write(byte(0));  //显示自定义字符,字符编号为0
}

以上面的案例我们已经基本上解决了游戏图像显示的问题,就是利用数组去创建矩阵空间,通过一系列判断决定给哪个元素(像素)赋“1”。

游戏舞台的搭建:

我们界定的舞台范围是2*4个显示单元

#define Point_height 16 //竖向像素长度
#define Point_width 20 //横向像素长度

//定义游戏范围,X=4*5 , Y=2*8
bool Matrix[Point_height][Point_width] = {//数组类型须为byte或bool,否则动态内存空间不足,也可以更换主控为mega2560来解决,我们这里使用的是Uno
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},

 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
};

整体代码预览

#include <LiquidCrystal_I2C.h>
#include <Wire.h>
#define width_LCD 16  //lcd显示宽度
#define height_LCD 2  //lcd显示高度
#define address_LCD 0x27  //lcd1602的I2C总线地址
#define Point_height 16 //像素高度
#define Point_width 20  //像素宽度
#define MAXIMA_LONGITUD_SERPIENTE (Point_height * Point_width)//16*20=320
//蛇最大长度
/**********操纵杆方向**********/
#define joystick_right 0    //右
#define joystick_left 1 //左
#define joystick_top 2      //上
#define joystick_Bottom 3 //下

// 读取操纵杆
const int pinX = A0,pinY = A1;
 /*X轴为A0    Y轴为A1*/
LiquidCrystal_I2C  lcd(address_LCD, width_LCD, height_LCD);//实例化lcd
//定义游戏范围,X=4*5 , Y=2*8
bool Matrix[Point_height][Point_width] = {//数组类型必须为byte或bool
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},

 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
 {0,0,0,0,0,   0,0,0,0,0,    0,0,0,0,0,   0,0,0,0,0,},
};

class Snakes {  //蛇类
 public:
 int x, y;
 Snakes(int a, int b) {  //后续作为矩阵坐标存放到数组中
 x = a; y = b;
 }
 Snakes() {
 }
};

//创建了一个蛇的对象数组,可显示的像素长度为320,相当于是蛇自身的矩阵,后面和食物矩阵做对比
Snakes snk[MAXIMA_LONGITUD_SERPIENTE];
int snakeLength = 0;  //蛇的长度,动态变化
int foodX, foodY;  //定义食物的(x,y)
int direccion = 0;  //判断条件:(上、下、左、右),这里是0,所以缺省往右走
int score = 0;      //累计的分数

void Add_block(int x, int y) {  //设定蛇的初始坐标和长度,调用一次蛇的长度就递增一次
 snk[snakeLength] = Snakes(x, y);//将坐标定位到矩阵中
 snakeLength++;  //计长度,
}

// 计算放置食物的随机坐标
void random_food() {
 foodX = random(0, Point_width);   //0~20内随机
 foodY = random(0, Point_height);  //0~16内随机
}

// 把食物放在矩阵上
void place_food() {
 Matrix[foodY][foodX] = 1;
}

//把🐍的坐标放在矩阵中,将蛇头至蛇尾的像素点亮
void place_snake() {
 for (int i = 0; i < snakeLength; i++) 
 {//这里涉及到传统坐标概念和数组的冲突,数组先数行,再数列
 int x = snk[i].y,
 y = snk[i].x;
 Matrix[x][y] = 1;
 }
}

//移动蛇的坐标
void move_snake() {
 for (int i = snakeLength - 1; i >= 1; i--) {//由蛇自身长度为条件,蛇尾往蛇头方向缩进
 snk[i].x = snk[i - 1].x;
 snk[i].y = snk[i - 1].y; 
 }
 switch (direccion) {//根据之前获取的摇杆方位判断
 case joystick_right:  /*如果是右边*/
 if (snk[0].x + 1 >= Point_width)//如果蛇头超出右边界,那蛇头的x坐标就从0开始回到左边
 snk[0].x = 0;
 else snk[0].x++;    //否则继续前进
 break;
 case joystick_left:   /*如果是左边*/
 if (snk[0].x <= 0)
 snk[0].x = Point_width - 1;//如果蛇头超出左边界,那蛇头的x坐标就从19开始回到右边
 else snk[0].x--;    //否则继续前进
 break;
 case joystick_top:    /*如果是上边*/
 if (snk[0].y <= 0)//如果蛇头超出上边界,那蛇头的y坐标就从15开始回到下边
 snk[0].y = Point_height - 1;
 else snk[0].y--;    //否则继续前进
 break;
 case joystick_Bottom: /*如果是下边*/
 if (snk[0].y + 1 >= Point_height)//如果蛇头超出下边界,那蛇头的y坐标就从0开始回到上边
 snk[0].y = 0;
 else snk[0].y++;    //否则继续前进
 break;
 }
}
//改变方向(获取到的摇杆方向)
void change_direction(int new_direction) {
 if (  //如果摇杆没有方向
 new_direction != joystick_right
 && new_direction != joystick_left
 && new_direction != joystick_top
 && new_direction != joystick_Bottom
 ) {
 return;
 }
 if (//
 (new_direction == joystick_right || new_direction == joystick_left)
 && (direccion == joystick_right || direccion == joystick_left)
 ) return;

 if (
 (new_direction == joystick_top || new_direction == joystick_Bottom)
 && (direccion == joystick_top || direccion == joystick_Bottom)
 ) return;

 direccion = new_direction; //判断条件=摇杆方向
}

// 从操纵杆读取
int Get_direction() {  //获取方向

 int valorX = analogRead(pinX),
 valorY = analogRead(pinY);
 if (valorX > 800) {     //此处数值可根据调试时的实际数值进行更改,以下if判断同理
 return joystick_Bottom;
 } else if (valorX < 200) {
 return joystick_top;
 }

 if (valorY > 800) {
 return joystick_right;
 } else if (valorY < 200) {
 return joystick_left;
 }
 return -1;//无响应,继续执行变量定义时所赋予的初始方向,变量:direccion
}

void setup() {
 lcd.init();        //初始化lcd
 lcd.backlight();    //打开lcd背光
 for (int i = 0; i < 3; i++) {  //将🐍的坐标定位到矩阵中
 Add_block(5, i);
 }
 //Serial.begin(9600);
 random_food();     //计算放置食物的随机坐标

 lcd.setCursor(0, 0);
 lcd.print("Snake_game");
 lcd.setCursor(0, 1);
 lcd.print("By YouShuHeng");
 delay(1500);
 lcd.clear();
}

void Reset_matrix() {  //将矩阵中320个像素赋false,关闭像素显示

 for (int y = 0; y < 16; y++) {
 for (int x = 0; x < 20; x++) {
 Matrix[y][x] = false;
 }
 }
}
/*显示分数,游戏过程保持显示*/
void Score() {
 lcd.setCursor(6, 0);
 lcd.print("score:");
 lcd.setCursor(12, 0);
 lcd.print(score);
}
/*绘制矩阵*/
void Draw_matrix() {
 byte chardata[8];         //自定义字符数据
 int numeroFigura = 0;   //自定义字符编号
 /*
 * 横向'Z'型遍历,直到遍历完整个16*2个光标单元的矩阵空间
 */
 for (int cuadritoX = 0; cuadritoX < 4; cuadritoX++) {
 for (int cuadritoY = 0; cuadritoY < 2; cuadritoY++) {
 /*
 * 遍历单个光标显示单元,一个单元为8*5个像素点,根据上面的嵌套循坏来定位是该矩阵的哪个光标单元
 */
 for (int x = 0; x < 8; x++) {
 int numero = 0;
 int indice = cuadritoY == 0 ? x : (x + 8);  //行:
 int inicio = cuadritoX * 5;                 //列:
 /*
 *二维数组第二个下表为列,所以共有5个列判断,构成一个光标显示单元;
 *根据之前已经实现的🐍和食物都已在矩阵中标记空间坐标,此时五个遍历判断该光标显示单元有哪几个像素点应该被点亮;
 *若想要一个一个像素点的显示,就需要用到自定义字符,我们将需要显示的像素位用十进制的8421码来表示,从左到右起,高位到低位
 */
 if (Matrix[indice][inicio + 0] == 1)
 numero += 16;
 if (Matrix[indice][inicio + 1] == 1)
 numero += 8;
 if (Matrix[indice][inicio + 2] == 1)
 numero += 4;
 if (Matrix[indice][inicio + 3] == 1)
 numero += 2;
 if (Matrix[indice][inicio + 4] == 1)
 numero += 1;

 chardata[x] = numero; //8行的像素显示信息存入数组
 }

 lcd.createChar(numeroFigura, chardata);  //(自定义字符编号,自定义字符数据)arduino支持8个5*8像素自定义字符
 lcd.setCursor(cuadritoX, cuadritoY);   // 在当前遍历停留的光标单元
 lcd.write(byte(numeroFigura));         //写入该编号所对应的自定义字符数据
 numeroFigura++;                         //转入到下一个自定义字符编号(共8个)
 }
 }
}

bool collisionfood() {    //与食物相撞,蛇头坐标与食物坐标相比较
 return snk[0].x == foodX && snk[0].y == foodY;
}

void loop() {
 Reset_matrix();  //重置矩阵
 change_direction(Get_direction());//获取摇杆方向
 move_snake();             //根据摇杆方位来对蛇进行矩阵移动
 place_snake();           //把蛇的坐标放在矩阵中
 place_food();             //把食物放在矩阵上
 Draw_matrix();                //绘制矩阵

 if (collisionfood()) {   //若吃到了食物
 score++;                    //分数+1
 random_food();         //随机生成下一个食物的坐标,等待下一轮循环将其显示出来
 Add_block(0, 0);        //增加蛇的长度,该函数的两个实参(0,0)表示不增加新的坐标,只对长度变量进行递增
 }

 Score();    //显示分数
 delay(10);  //蛇移动速度,编辑此处可调整游戏难度
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容