购物导航网欢迎您!!!

网站收藏健康资讯网联系我们

导航菜单

坦克大战,物理引擎 随机地图:这个嵌入式坦克大战不简单

本文将带您深入了解一个基于树莓派RP2040芯片的嵌入式项目——《坦克大战》游戏的开发过程。该项目不仅展示了RP2040GameKit平台的强大功能,还通过LCD屏幕、按键、摇杆和蜂鸣器等硬件设备,实现了经典游戏的创新移植,为嵌入式开发爱好者提供了一个极具实践价值的案例。

项目介绍

本项目依托2022寒假在家练,基于RP2040GameKit平台,实现了坦克大战游戏的制作。本项目通过LCD屏幕、按键和四向摇杆实现了游戏的游玩,并调用蜂鸣器实现了游戏配乐与游戏音效,取得了良好的游戏体验,具有一定的娱乐价值。

项目任务

设计或移植一款经典的游戏(硬禾学堂上已经实现的贪吃蛇等游戏除外),通过LCD屏显示,通过按键和四向摇杆控制游戏的动作;

在游戏中要通过蜂鸣器播放背景音乐;

设计思路

本项目设计思路为,围绕RP2040GAMEKIT,首先对RP2040的板载硬件进行实验使用,初步掌握板载的IO硬件以及RP2040能够实现的功能与实际性能。再根据硬件的具体功能,实现游戏设计。在设计期间,穿插硬件调研与资料搜集,案例搜集与代码实现两个过程,以辅助硬件实验与游戏设计进行。游戏设计的主要思路如下:

硬件与驱动介绍

RP2040GAMEKIT是一款嵌入式系统的学习平台,其包含的主要特点如下:

采用树莓派Pico核心芯片RP2040:

双核Arm Cortex M0 内核,可以运行到133MHz

264KB内存

性能强大、高度灵活的可编程IO可用于高速数字接口

片内温度传感器、并支持外部4路模拟信号输入,内部ADC采样率高达500Ksps、12位精度

支持MicroPython、C、C 编程

板上功能

240*240分辨率的彩色IPS LCD,SPI接口,控制器为ST7789

四向摇杆 2个轻触按键 一个三轴姿态传感器MMA7660用做输入控制

板上外扩2MB Flash,预刷MicroPython的UF2固件

一个红外接收管 一个红外发射管

一个三轴姿态传感器MMA7660

一个蜂鸣器

双排16Pin连接器,有SPI、I2C以及2路模拟信号输入

可以使用MicroPython、C、C 编程

USB Type C连接器用于供电、程序下载

硬件驱动

硬件驱动:

屏幕驱动:ST7789 LCD驱动(含两类:C语言库驱动(刷新屏幕);python库驱动(激活屏幕))

按键驱动:my_input.py

蜂鸣器驱动:buzzer_music.py

游戏功能与图片展示

本项目实现的主要功能如下:

欢迎与重新开始界面

本项目实现人性化的游戏设计,为游戏设计了开始欢迎界面,按Start键可进入游戏。在一方获得胜利之后,会进入游戏获胜界面,即重新开始界面,按B键即可重新开始游戏。玩家可根据自己的需求,选择游戏的开始与结束。

欢迎与重新开始界面图片展示:

坦克大战

坦克大战

拓展功能:在游玩过程当中,如玩家觉得游戏较为困难,可按Start键重新载入包含新地图的游戏。

游戏实际内容

本项目设计的游戏为双人对战游戏,由两位玩家进行回合制对战。

游戏规则:

每一位玩家通过控制自己的坦克击毁敌方的坦克,率先击毁敌方坦克的一方获得胜利。

游戏操作:

每一位玩家,可通过调整炮弹的发射力度与发射角度,进行炮弹发射调节。调节方式是使用摇杆进行上下摇动调节,通过B键可以在发射力度与发射角度之间进行切换。在游戏过程当中,每一位玩家的炮弹发射信息将在各自坦克位置的正上方进行显示,用以玩家参考发射状态。在调节完成后,按A键进行炮弹发射。若击中敌方,则玩家获得胜利。若未击中,则切换为下一位玩家进行操作。

游戏内容图片展示:

坦克大战

游戏音乐部分

游戏音乐部分主要分为背景音乐与游戏特效音乐两个部分。

背景音乐:

