基于STM32的摇杆开关控制小恐龙游戏(附源码)


一、 前言

最近有看到别人在OLED屏幕上玩小恐龙,所幸查阅下资料,并下好源码。可惜他的源码的主控是STM32F103ZET6,用的是STM32CubeIDE,采用的是HAL库编写。我目前手头上并没有该主控的开发板,只能自己手动移植到STM32F103C8T6上,觉得用按键来操作小恐龙跳跃不是很舒服,所以自己改用摇杆开关用遥控向上来进行跳跃,更顺手些。

谷歌小恐龙

谷歌小恐龙是指谷歌 Chrome 浏览器中的一个小游戏,通常出现在没有网络连接时的页面(称为“无网页游戏”或“离线游戏”)。这款游戏名为“恐龙跳跃”(T-Rex Runner),玩家需要控制一只恐龙跳过障碍物,随着游戏时间的推移,速度会逐渐加快,挑战也会变得更大。

在这里插入图片描述

在这里插入图片描述

谷歌小恐龙游戏在线玩


二、硬件

老样子,用面板上搭建一个简单的实验环境。

共需要一块最小系统板,一个0.96寸的OLED显示屏(IIC驱动)和一个摇杆开关。

硬件相对比较简单,很容易实现,感兴趣的可以试下。

引脚连接:(如需更改,直接修改对应.h文件里面的宏定义)

摇杆开关X输出:PB0
摇杆开关Y输出:PB1
摇杆开关按键:PB10
OLED显示屏SCL:PA0
OLED显示屏SDA:PA1

在这里插入图片描述

对摇杆开关不了解的可以看下摇杆开关的简单应用

对OLED不了解的可以看下0.96寸OLED(IIC接口)显示屏的图像显示应用

在这里插入图片描述

三、软件

3.1 摇杆开关

这里的ADCX和ADCY的引脚要是ADC引脚,还要看一下具体的DMA通道。至于下面那个按键就是摇杆开关的SW开关引脚了,任意引脚即可。

/* Defines ------------------------------------------------------------------*/
#define Rocker_GPIO_RCC   RCC_APB2Periph_GPIOB
#define Rocker_GPIO_Port  GPIOB
#define ADCX_Pin          GPIO_Pin_0
#define ADCY_Pin          GPIO_Pin_1

#define Key_GPIO_RCC      RCC_APB2Periph_GPIOB
#define Key_GPIO_Port     GPIOB
#define Key_Pin           GPIO_Pin_10//根据实际的引脚修改

/* Variables Define ---------------------------------------------------------*/
uint16_t conv_val[4] = {0};

float LeftTwoAxisKey_X = 0;
float LeftTwoAxisKey_Y = 0;
/* Function prototypes ------------------------------------------------------*/
/*******************************************************************************
 * 函数名:User_Rocker_Init
 * 描述  :摇杆开关相关变量初始化
 * 输入  :void
 * 输出  :void
 * 调用  :初始化
 * 备注  :
 *******************************************************************************/
void User_Rocker_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	DMA_InitTypeDef DMA_InitStructure;
	ADC_InitTypeDef ADC_InitStructure;

	RCC_APB2PeriphClockCmd(Rocker_GPIO_RCC, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

	GPIO_InitStructure.GPIO_Pin = ADCX_Pin;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(Rocker_GPIO_Port, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = ADCY_Pin;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(Rocker_GPIO_Port, &GPIO_InitStructure);

	DMA_DeInit(DMA1_Channel1);									   // 将DMA的通道1寄存器重设为缺省值
	DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&(ADC1->DR)); // 外设基址为:ADC 数据寄存器地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (u32)conv_val;		   // 存储器地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;			   // 数据源来自外设
	// 缓冲区大小,应该等于数据目的地的大小
	DMA_InitStructure.DMA_BufferSize = 4;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			// 外设寄存器只有一个,地址不用递增
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						// 存储器地址递增
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据大小为半字,即两个字节
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			// 内存数据大小也为半字,跟外设数据大小相同
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;								// 循环传输模式
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;							// DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								// 禁止存储器到存储器模式,因为是从外设到存储器
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);								// 初始化DMA
	DMA_Cmd(DMA1_Channel1, ENABLE);												// 使能 DMA 通道

	ADC_DeInit(ADC1); // ADC1复位
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_NbrOfChannel = 2;
	ADC_Init(ADC1, &ADC_InitStructure);
	RCC_ADCCLKConfig(RCC_PCLK2_Div8);

	// 配置ADC 通道的转换顺序和采样时间
	ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_9, 2, ADC_SampleTime_55Cycles5);

	ADC_DMACmd(ADC1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);		// 使能adc1
	ADC_ResetCalibration(ADC1); // 复位ADC1校准
	while (ADC_GetResetCalibrationStatus(ADC1))
		;						// 等待ADC1复位校准结束
	ADC_StartCalibration(ADC1); // 开启ADC1校准
	while (ADC_GetCalibrationStatus(ADC1))
		; // 等待ADC1校准结束
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

