Background

Verilog 打砖块游戏

代码来源:https://www.fpga4fun.com/BreakoutGame.html


项目结构

从网站上下载下来的代码很简洁,只有四个文件:

Terminal window
.
├── breakout_playfield.v
├── breakout_videogen.v
├── breakout.ucf
└── breakout.v
1 directory, 4 files

其中:

breakout.ucf 则是约束文件,它负责引脚映射。

breakout.v

breakout.v 开始。

// (c) KNJN LLC 2024 for fpga4fun.com
////////////////////////////////////////////////////////////////////////
module breakout(
input clk,
output VGA_HS, VGA_VS, VGA_R, VGA_G, VGA_B,
output [1:0] LED,
output audioR, audioL
);
wire [9:0] PaddleX; // paddle position
// if you have a way to control the paddle, make sure to update PaddleX here
assign PaddleX = 10'd900; // othersise this line puts the paddle off-screen to run the game in demo mode
wire DrawArea, hSync, vSync, red, green, blue, Collision, BrickHit;
breakout_videogen myVideoGen(
.clk(clk),
.PaddleX(PaddleX),
.DrawArea(DrawArea), .hSync(hSync), .vSync(vSync), .red(red), .green(green), .blue(blue),
.Collision(Collision), .BrickHit(BrickHit)
);
assign VGA_R = DrawArea & red;
assign VGA_G = DrawArea & green;
assign VGA_B = DrawArea & blue;
assign VGA_HS = ~hSync;
assign VGA_VS = ~vSync;
assign LED = {BrickHit, Collision & ~BrickHit};
reg [15:0] audio; always @(posedge clk) audio <= audio + Collision + BrickHit;
assign audioR = audio[15];
assign audioL = audio[15];
endmodule

开头先是连接 VGA 输出信号(RGB、同步信号),然后处理球拍位置。虽然但是,这个球拍在这个程序里是一个写死的常量,如果有控制球拍的逻辑还要另外跟它信号连上。

然后和 breakout_videogen.v 有关的代码,和传视频信号有关的,放到后面再看。

breakout_videogen.v

