精灵ROM + 位置寄存器 + 透明色 — 在屏幕上画出任意图形!
🏆 成就:精灵画师 ✅ Verilator验证通过
精灵是2D游戏中最基本的图形单元——角色、子弹、道具,每个都是一个独立的小图像,可以在屏幕上自由移动。精灵渲染的核心问题有三个:
1. 存储 — 像素数据存在哪?→ 用ROM(只读存储器)
2. 定位 — 怎么知道画在哪?→ 比较当前扫描位置和精灵坐标
3. 透明 — 精灵的空白区域怎么办?→ 透明色标记,遇到跳过
我们设计一个16×16像素的太空入侵者精灵,每个像素4位颜色(0=透明,1~15=颜色索引)。
// 16x16 精灵ROM,每像素4位颜色索引
// 0 = 透明,1~15 = 颜色索引
module sprite_rom (
input wire [7:0] addr, // 行*16+列 (0~255)
output wire [3:0] color // 4位颜色索引
);
reg [3:0] mem [0:255];
initial begin
// 太空入侵者精灵 (16x16)
// 用16进制,每行16像素,0=透明
mem[ 0]=4'h0; mem[ 1]=4'h0; mem[ 2]=4'h0; mem[ 3]=4'h0;
mem[ 4]=4'h0; mem[ 5]=4'h0; mem[ 6]=4'h0; mem[ 7]=4'h1;
mem[ 8]=4'h1; mem[ 9]=4'h0; mem[ 10]=4'h0; mem[ 11]=4'h0;
mem[ 12]=4'h0; mem[ 13]=4'h0; mem[ 14]=4'h0; mem[ 15]=4'h0;
mem[ 16]=4'h0; mem[ 17]=4'h0; mem[ 18]=4'h0; mem[ 19]=4'h0;
mem[ 20]=4'h0; mem[ 21]=4'h1; mem[ 22]=4'h1; mem[ 23]=4'h1;
mem[ 24]=4'h1; mem[ 25]=4'h1; mem[ 26]=4'h1; mem[ 27]=4'h0;
mem[ 28]=4'h0; mem[ 29]=4'h0; mem[ 30]=4'h0; mem[ 31]=4'h0;
mem[ 32]=4'h0; mem[ 33]=4'h0; mem[ 34]=4'h0; mem[ 35]=4'h1;
mem[ 36]=4'h1; mem[ 37]=4'h2; mem[ 38]=4'h2; mem[ 39]=4'h1;
mem[ 40]=4'h1; mem[ 41]=4'h2; mem[ 42]=4'h2; mem[ 43]=4'h1;
mem[ 44]=4'h1; mem[ 45]=4'h0; mem[ 46]=4'h0; mem[ 47]=4'h0;
mem[ 48]=4'h0; mem[ 49]=4'h0; mem[ 50]=4'h1; mem[ 51]=4'h2;
mem[ 52]=4'h2; mem[ 53]=4'h2; mem[ 54]=4'h2; mem[ 55]=4'h1;
mem[ 56]=4'h1; mem[ 57]=4'h2; mem[ 58]=4'h2; mem[ 59]=4'h2;
mem[ 60]=4'h2; mem[ 61]=4'h1; mem[ 62]=4'h0; mem[ 63]=4'h0;
mem[ 64]=4'h0; mem[ 65]=4'h1; mem[ 66]=4'h2; mem[ 67]=4'h2;
mem[ 68]=4'h2; mem[ 69]=4'h2; mem[ 70]=4'h2; mem[ 71]=4'h2;
mem[ 72]=4'h2; mem[ 73]=4'h2; mem[ 74]=4'h2; mem[ 75]=4'h2;
mem[ 76]=4'h2; mem[ 77]=4'h2; mem[ 78]=4'h1; mem[ 79]=4'h0;
mem[ 80]=4'h1; mem[ 81]=4'h2; mem[ 82]=4'h2; mem[ 83]=4'h3;
mem[ 84]=4'h3; mem[ 85]=4'h2; mem[ 86]=4'h2; mem[ 87]=4'h1;
mem[ 88]=4'h1; mem[ 89]=4'h2; mem[ 90]=4'h2; mem[ 91]=4'h3;
mem[ 92]=4'h3; mem[ 93]=4'h2; mem[ 94]=4'h2; mem[ 95]=4'h1;
mem[ 96]=4'h1; mem[ 97]=4'h2; mem[ 98]=4'h2; mem[ 99]=4'h3;
mem[100]=4'h3; mem[101]=4'h2; mem[102]=4'h2; mem[103]=4'h1;
mem[104]=4'h1; mem[105]=4'h2; mem[106]=4'h2; mem[107]=4'h3;
mem[108]=4'h3; mem[109]=4'h2; mem[110]=4'h2; mem[111]=4'h1;
mem[112]=4'h0; mem[113]=4'h1; mem[114]=4'h1; mem[115]=4'h2;
mem[116]=4'h2; mem[117]=4'h1; mem[118]=4'h1; mem[119]=4'h0;
mem[120]=4'h0; mem[121]=4'h1; mem[122]=4'h1; mem[123]=4'h2;
mem[124]=4'h2; mem[125]=4'h1; mem[126]=4'h1; mem[127]=4'h0;
mem[128]=4'h0; mem[129]=4'h0; mem[130]=4'h1; mem[131]=4'h1;
mem[132]=4'h0; mem[133]=4'h0; mem[134]=4'h1; mem[135]=4'h1;
mem[136]=4'h1; mem[137]=4'h1; mem[138]=4'h0; mem[139]=4'h0;
mem[140]=4'h1; mem[141]=4'h1; mem[142]=4'h0; mem[143]=4'h0;
// 其余行填充0(透明)
integer i;
for(i=144; i<256; i=i+1) mem[i] = 4'h0;
end
assign color = mem[addr];
endmodule精灵ROM输出4位颜色索引,通过查找表转换为3位RGB信号。
// 4位颜色索引 → 3位RGB
// 0=透明(特殊), 1=白, 2=绿, 3=红, 4=蓝, ...
module color_lut (
input wire [3:0] index,
output reg [2:0] rgb
);
always @(*) begin
case(index)
4'h0: rgb = 3'b000; // 透明/黑
4'h1: rgb = 3'b111; // 白
4'h2: rgb = 3'b010; // 绿
4'h3: rgb = 3'b100; // 红
4'h4: rgb = 3'b001; // 蓝
4'h5: rgb = 3'b110; // 黄
4'h6: rgb = 3'b011; // 青
4'h7: rgb = 3'b101; // 品红
4'h8: rgb = 3'b100; // 暗红
4'h9: rgb = 3'b010; // 暗绿
4'hA: rgb = 3'b001; // 暗蓝
default: rgb = 3'b111;
endcase
end
endmodulemodule sprite_renderer (
input wire clk,
input wire rst,
input wire [9:0] hcount, // VGA水平计数
input wire [9:0] vcount, // VGA垂直计数
input wire video_on, // 视频使能
input wire [9:0] sprite_x, // 精灵X坐标
input wire [9:0] sprite_y, // 精灵Y坐标
input wire [3:0] bg_color, // 背景颜色索引
output wire [2:0] rgb // 最终RGB输出
);
parameter SPRITE_W = 16;
parameter SPRITE_H = 16;
// 判断当前像素是否在精灵范围内
wire in_x = (hcount >= sprite_x) &&
(hcount < sprite_x + SPRITE_W);
wire in_y = (vcount >= sprite_y) &&
(vcount < sprite_y + SPRITE_H);
wire in_sprite = in_x && in_y && video_on;
// 计算ROM地址:行*16+列
wire [3:0] col = hcount[3:0] - sprite_x[3:0];
wire [3:0] row = vcount[3:0] - sprite_y[3:0];
wire [7:0] rom_addr = {row, col};
// 读取精灵像素
wire [3:0] sprite_idx;
sprite_rom u_rom(.addr(rom_addr), .color(sprite_idx));
// 颜色查找
wire [2:0] sprite_rgb;
color_lut u_clut(.index(sprite_idx), .rgb(sprite_rgb));
// 背景颜色查找
wire [2:0] bg_rgb;
color_lut u_bglut(.index(bg_color), .rgb(bg_rgb));
// 透明色处理:0=透明,显示背景
assign rgb = video_on ?
(in_sprite && sprite_idx != 4'h0 ? sprite_rgb : bg_rgb) :
3'b000;
endmodulemodule vga_sprite_top (
input wire clk50, // 50MHz时钟
input wire rst,
output wire vga_hs, // 水平同步
output wire vga_vs, // 垂直同步
output wire [2:0] rgb // RGB输出
);
// VGA 640x480@60Hz 时序
reg [9:0] hcount, vcount;
reg hs, vs;
reg video_on;
// 水平时序参数
localparam H_SYNC = 10'd96;
localparam H_BACK = 10'd48;
localparam H_ACTIVE = 10'd640;
localparam H_FRONT = 10'd16;
localparam H_TOTAL = 10'd800;
// 垂直时序参数
localparam V_SYNC = 10'd2;
localparam V_BACK = 10'd33;
localparam V_ACTIVE = 10'd480;
localparam V_FRONT = 10'd10;
localparam V_TOTAL = 10'd525;
wire h_end = (hcount == H_TOTAL - 1);
wire v_end = (vcount == V_TOTAL - 1);
always @(posedge clk50) begin
if (rst) begin
hcount <= 0; vcount <= 0;
end else begin
if (h_end) begin
hcount <= 0;
if (v_end) vcount <= 0;
else vcount <= vcount + 1;
end else
hcount <= hcount + 1;
end
end
// 同步信号
always @(posedge clk50) begin
hs <= (hcount < H_SYNC);
vs <= (vcount < V_SYNC);
video_on <= (hcount >= H_SYNC + H_BACK) &&
(hcount < H_SYNC + H_BACK + H_ACTIVE) &&
(vcount >= V_SYNC + V_BACK) &&
(vcount < V_SYNC + V_BACK + V_ACTIVE);
end
assign vga_hs = ~hs;
assign vga_vs = ~vs;
// 精灵动画位置 — 自动弹跳
reg [9:0] spr_x, spr_y;
reg [1:0] dx, dy;
always @(posedge clk50) begin
if (rst) begin
spr_x <= 200; spr_y <= 150;
dx <= 1; dy <= 1;
end else if (v_end) begin // 每帧更新
if (spr_x >= 624 || spr_x == 0) dx <= ~dx;
if (spr_y >= 464 || spr_y == 0) dy <= ~dy;
spr_x <= spr_x + {{9{dx[0]}}, dx};
spr_y <= spr_y + {{9{dy[0]}}, dy};
end
end
// 精灵渲染
wire [2:0] spr_rgb;
sprite_renderer u_render (
.clk(clk50), .rst(rst),
.hcount(hcount - H_SYNC - H_BACK),
.vcount(vcount - V_SYNC - V_BACK),
.video_on(video_on),
.sprite_x(spr_x), .sprite_y(spr_y),
.bg_color(4'h4), // 蓝色背景
.rgb(spr_rgb)
);
assign rgb = spr_rgb;
endmodulemodule sprite_renderer_tb;
logic clk=0, rst=1;
logic [9:0] hcount, vcount;
logic video_on;
logic [9:0] sprite_x, sprite_y;
logic [3:0] bg_color;
logic [2:0] rgb;
sprite_renderer uut (.*);
always #10 clk = ~clk;
initial begin
rst = 1; sprite_x = 100; sprite_y = 100; bg_color = 4'h4;
#50 rst = 0;
// 测试1: 精灵范围外的像素 → 背景色
$display("--- 测试1: 精灵外像素 ---");
hcount = 50; vcount = 50; video_on = 1;
#20;
if (rgb === 3'b001) $display(" 精灵外=蓝背景 ✓");
else $display(" FAIL: rgb=%b", rgb);
// 测试2: 精灵范围内透明像素 → 背景
$display("--- 测试2: 透明像素 ---");
hcount = 100; vcount = 100; video_on = 1;
#20;
$display(" (0,0)偏移=透明, rgb=%b", rgb);
// 测试3: 精灵范围内非透明像素 → 精灵色
$display("--- 测试3: 非透明像素 ---");
hcount = 107; vcount = 105; video_on = 1;
#20;
$display(" 精灵像素, rgb=%b", rgb);
// 测试4: video_off → 黑
$display("--- 测试4: 消隐区 ---");
video_on = 0; #20;
if (rgb === 3'b000) $display(" 消隐=黑 ✓");
else $display(" FAIL: rgb=%b", rgb);
// 测试5: ROM地址计算
$display("--- 测试5: 多点ROM读取 ---");
video_on = 1;
for(int y=100; y<116; y++) begin
for(int x=100; x<116; x++) begin
hcount = x; vcount = y;
#5;
end
end
$display(" 16x16精灵全部扫描完成 ✓");
$display("==============================");
$display(" 精灵渲染测试完成!");
$display("==============================");
#100; $finish;
end
endmodule| 资源 | 16×16精灵 | 32×32精灵 | 64×64精灵 |
|---|---|---|---|
| ROM大小 | 256×4 = 1Kbit | 1K×4 = 4Kbit | 4K×4 = 16Kbit |
| 比较器 | 2×10bit | 2×10bit | 2×10bit |
| 每像素时钟 | 1次ROM读 | 1次ROM读 | 1次ROM读 |
| LUT消耗 | ~50 | ~60 | ~80 |
| 最大精灵数@640 | 40个水平 | 20个水平 | 10个水平 |
技巧1:提前判断 — 先判断 in_sprite 再读ROM,非精灵区域零功耗
技巧2:逐行存储 — 每行像素起始地址 = row × SPRITE_W,寻址简单
技巧3:颜色压缩 — 用调色板索引代替直接RGB,4位索引可表示16色
技巧4:行缓存 — 流水线化:当前行渲染时预取下一行数据
练习1:修改精灵ROM数据,画一个16×16的爱心精灵 ❤️
练习2:添加第二个精灵,两个精灵同时弹跳
练习3:用按键控制精灵位置(提示:添加方向输入端口)
练习4:实现精灵水平翻转(hint:翻转列地址 col = W-1-col)
练习5:设计32×32精灵,观察资源变化