C51 贪吃蛇 基于 Proteus V1.0

0. 效果演示

在这里插入图片描述


1. 开发环境

  • 系统:window10 专业版。
  • 开发软件:Keil5
  • 仿真软件:Proteus

PS:软件下载地址 Proteus电路仿真及应用(51单片机系列)


2. 项目地址

  • https://gitee.com/silver-blood-inn/c51-sankegame-base-on-proteus

3. 项目目录

├─code        # 项目文件
│  ├─bsp      # 外设模块封装目录,比如点阵 matrix.c、方向按键 dir_key.c 等
│  ├─output   # 编译后的hex文件输出位置
│  ├─project  # 存放keil的project目录
│  └─user     # main.c 和 pbdata.c共享模块
└─proteus     # 仿真画图存放位置

PS : 关于项目为什么结构为什么这样划分,可以看此视频 51单片机入门学习—以最通俗易懂的语言讲解!P25 - P28 关于模块化编程的讲解。


4. 设计与开发

  • 代码如果看着比较凌乱,可以使用代码 配合下方各个模块的思路进行理解就可以啦 :)。

4.1 整体原理图

在这里插入图片描述

4.2 方向键模块

        对于方向键我使用的是矩阵键盘的方式完成各个按键的检测(如果不了解矩阵键盘怎么用请看此视频 P10 矩阵键盘)。

  • 封装模块:将四个按键和其检测代码封装到 dir_key.c 中,检测那个按键按下就修改dir_key.c 中的 dir 这个变量。外部使用这个模块的时候 extern unsigned char dir; 即可引入这个变量来判断此时的方向,具体代码如下:
#include "PBDATA.H"

unsigned char dir = 0; // 默认向上

void DirKeyScan()
{
   unsigned char temp0 = 0, temp1 = 0, temp2 = 0;
   P1 = 0xFC;  // 1111 1100
   if(P1 != 0xFC) {  // 检测矩阵按键那行按下
   	delay(20);
   	temp1 = P1;
   	
   	// 列
   	P1 = 0xF3; // 1111 0011
   	if(P1 != 0xF3) {  // 检测矩阵按键那列按下
   		temp2 = P1;
   	}
   }
   
   temp0 = temp1 | temp2;
   if(temp0 == 0xFA) {  // 1111 1010 上
   	dir = (dir == 2) ? dir : 0;
   } else if(temp0 == 0xF6) {  // 1111 0110 下
   	dir = (dir == 0) ? dir : 2;
   } else if(temp0 == 0xF9) {  // 1111 1001 左
   	dir = (dir == 1) ? dir : 3;
   } else if(temp0 == 0xF5) { // 1111 0101 右
   	dir = (dir == 3) ? dir : 1;
   }
}

4.3 点阵模块

  • 如果不了解点阵怎么使用可以参考此视频 : P17 8X8 点阵
  • 封装模块: 将点阵显示的相关函数封装到 matrix.c 文件当中,具体代码如下:
#include "PBDATA.H"

static uchar code indexs[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};  // 行位码

static uchar tab[8] = {0};   // 列位码

// 清屏函数
void Matrix_Clear()
{
	COLP = 0x00;
}

// 生成列码
void Matrix_GenerateTab(uchar rowIndexs[], uchar colIndexs[], uchar length) 
{
	uchar i = 0;
	// 清掉上次形成的列码
	for(i = 0; i < 8; i++) {
		tab[i] = 0;
	}
	
	//形成列位码
	for(i = 0; i < length; i++) {
		// rowIndexs[i] 行显示的列追加 tab[rowIndexs[i]];
		tab[rowIndexs[i]] += indexs[colIndexs[i]];
	}
}

// 使用编码点亮点阵
void Matrix_ShowPointByCode()
{
	uchar i = 0;
	
	// 逐行显示 
	for(i = 0; i < 8; i++) {
		Matrix_Clear();
		ROWP = ~indexs[i];
		COLP = tab[i];  
	}
}

void Matrix_ShowAll() 
{
	uchar i = 0;
	
	// 逐行显示 
	for(i = 0; i < 8; i++) {
		Matrix_Clear();
		ROWP = ~indexs[i];
		COLP = 0xFF;  
	}
}

4.4 整体逻辑说明

4.4.1 点阵怎么刷新

  • 点阵刷新我放在 main() 函数当中死循环中,这样能最大程度保证点阵屏幕的刷新速度够快,视觉暂留效果达到最佳。
  • 具体代码看下方 main.c

4.4.2 按键在哪里检测

  • 按键不需要很高频率去检测,50ms 检测一次完全够用,且如果跟点刷新放在一条主线上会影响点阵的显示效果,因此我将其放在了定时器0 中,每隔50ms 检测下即可,并未出现什么问题。
  • 具体代码看下方 main.c

4.4.3 蛇怎么移动

  • 我将蛇移动也放到定时器0中,不过移动频率使用全局变量 speed控制,也就是隔 speed * 50ms 移动一次,可以自行调整,并且增加后续功能也可以吃的食物越多,调整 speed 变量的值。
  • 如果大家不知道蛇具体移动怎么实现,这里坐下简单说明,如下代码为蛇的一次移动的核心逻辑。
//1. 使用两个数组存储身体行和列 (uchar 即 unsigned char);
//   如下则表示蛇头到蛇尾分别在0行0列,0行1列,0行2列。
uchar bodyRow[16] = {0, 0, 0}; 
uchar bodyCol[16] = {0, 1, 2};