本项目依照游戏场景设计了三种背景音乐,分别为欢迎界面BGM、游戏战斗BGM与游戏结束BGM,背景音乐的旋律如下表格所示:

坦克大战

说明:在对应的场景下,本项目采用重复循环式播放每一段旋律作为背景音乐。

游戏特效音乐:

本项目依照游戏功能,设计了两类游戏特效音乐,分别是炮弹发射音效与坦克击毁音效,特效音乐如下表格所示:

坦克大战

说明:特效音乐在玩家进行相应操作后进行播放。

音乐操作:

本项目为游戏音乐设立了开关,玩家可在游戏过程当中,使用Select键进行游戏音乐的开关。

主要代码段及其说明

本项目主要代码段如下,说明见代码段注释。

游戏主控代码

游戏逻辑:

def run_game(): while True: global key_mode, game_state,WinPage #坦克主体绘制 if game_state not in [game_over_1,game_over_2]: tank1.draw () tank2.draw () #地图背景绘制 if game_state==start: display.fill(SKY_COLOR) ground.draw() game_state=player1 #炮弹动画绘制 if (game_state =="player1fire"or game_state =="player2fire"): shell.draw() utime.sleep(0.1) #炮弹发射参数栏绘制 if (game_state =="player1"or game_state =="player1fire"): display.text(font_text,"Player 1",10,10,TEXT_COLOR) if (key_mode =="power"): display.text(font_text,"Power:{:>3d}".format(tank1.get_gun_power()) "%",10,20,TEXT_COLOR_ACTIVE) else: display.text(font_text,"Power:{:>3d}".format(tank1.get_gun_power()) "%",10,20,TEXT_COLOR) if (key_mode =="angle"): display.text(font_text,"Angle:{:>3d}".format(tank1.get_gun_angle()) ,10,30,TEXT_COLOR_ACTIVE) else: display.text(font_text,"Angle:{:>3d}".format(tank1.get_gun_angle()) ,10,30,TEXT_COLOR) if (game_state =="player2"or game_state =="player2fire"): display.text(font_text,"Player 2",150,10,TEXT_COLOR) if (key_mode =="power"): display.text(font_text,"Power:{:>3d}".format(tank2.get_gun_power()) "%",150,20,TEXT_COLOR_ACTIVE) else: display.text(font_text,"Power:{:>3d}".format(tank2.get_gun_power()) "%",150,20,TEXT_COLOR) if (key_mode =="angle"): display.text(font_text,"Angle:{:>3d}".format(tank2.get_gun_angle()) ,150,30,TEXT_COLOR_ACTIVE) else: display.text(font_text,"Angle:{:>3d}".format(tank2.get_gun_angle()) ,150,30,TEXT_COLOR) if (game_state =="game_over_1"and WinPage): display.fill(st7789.BLACK) display.text(font_text_big,"Game Over",50,20,TEXT_COLOR) display.text(font_text_big,"Player 1",60,80,CELE_COLOR) display.text(font_text_big,"Wins!",80,110,CELE_COLOR) display.text(font_text_big,"PRESS B",60,175,TEXT_COLOR) display.text(font_text_big,"TO CONTINUE.",30,200,TEXT_COLOR) WinPage=0 if (game_state =="game_over_2"and WinPage): display.fill(st7789.BLACK) display.text(font_text_big,"Game Over",50,20,TEXT_COLOR) display.text(font_text_big,"Player 2",60,80,CELE_COLOR) display.text(font_text_big,"Wins!",80,110,CELE_COLOR) display.text(font_text_big,"PRESS B",60,175,TEXT_COLOR) display.text(font_text_big,"TO CONTINUE.",30,200,TEXT_COLOR) WinPage=0 #炮弹发射流程实现 if (game_state ==player1): player1_fired =player_keyboard("left") if (player1_fired == True): #发射过程实现 gun_positions = tank1.calc_gun_positions() start_shell_pos = (gun_positions[3][0],gun_positions[3][1] 2) shell.set_start_position(start_shell_pos) shell.set_current_position(start_shell_pos) global key_mode, game_state game_state =player1fire musicPlay.shotSoundPlay() print(game_state) shell.set_angle(math.radians (tank1.get_gun_angle())) shell.set_power(tank1.get_gun_power() /40) shell.set_time(0) if (game_state ==player1fire): #发射后游戏逻辑实现 shell.update_shell_position ("left") #检测射击状态 shell_value =detect_hit("left") #坦克状态 if (shell_value >=20): game_state =game_over_1 elif (shell_value >=10): key_mode ="angle" game_state =player2 #player2逻辑同player1 if (game_state ==player2): player2_fired =player_keyboard("right") if (player2_fired == True): gun_positions = tank2.calc_gun_positions () start_shell_pos = (gun_positions[3][0],gun_positions[3][1] 2) shell.set_start_position(start_shell_pos) shell.set_current_position(start_shell_pos) game_state =player2fire musicPlay.shotSoundPlay() shell.set_angle(math.radians (tank2.get_gun_angle())) shell.set_power(tank2.get_gun_power() /40) shell.set_time(0) if (game_state ==player2fire): shell.update_shell_position ("right") shell_value =detect_hit("right") if (shell_value >=20): game_state =game_over_2 elif (shell_value >=10): game_state =player1 key_mode ="angle" #游戏结束界面 if (game_state ==game_over_1or game_state ==game_over_2): global bgmTimer bgmTimer.deinit() if (my_input.B()) : # Reset position of tanks and terrain setup() musicPlay.overPlay() #地图重载入 if my_input.Start(): setup() #音效开关 if (my_input.Select()==1): musicPlay.switch()

