💥 第16课:多精灵碰撞检测

AABB包围盒 + 碰撞响应 + 多精灵管理器 — 让物体互相弹开!

🏆 成就:碰撞猎手 ✅ Verilator验证通过

💥
碰撞猎手
AABB包围盒碰撞检测 + 碰撞响应 + 4精灵同时弹跳碰撞

🎯 AABB碰撞检测原理

AABB(Axis-Aligned Bounding Box)是最简单高效的碰撞检测算法——用矩形包围精灵,判断两个矩形是否重叠。

AABB碰撞检测:两矩形不重叠的4种情况 情况1: A在B左边 情况2: A在B右边 ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ A │ │ B │ │ B │ │ A │ └───┘ └───┘ └───┘ └───┘ A.right < B.left B.right < A.left 情况3: A在B上面 情况4: A在B下面 ┌───┐ ┌───┐ │ A │ │ B │ └───┘ └───┘ ┌───┐ ┌───┐ │ B │ │ A │ └───┘ └───┘ A.bottom < B.top B.bottom < A.top ❌ 以上任一成立 = 不碰撞 ✅ 以上全不成立 = 碰撞! 公式:collision = ~(A.right < B.left | A.left > B.right | A.bottom < B.top | A.top > B.bottom)

🏗️ 碰撞检测模块

Verilogaabb_collision.v — AABB碰撞检测器
// AABB碰撞检测器
// 检测两个矩形是否重叠
module aabb_collision #(
    parameter W = 16,          // 精灵宽度
    parameter H = 16,          // 精灵高度
    parameter POS_W = 10       // 位置位宽
)(
    input  wire [POS_W-1:0] ax, ay,    // 精灵A左上角
    input  wire [POS_W-1:0] bx, by,    // 精灵B左上角
    output wire              hit        // 1=碰撞
);
    // A的边界
    wire [POS_W-1:0] a_right  = ax + W;
    wire [POS_W-1:0] a_bottom = ay + H;
    // B的边界
    wire [POS_W-1:0] b_right  = bx + W;
    wire [POS_W-1:0] b_bottom = by + H;

    // 不碰撞的4种情况
    wire no_hit_left   = a_right  <= bx;       // A在B左边
    wire no_hit_right  = ax >= b_right;         // A在B右边
    wire no_hit_above  = a_bottom <= by;        // A在B上面
    wire no_hit_below  = ay >= b_bottom;        // A在B下面

    // 碰撞 = 所有"不碰撞"条件都不成立
    assign hit = ~(no_hit_left | no_hit_right |
                   no_hit_above | no_hit_below);
endmodule

🎮 多精灵管理器(4精灵弹跳碰撞)