// 2. 移动身体 : 将前一个位置的行列赋值给后一个位置即完成蛇身体向头部的一次移动
for(i = snakeBodyLength - 1; i > 0; i--) {
	bodyRow[i] = bodyRow[i - 1];
	bodyCol[i] = bodyCol[i - 1];
}

// 生成新的蛇头
bodyRow[0] = bodyRow[0] + dirRow[dir];
bodyCol[0] = bodyCol[0] + dirCol[dir];
  • 具体代码看下方 main.c

4.4.4 游戏规则

  • 蛇死亡时候点阵会全亮。
  • 蛇死亡的条件为:撞到自身或边界。

4.5 main.c

#include "PBDATA.H"



/****************************************
 蛇体 和 食物 相关的数据 和 函数
*****************************************/
uchar bodyRow[16], bodyCol[16];  // 蛇身的数组

uchar code dirRow[4] = {-1, 0, 1, 0};  // 上右下左方向X增量
uchar code dirCol[4] = {0, 1, 0, -1};  // 上右下左方向Y增量
extern uchar dir;
uchar maxRow = 8, maxCol = 8;  // 行的范围 [0, maxRow), 列范围同理
uchar speed = 5;  // 蛇的速度,单位是50ms
uchar snakeBodyLength = 0;  // 蛇身体的长度
uchar isDead = 0;  // 表示蛇是否死亡

uchar foodRow = 0, foodCol = 0, needCreate = 1;  // 食物坐标和食物是否被吃标志
 
void InitSnake()
{
	bodyRow[0] = 7;
	bodyCol[0] = 2;
	bodyRow[1] = 7;
	bodyCol[1] = 1;
	
	snakeBodyLength = 2;
	
	dir = 1;
	
	// 生成蛇打印的内容
	Matrix_GenerateTab(bodyRow, bodyCol, snakeBodyLength);
	
	// 设置时间种子
	srand(0);
}

void GenerateFood()
{
	uchar i = 0;
	while(needCreate) {
		// 随机生成 Row 和 Col
		foodRow = rand() % maxRow;
		foodCol = rand() % maxCol;
		
		// 判断食物是否和当前蛇身体冲突 
		for(i = 0; i < snakeBodyLength; i++) {
			if(bodyRow[i] == foodRow && bodyCol[i] == foodCol) {
				break;
			}
		}
		if(i == snakeBodyLength) {
			needCreate = 0;
			bodyRow[snakeBodyLength] = foodRow;
			bodyCol[snakeBodyLength] = foodCol;
		} 
	}
}

/****************************************
 定时器相关的代码
*****************************************/

uchar count;  // time = count * 50ms

void InitTimer()
{
  TMOD = 0x01;
  
  // 初始值 : 50ms
  TH0 = (65536 - 50000) / 256;  // 初始值取高八位
  TL0 = (65536 - 50000) % 256;  // 初始值取低八位
  
  // 中断开启
  ET0 = 1;  // 开启定时器0的中断
  EA = 1;   // 开启总的中断

  // 配置TCON
  // TR0 : 1, 启动定时器0
  TR0 = 1;    
}

void TimerIsr() interrupt 1
{
	uchar nextHeadRow = 0, nextHeadCol = 0, i = 0;
	
  // 重新装填
  // 初始值 : 50ms
  TH0 = (65536 - 50000) / 256;  // 初始值取高八位
  TL0 = (65536 - 50000) % 256;  // 初始值取低八位
  
  if(count == speed && !isDead) {  // count * 50ms 触发一次
    count = 0;
	
		// 生成食物
		GenerateFood();
		
		// 预测蛇头
		nextHeadRow = bodyRow[0] + dirRow[dir];
		nextHeadCol = bodyCol[0] + dirCol[dir];
		if(nextHeadRow >= maxRow || nextHeadRow < 0 
			 || nextHeadCol >= maxCol || nextHeadCol < 0) {
				isDead = 1;
				return;
		}
		
		// 身体撞击
		for(i = 0; i < snakeBodyLength; i++) {
			if(nextHeadRow == bodyRow[i] && nextHeadCol == bodyCol[i]) {
				isDead = 1;
				return;
			}
		}
		
		
		// 吃到食物与否
		if(nextHeadRow == foodRow && nextHeadCol == foodCol) {
			snakeBodyLength += 1;
			needCreate = 1;
			
			// 再生成新的食物
			GenerateFood();
		}
		
		// 蛇身体移动
		for(i = snakeBodyLength - 1; i > 0; i--) {
			bodyRow[i] = bodyRow[i - 1];
			bodyCol[i] = bodyCol[i - 1];
		}
		
		// 新的蛇头
		bodyRow[0] = nextHeadRow;
		bodyCol[0] = nextHeadCol;
		
		
		// 生成需要显示的图形
		Matrix_GenerateTab(bodyRow, bodyCol, snakeBodyLength + 1);
	}
	count++;	
	
	// 每50ms进行一次按键扫描
	DirKeyScan();
}

void main()
{
	uchar i = 0, a = 0;

	count = speed;
	InitSnake();
	InitTimer();

	while(1){
		if(isDead) {
			Matrix_ShowAll();
		} else {
			// 打印蛇的身体
			Matrix_ShowPointByCode();		
		}
	}
}

5. 不足与展望

  • 游戏功能较为简单,仅仅实现了贪吃蛇的核心功能,其实还可以加入 LCD1602等模块显示游戏信息,如分数、蛇长度这样。
  • 在仿真当中可以加上复位电路,这样不用每次死亡要点击仿真重新运行。
  • 2
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值