🏓 第6课:乒乓球游戏

完整游戏:挡板+球+碰撞+计分

🏆 成就:游戏开发者 ✅ Verilator验证

🏓
游戏开发者
Verilator仿真完整Pong游戏

🎮 Pong游戏设计

经典Pong:球弹跳+挡板AI+碰撞检测+计分。AABB碰撞检测是核心。

Verilogpong.v碰撞检测
// 左挡板碰撞
if(ball_x<=PAD_MARGIN+PAD_W &&
   ball_y+BALL_SZ>lp_y && ball_yball_y+4) lp_y<=lp_y-2;

📐 AABB碰撞算法详解

挡板碰撞检测: 球飞向左挡板(PAD_MARGIN=20, PAD_W=8): ┌──────────────┐ │ │██│ ← 左挡板(lp_y~lp_y+40) │ ●→ │██│ ball_x 正在减小 │ 球 │██│ ball_vx < 0 │ │██│ └──────────────┘ 碰撞条件(三个同时满足): 1. ball_x <= PAD_MARGIN + PAD_W ← 到达挡板右边缘 2. ball_y + BALL_SZ > lp_y ← 球底在挡板顶之下 3. ball_y < lp_y + PAD_H ← 球顶在挡板底之上 碰撞后: ball_vx = -ball_vx (反弹) ball_x = PAD_MARGIN + PAD_W + 1 (防穿透!) 未碰撞 = 得分! 球飞过挡板 → 对方加分 → 球重置中央

🧪 Verilator完整测试台

SystemVerilogpong_tb.sv
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

🔬 进阶:增加游戏元素

1. 球速递增:每次碰撞后球速+1

2. 球角度变化:碰撞位置影响反弹角度

3. 键盘控制:替换AI为PS/2键盘(第7课)

4. 音效:碰撞时发出声音(第8课)

5. 数字计分:七段数码管显示(第14课)

🔬 球角度变化:击中挡板不同位置反弹角度不同

Verilog角度反弹逻辑
// 球击中挡板不同位置 → 不同反弹角度
// 击中挡板中心 → 水平反弹
// 击中挡板边缘 → 大角度反弹

// 计算球在挡板上的相对位置 (-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波形

🛠️ 编译运行

Bash编译运行步骤
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有概率失误。

📏 Pong游戏参数调优指南

参数默认值调优建议
BALL_SZ84=小球/16=大球
PAD_W8固定8即可
PAD_H4020=小挡板/80=大挡板
PAD_MARGIN2010=贴近边缘/40=远离
AI速度+2+1=简单/+3=困难
球速±2±1=慢速/±3=快速
WIN_SCORE95=短局/21=长局

💡 Pong游戏历史:Pong是1972年Atari推出的街机游戏,是电子游戏产业的起点之一。原始Pong没有CPU——全部用分立逻辑门实现!我们用FPGA实现Pong,某种程度上是在回归本源——用硬件而非软件来做游戏。FPGA版本的Pong和1972年原版在实现思路上惊人地相似!

📐 AABB碰撞算法

挡板碰撞检测: 球飞向左挡板: 碰撞条件(三个同时满足): ball_x <= PAD_MARGIN + PAD_W ← 到达挡板右边缘 ball_y + BALL_SZ > lp_y ← 球底在挡板顶之下 ball_y < lp_y + PAD_H ← 球顶在挡板底之上 → 碰撞! ball_vx = -ball_vx (反弹) → 防穿透: ball_x = PAD_MARGIN + PAD_W + 1 未碰撞 → 球飞过挡板 → 对方加分

🔬 进阶:增加游戏元素

1. 球速递增:每次碰撞后球速+1

2. 球角度变化:碰撞位置影响反弹角度

3. 键盘控制:替换AI为PS/2键盘(第7课)

4. 音效:碰撞时发出声音(第8课)

5. 数字计分:七段数码管显示(第14课)

💡 调试技巧

穿透问题:球速太快穿过挡板 → 加防穿透代码

重复碰撞:确保只反弹一次

• 用GTKWave查看ball_x,ball_vx,score波形

🧪 Verilator完整测试台

SystemVerilogpong_tb.sv
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