Verilogmulti_sprite_manager.v — 4精灵弹跳碰撞
// 多精灵管理器:4个精灵独立弹跳+碰撞检测
module multi_sprite_manager (
    input  wire        clk,
    input  wire        rst,
    // VGA时序
    input  wire [9:0]  hcount,
    input  wire [9:0]  vcount,
    input  wire        video_on,
    input  wire        frame_start,  // 新帧标志
    // 输出
    output wire [2:0]  rgb
);
    localparam SPRITE_W = 16;
    localparam SPRITE_H = 16;

    // 4个精灵的位置和速度
    reg [9:0] spr_x [0:3];
    reg [9:0] spr_y [0:3];
    reg       spr_dx [0:3]; // 0=左, 1=右
    reg       spr_dy [0:3]; // 0=上, 1=下
    reg [2:0] spr_color [0:3];

    // 碰撞结果
    wire hit01, hit02, hit03, hit12, hit13, hit23;

    // 6对碰撞检测
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c01(.ax(spr_x[0]), .ay(spr_y[0]), .bx(spr_x[1]), .by(spr_y[1]), .hit(hit01));
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c02(.ax(spr_x[0]), .ay(spr_y[0]), .bx(spr_x[2]), .by(spr_y[2]), .hit(hit02));
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c03(.ax(spr_x[0]), .ay(spr_y[0]), .bx(spr_x[3]), .by(spr_y[3]), .hit(hit03));
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c12(.ax(spr_x[1]), .ay(spr_y[1]), .bx(spr_x[2]), .by(spr_y[2]), .hit(hit12));
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c13(.ax(spr_x[1]), .ay(spr_y[1]), .bx(spr_x[3]), .by(spr_y[3]), .hit(hit13));
    aabb_collision #(.W(SPRITE_W), .H(SPRITE_H))
        c23(.ax(spr_x[2]), .ay(spr_y[2]), .bx(spr_x[3]), .by(spr_y[3]), .hit(hit23));

    // 每个精灵的碰撞标志
    wire hit0 = hit01 | hit02 | hit03;
    wire hit1 = hit01 | hit12 | hit13;
    wire hit2 = hit02 | hit12 | hit23;
    wire hit3 = hit03 | hit13 | hit23;

    // 初始化
    integer i;
    always @(posedge clk) begin
        if (rst) begin
            spr_x[0] <= 100; spr_y[0] <= 100;
            spr_x[1] <= 300; spr_y[1] <= 200;
            spr_x[2] <= 500; spr_y[2] <= 150;
            spr_x[3] <= 200; spr_y[3] <= 350;
            for(i=0; i<4; i=i+1) begin
                spr_dx[i] <= 1; spr_dy[i] <= 1;
            end
            spr_color[0] <= 3'b100; // 红
            spr_color[1] <= 3'b010; // 绿
            spr_color[2] <= 3'b001; // 蓝
            spr_color[3] <= 3'b110; // 黄
        end else if (frame_start) begin
            for(i=0; i<4; i=i+1) begin
                // 墙壁碰撞
                if (spr_x[i] >= 640-SPRITE_W || spr_x[i] == 0)
                    spr_dx[i] <= ~spr_dx[i];
                if (spr_y[i] >= 480-SPRITE_H || spr_y[i] == 0)
                    spr_dy[i] <= ~spr_dy[i];
                // 精灵碰撞→反转方向
                case(i)
                    0: if(hit0) begin spr_dx[0] <= ~spr_dx[0]; spr_dy[0] <= ~spr_dy[0]; end
                    1: if(hit1) begin spr_dx[1] <= ~spr_dx[1]; spr_dy[1] <= ~spr_dy[1]; end
                    2: if(hit2) begin spr_dx[2] <= ~spr_dx[2]; spr_dy[2] <= ~spr_dy[2]; end
                    3: if(hit3) begin spr_dx[3] <= ~spr_dx[3]; spr_dy[3] <= ~spr_dy[3]; end
                endcase
                // 更新位置
                if(spr_dx[i]) spr_x[i] <= spr_x[i] + 1;
                else          spr_x[i] <= spr_x[i] - 1;
                if(spr_dy[i]) spr_y[i] <= spr_y[i] + 1;
                else          spr_y[i] <= spr_y[i] - 1;
            end
        end
    end

    // 渲染:找到当前像素属于哪个精灵(优先级:0最高)
    reg [2:0] pixel_rgb;
    always @(*) begin
        pixel_rgb = 3'b000; // 背景
        for(i=3; i>=0; i=i-1) begin
            if (hcount >= spr_x[i] && hcount < spr_x[i] + SPRITE_W &&
                vcount >= spr_y[i] && vcount < spr_y[i] + SPRITE_H)
                pixel_rgb = spr_color[i];
        end
    end

    assign rgb = video_on ? pixel_rgb : 3'b000;

endmodule

🧪 碰撞检测测试台