炮弹逻辑检测:

def detect_hit (left_right): #获取炮弹位置 (shell_x, shell_y) = shell.get_current_position() #设置击中区域 if(left_right =="left"): shell_x =2 else: shell_x -=2 shell_y =2 offset_position = (math.floor(shell_x), math.floor(shell_y)) #检测炮弹位置(炮弹绘制) if(shell_x > width or shell_x <=0or shell_y >= height): shell.shell_hide() return10 if(shell_y <1): if(shell_y <0-height): shell.shell_hide() return10 shell.shell_hide() return1 #计算后一帧位置 shell_x=offset_position[0] shell_y=offset_position[1] tank1_pos=tank1.position tank2_pos=tank2.position land_pos=ground.get_land_height() if(shell_y >= land_pos[shell_x]): shell.shell_hide() return11 xscale=15 y_up_scale=0 y_down_scale=15 #击中逻辑 if(left_right ==leftand tank2_pos[0] 3<=shell_x <= tank2_pos[0] xscale and tank2_pos[1]-y_down_scale<=shell_y <= tank2_pos[1] y_up_scale): musicPlay.shotedSoundPlay() return20 if(left_right ==rightand tank1_pos[0]<=shell_x <= tank1_pos[0] xscale-3and tank1_pos[1]-y_down_scale<=shell_y <= tank1_pos[1] y_up_scale): musicPlay.shotedSoundPlay() return20 return0

地图背景实现

classLand: def__init__(self, display, ground_color): self.display = display self.ground_color = ground_color self.screen_size = (240,240) self.setup() defsetup(self): self.land_y_positions = [0] *240 #随机地图生成 left_tank_x_position = random.randint (10,int(self.screen_size[0]/2)-30) right_tank_x_position = random.randint (int(self.screen_size[0]/2) 30,self.screen_size[0]-40) self.tank1_position = (left_tank_x_position,0) self.tank2_position = (right_tank_x_position,0) current_land_x =0 next_land_x =0 LAND_CHUNK_SIZE current_land_y = random.randint (50,240-20) self.land_y_positions[current_land_x] = current_land_y while(current_land_x < self.screen_size[0]): if(current_land_x == left_tank_x_position): self.tank1_position = (current_land_x,int(current_land_y)) foriinrange(0, LAND_TANK_SIZE): self.land_y_positions[current_land_x] =int(current_land_y) current_land_x =1 continue elif(current_land_x == right_tank_x_position): self.tank2_position = (current_land_x,int(current_land_y)) foriinrange(0, LAND_TANK_SIZE): self.land_y_positions[current_land_x] =int(current_land_y) current_land_x =1 continue # 坦克位置生成 if(current_land_x < left_tank_x_positionandcurrent_land_x LAND_CHUNK_SIZE >= left_tank_x_position): next_land_x = left_tank_x_position elif(current_land_x < right_tank_x_positionandcurrent_land_x LAND_CHUNK_SIZE >= right_tank_x_position): next_land_x = right_tank_x_position elif(current_land_x LAND_CHUNK_SIZE > self.screen_size[0]): next_land_x = self.screen_size[0] else: next_land_x = current_land_x LAND_CHUNK_SIZE next_land_y = current_land_y random.randint(0-LAND_MAX_CHG,LAND_MAX_CHG) if(next_land_y > self.screen_size[1]): next_land_y = self.screen_size[1] if(next_land_y < LAND_MIN_Y): next_land_y = LAND_MIN_Y if(next_land_y == current_land_yornext_land_x == current_land_x): y_delta =0 else: y_delta = (next_land_y - current_land_y) / (next_land_x - current_land_x) foriinrange(current_land_x, next_land_x): current_land_y = y_delta self.land_y_positions[current_land_x] =int(current_land_y) current_land_x =1 #坦克位置返回 defget_tank1_position(self): returnself.tank1_position defget_tank2_position(self): returnself.tank2_position #位置高度获取 defget_land_height(self): returnself.land_y_positions #地图绘制 defdraw(self): current_land_x =0 forthis_posinself.land_y_positions: forthis_yinrange(self.land_y_positions[current_land_x], self.screen_size[1]): self.display.pixel(current_land_x,int(this_y),self.ground_color) current_land_x =1

