🗺️ 第17课:滚屏游戏引擎

瓦片地图 + 视口滚动 + 背景渲染 — 制作可滚动的2D世界!

🏆 成就:滚屏大师 ✅ Verilator验证通过

🗺️
滚屏大师
瓦片地图ROM + 视口偏移 + 多层滚屏速度差

🗺️ 瓦片地图原理

2D游戏世界通常由瓦片(Tile)构成——把大地图切成小方块,每个方块用编号表示类型(地板、墙壁、草地…)。渲染时只需查表,就能用极少的存储画出巨大的世界!

瓦片地图滚动原理 大地图 (64×48 瓦片) 可见视口 (32×24 瓦片) ┌────────────────────┐ ┌──────────────┐ │ ■ □ ■ □ ■ □ ■ □ │ │ ■ □ ■ □ │ │ □ □ □ □ □ □ □ □ │ ←── │ □ □ □ □ │ │ ■ □ ■ □ ■ □ ■ □ │ scroll│ ■ □ ■ □ │ │ □ □ □ □ □ □ □ □ │ offset│ □ □ □ □ │ │ ... │ └──────────────┘ └────────────────────┘ ↑ 5×4 瓦片 = 20字节 每8×8像素/瓦片 总像素: 512×384 视口位置 = (scroll_x, scroll_y) 瓦片坐标 = (pixel_pos + scroll_offset) / 8 瓦片内偏移 = (pixel_pos + scroll_offset) % 8

📐 瓦片地图ROM设计

Verilogtile_map_rom.v — 瓦片地图数据
// 瓦片地图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

Verilogtile_pixel_rom.v — 8×8瓦片像素数据
// 瓦片像素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

🚀 滚屏引擎核心

Verilogscroll_engine.v — 视口滚动控制器
// 滚屏游戏引擎 - 视口滚动控制器
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;
endmodule

🧪 滚屏引擎测试

SystemVerilogscroll_engine_tb.sv — 滚屏测试台
module 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游戏用视差滚屏营造纵深感:远处背景慢、近处前景快。

视差滚屏层速度 远景层 (scroll_x/4) ─── 山脉 ─── 1/4速 中景层 (scroll_x/2) ─── 树木 ─── 1/2速 近景层 (scroll_x) ─── 地面 ─── 全速 FPGA实现: 每层独立的scroll offset layer0_scroll = scroll_x >> 2; // 远 layer1_scroll = scroll_x >> 1; // 中 layer2_scroll = scroll_x; // 近

📊 滚屏引擎资源分析

资源8×8瓦片16×16瓦片32×32瓦片
瓦片ROM32×24=768条目16×12=192条目8×6=48条目
像素ROM16种×64字=1K16种×256字=4K16种×1K字=16K
加法器2个10bit2个10bit2个10bit
LUT消耗~200~250~350

💡 关键优化:瓦片ROM和像素ROM可以合并成一个双端口ROM——一次读瓦片ID,一次读像素颜色,节省布线资源。或者用BRAM存储大地图,单周期读取。

🏋️ 练习

练习1:修改瓦片地图,设计一个迷宫关卡

练习2:实现视差滚屏,3层不同速度

练习3:用按键控制滚动方向(上下左右)

练习4:添加精灵叠加在滚动的地图上

练习5:用BRAM存储256×256的大地图

🗺️
滚屏大师
完成本课练习,掌握瓦片地图滚动渲染!

🔧 Verilator验证步骤

步骤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效果