/*******************************************************************************
 * 函数名:Key_Init
 * 描述  :按键初始化
 * 输入  :void
 * 输出  :void
 * 调用  :初始化
 * 备注  :
 *******************************************************************************/
void User_Key_Init(void)
{
	/*定义一个GPIO_InitTypeDef类型的结构体*/
	GPIO_InitTypeDef GPIO_InitStructure;

	/*开启LED相关的GPIO外设时钟*/
	RCC_APB2PeriphClockCmd(Key_GPIO_RCC, ENABLE);

	/*设置引脚模式为上拉输入*/
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;

	/*选择要控制的GPIO引脚*/
	GPIO_InitStructure.GPIO_Pin = Key_Pin;

	/*调用库函数,初始化GPIO*/
	GPIO_Init(Key_GPIO_Port, &GPIO_InitStructure);
}

/*******************************************************************************
 * 函数名:Key_IN_Read
 * 描述  :Key输入模式读取引脚状态
 * 输入  :void
 * 输出  :uint8_t
 * 调用  :初始化
 * 备注  :
 *******************************************************************************/
uint8_t Key_IN_Read(void)
{
	return GPIO_ReadInputDataBit(Key_GPIO_Port, Key_Pin);
}

/*******************************************************************************
 * 函数名:Rocker_Scan
 * 描述  :摇杆开关扫描
 * 输入  :void
 * 输出  :void
 * 调用  :内部调用
 * 备注  :
 *******************************************************************************/
int Rocker_Scan(void)
{
	LeftTwoAxisKey_X = conv_val[0] * 3.3 / 4096;
	LeftTwoAxisKey_Y = conv_val[1] * 3.3 / 4096;
	//	printf("LeftTwoAxisKey_X=%f  LeftTwoAxisKey_Y=%f\r\n",LeftTwoAxisKey_X,LeftTwoAxisKey_Y);

	/*********************判断摇杆开关方向****************************/
	if ((LeftTwoAxisKey_X > 1) && (LeftTwoAxisKey_Y < 1))
	{
		//       printf("Up \r\n");
		return 1;
	}
	if ((LeftTwoAxisKey_X > 1) && (LeftTwoAxisKey_Y > 2))
	{
		//        printf("Down \r\n");
	}

	/*********************判断摇杆按键是否按下****************************/
	if (Key_IN_Read())
	{
		delay_syms(2);
		if (Key_IN_Read())
		{
			return 2;
		}
	}
	return 0;
}

3.2 OLED屏幕

#define IIC_GPIO_Port GPIOA
#define SDA_Pin       GPIO_Pin_1
#define SCL_Pin       GPIO_Pin_0//根据实际的引脚修改

#define OLED_SCLK_Clr() GPIO_ResetBits(IIC_GPIO_Port,SCL_Pin)//SCL
#define OLED_SCLK_Set() GPIO_SetBits(IIC_GPIO_Port,SCL_Pin)

#define OLED_SDIN_Clr() GPIO_ResetBits(IIC_GPIO_Port,SDA_Pin)//SDA
#define OLED_SDIN_Set() GPIO_SetBits(IIC_GPIO_Port,SDA_Pin)

