基于FPGA的贪吃蛇游戏 之代码解析

基于FPGA的贪吃蛇游戏 之代码解析

1. 代码结构

代码结构包含7.v文件。

 

 

下面依次解析。

 

 

 

2. 代码解析

(1) seg_display.v

数码管的译码模块是最熟悉,最简单的模块了。这里是共阳极的数码管,用case语句编码即可。从上图可以看到,这个模块被例化了3次,分别驱动3个数码管显示,百,十,个位的数字。

always @(seg_data)

begin

        case(seg_data)

        4'b0001: seg_out= 7'b1111001;       //数码管显示数字1  

        4'b0010: seg_out= 7'b0100100;   //数码管显示数字2  

        4'b0011: seg_out= 7'b0110000;   //数码管显示数字3  

        4'b0100: seg_out= 7'b0011001;   //数码管显示数字4  

        4'b0101: seg_out= 7'b0010010;   //数码管显示数字5  

        4'b0110: seg_out= 7'b0000010;   //数码管显示数字6  

        4'b0111: seg_out= 7'b1111000;   //数码管显示数字7  

        4'b1000: seg_out= 7'b0000000;   //数码管显示数字8  

        4'b1001: seg_out= 7'b0011000;   //数码管显示数字9  

        4'b0000: seg_out= 7'b1000000;       //数码管显示数字0

        endcase

end

 

(2) score_ctrl.v

数码管计分模块,稍稍难一点。要实现游戏中吃一个食物,加一分,并把分数转换成BCD码,以百、十、个位,送到3个数码管显示。

计分的结果bin_data,其实就是一个8位的计数器,因为游戏规则限定了计分的最大值是100,也就限定了这个计数器的位宽。那么就按常规的计数器设计,用ifelse语句,按优先级来设计先后顺序。先判断复位,计数结果清零,再判断是否计到最大值,是也清零,否就加一,最后剩余的情况就是计数结果保持不变。当然,这个模块比常规的计数器设计多了判断是否在RESTART状态,是也清零,加1的条件要看是否吃到食物,即add_cube是否为1。然后就是用通用的取模方式,取百、十、个的数字。

always@(posedge clk or negedge rst_n) begin

        if(!rst_n)                                       //复位时分数归零

            bin_data <= 0;

        else if(game_status==RESTART)                   //重启状态下分数归零

            bin_data <= 0;  

        else if(add_cube==1 && bin_data < 8'd100)           //当分数不超过100的时候,蛇每吃掉一个苹果计数器就+1

            bin_data <= bin_data + 1;

        else 

            bin_data <= bin_data;

        end

   

    assign bcd_data[3:0]  = bin_data%10;                //算出十进制数的个位

    assign bcd_data[7:4]  = (bin_data/10)%10;           //算出十进制数的十位

    assign bcd_data[11:8] = (bin_data/100)%10;      //算出十进制数的百位

 

(3) VGA_ctrl.v

VGA控制模块,实现:

① 游戏开始时,显示欢迎界面,就是存存在rom里的图片;

② 显示游戏难度的色块和字符色块;

③ 游戏进行中,显示蛇身和食物;

④ 游戏结束,显示分数。

 

这里用的是640*480@60Hz的模式,用ADV7123驱动VGA端口。VGA显示,也是整个设计里最核心的部分,首先,需要弄清楚行扫描和场扫描的时序。

 

 

 

 

当然,每一段的参数很容易查到。难点在于理解两种同步信号的时序,先后由哪些段组成。然后,再对照代码去理解同步信号的高低电平持续的时间长度,就很容易了。这里,系统时钟为何选择25MHz,也是根据640*480*60近似得到的。

第二个难点,就是显示的对象是图片,文字,色块等等多种,需要不同的存储方式,其中色块的划分,是最基础的,很多图形都是由基本的色块组成。代码里用了case区分扫描的对象种类,snake_show这个信号,来判断扫描到的是什么对象,再分别定义显示。

第三个就是坐标的区分,因为用到了色块来表示不同的对象,而色块又是由一个个的像素点组成的,所以要弄清楚比如:食物的坐标,像素的坐标等等,还有各自的有效范围。

VGA_ctrl模块的框图如下:

 

 

根据框图,比较容易判断信号的输入、输入属性。

代码首先就是端口声明,食物的坐标行比列位宽大,是根据640480来判断的。bcd_data是计分模块输入的百、十、个的值,game_status是游戏的状态,snake_show是显示的对象,即扫描的点是什么,vga_blank_nADV7123的消隐信号,就是在非有效显示区域为0vga_hs,vga_vs是行扫描和场扫描信号,vga_rgb888RGB信号。pos_x,pos_y是像素的坐标。

下面依次解析这个代码:

1) 为了使代码更清晰,增强代码的可读性,游戏状态,扫描(显示)对象,色彩都用本地参数定义。

