🏃 第5课:精灵动画

精灵概念 + 位置寄存器 + 边界弹跳

🏆 成就:精灵驯兽师 ✅ Verilator验证

🏃
精灵驯兽师
Verilator仿真小方块在屏幕上弹跳

🎮 精灵=位置+图案

精灵是可以在屏幕上移动的独立图案。每个VSYNC帧更新位置,碰到边界反弹。

Verilogvga_sprite.v核心
reg [9:0] sprite_x, sprite_y;
reg signed [1:0] vel_x, vel_y;
// 每帧更新
always @(posedge clk) if(vsync_fall) begin
    sprite_x <= sprite_x + vel_x;
    sprite_y <= sprite_y + vel_y;
    if(sprite_x<=0 || sprite_x>=632) vel_x<=-vel_x;
    if(sprite_y<=0 || sprite_y>=472) vel_y<=-vel_y;
end
wire in_sprite=(hcount>=sprite_x)&&(hcount=sprite_y)&&(vcount

📐 碰撞检测详解

边界反弹检测(X方向): sprite_x = 0 → 撞左墙 → vel_x: -1 → +1 sprite_x = 632 → 撞右墙 → vel_x: +1 → -1 (640 - 8 = 632) ┌──────────────────────────────────┐ ││ ││ ││ ■→ ││ ││ sprite_x=0 ││ ││ vel_x=+1 ││ ││ ■← ││ ││ sprite_x=632 ││ ││ vel_x=-1 ││ └──────────────────────────────────┘ x=0 x=640

🧪 Verilator完整测试台

SystemVerilogvga_sprite_tb.sv
module vga_sprite_tb;
    logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
    vga_sprite uut(.*);
    initial clk=0; always #20 clk=~clk;

    int frame_count=0; logic vsync_prev;
    logic [9:0] last_x,last_y; int bounce_count;
    always @(posedge clk) begin
        vsync_prev<=vsync;
        if(vsync&&!vsync_prev) begin
            frame_count++;
            if(frame_count>=2) begin
                if(uut.sprite_x!=last_x || uut.sprite_y!=last_y)
                    $display("  帧%0d: pos=(%0d,%0d)",
                        frame_count,uut.sprite_x,uut.sprite_y);
                if(uut.sprite_x<=1 || uut.sprite_x>=632 ||
                   uut.sprite_y<=1 || uut.sprite_y>=472) begin
                    bounce_count++;
                    $display("  ↗ 反弹!pos=(%0d,%0d) 次数=%0d",
                        uut.sprite_x,uut.sprite_y,bounce_count);
                end
                last_x=uut.sprite_x; last_y=uut.sprite_y;
            end
            if(bounce_count>=4) begin
                $display("精灵弹跳验证完成! 反弹=%0d ✓",bounce_count);
                $finish;
            end
        end
    end
    initial begin rst_n=0;#100;rst_n=1;end
    initial #500_000_000 $finish;
endmodule

🔬 进阶:精灵拖尾效果

Verilog拖尾效果片段
reg [9:0] trail_x[0:3], trail_y[0:3];
always @(posedge clk) if(vsync_fall) begin
    trail_x[3]<=trail_x[2]; trail_x[2]<=trail_x[1];
    trail_x[1]<=trail_x[0]; trail_x[0]<=sprite_x;
    // Y同理
end

// 渲染:越旧的拖尾颜色越暗
wire in_trail3 = (hcount>=trail_x[3])&&(hcount<trail_x[3]+8)&&
                (vcount>=trail_y[3])&&(vcount<trail_y[3]+8);
// ... in_trail2, in_trail1 类似

wire [2:0] sprite_rgb = in_sprite ? 3'b100 :
                        in_trail0 ? 3'b100 :
                        in_trail1 ? 3'b100 :
                        in_trail2 ? 3'b100 : 3'b000;

🔬 多精灵系统设计

FPGA的天然优势就是并行!CPU渲染100个精灵需要循环100次,FPGA可以同时判断100个精灵的位置——零额外开销。

Verilog多精灵渲染
// 4个精灵的位置和颜色
reg [9:0] sprite_x [0:3];
reg [9:0] sprite_y [0:3];
reg [2:0] sprite_color [0:3]; // 每个精灵不同颜色

// 并行判断所有精灵 — 零额外延迟!
wire [3:0] in_sprite;
genvar g;
generate for(g=0;g<4;g++) begin
    assign in_sprite[g] = (hcount >= sprite_x[g]) &&
                          (hcount < sprite_x[g] + 8) &&
                          (vcount >= sprite_y[g]) &&
                          (vcount < sprite_y[g] + 8);
end endgenerate

// 优先级:精灵0 > 精灵1 > 精灵2 > 精灵3
wire [2:0] final_color = in_sprite[0] ? sprite_color[0] :
                         in_sprite[1] ? sprite_color[1] :
                         in_sprite[2] ? sprite_color[2] :
                         in_sprite[3] ? sprite_color[3] :
                         3'b011; // 背景色

📐 精灵速度控制进阶

1. 固定速度:每帧移动1像素 → 60像素/秒

2. 加速速度:每帧移动2像素 → 120像素/秒

3. 小数速度:每3帧移动1像素 → 20像素/秒(用计数器实现)

4. 变速运动:每帧加速度变化 → 模拟重力效果

🎯 实战挑战

挑战1:添加第二个精灵(蓝色),两个同时弹跳

挑战2:实现穿越模式(从对面出来而不是反弹)

挑战3:用ROM笑脸替换纯色方块

🛠️ 编译运行

Bash编译运行步骤
verilator --binary -j 0 --trace \
    vga_sync.v vga_sprite.v vga_sprite_tb.sv
./obj_dir/Vvga_sprite_tb

# 预期输出:
#   帧2: pos=(101,101)
#   帧3: pos=(102,102)
#   ...
#   ↗ 反弹!pos=(632,xxx) 次数=1
#   ↗ 反弹!pos=(xxx,472) 次数=2
#   精灵弹跳验证完成! 反弹=4 ✓

# 提示:仿真可能需要较长时间等待精灵弹到边界
# 可以调整初始位置更靠近边界加速测试

🧠 概念检查清单

✅ 精灵的三个核心属性是什么?

✅ 为什么用VSYNC信号触发位置更新?

✅ 边界反弹的检测条件怎么写?

✅ 如何判断当前像素属于哪个精灵?

✅ 为什么FPGA天然适合多精灵渲染?

✅ 精灵速度控制有哪些方法?

🔮 下一课预告

下一课我们将用精灵技术实现经典Pong游戏!球体弹跳+挡板碰撞+自动AI+计分系统——你的第一个完整FPGA游戏!

📐 碰撞检测详解

边界反弹检测(X方向): sprite_x=0 → 撞左墙 → vel_x: -1→+1 sprite_x=632 → 撞右墙 → vel_x: +1→-1 (640-8=632) ┌──────────────────────────────┐ ││ ││ ││ ■→ ││ ││ sprite_x=0, vel_x=+1 ││ ││ ■← ││ ││ sprite_x=632 ││ └──────────────────────────────┘

🔬 进阶:精灵拖尾效果

Verilog拖尾效果片段
reg [9:0] trail_x[0:3], trail_y[0:3];
always @(posedge clk) if(vsync_fall) begin
    trail_x[3]<=trail_x[2]; trail_x[2]<=trail_x[1];
    trail_x[1]<=trail_x[0]; trail_x[0]<=sprite_x;
    // Y方向同理
end

🎯 精灵系统设计要点

1. 多精灵:FPGA天然并行!复制位置寄存器即可

2. 速度控制:速度寄存器或"每N帧移动1像素"

3. 精灵层叠:用优先级决定重叠时谁在上面

4. 把纯色方块替换为ROM笑脸(第4课内容)

🧪 Verilator完整测试台

SystemVerilogvga_sprite_tb.sv
module vga_sprite_tb;
    logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
    vga_sprite uut(.*);
    initial clk=0; always #20 clk=~clk;

    int frame_count=0; logic vsync_prev;
    logic [9:0] last_x,last_y; int bounce_count;
    always @(posedge clk) begin
        vsync_prev<=vsync;
        if(vsync&&!vsync_prev) begin
            frame_count++;
            if(frame_count>=2) begin
                if(uut.sprite_x!=last_x||uut.sprite_y!=last_y)
                    $display("帧%0d: pos=(%0d,%0d)",frame_count,uut.sprite_x,uut.sprite_y);
                if(uut.sprite_x<=1||uut.sprite_x>=632||
                   uut.sprite_y<=1||uut.sprite_y>=472) begin
                    bounce_count++;
                    $display("↗ 反弹! pos=(%0d,%0d) 次数=%0d",uut.sprite_x,uut.sprite_y,bounce_count);
                end
                last_x=uut.sprite_x; last_y=uut.sprite_y;
            end
            if(bounce_count>=4) begin
                $display("精灵弹跳验证完成! 反弹=%0d ✓",bounce_count);
                $finish;
            end
        end
    end
    initial begin rst_n=0;#100;rst_n=1;end
    initial #500_000_000 $finish;
endmodule