//初始化SSD1306
void OLED_Init(void)
{
	IIC_Init();//IIC引脚初始化
	
	delay_syms(500);
	
	OLED_WR_Byte(0xAE,OLED_CMD);//--display off
	OLED_WR_Byte(0x00,OLED_CMD);//---set low column address
	OLED_WR_Byte(0x10,OLED_CMD);//---set high column address
	OLED_WR_Byte(0x40,OLED_CMD);//--set start line address
	OLED_WR_Byte(0xB0,OLED_CMD);//--set page address
	OLED_WR_Byte(0x81,OLED_CMD); // contract control
	OLED_WR_Byte(0xFF,OLED_CMD);//--128
	OLED_WR_Byte(0xA1,OLED_CMD);//set segment remap
	OLED_WR_Byte(0xA6,OLED_CMD);//--normal / reverse
	OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
	OLED_WR_Byte(0x3F,OLED_CMD);//--1/32 duty
	OLED_WR_Byte(0xC8,OLED_CMD);//Com scan direction
	OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset
	OLED_WR_Byte(0x00,OLED_CMD);//

	OLED_WR_Byte(0xD5,OLED_CMD);//set osc division
	OLED_WR_Byte(0x80,OLED_CMD);//

	OLED_WR_Byte(0xD8,OLED_CMD);//set area color mode off
	OLED_WR_Byte(0x05,OLED_CMD);//

	OLED_WR_Byte(0xD9,OLED_CMD);//Set Pre-Charge Period
	OLED_WR_Byte(0xF1,OLED_CMD);//

	OLED_WR_Byte(0xDA,OLED_CMD);//set com pin configuartion
	OLED_WR_Byte(0x12,OLED_CMD);//

	OLED_WR_Byte(0xDB,OLED_CMD);//set Vcomh
	OLED_WR_Byte(0x30,OLED_CMD);//

	OLED_WR_Byte(0x8D,OLED_CMD);//set charge pump enable
	OLED_WR_Byte(0x14,OLED_CMD);//

	OLED_WR_Byte(0xAF,OLED_CMD);//--turn on oled panel
	
	delay_syms(500);
	
	OLED_FillPicture(0x0);
}

//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{
	uint8_t i,n;
	for(i=0;i<8;i++)
	{
		OLED_WR_Byte (0xb0+i,OLED_CMD);    //设置页地址(0~7)
		OLED_WR_Byte (0x00,OLED_CMD);      //设置显示位置—列低地址
		OLED_WR_Byte (0x10,OLED_CMD);      //设置显示位置—列高地址
		for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA);
	} //更新显示
}

// 绘制重启
void OLED_DrawRestart()
{
	unsigned int j=0;
	unsigned char x, y;
	unsigned char byte;
	//OLED_SetPos(0, 0);
	for (y = 2; y < 5; y++)
	{
		OLED_SetPos(52, y);
		IIC_Start();
		IIC_WriteByte(0x78);
		IIC_WaitAck();
		IIC_WriteByte(0x40);
		IIC_WaitAck();
		for (x = 0; x < 24; x++)
		{
			byte = RESTART[j++];
			IIC_WriteByte(byte);
			IIC_WaitAck();
		}
		IIC_Stop();
	}
	OLED_ShowString(10, 3, "GAME", 16);
	OLED_ShowString(86, 3, "OVER", 16);
}

// 绘制小恐龙
void OLED_DrawDino()
{
	static unsigned char dino_dir = 0;
	unsigned int j=0;
	unsigned char x, y;
	unsigned char byte;

	dino_dir++;
	dino_dir = dino_dir%2;
	for(y=0; y<2; y++)
	{
		OLED_SetPos(16, 6+y);
		IIC_Start();
		IIC_WriteByte(0x78);
		IIC_WaitAck();
		IIC_WriteByte(0x40);
		IIC_WaitAck();
		for (x = 0; x < 16; x++)
		{
			j = y*16 + x;
			byte = DINO[dino_dir][j];

			IIC_WriteByte(byte);
			IIC_WaitAck();
		}
		IIC_Stop();
	}
}

