🏠 课程总览 > 第21课
第21课: NES完整游戏
飞船躲避游戏:控制+移动+障碍+碰撞+帧缓冲渲染
🏆 完整NES风格游戏仿真
✅ Verilator仿真验证通过
📖 核心概念
- 游戏状态机:menu→play→game_over三态切换
- 飞船控制:4方向移动,边界检测防止越界
- 障碍物生成:3个障碍从上方下落,到底部重置
- 碰撞检测:飞船3×3与障碍2×1范围重叠判定
- 帧缓冲渲染:先清屏→画飞船→画障碍→输出像素
💡 关键思路:本课的核心是游戏状态机——menu→play→game_over三态切换。
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第21课: NES完整游戏
module nes_game (
input wire clk, input wire rst_n,
input wire btn_left, btn_right, btn_up, btn_down,
output reg [7:0] vga_r, vga_g, vga_b,
output reg [7:0] scan_x, scan_y, output reg pixel_valid,
output reg [15:0] score, output reg game_over
);
localparam W=64, H=48;
reg [1:0] frame_buf [0:63][0:47];
reg [6:0] ship_x; reg [5:0] ship_y;
reg [6:0] obs_x [0:2]; reg [5:0] obs_y [0:2]; reg obs_active [0:2];
reg [1:0] state; reg [7:0] frame_cnt; reg [15:0] score_cnt;
integer i, j;
initial begin for(j=0;j<48;j=j+1) for(i=0;i<64;i=i+1) frame_buf[i][j]=0; end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin ship_x<=30;ship_y<=40;state<=0;frame_cnt<=0;score<=0;score_cnt<=0;game_over<=0;
for(i=0;i<3;i=i+1) begin obs_x[i]<=i*20+10;obs_y[i]<=0;obs_active[i]<=1;end end
else begin frame_cnt<=frame_cnt+1;
case(state)
0: if(btn_left||btn_right||btn_up||btn_down) begin state<=1;score_cnt<=0;game_over<=0;end
1: begin
if(btn_left&&ship_x>2) ship_x<=ship_x-1; if(btn_right&&ship_x<W-3) ship_x<=ship_x+1;
if(btn_up&&ship_y>2) ship_y<=ship_y-1; if(btn_down&&ship_y<H-3) ship_y<=ship_y+1;
if(frame_cnt[1:0]==0) begin
for(i=0;i<3;i=i+1) if(obs_active[i]) begin obs_y[i]<=obs_y[i]+1;
if(obs_y[i]>=H-1) begin obs_y[i]<=0;obs_x[i]<={frame_cnt[3:0],3'b000}+i*20;end end
score_cnt<=score_cnt+1;score<=score_cnt[15:0];end
for(i=0;i<3;i=i+1) if(obs_active[i])
if(ship_x>=obs_x[i]-1&&ship_x<=obs_x[i]+1&&ship_y>=obs_y[i]-1&&ship_y<=obs_y[i]+1)
begin state<=2;game_over<=1;end
end
2: if(btn_left||btn_right) begin state<=0;ship_x<=30;ship_y<=40;
for(i=0;i<3;i=i+1) begin obs_x[i]<=i*20+10;obs_y[i]<=0;end end
endcase
if(frame_cnt[0]==0) begin
begin : clear_buf
integer cx,cy; for(cy=0;cy<H;cy=cy+1) for(cx=0;cx<W;cx=cx+1) frame_buf[cx][cy]=2'd0;
end
frame_buf[ship_x][ship_y] = 2'd3;
if(ship_x>0) frame_buf[ship_x-1][ship_y] = 2'd3;
if(ship_x<W-1) frame_buf[ship_x+1][ship_y] = 2'd3;
if(ship_y>0) frame_buf[ship_x][ship_y-1] = 2'd3;
if(ship_y<H-1) frame_buf[ship_x][ship_y+1] = 2'd3;
for(i=0;i<3;i=i+1) if(obs_active[i]) begin
frame_buf[obs_x[i]][obs_y[i]] = 2'd2;
if(obs_x[i]>0) frame_buf[obs_x[i]-1][obs_y[i]] = 2'd2;end
end
end
end
always @(posedge clk) begin pixel_valid<=1;scan_x<=scan_x+1;
if(scan_x>=W-1) begin scan_x<=0;scan_y<=scan_y+1;if(scan_y>=H-1) scan_y<=0;end
case(frame_buf[scan_x][scan_y])
0:begin vga_r<=8'h00;vga_g<=8'h00;vga_b<=8'h20;end
2:begin vga_r<=8'hFF;vga_g<=8'h33;vga_b<=8'h33;end
3:begin vga_r<=8'h33;vga_g<=8'hFF;vga_b<=8'h33;end
default:begin vga_r<=0;vga_g<=0;vga_b<=0;end endcase
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk,rst_n; reg btn_left,btn_right,btn_up,btn_down;
wire [7:0] vga_r,vga_g,vga_b,scan_x,scan_y; wire pixel_valid; wire [15:0] score; wire game_over;
nes_game uut (.clk(clk),.rst_n(rst_n),.btn_left(btn_left),.btn_right(btn_right),.btn_up(btn_up),.btn_down(btn_down),.vga_r(vga_r),.vga_g(vga_g),.vga_b(vga_b),.scan_x(scan_x),.scan_y(scan_y),.pixel_valid(pixel_valid),.score(score),.game_over(game_over));
always clk = #10 ~clk;
initial begin
$dumpfile("nesgame.vcd");$dumpvars(0,tb);clk=0;rst_n=0;btn_left=0;btn_right=0;btn_up=0;btn_down=0;
repeat(5)@(posedge clk);rst_n=1;
$display("=== NES完整游戏仿真 ===");$display("完整NES风格游戏仿真");$display("");
$display("--- 游戏规格 ---");$display(" 类型: 飞船躲避");$display(" 分辨率: 64x48");$display(" 飞船: 3x3绿色");$display(" 障碍: 红色");$display(" 控制: 上下左右");$display("");
$display("--- 开始游戏 ---");btn_right=1;@(posedge clk);btn_right=0;repeat(10)@(posedge clk);
$display(" 状态: state=%0d, ship=(%0d,%0d)",uut.state,uut.ship_x,uut.ship_y);
$display("");$display("--- 控制测试 ---");btn_left=1;repeat(10)@(posedge clk);btn_left=0;
$display(" 左移后: ship_x=%0d",uut.ship_x);
btn_up=1;repeat(10)@(posedge clk);btn_up=0;$display(" 上移后: ship_y=%0d",uut.ship_y);
$display("");$display("--- 障碍物状态 ---");
begin integer k;for(k=0;k<3;k=k+1) $display(" 障碍%0d: (%0d,%0d) active=%0d",k,uut.obs_x[k],uut.obs_y[k],uut.obs_active[k]);end
$display("");$display("--- 帧缓冲内容(缩略) ---");
begin integer fy,fx,ship_px;ship_px=0;
for(fy=0;fy<48;fy=fy+4) begin $write(" ");for(fx=0;fx<64;fx=fx+4) begin
case(uut.frame_buf[fx][fy]) 0:$write("·");2:$write("█");3:begin $write("▲");ship_px=1;end default:$write("░");endcase;end $write("\n");end
if(ship_px) $display(" ✅ 飞船在帧缓冲中可见");end
$display("");$display("✅ 完整NES风格游戏仿真验证通过!");$display("🏆 成就解锁: 完整NES风格游戏仿真!");$finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== NES完整游戏仿真 ===
完整NES风格游戏仿真
--- 游戏规格 ---
类型: 飞船躲避
分辨率: 64x48
飞船: 3x3绿色
障碍: 红色
控制: 上下左右
--- 开始游戏 ---
状态: state=1, ship=(30,40)
--- 控制测试 ---
左移后: ship_x=20
上移后: ship_y=30
--- 障碍物状态 ---
障碍0: (10,7) active=1
障碍1: (30,7) active=1
障碍2: (50,7) active=1
--- 帧缓冲内容(缩略) ---
················
················
················
················
················
················
················
················
·····▲··········
················
················
················
✅ 飞船在帧缓冲中可见
✅ 完整NES风格游戏仿真验证通过!
🏆 成就解锁: 完整NES风格游戏仿真!
- tb.sv:26: Verilog $finish
🔧 编译和运行
# 编译
verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \
--build -j 4 -o sim \
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \
-Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \
-Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKSEQ
# 运行
./obj_dir/sim
# 查看波形(可选)
gtkwave sim.vcd
🎮 实战步骤
1
游戏状态机:3个状态:menu(0)→play(1)→game_over(2)。menu按任意键开始,game_over按键重启。
2
飞船控制:4方向移动,每帧移动1像素。边界检测:ship_x > 2防止移出左边,ship_x < W-3防止移出右边。
3
障碍物生成:3个障碍物从顶部下落,每4帧移动1像素。到达底部后重置到顶部,X位置用frame_cnt生成伪随机值。
4
帧缓冲渲染:先清屏(0),再画飞船(3=绿色3×3),再画障碍(2=红色2×1)。帧缓冲用blocking赋值避免Verilator的BLKLOOPINIT限制。
🎮 经典躲避游戏设计
难度曲线:好游戏难度渐进——开始障碍慢且少,随时间加速加密。每30秒增加一个障碍或提升速度0.5x。
反馈设计:躲避成功→得分音+数字+1,碰撞→爆炸音+闪屏。每一帧的反馈让玩家"感觉"游戏。
视觉风格:深蓝背景+绿色飞船+红色障碍=高对比度。NES游戏常用高饱和度配色确保CRT上的可见性。
🏆
完整NES风格游戏仿真
✅ Verilator仿真验证通过
🧠 知识扩展
游戏循环:游戏主循环:输入读取→状态更新→碰撞检测→渲染。在Verilog中,每个时钟周期执行一步。frame_cnt控制帧率。
碰撞优化:矩形碰撞检测(AABB)最简单:A.x1B.x1 && A.y1B.y1。NES游戏常用缩小的碰撞框,比视觉精灵小几个像素,让游戏手感更好。
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率