AABB包围盒 + 碰撞响应 + 多精灵管理器 — 让物体互相弹开!
🏆 成就:碰撞猎手 ✅ Verilator验证通过
AABB(Axis-Aligned Bounding Box)是最简单高效的碰撞检测算法——用矩形包围精灵,判断两个矩形是否重叠。
// 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个精灵独立弹跳+碰撞检测
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;
endmodulemodule 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 | 碰撞对数C(N,2) | 比较器数 | LUT消耗 |
|---|---|---|---|
| 2 | 1 | 1×4=4 | ~40 |
| 4 | 6 | 6×4=24 | ~240 |
| 8 | 28 | 28×4=112 | ~1120 |
| 16 | 120 | 120×4=480 | ~4800 |
💡 优化技巧:空间分区
精灵数多时,O(N²)全检测太浪费。用网格分区:把屏幕分成格子,每帧只检测同格和相邻格的精灵对。FPGA实现:每个精灵坐标除以格子大小得到格子号,用CAM(内容寻址存储器)快速查找同格精灵。
1. 弹性碰撞 — 交换速度分量(本课实现),简单高效
2. 推出法 — 把重叠的精灵沿最短轴推出,防止卡住
3. 回退法 — 碰撞后位置回退到碰撞前,更精确
4. 精确像素碰撞 — 先AABB粗检测,再逐像素细检测
练习1:添加碰撞计数器,每碰撞一次LED闪烁
练习2:实现碰撞后的弹性碰撞速度交换(而非简单反转)
练习3:扩展到8个精灵,观察资源消耗
练习4:加入碰撞音效输出(碰撞时拉高audio信号1帧)
练习5:实现精灵不同大小的碰撞检测(不同W/H参数)