坦克对象实现

classTank: def__init__(self, display, left_right, tank_color,background_color): self.display = display self.left_right = left_right self.tank_color = tank_color self.background_color=background_color self.position = (0,0) #初始角度设置 if(left_right =="left"): self.gun_angle =10 else: self.gun_angle =50 #初始力度设置 self.gun_power =40 self.post_gun_position=self.calc_gun_positions() #设置位置 defset_position(self, position): self.position = position #获取位置 defget_position(self): returnself.position #设置炮管角度 defset_gun_angle(self, angle): self.gun_angle = angle #改变炮管角度 defchange_gun_angle(self, amount): self.gun_angle = amount ifself.gun_angle >85: self.gun_angle =85 ifself.gun_angle < -20: self.gun_angle = -20 #获取炮管角度 defget_gun_angle(self): returnself.gun_angle #设置力度 defset_gun_power(self, power): self.gun_power = power #改变力度 defchange_gun_power(self, amount): self.gun_power = amount ifself.gun_power >100: self.gun_power =100 ifself.gun_power <10: self.gun_power =10 #获取力度 defget_gun_power(self): returnself.gun_power #绘制坦克 defdraw(self): # 履带 self.display.hline(self.position[0] 6, self.position[1]-10,20,self.tank_color) self.display.hline(self.position[0] 5, self.position[1]-9,22,self.tank_color) self.display.hline(self.position[0] 4, self.position[1]-8,24,self.tank_color) self.display.hline(self.position[0] 3, self.position[1]-7,26,self.tank_color) self.display.hline(self.position[0] 2, self.position[1]-6,28,self.tank_color) self.display.hline(self.position[0] 1, self.position[1]-5,30,self.tank_color) self.display.hline(self.position[0] 2, self.position[1]-4,28,self.tank_color) self.display.hline(self.position[0] 3, self.position[1]-3,26,self.tank_color) self.display.hline(self.position[0] 4, self.position[1]-2,24,self.tank_color) self.display.hline(self.position[0] 5, self.position[1]-1,22,self.tank_color) self.display.hline(self.position[0] 6, self.position[1],20,self.tank_color) # 主体 self.display.rect(self.position[0] 7, self.position[1]-13,18,3,self.tank_color) self.display.hline(self.position[0] 8, self.position[1]-14,16,self.tank_color) self.display.hline(self.position[0] 6, self.position[1]-15,20,self.tank_color) self.display.hline(self.position[0] 8, self.position[1]-16,16,self.tank_color) # 炮管 self.erase_gun(self.post_gun_position) self.draw_gun(self.calc_gun_positions()) self.post_gun_position=self.calc_gun_positions() #擦除炮管 deferase_gun(self, gun_positions): if(self.left_right =="left"): start_x = gun_positions[1][0] start_y = gun_positions[1][1] end_x = gun_positions[2][0] end_y = gun_positions[2][1] else: start_x = gun_positions[2][0] start_y = gun_positions[2][1] end_x = gun_positions[1][0] end_y = gun_positions[1][1] if(end_y == start_yorend_x == start_x): y_delta =0 else: y_delta = (end_y - start_y) / (end_x - start_x) current_x =int(start_x) current_y =int(start_y) forxinrange(start_x, end_x): fory_offsetinrange(0, GUN_DIAMETER): self.display.pixel(current_x,int(current_y y_offset),self.background_color) current_x =1 current_y = y_delta #绘制炮管 defdraw_gun(self, gun_positions): if(self.left_right =="left"): start_x = gun_positions[1][0] start_y = gun_positions[1][1] end_x = gun_positions[2][0] end_y = gun_positions[2][1] else: start_x = gun_positions[2][0] start_y = gun_positions[2][1] end_x = gun_positions[1][0] end_y = gun_positions[1][1] if(end_y == start_yorend_x == start_x): y_delta =0 else: y_delta = (end_y - start_y) / (end_x - start_x) current_x =int(start_x) current_y =int(start_y) forxinrange(start_x, end_x): fory_offsetinrange(0, GUN_DIAMETER): self.display.pixel(current_x,int(current_y y_offset),self.tank_color) current_x =1 current_y = y_delta #计算炮管位置 defcalc_gun_positions(self): (xpos, ypos) = self.position if(self.left_right =="right"): gun_start_pos_top = (xpos 6, ypos-15) else: gun_start_pos_top = (xpos 26, ypos-15) relative_angle = self.gun_angle if(self.left_right =="right"): relative_angle =180- self.gun_angle angle_rads = relative_angle * (math.pi /180) gun_vector = (math.cos(angle_rads), math.sin(angle_rads) * -1) if(self.left_right =="right"): temp_angle_rads = math.radians(relative_angle -90) else: temp_angle_rads = math.radians(relative_angle 90) temp_vector = (math.cos(temp_angle_rads), math.sin(temp_angle_rads) * -1) gun_start_pos_bottom = (gun_start_pos_top[0] temp_vector[0] * GUN_DIAMETER, gun_start_pos_top[1] temp_vector[1] * GUN_DIAMETER) gun_positions = [ gun_start_pos_bottom, gun_start_pos_top, (gun_start_pos_top[0] gun_vector[0] * GUN_LENGTH, gun_start_pos_top[1] gun_vector[1] * GUN_LENGTH), (gun_start_pos_bottom[0] gun_vector[0] * GUN_LENGTH, gun_start_pos_bottom[1] gun_vector[1] * GUN_LENGTH), ] returngun_positions

