基于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个16进制值为0x00,大小为1字节==8位二进制值:B 0 0 0 0 0
横向第2个16进制值为0xE0,大小为1字节==8位二进制值:B 1 1 1 0 0
横向第3个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0
横向第4个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0
横向第5个16进制值为0xA0,大小为1字节==8位二进制值:B 1 0 1 0 0
横向第6个16进制值为0xE0,大小为1字节==8位二进制值:B 1 1 1 0 0
横向第7个16进制值为0x00,大小为1字节==8位二进制值:B 0 0 0 0 0
横向第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); //蛇移动速度,编辑此处可调整游戏难度
}