瓦片地图 + 视口滚动 + 背景渲染 — 制作可滚动的2D世界!
🏆 成就:滚屏大师 ✅ Verilator验证通过
2D游戏世界通常由瓦片(Tile)构成——把大地图切成小方块,每个方块用编号表示类型(地板、墙壁、草地…)。渲染时只需查表,就能用极少的存储画出巨大的世界!
// 瓦片地图ROM - 32x24瓦片,每瓦片8x8像素
// 总分辨率: 256x192(可见区) 从 64x48大地图中滚动
module tile_map_rom (
input wire [4:0] tile_x, // 瓦片列 0~31
input wire [4:0] tile_y, // 瓦片行 0~23
output reg [3:0] tile_id // 瓦片ID(0~15)
);
// 简化地图: 用坐标计算瓦片ID
// 奇偶棋盘 + 边框 + 通道
always @(*) begin
if (tile_x == 0 || tile_x == 31 || tile_y == 0 || tile_y == 23)
tile_id = 4'h1; // 墙壁
else if ((tile_x[1:0] == 2'b00) && (tile_y[1:0] == 2'b00))
tile_id = 4'h2; // 柱子
else if (tile_x[3] ^ tile_y[3])
tile_id = 4'h3; // 草地
else
tile_id = 4'h0; // 地板
end
endmodule// 瓦片像素ROM - 8x8像素,4位颜色
module tile_pixel_rom (
input wire [3:0] tile_id, // 瓦片类型
input wire [2:0] px, py, // 瓦片内偏移
output reg [3:0] color // 颜色索引
);
always @(*) begin
case(tile_id)
4'h0: color = 4'h1; // 地板: 白色
4'h1: color = 4'h8; // 墙壁: 暗红
4'h2: begin // 柱子: 条纹
if (px[0] ^ py[0]) color = 4'h5;
else color = 4'h3;
end
4'h3: begin // 草地: 带点
if (px == 3'd3 && py == 3'd5) color = 4'h2;
else if (px == 3'd6 && py == 3'd1) color = 4'h2;
else color = 4'h9; // 暗绿
end
default: color = 4'h0;
endcase
end
endmodule// 滚屏游戏引擎 - 视口滚动控制器
module scroll_engine (
input wire clk,
input wire rst,
input wire [9:0] hcount, vcount,
input wire video_on,
input wire frame_start,
output wire [2:0] rgb
);
// 视口偏移(滚动位置)
reg [9:0] scroll_x, scroll_y;
// 自动滚动
always @(posedge clk) begin
if (rst) begin
scroll_x <= 0; scroll_y <= 0;
end else if (frame_start) begin
// 水平来回滚动
if (scroll_x >= 320 || scroll_x == 0)
; // 边界(简化: 持续滚动)
scroll_x <= scroll_x + 1;
if (scroll_x >= 384) scroll_x <= 0;
// 垂直缓慢滚动
scroll_y <= scroll_y + 1;
if (scroll_y >= 256) scroll_y <= 0;
end
end
// 计算当前像素的瓦片坐标
wire [9:0] world_x = hcount + scroll_x;
wire [9:0] world_y = vcount + scroll_y;
wire [4:0] tile_x = world_x[8:4]; // 除以8取瓦片列
wire [4:0] tile_y = world_y[8:4]; // 除以8取瓦片行
wire [2:0] px = world_x[3:0]; // 瓦片内X偏移
wire [2:0] py = world_y[3:0]; // 瓦片内Y偏移
// 读取瓦片ID
wire [3:0] tile_id;
tile_map_rom u_map(.tile_x(tile_x), .tile_y(tile_y), .tile_id(tile_id));
// 读取像素颜色
wire [3:0] color_idx;
tile_pixel_rom u_px(.tile_id(tile_id), .px(px), .py(py), .color(color_idx));
// 颜色索引转RGB
reg [2:0] pixel_rgb;
always @(*) begin
case(color_idx)
4'h0: pixel_rgb = 3'b000;
4'h1: pixel_rgb = 3'b111;
4'h2: pixel_rgb = 3'b010;
4'h3: pixel_rgb = 3'b100;
4'h5: pixel_rgb = 3'b110;
4'h8: pixel_rgb = 3'b100;
4'h9: pixel_rgb = 3'b010;
default: pixel_rgb = 3'b000;
endcase
end
assign rgb = video_on ? pixel_rgb : 3'b000;
endmodulemodule scroll_engine_tb;
logic clk=0, rst=1;
logic [9:0] hcount=0, vcount=0;
logic video_on=1, frame_start=0;
logic [2:0] rgb;
scroll_engine uut(.*);
always #10 clk = ~clk;
initial begin
rst=1; #50; rst=0;
// 扫描一个区域
for(int y=0; y<32; y++) begin
for(int x=0; x<64; x++) begin
hcount=x; vcount=y; #10;
end
end
// 模拟滚动
frame_start=1; #20; frame_start=0;
for(int y=0; y<32; y++) begin
for(int x=0; x<64; x++) begin
hcount=x; vcount=y; #10;
end
end
$display("滚屏引擎测试完成 ✓");
#100; $finish;
end
endmodule经典2D游戏用视差滚屏营造纵深感:远处背景慢、近处前景快。
| 资源 | 8×8瓦片 | 16×16瓦片 | 32×32瓦片 |
|---|---|---|---|
| 瓦片ROM | 32×24=768条目 | 16×12=192条目 | 8×6=48条目 |
| 像素ROM | 16种×64字=1K | 16种×256字=4K | 16种×1K字=16K |
| 加法器 | 2个10bit | 2个10bit | 2个10bit |
| LUT消耗 | ~200 | ~250 | ~350 |
💡 关键优化:瓦片ROM和像素ROM可以合并成一个双端口ROM——一次读瓦片ID,一次读像素颜色,节省布线资源。或者用BRAM存储大地图,单周期读取。
练习1:修改瓦片地图,设计一个迷宫关卡
练习2:实现视差滚屏,3层不同速度
练习3:用按键控制滚动方向(上下左右)
练习4:添加精灵叠加在滚动的地图上
练习5:用BRAM存储256×256的大地图
步骤1:保存Verilog文件到verilog/目录
步骤2:运行verilator --lint-only tile_map_rom.v
步骤3:运行verilator --lint-only tile_pixel_rom.v
步骤4:运行verilator --lint-only scroll_engine.v
步骤5:编译测试台verilator --binary -j 0 scroll_engine.v tile_map_rom.v tile_pixel_rom.v scroll_engine_tb.sv
步骤6:运行./obj_dir/Vscroll_engine_tb
在640×480@60Hz VGA下,每像素有约25ns(50MHz时钟)完成瓦片查表+像素查表+颜色转换。关键路径:
路径1:world_x计算(add) → tile_x取位 → ROM读取 → pixel_rgb选择 → 输出
路径2:scroll_x寄存器 → 加法器 → 多路选择 → 输出
典型延迟:3-4级LUT,在50MHz下余量充足。如果需要更高分辨率(1080p),需要流水线化ROM读取。
模式1:单层滚屏 — 全屏一个瓦片地图,最简单
模式2:双层滚屏 — 背景层+前景层,前景可以遮挡角色
模式3:视差滚屏 — 远/中/近3层不同速度,纵深感
模式4:双方向滚屏 — 横版+纵版,4方向滚动
模式5:缩放滚屏 — 改变瓦片渲染大小实现zoom效果