// 绘制跳跃小恐龙
int OLED_DrawDinoJump(char reset)
{
	char speed_arr[] = {1, 1, 3, 3, 4, 4, 5, 6, 7};
	static char speed_idx = sizeof(speed_arr)-1;
	static int height = 0;
	static char dir = 0;
	//char speed = 4;

	unsigned int j=0;
	unsigned char x, y;
	char offset = 0;
	unsigned char byte;
	if(reset == 1)
	{
		height = 0;
		dir = 0;
		speed_idx = sizeof(speed_arr)-1;
		return 0;
	}
	if (dir==0)
	{
		height += speed_arr[speed_idx];
		speed_idx --;
		if (speed_idx<0) speed_idx = 0;
	}
	if (dir==1)
	{
		height -= speed_arr[speed_idx];
		speed_idx ++;
		if (speed_idx>sizeof(speed_arr)-1) speed_idx = sizeof(speed_arr)-1;
	}
	if(height >= 31)
	{
		dir = 1;
		height = 31;
	}
	if(height <= 0)
	{
		dir = 0;
		height = 0;
	}
	if(height <= 7) offset = 0;
	else if(height <= 15) offset = 1;
	else if(height <= 23) offset = 2;
	else if(height <= 31) offset = 3;
	else offset = 4;

	for(y=0; y<3; y++) // 4
	{
		OLED_SetPos(16, 5- offset + y);

		IIC_Start();
		IIC_WriteByte(0x78);
		IIC_WaitAck();
		IIC_WriteByte(0x40);
		IIC_WaitAck();
		for (x = 0; x < 16; x++) // 32
		{
			j = y*16 + x; // 32
			byte = DINO_JUMP[height%8][j];

			IIC_WriteByte(byte);
			IIC_WaitAck();
		}
		IIC_Stop();
	}
	if (dir == 0) oled_drawbmp_block_clear(16, 8- offset, 16);
	if (dir == 1) oled_drawbmp_block_clear(16, 4- offset, 16);
	return height;
}

// 绘制随机出现的仙人掌障碍物
int OLED_DrawCactusRandom(unsigned char ver, unsigned char reset)
{
	char speed = 5;
	static int pos = 128;
	int start_x = 0;
	int length = 0;

	unsigned int i=0, j=0;
	unsigned char x, y;
	unsigned char byte;
	if (reset == 1)
	{
		pos = 128;
		oled_drawbmp_block_clear(0, 6, speed);
		return 128;
	}
	if (ver == 0) length = 8; //sizeof(CACTUS_1) / 2;
	else if (ver == 1) length = 16; //sizeof(CACTUS_2) / 2;
	else if (ver == 2 || ver == 3) length = 24;

	for(y=0; y<2; y++)
	{
		if(pos < 0)
		{
			start_x = -pos;
			OLED_SetPos(0, 6+y);
		}
		else
		{
			OLED_SetPos(pos, 6+y);
		}

		IIC_Start();
		IIC_WriteByte(0x78);
		IIC_WaitAck();
		IIC_WriteByte(0x40);
		IIC_WaitAck();

		for (x = start_x; x < length; x++)
		{
			if (pos + x > 127) break;

			j = y*length + x;
			if (ver == 0) byte = CACTUS_1[j];
			else if (ver == 1) byte = CACTUS_2[j];
			else if(ver == 2) byte = CACTUS_3[j];
			else byte = CACTUS_4[j];

			IIC_WriteByte(byte);
			IIC_WaitAck();
		}
		IIC_Stop();
	}

	oled_drawbmp_block_clear(pos + length, 6, speed);

	pos = pos - speed;
	return pos + speed;
}

// 绘制云朵
void OLED_DrawCloud()
{
	static int pos = 128;
	static char height=0;
	char speed = 3;
	unsigned int i=0;
	int x;
	int start_x = 0;
	int length = sizeof(CLOUD);
	unsigned char byte;

	//if (pos + length <= -speed) pos = 128;

	if (pos + length <= -speed)
	{
		pos = 128;
		height = rand()%3;
	}
	if(pos < 0)
	{
		start_x = -pos;
		OLED_SetPos(0, 1+height);
	}
	else
	{
		OLED_SetPos(pos, 1+height);
	}

	IIC_Start();
	IIC_WriteByte(0x78);
	IIC_WaitAck();
	IIC_WriteByte(0x40);
	IIC_WaitAck();
	for (x = start_x; x < length + speed; x++)
	{
		if (pos + x > 127) break;
		if (x < length) byte = CLOUD[x];
		else byte = 0x0;

		IIC_WriteByte(byte);
		IIC_WaitAck();
	}
	IIC_Stop();

	pos = pos - speed;
}

四、展示

大概是下面这样子,如果觉得速度有点慢或者快,修改cur_speed 的值,也就是修改score/XX,改XX的大小。

cur_speed = score/20;
if (cur_speed > 29) cur_speed = 29;
delay_syms(30 - cur_speed);

屏闪的问题是手机相机拍摄的问题。实物是没问题的。

基于STM32和OLED的小恐龙游戏项目(摇杆开关控制)


五、总结

今天主要讲了基于STM32的摇杆开关控制小恐龙游戏,主要将原有的按键方式修改为摇杆控制。

感谢你的观看!

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

xiaobuding_QAQ

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值