完整游戏:挡板+球+碰撞+计分
🏆 成就:游戏开发者 ✅ Verilator验证
经典Pong:球弹跳+挡板AI+碰撞检测+计分。AABB碰撞检测是核心。
// 左挡板碰撞 if(ball_x<=PAD_MARGIN+PAD_W && ball_y+BALL_SZ>lp_y && ball_yball_y+4) lp_y<=lp_y-2;
module pong_tb;
logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
pong uut(.*);
initial clk=0; always #20 clk=~clk;
int frame_count=0; logic vsync_prev; int score_events;
always @(posedge clk) begin
vsync_prev<=vsync;
if(vsync&&!vsync_prev) begin
frame_count++;
if(frame_count%30==0)
$display(" 帧%0d: ball=(%0d,%0d) score=%0d:%0d",
frame_count,uut.ball_x,uut.ball_y,
uut.score_l,uut.score_r);
if(uut.score_l>0 || uut.score_r>0) score_events++;
if(score_events>=3) begin
$display("Pong验证完成! 比分%0d:%0d ✓",
uut.score_l,uut.score_r);
$finish;
end
end
end
initial begin rst_n=0;#100;rst_n=1;end
initial #500_000_000 $finish;
endmodule1. 球速递增:每次碰撞后球速+1
2. 球角度变化:碰撞位置影响反弹角度
3. 键盘控制:替换AI为PS/2键盘(第7课)
4. 音效:碰撞时发出声音(第8课)
5. 数字计分:七段数码管显示(第14课)
// 球击中挡板不同位置 → 不同反弹角度
// 击中挡板中心 → 水平反弹
// 击中挡板边缘 → 大角度反弹
// 计算球在挡板上的相对位置 (-20 ~ +20)
wire signed [5:0] hit_pos;
assign hit_pos = (ball_y + 4) - (lp_y + 20); // 相对中心偏移
always @(posedge clk) if(vsync_fall && left_hit) begin
ball_vx <= 2; // 向右反弹
case(1)
(hit_pos < -12): ball_vy <= -2; // 上边缘 → 大角度上
(hit_pos < -4): ball_vy <= -1; // 偏上 → 小角度上
(hit_pos < 4): ball_vy <= 0; // 中心 → 水平
(hit_pos < 12): ball_vy <= 1; // 偏下 → 小角度下
default: ball_vy <= 2; // 下边缘 → 大角度下
endcase
end挑战1:添加球速递增(每次碰撞后+1)
挑战2:实现球角度变化(击中挡板边缘=大角度)
挑战3:替换AI为键盘控制(第7课内容)
挑战4:碰撞时播放音效(第8课内容)
Pong的bug通常在碰撞检测:
• 穿透:球速太快穿过挡板 → 加防穿透代码
• 重复碰撞:同一帧多次检测 → 确保只反弹一次
• 边界抖动:挡板卡在边界 → 添加边界限制
用GTKWave查看ball_x, ball_vx, score波形
verilator --binary -j 0 --trace \
vga_sync.v pong.v pong_tb.sv
./obj_dir/Vpong_tb
# 预期输出:
# 帧30: ball=(xxx,xxx) score=0:0
# 帧60: ball=(xxx,xxx) score=0:0
# ...
# Pong验证完成! 比分1:0 ✓
# 提示:AI挡板会跟踪球,所以得分需要等球速度
# 超过AI跟踪速度才能得分
# 可以降低AI速度(改为+1而非+2)加速得分✅ 游戏循环:输入→更新→渲染,每帧执行一次
✅ AABB碰撞检测的三个条件是什么?
✅ 为什么需要防穿透代码?
✅ AI挡板如何追踪球?有什么局限?
✅ 计分后为什么要重置球的位置和速度?
✅ 如何让球击中挡板不同位置产生不同角度?
下一课学习PS/2键盘——用键盘控制Pong挡板!PS/2协议时序、扫描码解析、两级同步器防亚稳态。
Q: 球穿透挡板? 球速(2像素/帧)可能大于挡板厚度。添加防穿透:碰撞后强制设置ball_x位置。
Q: 挡板越界? 添加边界限制:if(lp_y>=480-PAD_H) lp_y<=440;
Q: 分数异常增长? 确保得分后重置球位置和速度,避免球卡在边界反复得分。
Q: AI太强永远不得分? 降低AI速度(+1而非+2)或让AI有概率失误。
| 参数 | 默认值 | 调优建议 |
|---|---|---|
| BALL_SZ | 8 | 4=小球/16=大球 |
| PAD_W | 8 | 固定8即可 |
| PAD_H | 40 | 20=小挡板/80=大挡板 |
| PAD_MARGIN | 20 | 10=贴近边缘/40=远离 |
| AI速度 | +2 | +1=简单/+3=困难 |
| 球速 | ±2 | ±1=慢速/±3=快速 |
| WIN_SCORE | 9 | 5=短局/21=长局 |
💡 Pong游戏历史:Pong是1972年Atari推出的街机游戏,是电子游戏产业的起点之一。原始Pong没有CPU——全部用分立逻辑门实现!我们用FPGA实现Pong,某种程度上是在回归本源——用硬件而非软件来做游戏。FPGA版本的Pong和1972年原版在实现思路上惊人地相似!
1. 球速递增:每次碰撞后球速+1
2. 球角度变化:碰撞位置影响反弹角度
3. 键盘控制:替换AI为PS/2键盘(第7课)
4. 音效:碰撞时发出声音(第8课)
5. 数字计分:七段数码管显示(第14课)
• 穿透问题:球速太快穿过挡板 → 加防穿透代码
• 重复碰撞:确保只反弹一次
• 用GTKWave查看ball_x,ball_vx,score波形
module pong_tb;
logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
pong uut(.*);
initial clk=0; always #20 clk=~clk;
int frame_count=0; logic vsync_prev; int score_events;
always @(posedge clk) begin
vsync_prev<=vsync;
if(vsync&&!vsync_prev) begin
frame_count++;
if(frame_count%30==0)
$display("帧%0d: ball=(%0d,%0d) score=%0d:%0d",
frame_count,uut.ball_x,uut.ball_y,uut.score_l,uut.score_r);
if(uut.score_l>0||uut.score_r>0) score_events++;
if(score_events>=3) begin
$display("Pong验证完成! 比分%0d:%0d ✓",uut.score_l,uut.score_r);
$finish;
end
end
end
initial begin rst_n=0;#100;rst_n=1;end
initial #500_000_000 $finish;
endmodule