localparam RESTART = 2'b00;        //游戏重启

    localparam START = 2'b01;           //游戏开始

    localparam PLAY = 2'b10;            //游戏进行

    localparam DIE = 2'b11;             //游戏结束

 

    localparam NONE = 2'b00;

    localparam HEAD = 2'b01;

    localparam BODY = 2'b10;

    localparam WALL = 2'b11;

   

    localparam  RED = 24'b11111111_00000000_00000000; //红色  

    localparam  GREEN = 24'b00000000_111111111_00000000; //绿色  

    localparam  BLUE = 24'b00000000_00000000_11111111; //蓝色

     localparam YELLOW = 24'b11111111_11111111_00000000; //黄色

    localparam  PINK = 24'b11111111_00000000_11111111; //粉色

     localparam WHITE = 24'b11111111_11111111_11111111; //白色

     localparam BLACK = 24'b00000000_00000000_00000000; //黑色

2) 先设计行周期和场周期计数器,再用计数结果生产同步信号。

// 行周期计数器的实现

    always @ (posedge clk, negedge rst_n)

        if (!rst_n)

            cnt_hs <= 0;

        else

            if (cnt_hs < HS_E - 1)

                cnt_hs <= cnt_hs + 1'b1;

            else

                cnt_hs <= 0;

                     

    // 场周期计数器的实现        

    always @ (posedge clk, negedge rst_n)

        if (!rst_n)

            cnt_vs <= 0;

        else

            if (cnt_hs == HS_E - 1)

                if (cnt_vs < VS_E - 1)

                    cnt_vs <= cnt_vs + 1'b1;

                else

                    cnt_vs <= 0;

            else

                cnt_vs <= cnt_vs;

                     

    // 行同步信号时序的产生

    always @ (posedge clk, negedge rst_n)

        if (!rst_n)

            vga_hs <= 1'b1;

        else

            if (cnt_hs < HS_A - 1) //同步之前vga_hs信号都是低, 同步之后(a)vga_hs信号是高

                vga_hs <= 1'b0;

            else

                vga_hs <= 1'b1;

     

     // 场同步信号时序的产生        

    always @ (posedge clk, negedge rst_n)

        if (!rst_n)

            vga_vs <= 1'b1;

        else

            if (cnt_vs < VS_A - 1) //同步之前vga_vs 信号都是低, 同步之后(a)vga_vs 信号是高

                vga_vs <= 1'b0;

            else

                vga_vs <= 1'b1;

然后,用行有效段和列有效段圈定有效显示区域。

assign en_hs = (cnt_hs > HS_A + HS_B - 1)&& (cnt_hs < HS_E - HS_D);//en_vs 将有效数据q段标出来了,有效数据q段en_hs 才为高,否则为低

        assign en_vs = (cnt_vs > VS_A + VS_B - 1) && (cnt_vs < VS_E - VS_D);//将vga显示的有效像素点位置全部标注出来了

        assign en = en_hs && en_vs;

        assign vga_blank_n = en;

像素的坐标范围也是在有效显示区内,所以

