找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 134|回复: 0
打印 上一主题 下一主题
收起左侧

PONG...PONG 8x8 LED 矩阵的复古玩法

[复制链接]
跳转到指定楼层
楼主
。。。上一次和同学们(同学=一同学习者,同游=一同云游者,同居? 。。。 以此类推,不一而论)瞎聊了一点有关二进制数的宏定义以及用 bit map 手工输入点阵图形的事(http://www.51hei.com/bbs/dpj-237651-1.html), 但没有提及如何玩 51 单板机上那个 8x8 LED 矩阵。 BBC Micro:bit 上有一组 5 x 5 的LED 矩阵,官方很认证地开发了一个"喜怒哀乐"表情包,意欲把那个稀缺资源的利用发挥到极致,考虑到Micro:bit不过是儿童的玩具,我就不把那个表情包port到51 单板机上了。对于认真的程序员,把 PONG 这个古老的游戏port过来,以表示对前辈程序员的敬意,视乎更有一点意思?

。。。闲话少叙,我们单刀直入,先在那个 8x8LED 矩阵上实现单个 LED 的动画,而后展开相应的数据结构,以实现“乒乓球”和“乒乓板”之间的互动。为此,我们来看看如何实现用(x,y)一对坐标来实现对64个LED当中任何一个LED的“寻址” 。。。 以下是N种方案当中的一种:

u8 data bitBuffer []= {
  b00111000,  //x(7,0)       *(7,7)
  b01111100,  //  |
  b01111110,  //  |
  b00111111,  //  |
  b00111111,  //  |   *(x,y)
  b01111110,  //  |
  b01111100,  //  |  
  b00111000   //(0,0)-------->y(0,7)
};
按照单板机上具体的硬件设计,我们可以把(x,y)映射为8x8LED矩阵的行列端口控制信号,以我手上那块单板机为例(http://www.51hei.com/bbs/dpj-237407-1.html),其映射关系如下:

(x,y) --> ( LedMatrixPort_Col = ~(1<<(7-x)) ,74hc595_Dout = 1<<(7-y))  )
这里,x 和 y 的值域都是整数闭区间 [0,7].

如你想要点亮(x,y)位置上单独一颗LED,调用以下函数即可:
//void _8x8ledMatrixShow(u8 x,u8 y) compact{
//       
//        LedMatrixPort_Col = ~(1<<(7-x));
//        hc595_write_data(1<<(7-y));
//        delay_10us(100);       //延时一段时间,等待显示稳定
//        hc595_write_data(0x00);//消影
//}

有了这个函数,你可以让这颗被点亮的LED在各种(数学)轨迹上游走,实现任意动画效果, piece of cake?

细心的同学会注意到我把上述函数注释屏蔽掉了,表示我在实际的PONG游戏代码里并没有使用这个函数。在游戏更新“乒乓球”位置的代码中,我直接利用了(x,y)和行-列端口的映射关系,同时把球的位置信息和“乒乓板”的位置信息一起缓存在bitBuffer 里面,当所有需要更新的数据都准备好之后才一次性地“刷新” LED矩阵的显示,这是几乎所有游戏编程普遍使用的所谓double buffer 技术,大家可以谁便参考一本游戏编程的书籍了解其原委,我不讲了。


避免调用上述函数,对于51 单片机这样的系统还考虑到硬件资源问题。51单片机的片内 数据RAM 非常贫乏,只能实现一个很浅的stack, 如果在函数调用时使用很多参数,最糟的情况下(如递归调用带参数的函数),stack 很容易overflow, 导致程序崩溃。因此,大家在不影响程序代码架构清晰的情况下,尽量使用全局变量为上策。

okay,我们继续PONG的编程。。。在游戏世界里,“球”和“板”都是所谓的 objetcs (东东? sprite ? whatever ),如果开发环境比较豪华,我们应该用类似C++ 的class 来封装这些东东的属性和行为,但这里比较简陋,我们就用struct 来简单凑合一下吧。。。

typedef struct {
        signed char pTop;   // 球板的顶端 y 坐标
        signed char pShift; // 球板的顶端 x 坐标,缺省值为 7,如果<7, 表示迎着球的方向击球,如果值不变,就是简单的挡球。。。
        u8 score;               // 用于计分
} sPaddle;

sPaddle myPaddle;    // 左侧球板,手工通过按钮实现上下移动或由 MPU 判断球的来路自动移动
sPaddle mpuPaddle;  // 右侧球板,由 MPU 判断球的来路自动移动


struct {
        signed char x, y;   // 球的位置
        signed char vx,vy; // 球的速度
}ball;


在游戏初始化时,我们必须给这些东东一些初始值,例如:

        if(bGameReStart)// 如果全局变量 bGameReStart ==1 , 从新开局
        {
                LED_PORT=0xff; // 熄灭 8x8 LED 矩阵
                srand (rand());   //  随机数下种, 要在main.c 开头处加上 #inlude <stdlib.h> 才能调用 srand 和 rand 函数

                myPaddle.pTop = 3; myPaddle.pShift  = 0;myPaddle.score  = 0;
                mpuPaddle.pTop = rand() % 6;mpuPaddle.pShift = 7;mpuPaddle.score = 0;
                ball.x = 6;
                ball.y =  myPaddle.pTop + rand() % 3;    //ball start by human
                ball.vx = 1 + rand()%2; //  相当于 random(1,2);
                ball.vy = -2 + rand()%5;// 相当于 random(-2,2);     avoid use function stack...

                bGameReStart =0; // don't keep running the code inside  {}
        }


世上所有游戏的灵魂是随机数,包括你在“真实”世界里面玩现实版的“真人游戏”, 所以我对随机数多啰嗦几句。。。由于硬件随机数发生器非常昂贵,大多数数字系统都采用软件伪随机数,其原理大家自行wikipeida, 我只解释上面的代码里,我为何免用 random(上限,下限) 这样的函数,而是直接用 % 运算符来取指定范围的随机数。

在<stdlib.h>库里,rand() 的16位整数值域是 [0, 32767],    random(上限,下限) 的定义通常类似以下形式:
unsigned int  random(unsigned int  lo,unsigned int  hi) {
  return ( lo +  (hi - lo)* rand()/32768 );
}

或者,限于返回8位(signed)整数:
signed char random(signed char lo, signed char hi){
return  (lo +  rand()%((hi-lo+1)) );
}


但这样的函数定义都有参数调用的开销,前一个还涉及“昂贵”的除法,这些都是资源匮乏的单板机需要尽量避免的。通常在C语言里,我们可以采用宏定义来重写函数定义,调用时采用 inline 宏定义,这样可以避免运行时的开销,把计算的负担分配到编译时,由编译器来代劳。同学们可以自行实验利用宏来修改上面的代码,提高程序的可读性。

接下来就是游戏的主循环。。。

L_GAME_START:
    bGameReStart =1;                       
    while(1)
                {
                                key_matrix_flip_scan(key_value);  // 4 x 4 按钮矩阵扫描
                                if (key_value ==2 || key_value == 5 )OE_74HC595 = 1; //turn off 8x8 LED matrix
                                        else OE_74HC595 = 0; // enable it otherwsie


                       
                                switch (key_value)
                                {
                                        case 1:     // 按钮- 1
                                                        _8x8ledMatrixDisplay(mPONG);
                                                         bGameReStart = 1;  // 从新开始游戏
                                                        continue;
                                        case mPADDLE_UPP-1:               
                                        case mPADDLE_UPP:
                                             myPaddle.pShift = mPADDLE_UPP - key_value;
                                                                myPaddle.pTop -=2;
                                                                if (myPaddle.pTop < 0)myPaddle.pTop =0;
                                                                goto L_GAME_UPDATE;       
                                       
                                        case mPADDLE_UP-1:
                                        case mPADDLE_UP:
                                                                myPaddle.pShift = mPADDLE_UP - key_value;                               
                                                                myPaddle.pTop -=1;
                                                                if (myPaddle.pTop < 0)myPaddle.pTop =0;
                                       
                                                                goto L_GAME_UPDATE;       
                                        case mPADDLE_DOWN-2:
                                        case mPADDLE_DOWN-1:
                                        case mPADDLE_DOWN:
                                                                myPaddle.pShift = mPADDLE_DOWN - key_value;
                                                                myPaddle.pTop +=1;
                                                                if (myPaddle.pTop > 4)myPaddle.pTop =5;
                                       
                                                                goto L_GAME_UPDATE;       
                                       
                                        case mPADDLE_DOWND-3:
                                        case mPADDLE_DOWND-2:
                                        case mPADDLE_DOWND-1:
                                        case mPADDLE_DOWND:  
                                                                myPaddle.pShift = mPADDLE_DOWND - key_value;
                                                                myPaddle.pTop +=2;
                                                                if (myPaddle.pTop > 4)myPaddle.pTop =5;
                                                                goto L_GAME_UPDATE;               
                                       
                                        case mUPDATE_PONG:  
                                                             if(bGameReStart)// re-start game
                                                             {
                                                               // 游戏初始化代码,前面已经讲解,此处不再重复
                                                            }
                                               
                                        L_GAME_UPDATE:       
                                                            _8x8clearBitBuffer();         // 清除缓冲区内容
                                                 
                                                             // 画球板
                                                              bitBuffer[myPaddle.pShift]  = b11100000;
                                                              bitBuffer[myPaddle.pShift]  = bitBuffer[myPaddle.pShift]>>myPaddle.pTop;

                                                            // 更新球的位置
                                                             ball.x += ball.vx;
                                                             ball.y += ball.vy;
                                                 
                                                            // 更新球板的位置
                                                           if (ball.vx > 0 ) // ball coming to human
                                                          {
                                                            // 如果 MPU 控制球板,计算球板如何判断球路做相应的移动
                                                            }

                                                         if(myPaddle.pTop <0) myPaddle.pTop =0;
                                                         if (myPaddle.pTop > 4)myPaddle.pTop =5;


                                                       // 判断球板是否接住球,没有接住,对方就加分。。。
                                                       if(ball.x >= (7 - myPaddle.pShift) )
                                                      {
                                                         ball.x = 7;
                                                         ball.vx=-ball.vx; //左侧 X方向反弹
                                                         
                                                         if( abs(ball.vy)<=1)
                                                         {
                                                                 if ( ball.y < myPaddle.pTop || ball.y > (myPaddle.pTop+2))
                                                                 {
                                                                         mpuPaddle.score++;  // 球板没有挡住球,对方加分
                                                                 }
                                                         }
                                                         else
                                                         {
                                                             // 等等其它判断和计分逻辑 。。。
                                                         }
                                                     }
                                                 
                                                  if( ball.y <=0 )
                                                 {
                                                    ball.y = 0;       // 顶端 Y- 方向反弹
                                                    ball.vy=-ball.vy;
                                                 }
                                                 if( ball.y >= 7)
                                                 {
                                                    ball.y = 7;   // 底端 Y- 方向反弹
                                                     ball.vy=-ball.vy;
                                                 }
                                                   if( ball.x <=0 )
                                                 {
                                                   ball.x = 0;  //右侧 X方向反弹
                                                   ball.vx=-ball.vx;
                                                 }
                                                         
                                               
                                                 bitBuffer[7-ball.x] = 128>>ball.y;  // 把球的位置“写” 入缓存区域
                                               
                                                 _8x8ledMatrixDisplay(bitBuffer);    // 刷新 8x8 LED 显示
                                                 key_value = mUPDATE_PONG; // repeat run mUPDATE_PONG section
                                                 
                                                 // 在 7 段数码管上 显示计分
                                                ired_buf[0]=gsmg_code[mpuPaddle.score];
                                                ired_buf[3]=gsmg_code[myPaddle.score];
                                               
                                                smg_display(ired_buf,1);
                                                continue;                                       
                                }
                }
//END OF GAME


同学们可以看到我在这个循环里大大冒犯了一下 Goto 语句的批评者,我是实用主义者,goto 语句在这里的使用,既有效也不影响程序的可读性。C就是高级的汇编语言啊,汇编语言大量使用各种 JMP 指令,C语言里适当使用 goto 语句,大家可以理直气壮。

这个循环的上面有一小段和 PZ 单板机设计缺陷有关的代码:


if (key_value ==2 || key_value == 5 )OE_74HC595 = 1; //turn off 8x8 LED matrix
        else OE_74HC595 = 0; // enable it otherwsie


板子的设计者提示玩家,如果要禁止 8x8 LED 矩阵,可以使用跳针,其实没有这个必要,这个跳针设计是多余的。8x8 LED 矩阵的开关,完全可以通过软件加以控制。我在板子上加了一根飞线,用 sbit OE_74HC595 = P1^7 定义了其用法,接下来就可以用P1^7 端口控制74HC595芯片,从而实现8x8 LED 矩阵的开和关,大家留意一下下面视频和图片里的飞线即可。

.....  ......  ...... let's call it a day. .................. .................... ................... ........................ ........... .......... .............















评分

参与人数 1黑币 +70 收起 理由
admin + 70 共享资料的黑币奖励!

查看全部评分

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享淘帖 顶 踩
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表