炮弹对象实现

classShell: def__init__(self, display, shell_color,background_color): self.display = display self.shell_color = shell_color self.background_color = background_color self.start_position = (0,0) self.current_position = (0,0) self.power =1 self.angle =0 self.time =0 self.post_position=self.current_position self.shell_size=2 defset_start_position(self, position): self.start_position = position defget_start_position(self): returnself.start_position defset_current_position(self, position): self.current_position = position defget_current_position(self): returnself.current_position defset_angle(self, angle): self.angle = angle defset_power(self, power): self.power = power defset_time(self, time): self.time = time #绘制炮弹 defdraw(self): (xpos, ypos) = self.current_position (postXpos,postYpos)=self.post_position self.display.rect(int(postXpos),int(postYpos),self.shell_size,self.shell_size,self.background_color) self.display.rect(int(xpos),int(ypos), self.shell_size, self.shell_size,self.shell_color) self.post_position=self.current_position #隐藏炮弹轨迹 defshell_hide(self): (postXpos,postYpos)=self.post_position self.display.rect(int(postXpos),int(postYpos),self.shell_size,self.shell_size,self.background_color) #更新炮弹位置 defupdate_shell_position(self, left_right): init_velocity_y = self.power * math.sin(self.angle) if(left_right ==left): init_velocity_x = self.power * math.cos(self.angle) else: init_velocity_x = self.power * math.cos(math.pi - self.angle) #设置重力参数 GRAVITY_CONSTANT =0.008 #设置距离敏感程度 DISTANCE_CONSTANT =1.5 #设置风阻系数 wind_value =1 shell_x = self.start_position[0] init_velocity_x * self.time * DISTANCE_CONSTANT shell_y = self.start_position[1] -1* ((init_velocity_y * self.time) - (0.5* GRAVITY_CONSTANT * self.time * self.time * wind_value)) self.current_position = (shell_x, shell_y) self.time =4