assign pos_x = en ? (cnt_hs - (HS_A + HS_B - 1'b1)) : 0;

        assign pos_y = en ? (cnt_vs - (VS_A + VS_B - 1'b1)) : 0;

过程语句块里,需要处理不同的状态,不同的输出。两个参数:cnt_clk用来计时6秒,显示欢迎界面的图片,cnt用来计时4秒,在游戏结束状态,蛇身闪烁。

显示图片

else if ( game_status == RESTART) begin

                cnt<=0;

                if(cnt_clk < 150000000 )begin//“欢迎来到贪吃蛇游戏”的画面停留6s 时钟25M 0.04us*150_000_000=6s

                    cnt_clk <= cnt_clk+1;  

                    if(picture_flag_enable) begin//picture_flag_enable不等同于en,因为picture_flag_enable可以是比640*480还小的区域

                        vga_rgb <= rom_data;

                    end

                    else begin

                        vga_rgb<= 24'b000000000000000000000000;

                    end

                end

显示字符加色块

else if(cnt_clk >= 150000000) begin

                    if(pos_x[9:4] >=15 && pos_x[9:4] < 25 && pos_y[9:4] >= 8 && pos_y[9:4] < 10&& char[char_y][159-char_x] == 1'b1) begin

                        vga_rgb<= WHITE; end//显示“请选择难度” 字符

                       

                    else if(pos_x[9:4] >=17 && pos_x[9:4] < 18 && pos_y[9:4] >= 15 && pos_y[9:4] < 16) begin

                        vga_rgb<= GREEN;end//显示“容易”的绿方块

                       

                    else if(pos_x[9:4] >=19 && pos_x[9:4] < 20 && pos_y[9:4] >= 15 && pos_y[9:4] < 16)begin

                        vga_rgb<= YELLOW;end//显示“中等”的黄方块

                       

                    else if(pos_x[9:4] >=21 && pos_x[9:4] < 22 && pos_y[9:4] >= 15 && pos_y[9:4] < 16)begin

                        vga_rgb<= RED;end//显示“困难”的红方块

                    else begin

                        vga_rgb<= BLACK;end

                end

显示墙,蛇,空气,食物

else if ( game_status == PLAY|game_status == START) begin//在游戏开始状态下 扫描食物、蛇头、蛇身体、墙

                //led[0]<=1;

                cnt<=0;

                if(pos_x[9:4] == apple_x && pos_y[9:4] == apple_y) begin

                    vga_rgb = PINK;

                end                

                else if(snake_show == NONE) begin

                    vga_rgb = BLACK; end

                else if(snake_show == WALL) begin

                    vga_rgb = RED;end

                else if(snake_show == HEAD|snake_show == BODY) begin

                    //vga_rgb = (snake_show == HEAD) ?  GREEN : BLUE;

                    case({pos_x[3:0],pos_y[3:0]})

                        8'b00000000:vga_rgb = BLACK;

                        8'b00000001:vga_rgb = BLACK;

                        8'b00000010:vga_rgb = BLACK;

                        default:vga_rgb = (snake_show == HEAD) ?  GREEN : BLUE;

                    endcase

                end

                else begin

                    vga_rgb<= BLACK;

                end

            end

显示100,比较繁琐,就是用色块组成100的形状

else if(bcd_data[11:8]==1'd1)begin//当计分达到100则封顶,代表游戏成功

                if(pos_x[9:4] >= 12 && pos_x[9:4] < 14 && pos_y[9:4] >= 8 && pos_y[9:4] < 16)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 12 && pos_x[9:4] < 14 && pos_y[9:4] >= 14 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

 

                else if(pos_x[9:4] >= 16 && pos_x[9:4] < 24 && pos_y[9:4] >= 8 && pos_y[9:4] < 10)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 16 && pos_x[9:4] < 24 && pos_y[9:4] >= 20 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 16 && pos_x[9:4] < 18 && pos_y[9:4] >= 8 && pos_y[9:4] < 16)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 22 && pos_x[9:4] < 24 && pos_y[9:4] >= 8 && pos_y[9:4] < 16)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 16 && pos_x[9:4] < 18 && pos_y[9:4] >= 14 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 22 && pos_x[9:4] < 24 && pos_y[9:4] >= 14 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

 

                else if(pos_x[9:4] >= 26 && pos_x[9:4] < 34 && pos_y[9:4] >= 8 && pos_y[9:4] < 10)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 26 && pos_x[9:4] < 34 && pos_y[9:4] >= 20 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 26 && pos_x[9:4] < 28 && pos_y[9:4] >= 8 && pos_y[9:4] < 16)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 32 && pos_x[9:4] < 34 && pos_y[9:4] >= 8 && pos_y[9:4] < 16)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 26 && pos_x[9:4] < 28 && pos_y[9:4] >= 14 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

                else if(pos_x[9:4] >= 32 && pos_x[9:4] < 34 && pos_y[9:4] >= 14 && pos_y[9:4] < 22)

                    vga_rgb = 24'hff80ff;

                else 

                    vga_rgb = BLACK;

                end

DIE状态,就分4秒前和4秒后,先显示不同对象,再显示分数。

(4)apple_generate.v

食物模块两个作用,一是产生食物坐标,二是判断食物是否被吃掉。

食物坐标用加法随机产生

always@(posedge clk)

        random_num <= random_num + 999;  //用加法产生随机数  

        //随机数高六位为食物x的坐标,低五位为苹果Y坐标

 

if(apple_x == head_x && apple_y == head_y) begin//当蛇头坐标和苹果坐标一样时,表示蛇吃掉一个苹果

                add_cube <= 1;

                apple_x <= (random_num[10:5] > 38) ? (random_num[10:5] - 25) : (random_num[10:5] == 0) ? 1 : random_num[10:5];

                apple_y <= (random_num[4:0] > 28) ? (random_num[4:0] - 3) : (random_num[4:0] == 0) ? 1:random_num[4:0];

            end    //判断随机数是否超出频幕坐标范围 将随机数转换为下个苹果的X Y坐标

 

(5)snake.v

蛇运动情况控制模块。

蛇移动的速度,由三个拨动开关选定。所谓速度其实就是隔多久移动一次。蛇身共16节,运动结果有三种状态:撞墙、撞自身、移动。撞墙就是判断蛇头的坐标是否与四面墙的坐标相同,撞自身就是判断蛇头的坐标是否与后面15节蛇身的坐标有相同,移动就是把前一节的坐标赋给后一节,并再次判断蛇头的坐标是否撞墙,若否,重新定义蛇头的坐标。

else begin

            clk_cnt <= clk_cnt + 1;

                if(clk_cnt == speed) begin

                    clk_cnt <= 0;

                    if(game_status==PLAY) begin

                        if((direct_r==UP && cube_y[0] == 1)||(direct_r==DOWN && cube_y[0] == 28)||(direct_r==LEFT && cube_x[0] == 1)||(direct_r==RIGHT && cube_x[0] == 38))begin

                            hit_wall <= 1; end//撞到墙壁                

                            //如果蛇是向上运动,且蛇头的y坐标跟上面墙的y坐标(cube_y[0] == 1)重合

                            //如果蛇是向下运动,且蛇头的y坐标跟下面墙的y坐标(cube_y[0] == 28)重合

                            //如果蛇是向左运动,且蛇头的x坐标跟左面墙的x坐标(cube_x[0] == 1)重合

                            //如果蛇是向右运动,且蛇头的x坐标跟上面墙的x坐标(cube_x[0] == 38)重合

                                           

                        else if((cube_y[0] == cube_y[1] && cube_x[0] == cube_x[1] && is_exist[1] == 1)||

                                (cube_y[0] == cube_y[2] && cube_x[0] == cube_x[2] && is_exist[2] == 1)||

                                (cube_y[0] == cube_y[3] && cube_x[0] == cube_x[3] && is_exist[3] == 1)||

                                (cube_y[0] == cube_y[4] && cube_x[0] == cube_x[4] && is_exist[4] == 1)||

                                (cube_y[0] == cube_y[5] && cube_x[0] == cube_x[5] && is_exist[5] == 1)||

                                (cube_y[0] == cube_y[6] && cube_x[0] == cube_x[6] && is_exist[6] == 1)||

                                (cube_y[0] == cube_y[7] && cube_x[0] == cube_x[7] && is_exist[7] == 1)||

                                (cube_y[0] == cube_y[8] && cube_x[0] == cube_x[8] && is_exist[8] == 1)||

                                (cube_y[0] == cube_y[9] && cube_x[0] == cube_x[9] && is_exist[9] == 1)||

                                (cube_y[0] == cube_y[10] && cube_x[0] == cube_x[10] && is_exist[10] == 1)||

                                (cube_y[0] == cube_y[11] && cube_x[0] == cube_x[11] && is_exist[11] == 1)||

                                (cube_y[0] == cube_y[12] && cube_x[0] == cube_x[12] && is_exist[12] == 1)||

                                (cube_y[0] == cube_y[13] && cube_x[0] == cube_x[13] && is_exist[13] == 1)||

                                (cube_y[0] == cube_y[14] && cube_x[0] == cube_x[14] && is_exist[14] == 1)||

                                (cube_y[0] == cube_y[15] && cube_x[0] == cube_x[15] && is_exist[15] == 1)) begin

                                hit_body <= 1; end//头的Y坐标=任一位身体的Y坐标 且 头的X坐标=任一位身体的X坐标 且 身体的该长度位存在,说明碰到身体

                            else begin

                                cube_x[1] <= cube_x[0];

                                cube_y[1] <= cube_y[0];

                                                           

                                cube_x[2] <= cube_x[1];

                                cube_y[2] <= cube_y[1];

                                                           

                                cube_x[3] <= cube_x[2];

                                cube_y[3] <= cube_y[2];

                                                           

                                cube_x[4] <= cube_x[3];

                                cube_y[4] <= cube_y[3];

                                                           

                                cube_x[5] <= cube_x[4];

                                cube_y[5] <= cube_y[4];

                                                           

                                cube_x[6] <= cube_x[5];

                                cube_y[6] <= cube_y[5];

                                                           

                                cube_x[7] <= cube_x[6];

                                cube_y[7] <= cube_y[6];

                                                           

                                cube_x[8] <= cube_x[7];

                                cube_y[8] <= cube_y[7];

                                                           

                                cube_x[9] <= cube_x[8];

                                cube_y[9] <= cube_y[8];

                                                           

                                cube_x[10] <= cube_x[9];

                                cube_y[10] <= cube_y[9];

                                                           

                                cube_x[11] <= cube_x[10];

                                cube_y[11] <= cube_y[10];

                                                           

                                cube_x[12] <= cube_x[11];

                                cube_y[12] <= cube_y[11];

                                                             

                                cube_x[13] <= cube_x[12];

                                cube_y[13] <= cube_y[12];

                                                           

                                cube_x[14] <= cube_x[13];

                                cube_y[14] <= cube_y[13];

                                                           

                                cube_x[15] <= cube_x[14];

                                cube_y[15] <= cube_y[14];

                                    //身体运动算法 本长度位移动的下个坐标为下一个长度位当前坐标 运动节拍按分频后的节奏

                                    //蛇身体运动,蛇块的前一块坐标赋给后一块,比如 第0个块的坐标赋给第1块,第1块的坐标赋给第2块。。。

                                if(direct_r==UP)begin

                                        if(cube_y[0] == 1)

                                        hit_wall <= 1;//撞上墙

                                        else

                                            cube_y[0] <= cube_y[0]-1;

                                        end

                                                               

                                else if(direct_r==DOWN)begin

                                        if(cube_y[0] == 28)

                                            hit_wall <= 1;//撞下墙

                                        else

                                            cube_y[0] <= cube_y[0] + 1;

                                        end

                                                                   

                                else if(direct_r==LEFT)begin

                                        if(cube_x[0] == 1)

                                            hit_wall <= 1;//撞左墙

                                        else

                                            cube_x[0] <= cube_x[0] - 1;                                        

                                        end

 

                                else if(direct_r==RIGHT)begin

                                        if(cube_x[0] == 38)

                                            hit_wall <= 1;//撞右墙

                                        else

                                            cube_x[0] <= cube_x[0] + 1;

                                        end

                                //根据按下按键判断是否撞墙 否则按规律改变头部坐标

                            end

                        end

                        end

                    end

                end

 

运动方向的状态判断,主要是避免出现无意义的往复运动。

always @(*) begin   //根据当前运动状态即按下键位判断下一步运动情况  

        case(direct_r)  

            UP: begin   //向上运动时,  方向可以左右变

                if(~key1_left)

                    direct_next = LEFT;

                else if(~key0_right)

                    direct_next = RIGHT;

                else

                    direct_next = UP;          

                end    

            DOWN: begin //向下运动时,  方向可以左右变

                if(~key1_left)

                    direct_next = LEFT;

                else if(~key0_right)

                    direct_next = RIGHT;

                else

                    direct_next = DOWN;        

                end    

                LEFT: begin //向左运动时,  方向可以上下变

                if(~key3_up)

                    direct_next = UP;

                else if(~key2_down)

                    direct_next = DOWN;

                else

                    direct_next = LEFT;        

                end

                RIGHT: begin //向右运动时,  方向可以上下变

                if(~key3_up)

                    direct_next = UP;

                else if(~key2_down)

                    direct_next = DOWN;

                else

                    direct_next = RIGHT;

                end 

        endcase

    end

 

 

蛇身长度的增长,通过判断是否吃下食物来解决。

if(add_cube) begin

                        cube_num <= cube_num + 1;

                        is_exist[cube_num] <= 1;

显示蛇身的哪一节,是靠is_exist[cube_num]来控制的。

通过坐标,和显示控制,输出显示对象。

always @(pos_x or pos_y ) begin                

        if(pos_x >= 0 && pos_x < 640 && pos_y >= 0 && pos_y < 480) begin

            if(pos_x[9:4] == 0 || pos_y[9:4] == 0 || pos_x[9:4] == 39 || pos_y[9:4] == 29)//在VGA可显示的坐标范围内标记出墙的坐标

                snake_show = WALL;//扫描墙

               

            else if(pos_x[9:4] == cube_x[0] && pos_y[9:4] == cube_y[0] && is_exist[0] == 1)

                snake_show = (snake_display == 1) ? HEAD : NONE;//扫描头

            else if

                ((pos_x[9:4] == cube_x[1] && pos_y[9:4] == cube_y[1] && is_exist[1] == 1)|

                 (pos_x[9:4] == cube_x[2] && pos_y[9:4] == cube_y[2] && is_exist[2] == 1)|

                 (pos_x[9:4] == cube_x[3] && pos_y[9:4] == cube_y[3] && is_exist[3] == 1)|

                 (pos_x[9:4] == cube_x[4] && pos_y[9:4] == cube_y[4] && is_exist[4] == 1)|

                 (pos_x[9:4] == cube_x[5] && pos_y[9:4] == cube_y[5] && is_exist[5] == 1)|

                 (pos_x[9:4] == cube_x[6] && pos_y[9:4] == cube_y[6] && is_exist[6] == 1)|

                 (pos_x[9:4] == cube_x[7] && pos_y[9:4] == cube_y[7] && is_exist[7] == 1)|

                 (pos_x[9:4] == cube_x[8] && pos_y[9:4] == cube_y[8] && is_exist[8] == 1)|

                 (pos_x[9:4] == cube_x[9] && pos_y[9:4] == cube_y[9] && is_exist[9] == 1)|

                 (pos_x[9:4] == cube_x[10] && pos_y[9:4] == cube_y[10] && is_exist[10] == 1)|

                 (pos_x[9:4] == cube_x[11] && pos_y[9:4] == cube_y[11] && is_exist[11] == 1)|

                 (pos_x[9:4] == cube_x[12] && pos_y[9:4] == cube_y[12] && is_exist[12] == 1)|

                 (pos_x[9:4] == cube_x[13] && pos_y[9:4] == cube_y[13] && is_exist[13] == 1)|

                 (pos_x[9:4] == cube_x[14] && pos_y[9:4] == cube_y[14] && is_exist[14] == 1)|

                 (pos_x[9:4] == cube_x[15] && pos_y[9:4] == cube_y[15] && is_exist[15] == 1))

                 snake_show = (snake_display == 1) ? BODY : NONE;//扫描身体

            else snake_show = NONE;

        end

 

(6)game_ctrl_unit.v

游戏控制模块,根据游戏状态,产生相应的控制信号。

snake_display是蛇整体显示标志。

case(game_status)    

                RESTART:begin           //游戏重启状态

                    cnt_clk<=cnt_clk+1;

                    if(cnt_clk>150000000)begin// "欢迎来到贪吃蛇游戏“ 界面显示需要6s时间

                        if(sw[0]||sw[1]||sw[2]) begin

                            game_status <= START;//选择游戏难度后进入START状态

                        end

                    end

                    else begin

                        game_status <= RESTART;

                    end

                end

               

                START:begin

                    if ((~key0_right) ||( ~key1_left) || (~key2_down) ||( ~key3_up))//四个按键有任意一个按键被按下即可开始游戏

                        game_status <= PLAY;

                    else 

                        game_status <= START;

                end

               

                PLAY:begin

                    if(hit_wall || hit_body||bcd_data[11:8]>=1'd1)//如果撞墙或者撞自身或计满100分则游戏结束

                       game_status <= DIE;

                    else

                       game_status <= PLAY;

                end

               

                //下面代码是在制造闪烁效果

                //snake_display信号初始化的时候为高

                //snake_display信号在0-0.5秒为高,在 0.5-1秒为低,在 1-1.5秒高 在1.5-2低 2-2.5秒高 在2.5-3秒低

                DIE:begin

                    if(flash_cnt <= 100_000_000) begin//flash_cnt计时4秒

                        flash_cnt <= flash_cnt + 1'b1;  

                        if(flash_cnt == 12_500_000)begin//0-0.5秒 高

                            snake_display <= 1'b0;end

                        else if(flash_cnt == 25_000_000)begin//0.5-1秒低

                            snake_display <= 1'b1;end

                        else if(flash_cnt == 37_500_000)begin//1-1.5秒高

                            snake_display <= 1'b0;end

                        else if(flash_cnt == 50_000_000)begin//1.5-2秒低

                            snake_display <= 1'b1;end

                        else if(flash_cnt == 62_500_000)begin//2-2.5秒高

                            snake_display <= 1'b0;end

                        else if(flash_cnt == 75_000_000)begin//2.5-3秒低

                            snake_display <= 1'b1;

                        end

                    end

                    //游戏结束后按任意按键重新开始

                    else if((~key0_right) ||( ~key1_left) || (~key2_down) ||( ~key3_up) ) begin

                        cnt_clk<=0;

                        flash_cnt<=0;

                        game_status <= RESTART;

 

                    end        

                    else begin

                        game_status <= DIE;

                    end

                end 

               

                default:begin                

                            game_status <= RESTART; //游状态 从游戏结束 到游戏重启

                    end

            endcase 

 

这里用了一段式代码描述了游戏状态转换,共四种状态:

复位进入重启状态,6秒延时,显示欢迎界面,然后拨动开关选择难度,进入开始状态,按下任意键进入游戏状态,撞墙,撞自身,或计满100分就结束游戏,在结束状态,蛇身闪烁3秒,按键再重启。

 

结语

研究这个设计,可以更全面的熟悉状态机的设计方法,VGA的驱动,以及代码编写的技巧。

 

posted on 2024-05-13 19:45  yf.x  阅读(271)  评论(0编辑  收藏  举报

导航