// (c) KNJN LLC 2024 for fpga4fun.com
////////////////////////////////////////////////////////////////////////
module breakout_videogen(
input clk,
input [9:0] PaddleX,
output reg DrawArea, hSync, vSync,
output red, green, blue,
output reg Collision, BrickHit
);
//localparam ballspeed = 2; // ball moves 4 pixels per frame
localparam ballspeed = 3; // ball moves 8 pixels per frame
reg [9:0] ballX = 100; // initial ball position
reg [8:0] ballY = 300;
reg ball_dirX, ball_dirY;
////////////////////////////////////////////////////////////////////////
parameter hDrawArea = 640;
parameter hSyncPorch = 16;
parameter hSyncLen = 96;
parameter hFrameSize = 800;
parameter vDrawArea = 480;
parameter vSyncPorch = 10;
parameter vSyncLen = 2;
parameter vFrameSize = 525;
reg [9:0] CounterX;
reg [8:0] CounterY;
always @(posedge clk) CounterX <= (CounterX==hFrameSize-1) ? 10'd0 : CounterX+10'd1;
always @(posedge clk) if(CounterX==hFrameSize-1) CounterY <= (CounterY==vFrameSize-1) ? 9'd0 : CounterY+9'd1;
always @(posedge clk) DrawArea <= (CounterX<hDrawArea) & (CounterY<vDrawArea);
always @(posedge clk) hSync <= (CounterX>=hDrawArea+hSyncPorch) & (CounterX<hDrawArea+hSyncPorch+hSyncLen);
always @(posedge clk) vSync <= (CounterY>=vDrawArea+vSyncPorch) & (CounterY<vDrawArea+vSyncPorch+vSyncLen);
////////////////////////////////////////////////////////////////////////
wire DrawBall, DrawBorder, DrawPaddle, DrawBrick, BrickHit_now, BrickHit_acq;
reg RestoreBrickwall = 1'b1;
reg MoveBall;
breakout_playfield #(hDrawArea, vDrawArea) game(
.clk(clk),
.PaddleX(PaddleX),
.CounterX(MoveBall ? ballX + {6'h00, {4{CounterX[0]}}} : CounterX),
.CounterY(MoveBall ? ballY + {5'h00, {4{CounterX[1]}}} : CounterY),
.ballX(ballX),
.ballY(ballY),
.DrawBall(DrawBall), .DrawBorder(DrawBorder), .DrawPaddle(DrawPaddle), .DrawBrick(DrawBrick),
.BrickHit_now(BrickHit_now), .BrickHit_acq(BrickHit_acq), .RestoreBrickwall(RestoreBrickwall)
);
// we are going to update the ball position during offscreen timing
wire FrameTick = (CounterX==hFrameSize-1) & (CounterY==vDrawArea-1);
always @(posedge clk) MoveBall <= MoveBall ? ~&CounterX[ballspeed+2:0] : FrameTick;
wire BounceableOject = DrawBorder | DrawPaddle | DrawBrick;
reg [3:0] HBC; always @(posedge clk) HBC <= {BounceableOject, HBC[3:1]}; // record the ball corners hits in HBC (HotBallCorner)
wire [15:0] updateDirX = 16'b01101101_10110110; // and update the ball direction if needed
wire [15:0] updateDirY = 16'b01111001_10011110;
always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h5 & updateDirX[HBC]) ball_dirX <= (~HBC[0] & HBC[1]) | (~HBC[2] & HBC[3]);
always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h5 & updateDirY[HBC]) ball_dirY <= (~HBC[0] & ~HBC[1]) | ( HBC[2] & HBC[3]);
always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h7) ballX <= ballX + {{9{ball_dirX}}, 1'b1}; // and then the ball position
always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h7) ballY <= ballY + {{8{ball_dirY}}, 1'b1};
// then get stats on ball collisions and brick hits
reg [2:0] BHA; always @(posedge clk) BHA <= {DrawBrick, BHA[2:1]};
assign BrickHit_now = MoveBall & CounterX[2] & BHA[0];
always @(posedge clk) if(FrameTick) BrickHit<=1'b0; else if(BrickHit_now) BrickHit<=1'b1;
reg [7:0] BrickHit_count=0; always @(posedge clk) BrickHit_count <= RestoreBrickwall ? 8'h00 : BrickHit_count + BrickHit_acq;
always @(posedge clk) RestoreBrickwall <= RestoreBrickwall ? ~FrameTick : (BrickHit_count==19*7) & ballY[8];
always @(posedge clk) if(FrameTick) Collision<=1'b0; else if(MoveBall & CounterX[2] & HBC[1]) Collision<=1'b1;
wire DrawAll = DrawBall | DrawBorder | DrawPaddle | DrawBrick;
assign red = DrawBrick;
assign green = DrawAll;
assign blue = DrawAll;
endmodule

查了一下,breakout_videogen.v 实现标准 VGA 640x480@60Hz 时序,即以下规范:

因为老 CRT 显示器是用电子束从左到右、从上到下逐行扫描的,扫描完一行需要时间回到下一行开始需要返回时间,所以需要同步脉冲来调整时序。好吧这个不重要。

接着通过两个计数器生成像素坐标:

总计 800×525=420,000800×525 = 420,000 个时钟周期完成一帧。

球的运动用 X 和 Y 两个分解方向表示,运动状态机由 MoveBall 控制。MoveBall 的逻辑是:当为真时检查~&CounterX[ballspeed+2:0](不全为 1 时继续),当为假时等待 FrameTick 重新启动,这里 +2 是控制球移动频率。也就是说:

碰撞检测通过 HBC(Hot Ball Corner)机制实现,HBC <= {BounceableOject, HBC[3:1]}进行右移操作,记录连续 4 个时钟周期的碰撞历史状态。系统使用两个 16 位查找表updateDirXupdateDirY,根据 HBC 的 4 位值索引决定是否更新球的方向,在时钟周期 5 更新方向,时钟周期 7 更新位置,避免了复杂的碰撞计算。

砖块碰撞通过 BHA 记录砖块碰撞历史,BrickHit_now检测当前击中状态,BrickHit作为帧级标志在每帧开始清零、有碰撞时置 1。BrickHit_count计数器跟踪总击中砖块数(19×7=133 块),当全部击中且球到达顶部时RestoreBrickwall重置砖墙。Collision标志类似地在每帧清零,球移动时检测到碰撞则置 1。

breakout_playfield.v

// (c) KNJN LLC 2024 for fpga4fun.com
/////////////////////////////////////////////////////////////////
module breakout_playfield(
input clk,
input [9:0] CounterX, ballX, PaddleX,
input [8:0] CounterY, ballY,
output reg DrawBall, DrawBorder, DrawPaddle,
output DrawBrick,
input BrickHit_now, RestoreBrickwall,
output BrickHit_acq
);
parameter hDrawArea = 640;
parameter vDrawArea = 480;
always @(posedge clk) DrawBall <= (CounterX>=ballX) && (CounterX<ballX+10'd16) && (CounterY>=ballY) && (CounterY<ballY+9'd16);
always @(posedge clk) DrawBorder <= (CounterX[9:2]==0) || (CounterX[9:2]==hDrawArea/4-1) || (CounterY[8:2]==0) || (CounterY[8:2]==vDrawArea/4-1);
always @(posedge clk) DrawPaddle <= (CounterX>=PaddleX) && (CounterX<=PaddleX+10'd64) && (CounterY>=vDrawArea-9'd46) && (CounterY<vDrawArea-9'd30);
// the brickwall starts at coordinates (16,48)
wire [9:0] BrickXo = CounterX-10'd16; wire [4:0] BrickX_H = BrickXo[9:5]; wire [4:0] BrickX_L = BrickXo[4:0];
wire [8:0] BrickYo = CounterY- 9'd48; wire [4:0] BrickY_H = BrickYo[8:4]; wire [3:0] BrickY_L = BrickYo[3:0];
wire [9:0] BrickA = {BrickY_H, BrickX_H}; // and is organized as a 32 x 32 matrix
reg [1023:0] RAMbrickwall; // in this blockram
reg BrickPresent, BrickHit_nowR;
always @(posedge clk)
begin
if(BrickHit_now | RestoreBrickwall) RAMbrickwall[BrickA] <= RestoreBrickwall ? BrickX_H<19 & BrickY_H<7 : 1'b0; // 19 x 7 brickwall
BrickPresent <= RAMbrickwall[BrickA];
BrickHit_nowR <= BrickHit_now;
end
assign BrickHit_acq = BrickPresent & BrickHit_nowR;
reg BrickBody; always @(posedge clk) BrickBody <= |BrickY_L[3:1] && |BrickX_L[4:1]; // leave two pixels between each brick
assign DrawBrick = BrickPresent & BrickBody;
endmodule

这个模块负责游戏场景中各个对象的绘制判断。球的绘制区域是 16×16 像素,通过比较当前扫描位置与球坐标确定;边框绘制使用位移操作CounterX[9:2]CounterY[8:2],实际上是将坐标除以 4,在 640×480 显示区域的边缘绘制 4 像素宽的边框;挡板绘制在屏幕底部,宽度 64 像素,位置在距离底部 30-46 像素的区域,跟随 PaddleX 水平移动。所有这些绘制判断都通过比较当前扫描坐标与对象坐标范围来实现。

砖墙从坐标 (16,48) 开始,通过坐标变换BrickXoBrickYo计算相对位置,然后分别提取高位和低位部分。BrickX_HBrickY_H作为 32×32 矩阵的索引,组成 10 位地址BrickA访问 1024 位的块 RAMRAMbrickwall。砖块的实际排列是 19×7 的矩阵,在RestoreBrickwall信号时恢复,在BrickHit_now信号时清除对应位置的砖块。BrickBody通过检查坐标的低位部分BrickY_L[3:1]BrickX_L[4:1]是否非零来在砖块间留出 2 像素的间隙,最终的DrawBrick信号需要同时满足砖块存在和在砖块主体区域的条件。BrickHit_acq信号在砖块存在且检测到击中时产生,用于游戏逻辑的砖块计数。

featured

JrHimself

© 2025 Aria

萌 ICP 备 20252003 号 GitHub Email