键盘操控实现

defplayer_keyboard(left_right): globalkey_mode #控制切换 if(my_input.B()==1) : ifkey_mode =="angle": key_mode ="power" else: key_mode ="angle" utime.sleep(0.01) #炮弹发射 if(my_input.A()==1) : returnTrue utime.sleep(0.1) # 炮管角度调整 adjustAngle=3 if(my_input.y()==-1) : if(key_mode =="angle"andleft_right ==left): tank1.change_gun_angle(adjustAngle) elif(key_mode =="angle"andleft_right ==right): tank2.change_gun_angle(adjustAngle) elif(key_mode =="power"andleft_right ==left): tank1.change_gun_power(adjustAngle) elif(key_mode =="power"andleft_right ==right): tank2.change_gun_power(adjustAngle) # 炮管能量调整 if(my_input.y()==1) : if(key_mode =="angle"andleft_right ==left): tank1.change_gun_angle(-adjustAngle) elif(key_mode =="angle"andleft_right ==right): tank2.change_gun_angle(-adjustAngle) elif(key_mode =="power"andleft_right ==left): tank1.change_gun_power(-adjustAngle) elif(key_mode =="power"andleft_right ==right): tank2.change_gun_power(-adjustAngle) returnFalse

音效对象实现

classmusicPlay: def__init__(self): self.play=1 self.welcomeMusic="0 A#6 1 43;1 C#7 1 43;2 B6 1 43;3 D7 1 43;4 A#6 1 43;5 C#7 1 43;6 B6 1 43;7 G6 1 43;8 B6 1 43;9 C#7 1 43;10 A6 1 43;11 D7 1 43" self.backGroundMusic="0 C5 1 43;1 A#4 1 43;3 G4 1 43;4 C5 1 43;7 G4 1 43;8 C5 1 43;10 A#4 1 43;11 G4 1 43;12 A4 1 43;13 B4 1 43;14 A#4 1 43;15 G#4 1 43;15 G4 1 43;6 A4 1 43" self.shotedSound="0 D3 1 43" self.shotSound="0 C3 1 43" self.overMusic="0 D#5 1 43;0 C#5 1 43;2 D#5 1 43;2 C#5 1 43;1 F5 1 43;3 D5 1 43;4 D#5 1 43;4 C#5 1 43;6 D#5 1 43;6 C#5 1 43;5 A#4 1 43;7 E5 1 43;8 C#5 1 43;8 D#5 1 43;9 A5 1 43;10 C#5 1 43;10 D#5 1 43;11 F#5 1 43" self.welcomeMusicPlay=music(self.welcomeMusic,pins=[Pin(23)]) self.backGroundMusicPlay = music(self.backGroundMusic, pins=[Pin(23)]) self.overMusicPlay = music (self.overMusic,pins=[Pin(23)]) #游戏背景音乐 defbgmPlay(self): ifself.play: self.backGroundMusicPlay.tick() #欢迎界面音乐 defwelPlay(self): ifself.play: self.welcomeMusicPlay.tick() sleep(0.1) #游戏结束界面音乐 defoverPlay(self): ifself.play: self.overMusicPlay.tick() sleep(0.1) #炮弹发射音效 defshotSoundPlay(self): ifself.play: sound=music(self.shotSound,pins=[Pin(23)]) sound.tick() sleep(0.1) sound.stop() #坦克击毁音效 defshotedSoundPlay(self): ifself.play: sound=music(self.shotedSound,pins=[Pin(23)]) sound.tick() sleep(0.1) sound.stop() #音乐停止 defstop(self): self.welcomeMusicPlay.stop() #音效开关 defswitch(self): ifself.play: self.play=0 else: self.play=1 self.stop()

项目总结

难题及解决方法:

问题1:屏幕刷新速度慢,画面游戏画面播放不流畅

解决:更换带有ST7789 LCD驱动C语言驱动的MicroPython固件

问题2:炮弹轨迹与炮管移动轨迹的残留

解决:使用背景色对上一帧轨迹进行填充

问题3:炮弹击中逻辑检测

解决:设计坦克的击中区域,对炮弹的每一帧进行计算,判断是否到达击中区域。

点击阅读原文查看更多内容~