2020征文-開發(fā)板鴻蒙Hi3861之俄羅斯方塊小游戲(附源碼)
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#zz
一、原理
俄羅斯方塊相信大家都玩過,首先把場景分成可移動部分、和固定部分;

- unsigned short data_blk[16]; //游戲固定部分
- unsigned short data_act[4]; //游戲移動部分
- unsigned char display_blk_data[53] = {0x40,0xff,0x55}; //游戲場景部分用于顯示
- unsigned char display_nst_data[17] = {0x40}; //游戲顯示將出場的下一個方塊
- unsigned char data_nst; //下一個方塊的內(nèi)容
- unsigned int score = 0; //得分
- unsigned int delay = 100000; //下降延時控制速度
- char row_act = -1; //活動方塊所在行數(shù)
- hi_i2c_data display_blk; //用于顯示
- hi_i2c_data display_nst; //用于顯示
固定場景部分大小為16x12, 用16個無符號short(16位)型表示,僅用到低12位;
可移動部分大小為4x12, 用4個無符號short(16位)型表示,僅用到低12位;
所有的方塊(19種)有預(yù)定義為block[19][4],下一個預(yù)告用一個無符號char型(0-18)表示19個其中的一個;
通過row_act(活動方塊所在行數(shù))控制活動方塊向下移動。
二、顯示
- void display(void)
- {
- //show the canvas
- unsigned short temp;
- for(unsigned char i=0;i<8;++i)
- {
- for(unsigned char j=0;j<12;++j)
- {
- for(unsigned char k=0;k<4;++k)
- {
- display_blk_data[3+j*4+k] = 0x00;
- temp = i*2>=row_act && i*2<row_act+4 ? data_blk[i*2]|data_act[i*2-row_act] : data_blk[i*2];
- display_blk_data[3+j*4+k] |= temp&1<<j ? img[k] : 0x00;
- 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];
- display_blk_data[3+j*4+k] |= temp&1<<j ? img[k]<<4 : 0x00;
- }
- }
- oled_write_data(0, i, &display_blk);
- }
- //show the nest block
- for(unsigned char i=0;i<2;++i)
- {
- for(unsigned char j=0;j<4;++j)
- {
- for(unsigned char k=0;k<4;++k)
- {
- display_nst_data[j*4+k+1] = 0;
- display_nst_data[j*4+k+1] |= block[data_nst][i*2]&0x10<<j ? img[k] : 0x00;
- display_nst_data[j*4+k+1] |= block[data_nst][i*2+1]&0x10<<j ? img[k]<<4 : 0x00;
- }
- }
- oled_write_data(64, i+1, &display_nst);
- }
- //show the score
- oled_write_num(64, 7, score, 0);
- }
顯示函數(shù)由三部分組成:游戲場景、下一塊預(yù)告、分?jǐn)?shù);
重點介紹一下游戲場景部分:
最外層i循環(huán)共8次,每次顯示16行中的兩行;
第二層j循環(huán)共12次,每次處理一行中的一個像素;
第三層k循環(huán)把第個游戲像素換算成用于顯示的4x4個像素
- 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 = 行數(shù)遇到可移動部分 ? 背景+前景 : 背景;
- display_blk_data[3+j*4+k] |= temp&1<<j ? img[k] : 0x00;
用于顯示的像素數(shù)據(jù) |= 顯性像素? img中的一列 : 不顯示;
下一塊預(yù)告部分與上面類似,相信能舉一反三的理解一下;
再簡單介紹一下顯示分?jǐn)?shù)的部分“void oled_write_num(hi_u8 x, hi_u8 y, unsigned int n, hi_bool zero)"
x y 是要顯示的數(shù)值所在的坐標(biāo),n是要顯示的數(shù)值,zero是否顯示前面的0;
- void oled_write_num(hi_u8 x, hi_u8 y, unsigned int n, hi_bool zero)
- {
- unsigned int number = n;
- unsigned char str_num[9];
- for(unsigned char i=0;i<8;++i)
- {
- str_num[7-i] = num[number%10];
- number /= 10;
- }
- str_num[8] = 0;
- if(zero)
- {
- oled_write_string_57(x, y, (hi_u8 *)str_num);
- }
- else
- {
- hi_u8 *p = str_num;
- for(;*p=='0';++p);
- oled_write_string_57(x, y, p);
- }
- }
這部分比較簡單相信大家都能理解,把int型按位轉(zhuǎn)換成字符串顯示,
如果去除前面的0直接將字符串的起始地址向后移動,直到有非0數(shù)字。
如果想仔細研究顯示原理請下載附件顯示驅(qū)動芯片數(shù)據(jù)手冊
三、方塊移動
- void block_left(void)
- {
- //限制移動代碼
- //move to right on screen left
- for(unsigned char i=0;i<4;++i)
- {
- data_act[i]>>=1;
- }
- }
直接把活動方塊進行移動操作即可,左右原理一樣;
就這么簡單? 當(dāng)然不是!
在移動前還要加一些限制:到邊界了不能再移動、有固定方塊阻擋不能移動
下面就是限制移動代碼,如果觸發(fā)限制移動條件,直接返回,不進行移動操作
- //if close to edge give up move
- for(unsigned char i=0;i<4;++i)
- {
- if(data_act[i]&0x0001)
- {
- return;
- }
- if((data_act[i]>>1) & data_blk[row_act+i])
- {
- return;
- }
- }
這個最燒腦的就是方塊的旋轉(zhuǎn)了,發(fā)視頻前就差旋轉(zhuǎn)函數(shù)沒有寫了,直到昨天才調(diào)到合適,
先看一下基礎(chǔ)代碼:
- static void block_turn(char* arg)
- {
- (void)arg;
- unsigned short turned[4]={0, 0, 0, 0};
- unsigned char i;
- for(i=0;i<12;++i)
- {
- if(data_act[0]&1<
- {
- break;
- }
- }
- for(unsigned char j=0;j<4;++j)
- {
- for(unsigned char k=0;k<4;++k)
- {
- turned[3-j] |= data_act[k]&1<<(i+j) ? 1<<(i+k) : 0;
- }
- }
- for(unsigned char j=0;j<4;++j)
- {
- data_act[j] = turned[j];
- }
- }
首先是聲明一個"turned[4]"用于存放旋轉(zhuǎn)后的方塊,為什么不直接在原圖旋轉(zhuǎn)呢?
第一個循環(huán)從低到高到位掃描找到方塊所在列,
第二個循環(huán)從找到方塊的列取4X4進行行列轉(zhuǎn)置,
第三個循環(huán)把旋轉(zhuǎn)后的方塊更新到當(dāng)前活動方塊。

重點:前面講了這是一個基礎(chǔ)代碼,功能實現(xiàn)了,但有一個問題不得不考慮:旋轉(zhuǎn)后干涉嗎?干涉怎么辦?
解析:除了上面不會干涉,下左右都可能因為旋轉(zhuǎn)干涉,干涉我就不轉(zhuǎn)了唄。
如圖旋轉(zhuǎn)會造成方塊下移:
- for(unsigned char j=0;turned[0]==0&&j<2;++j)
- {
- turned[0] = turned[1];
- turned[1] = turned[2];
- turned[2] = turned[3];
- turned[3] = 0;
- }
如果己經(jīng)在邊上了,可能會造成出界:

- for(;turned[0]&1<<12 || turned[1]&1<<12 || turned[2]&1<<12 || turned[3]&1<<12;)
- {
- for(unsigned char j=0;j<4;++j)
- {
- turned[j] >>= 1;
- }
- }
因為是左對齊的,所以左邊不會存在這個情況,且只有右邊有富裕空間剛好利用一下。
最近再檢測一下是否與固定方塊干涉:
- for(unsigned j=0;j<4;++j)
- {
- if(turned[j] & data_blk[row_act+j])
- {
- return;
- }
- }
以上條件都滿足了,才能執(zhí)行最后的更新到當(dāng)前活動方塊,否則放棄旋轉(zhuǎn)。
這也是為什么要事先聲明一個“turned[4]“,如果在原圖旋轉(zhuǎn)萬一干涉了還要轉(zhuǎn)回去!
四、按鍵的實現(xiàn)(重點)
按鍵用到了兩個接口分別是GPIO5和GPIO8,
- void init_key(void)
- {
- GpioInit();
- IoSetFunc(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_IO_FUNC_GPIO_5_GPIO);
- GpioSetDir(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_GPIO_DIR_IN);
- IoSetPull(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_IO_PULL_NONE);
- GpioRegisterIsrFunc(WIFI_IOT_IO_NAME_GPIO_5, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, key_press, NULL);
- IoSetFunc(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_FUNC_GPIO_8_GPIO);
- GpioSetDir(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_GPIO_DIR_IN);
- IoSetPull(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_PULL_UP);
- GpioRegisterIsrFunc(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_INT_TYPE_EDGE, WIFI_IOT_GPIO_EDGE_FALL_LEVEL_LOW, block_turn, NULL);
- }
這兩個接口還是有區(qū)別的,5#口上接了三個按鍵,8#口上一個按鍵,分別指定了中斷服務(wù)函數(shù):
8#比較簡單檢測到下降沿進行中斷服務(wù)程序(方塊旋轉(zhuǎn))即前面講到的“block_turn()”;
5#稍復(fù)雜一點,進行中斷服務(wù)程序后再進行AD轉(zhuǎn)換,通過AD轉(zhuǎn)換檢出是哪一個按鍵被按下,再進行不同的操作

當(dāng)不同的按鍵按下時,會通過AD檢測到不同的采樣值,可以通過計算得到,也可以通過實際采集得到:
讀取端口的模擬量值:
- hi_u16 read_key(void)
- {
- hi_u16 data=0;
- hi_adc_read(HI_ADC_CHANNEL_2, &data, HI_ADC_EQU_MODEL_4, HI_ADC_CUR_BAIS_DEFAULT, 10);
- return data;
- }
用到了自帶的“hi_adc_read”,參數(shù)分別是(要讀取的端口、接收數(shù)據(jù)的變量、取N次采樣平均結(jié)果、基準(zhǔn)電壓、采樣間隔)
這里讀的是端口2(見原理圖)取4次平均值,自動基準(zhǔn)電壓,10us間隔,
可以是方法還沒有完全掌握,更改基準(zhǔn)電壓沒有影響檢測值,而且當(dāng)沒有按鍵按下時應(yīng)該讀到3.3V的電壓,卻只讀到了1.8V的電壓。
后面再仔細研究后更新一下。
這里提供計算方法供參考:
當(dāng)S1按下 采集電壓 = 3.3 * 1 / (1+4.7) = 0.578947368V
采集到的值 = 4096 * 1 / (1+4.7) = 718
當(dāng)S2按下 采集電壓 = 3.3 * (1+1) / (1+1+4.7) = 0.985074627
采集到的值 = 4096 * (1+1) / (1+1+4.7) = 1223
以下參考值來源實際采集!
- static void key_press(char* arg)
- {
- (void)arg;
- unsigned int ret = read_key();
- usleep(500);
- if (abs(ret - read_key()) > 30)
- {
- return;
- }
- if(ret>300 && ret<360)
- {
- block_left();
- return;
- }
- if(ret>530 && ret<590)
- {
- block_right();
- return;
- }
- }
五、自然下降
向下移動就簡單多了,直接進行++就OK了。
- row_act++;
但是在加之前也有附加條件,是不是到底了?到底了是不是有滿足消除條件的行了?會不會已經(jīng)到頂行了?
- char flag = 0;
- for(unsigned char i=0;i<4;++i)
- {
- if(data_blk[row_act+i+1] & data_act[i])
- {
- flag = 1;
- break;
- }
- }
- if(flag || (row_act>11 && data_act[15-row_act]!=0))
- {
- for(unsigned char i=0;i<4;++i)
- {
- data_blk[row_act+i] |= data_act[i];
- data_act[i] = block[data_nst][i];
- }
- remove_full();
- row_act = -1;
- data_nst = get_next();
- //Game over
- if(data_blk[0])
- {
- oled_write_string_16(20, 3, (hi_u8 *)"Game over!");
- while(1)
- {
- usleep(5000);
- }
- }
- }
如果到底了(不管是到游戲場景的底部,還是遇到固定的方塊)當(dāng)前活動方法結(jié)束
當(dāng)前活動劃到固定方塊,重新在頂部生成新的方塊;
一個方塊落定后要判斷是否有滿足可消除的行,如果有消除;
如果最頂行都被固定方塊填充的時候判定“Game over!”。
如果有人還沒有配置好開發(fā)環(huán)境,也可以下載我編譯好的,直接用HiBurn燒進行去可以玩了!
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#zz