2020征文-开发板鸿蒙Hi3861之俄罗斯方块小游戏(附源码)

开发
文章由鸿蒙社区产出,想要了解更多内容请前往:51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com/#zz

[[357128]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#zz

一、原理

俄罗斯方块相信大家都玩过,首先把场景分成可移动部分、和固定部分;


  1. unsigned short data_blk[16]; //游戏固定部分  
  2. unsigned short data_act[4]; //游戏移动部分  
  3. unsigned char display_blk_data[53] = {0x40,0xff,0x55}; //游戏场景部分用于显示  
  4. unsigned char display_nst_data[17] = {0x40}; //游戏显示将出场的下一个方块  
  5. unsigned char data_nst; //下一个方块的内容  
  6. unsigned int score = 0; //得分  
  7. unsigned int delay = 100000; //下降延时控制速度  
  8. char row_act = -1; //活动方块所在行数  
  9. hi_i2c_data display_blk; //用于显示  
  10. hi_i2c_data display_nst; //用于显示 

 固定场景部分大小为16x12, 用16个无符号short(16位)型表示,仅用到低12位;

可移动部分大小为4x12, 用4个无符号short(16位)型表示,仅用到低12位;

所有的方块(19种)有预定义为block[19][4],下一个预告用一个无符号char型(0-18)表示19个其中的一个;

通过row_act(活动方块所在行数)控制活动方块向下移动。

二、显示

  1. void display(void) 
  2.     //show the canvas 
  3.     unsigned short temp
  4.     for(unsigned char i=0;i<8;++i) 
  5.     { 
  6.         for(unsigned char j=0;j<12;++j) 
  7.         { 
  8.             for(unsigned char k=0;k<4;++k) 
  9.             { 
  10.                 display_blk_data[3+j*4+k] = 0x00; 
  11.                 temp = i*2>=row_act && i*2<row_act+4 ? data_blk[i*2]|data_act[i*2-row_act] : data_blk[i*2]; 
  12.                 display_blk_data[3+j*4+k] |= temp&1<<j ? img[k] : 0x00; 
  13.                 temp = i*2+1>=row_act && i*2<row_act+3 ? data_blk[i*2+1]|data_act[i*2+1-row_act] : data_blk[i*2+1]; 
  14.                 display_blk_data[3+j*4+k] |= temp&1<<j ? img[k]<<4 : 0x00; 
  15.             } 
  16.         } 
  17.         oled_write_data(0, i, &display_blk); 
  18.     } 
  19.     //show the nest block 
  20.     for(unsigned char i=0;i<2;++i) 
  21.     { 
  22.         for(unsigned char j=0;j<4;++j) 
  23.         { 
  24.             for(unsigned char k=0;k<4;++k) 
  25.             { 
  26.                 display_nst_data[j*4+k+1] = 0; 
  27.                 display_nst_data[j*4+k+1] |= block[data_nst][i*2]&0x10<<j ? img[k] : 0x00; 
  28.                 display_nst_data[j*4+k+1] |= block[data_nst][i*2+1]&0x10<<j ? img[k]<<4 : 0x00; 
  29.             } 
  30.         } 
  31.         oled_write_data(64, i+1, &display_nst); 
  32.     } 
  33.     //show the score 
  34.     oled_write_num(64, 7, score, 0); 

 显示函数由三部分组成:游戏场景、下一块预告、分数;

重点介绍一下游戏场景部分:

最外层i循环共8次,每次显示16行中的两行;

第二层j循环共12次,每次处理一行中的一个像素;

第三层k循环把第个游戏像素换算成用于显示的4x4个像素

  1. temp = i*2>=row_act && i*2<row_act+4 ? data_blk[i*2]|data_act[i*2-row_act] : data_blk[i*2]; 

 temp = 行数遇到可移动部分 ? 背景+前景 : 背景;

  1. display_blk_data[3+j*4+k] |= temp&1<<j ? img[k] : 0x00; 

用于显示的像素数据 |= 显性像素? img中的一列 : 不显示;

下一块预告部分与上面类似,相信能举一反三的理解一下;

再简单介绍一下显示分数的部分“void oled_write_num(hi_u8 x, hi_u8 y, unsigned int n, hi_bool zero)"

x y 是要显示的数值所在的坐标,n是要显示的数值,zero是否显示前面的0;

  1. void oled_write_num(hi_u8 x, hi_u8 y, unsigned int n, hi_bool zero)  
  2.  
  3. unsigned int number = n;  
  4. unsigned char str_num[9];  
  5. for(unsigned char i=0;i<8;++i) 
  6.  
  7.  
  8. str_num[7-i] = num[number%10];  
  9. number /= 10;  
  10.  
  11. str_num[8] = 0;  
  12. if(zero) 
  13.  
  14.  
  15. oled_write_string_57(x, y, (hi_u8 *)str_num);  
  16.  
  17. else 
  18.  
  19.  
  20. hi_u8 *p = str_num;  
  21. for(;*p=='0';++p);  
  22. oled_write_string_57(x, y, p);  
  23.  

 这部分比较简单相信大家都能理解,把int型按位转换成字符串显示,

如果去除前面的0直接将字符串的起始地址向后移动,直到有非0数字。

如果想仔细研究显示原理请下载附件显示驱动芯片数据手册

三、方块移动

  1. void block_left(void)  
  2.  
  3. //限制移动代码  
  4. //move to right on screen left  
  5. for(unsigned char i=0;i<4;++i) 
  6.  
  7.  
  8. data_act[i]>>=1;  
  9.  

 直接把活动方块进行移动操作即可,左右原理一样;

就这么简单? 当然不是!

在移动前还要加一些限制:到边界了不能再移动、有固定方块阻挡不能移动

下面就是限制移动代码,如果触发限制移动条件,直接返回,不进行移动操作

  1. //if close to edge give up move  
  2. for(unsigned char i=0;i<4;++i) 
  3.  
  4.  
  5. if(data_act[i]&0x0001)  
  6.  
  7. return 
  8.  
  9. if((data_act[i]>>1) & data_blk[row_act+i])  
  10.  
  11. return 
  12.  

 这个最烧脑的就是方块的旋转了,发视频前就差旋转函数没有写了,直到昨天才调到合适,

先看一下基础代码: 

  1. static void block_turn(char* arg) 
  2.  
  3.  
  4. (void)arg; 
  5.  
  6. unsigned short turned[4]={0, 0, 0, 0}; 
  7.  
  8. unsigned char i; 
  9.  
  10. for(i=0;i<12;++i) 
  11.  
  12.  
  13. if(data_act[0]&1< 
  14.  
  15.  
  16. break; 
  17.  
  18.  
  19.  
  20. for(unsigned char j=0;j<4;++j) 
  21.  
  22.  
  23. for(unsigned char k=0;k<4;++k) 
  24.  
  25.  
  26. turned[3-j] |= data_act[k]&1<<(i+j) ? 1<<(i+k) : 0; 
  27.  
  28.  
  29.  
  30. for(unsigned char j=0;j<4;++j) 
  31.  
  32.  
  33. data_act[j] = turned[j]; 
  34.  
  35.  

 首先是声明一个"turned[4]"用于存放旋转后的方块,为什么不直接在原图旋转呢?

第一个循环从低到高到位扫描找到方块所在列,

第二个循环从找到方块的列取4X4进行行列转置,

第三个循环把旋转后的方块更新到当前活动方块。


重点:前面讲了这是一个基础代码,功能实现了,但有一个问题不得不考虑:旋转后干涉吗?干涉怎么办?

解析:除了上面不会干涉,下左右都可能因为旋转干涉,干涉我就不转了呗。

如图旋转会造成方块下移:

  1. for(unsigned char j=0;turned[0]==0&&j<2;++j) 
  2.    { 
  3.        turned[0] = turned[1]; 
  4.        turned[1] = turned[2]; 
  5.        turned[2] = turned[3]; 
  6.        turned[3] = 0; 
  7.    } 

 如果己经在边上了,可能会造成出界:


  1. for(;turned[0]&1<<12 || turned[1]&1<<12 || turned[2]&1<<12 || turned[3]&1<<12;) 
  2.    { 
  3.        for(unsigned char j=0;j<4;++j) 
  4.        { 
  5.            turned[j] >>= 1; 
  6.        }    
  7.    } 

 因为是左对齐的,所以左边不会存在这个情况,且只有右边有富裕空间刚好利用一下。

最近再检测一下是否与固定方块干涉:

  1. for(unsigned j=0;j<4;++j) 
  2.     if(turned[j] & data_blk[row_act+j]) 
  3.     { 
  4.         return
  5.     } 

 以上条件都满足了,才能执行最后的更新到当前活动方块,否则放弃旋转。

这也是为什么要事先声明一个“turned[4]“,如果在原图旋转万一干涉了还要转回去!

四、按键的实现(重点)

按键用到了两个接口分别是GPIO5和GPIO8,

  1. void init_key(void) 
  2.  
  3.  
  4. GpioInit(); 
  5.  
  6. IoSetFunc(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_IO_FUNC_GPIO_5_GPIO); 
  7.  
  8. GpioSetDir(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_GPIO_DIR_IN); 
  9.  
  10. IoSetPull(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_IO_PULL_NONE); 
  11.  
  12. GpioRegisterIsrFunc(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, key_press, NULL); 
  13.  
  14. IoSetFunc(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_FUNC_GPIO_8_GPIO); 
  15.  
  16. GpioSetDir(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_GPIO_DIR_IN); 
  17.  
  18. IoSetPull(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_PULL_UP); 
  19.  
  20. GpioRegisterIsrFunc(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, block_turn, NULL); 
  21.  

 这两个接口还是有区别的,5#口上接了三个按键,8#口上一个按键,分别指定了中断服务函数:

8#比较简单检测到下降沿进行中断服务程序(方块旋转)即前面讲到的“block_turn()”;

5#稍复杂一点,进行中断服务程序后再进行AD转换,通过AD转换检出是哪一个按键被按下,再进行不同的操作


当不同的按键按下时,会通过AD检测到不同的采样值,可以通过计算得到,也可以通过实际采集得到:

读取端口的模拟量值:

  1. hi_u16 read_key(void) 
  2.     hi_u16 data=0; 
  3.     hi_adc_read(HI_ADC_CHANNEL_2, &data, HI_ADC_EQU_MODEL_4, HI_ADC_CUR_BAIS_DEFAULT, 10); 
  4.     return data; 

 用到了自带的“hi_adc_read”,参数分别是(要读取的端口、接收数据的变量、取N次采样平均结果、基准电压、采样间隔)

这里读的是端口2(见原理图)取4次平均值,自动基准电压,10us间隔,

可以是方法还没有完全掌握,更改基准电压没有影响检测值,而且当没有按键按下时应该读到3.3V的电压,却只读到了1.8V的电压。

后面再仔细研究后更新一下。

这里提供计算方法供参考:

当S1按下 采集电压 = 3.3 * 1 / (1+4.7) = 0.578947368V

采集到的值 = 4096 * 1 / (1+4.7) = 718

当S2按下 采集电压 = 3.3 * (1+1) / (1+1+4.7) = 0.985074627

采集到的值 = 4096 * (1+1) / (1+1+4.7) = 1223

以下参考值来源实际采集!

  1. static void key_press(char* arg) 
  2.     (void)arg; 
  3.     unsigned int ret = read_key(); 
  4.     usleep(500); 
  5.     if (abs(ret - read_key()) > 30) 
  6.     { 
  7.         return
  8.     } 
  9.     if(ret>300 && ret<360) 
  10.     { 
  11.         block_left(); 
  12.         return
  13.     } 
  14.     if(ret>530 && ret<590) 
  15.     { 
  16.         block_right(); 
  17.         return
  18.     } 

 五、自然下降

向下移动就简单多了,直接进行++就OK了。

  1. row_act++; 

但是在加之前也有附加条件,是不是到底了?到底了是不是有满足消除条件的行了?会不会已经到顶行了?

  1. char flag = 0; 
  2.    for(unsigned char i=0;i<4;++i) 
  3.    { 
  4.        if(data_blk[row_act+i+1] & data_act[i]) 
  5.        { 
  6.            flag = 1; 
  7.            break; 
  8.        } 
  9.    } 
  10.    if(flag || (row_act>11 && data_act[15-row_act]!=0)) 
  11.    { 
  12.        for(unsigned char i=0;i<4;++i) 
  13.        { 
  14.            data_blk[row_act+i] |= data_act[i]; 
  15.            data_act[i] = block[data_nst][i]; 
  16.        } 
  17.        remove_full(); 
  18.        row_act = -1; 
  19.        data_nst = get_next(); 
  20.        //Game over 
  21.        if(data_blk[0]) 
  22.        { 
  23.            oled_write_string_16(20, 3, (hi_u8 *)"Game over!"); 
  24.            while(1) 
  25.            { 
  26.                usleep(5000); 
  27.            } 
  28.        } 
  29.    } 

 如果到底了(不管是到游戏场景的底部,还是遇到固定的方块)当前活动方法结束

当前活动划到固定方块,重新在顶部生成新的方块;

一个方块落定后要判断是否有满足可消除的行,如果有消除;

如果最顶行都被固定方块填充的时候判定“Game over!”。

如果有人还没有配置好开发环境,也可以下载我编译好的,直接用HiBurn烧进行去可以玩了! 

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#zz 

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2020-12-17 10:02:16

鸿蒙Hi3861开发板

2021-01-12 12:16:55

鸿蒙HarmonyOS游戏

2015-01-22 15:36:46

游戏源码

2020-12-15 11:57:49

Hi3861 HarmonyOS开发板

2011-06-13 18:21:12

2014-10-08 10:04:14

代码解释俄罗斯方块

2020-10-14 09:37:03

HiBurn鸿蒙.bin文件

2023-09-25 12:35:27

Python

2020-12-29 09:59:01

鸿蒙HarmonyOS智能家居

2020-10-30 17:42:36

鸿蒙 OS Hi386

2020-11-06 10:15:16

HiBurn

2021-06-25 15:32:13

鸿蒙HarmonyOS应用

2014-05-26 10:07:18

Javascript俄罗斯方块

2020-12-16 10:05:48

鸿蒙开发板Onenet平台

2020-12-08 12:36:41

Hi3861 ADC驱动开发

2015-04-28 09:21:28

JSJS俄罗斯方块游戏帝国

2020-02-27 13:43:14

Emacs俄罗斯方块应用

2020-05-19 17:26:21

Python俄罗斯方块游戏开发

2022-08-19 10:54:47

操作系统鸿蒙

2020-10-30 17:12:05

Hi3861
点赞
收藏

51CTO技术栈公众号