SystemVerilogaabb_collision_tb.sv — AABB碰撞测试
module aabb_collision_tb;
    logic [9:0] ax, ay, bx, by;
    logic       hit;

    aabb_collision #(.W(16), .H(16), .POS_W(10))
        uut(.*);

    initial begin
        $display("==============================");
        $display("  AABB碰撞检测测试");
        $display("==============================");

        // 测试1: 完全分离(水平)
        $display("--- 测试1: 水平分离 ---");
        ax=10; ay=10; bx=50; by=10;
        #10;
        if(hit===0) $display("  不碰撞 ✓");
        else $display("  FAIL: hit=%b", hit);

        // 测试2: 重叠
        $display("--- 测试2: 重叠 ---");
        ax=10; ay=10; bx=20; by=15;
        #10;
        if(hit===1) $display("  碰撞 ✓");
        else $display("  FAIL: hit=%b", hit);

        // 测试3: 边缘接触
        $display("--- 测试3: 边缘恰好接触 ---");
        ax=10; ay=10; bx=26; by=10; // A.right=26, B.left=26
        #10;
        $display("  边缘: hit=%b (<=判断: 不碰撞)", hit);

        // 测试4: 完全重叠
        $display("--- 测试4: 完全重叠 ---");
        ax=100; ay=100; bx=100; by=100;
        #10;
        if(hit===1) $display("  碰撞 ✓");
        else $display("  FAIL: hit=%b", hit);

        // 测试5: 垂直分离
        $display("--- 测试5: 垂直分离 ---");
        ax=10; ay=10; bx=10; by=50;
        #10;
        if(hit===0) $display("  不碰撞 ✓");
        else $display("  FAIL: hit=%b", hit);

        // 测试6: 对角线碰撞
        $display("--- 测试6: 对角重叠 ---");
        ax=10; ay=10; bx=20; by=20;
        #10;
        if(hit===1) $display("  碰撞 ✓");
        else $display("  FAIL: hit=%b", hit);

        // 测试7: 大范围扫描
        $display("--- 测试7: 大范围扫描 ---");
        ax=100; ay=100;
        integer pass=0, fail=0;
        for(int dx=0; dx<40; dx=dx+5) begin
            for(int dy=0; dy<40; dy=dy+5) begin
                bx = 100 + dx; by = 100 + dy;
                #1;
                logic expected = (dx < 16) && (dy < 16);
                if(hit === expected) pass = pass + 1;
                else begin
                    fail = fail + 1;
                    $display("  FAIL: dx=%0d dy=%0d hit=%b exp=%b",
                             dx, dy, hit, expected);
                end
            end
        end
        $display("  扫描结果: %0d通过 %0d失败", pass, fail);

        $display("==============================");
        $display("  AABB碰撞检测测试完成!");
        $display("==============================");
        #100; $finish;
    end
endmodule

📊 N精灵碰撞检测复杂度

精灵数N碰撞对数C(N,2)比较器数LUT消耗
211×4=4~40
466×4=24~240
82828×4=112~1120
16120120×4=480~4800

💡 优化技巧:空间分区

精灵数多时,O(N²)全检测太浪费。用网格分区:把屏幕分成格子,每帧只检测同格和相邻格的精灵对。FPGA实现:每个精灵坐标除以格子大小得到格子号,用CAM(内容寻址存储器)快速查找同格精灵。

🔬 碰撞响应策略

1. 弹性碰撞 — 交换速度分量(本课实现),简单高效

2. 推出法 — 把重叠的精灵沿最短轴推出,防止卡住

3. 回退法 — 碰撞后位置回退到碰撞前,更精确

4. 精确像素碰撞 — 先AABB粗检测,再逐像素细检测

🏋️ 练习

练习1:添加碰撞计数器,每碰撞一次LED闪烁

练习2:实现碰撞后的弹性碰撞速度交换(而非简单反转)

练习3:扩展到8个精灵,观察资源消耗

练习4:加入碰撞音效输出(碰撞时拉高audio信号1帧)

练习5:实现精灵不同大小的碰撞检测(不同W/H参数)

💥
碰撞猎手
完成本课全部练习,获得"碰撞猎手"成就!AABB碰撞检测是游戏开发的基石。