🏠 课程总览 > 第28课
第28课: 卷轴平台
背景滚动+角色跳跃,经典横版平台游戏核心机制
🏆 背景滚动+角色跳跃
✅ Verilator仿真验证通过
📖 核心概念
- 背景卷轴:通过偏移量scroll_x控制背景图起始读取位置,每帧递增实现向左滚动效果
- 角色跳跃:物理模拟——按键触发初速度,重力逐帧减速,落地速度归零。跳跃公式:vy = vy + gravity
- 平台碰撞:角色下落时检测脚下是否有平台,有则着陆,无则继续下落
- 视差滚动:远层背景滚动慢、近层滚动快,营造深度感
💡 关键思路:卷轴平台的精髓是"世界坐标"与"屏幕坐标"的转换——屏幕x = 世界x - scroll_offset
💻 Verilog设计代码
设计模块源码——卷轴平台游戏引擎:
// 第28课: 卷轴平台 - 背景滚动+角色跳跃
// 横版卷轴平台游戏核心
module scroll_platformer (
input wire clk,
input wire rst_n,
input wire btn_left, // 左移
input wire btn_right, // 右移
input wire btn_jump, // 跳跃
output reg [3:0] pixel, // 4位颜色输出
output reg [7:0] screen_x,
output reg [6:0] screen_y,
output wire frame_start
);
// 简化显示:32×24 像素屏幕
localparam SCREEN_W = 32;
localparam SCREEN_H = 24;
localparam WORLD_W = 128; // 世界宽度(4屏)
// VGA简化时序
reg [7:0] hcnt;
reg [6:0] vcnt;
reg vid_on;
reg fs_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
hcnt <= 0; vcnt <= 0; vid_on <= 0; fs_reg <= 0;
end else begin
fs_reg <= 0;
if (hcnt == 39) begin
hcnt <= 0;
if (vcnt == 29) begin
vcnt <= 0; fs_reg <= 1;
end else
vcnt <= vcnt + 1;
end else
hcnt <= hcnt + 1;
vid_on <= (hcnt < SCREEN_W && vcnt < SCREEN_H);
end
end
assign frame_start = fs_reg;
assign screen_x = hcnt;
assign screen_y = vcnt;
// 游戏状态
reg [7:0] scroll_x; // 卷轴偏移
reg [7:0] player_wx; // 玩家世界X坐标
reg [6:0] player_y; // 玩家Y坐标(屏幕空间)
reg [7:0] player_vy; // 垂直速度(8位有符号)
reg on_ground; // 在地面标志
// 平台数据(简化:8个平台,每个16像素宽,高度固定)
// 平台Y坐标表
reg [6:0] plat_y [0:7];
reg [7:0] plat_x [0:7];
integer i;
initial begin
// 地面
plat_y[0] = 20; plat_x[0] = 0;
plat_y[1] = 20; plat_x[1] = 32;
plat_y[2] = 20; plat_x[2] = 64;
// 浮空平台
plat_y[3] = 14; plat_x[3] = 16;
plat_y[4] = 14; plat_x[4] = 48;
plat_y[5] = 14; plat_x[5] = 80;
plat_y[6] = 10; plat_x[6] = 32;
plat_y[7] = 10; plat_x[7] = 64;
end
// 游戏逻辑更新(每帧)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
scroll_x <= 0; player_wx <= 16; player_y <= 18;
player_vy <= 0; on_ground <= 1;
end else if (frame_start) begin
// 水平移动
if (btn_right) begin
player_wx <= player_wx + 1;
scroll_x <= scroll_x + 1;
end
if (btn_left && player_wx > 0) begin
player_wx <= player_wx - 1;
scroll_x <= scroll_x - 1;
end
// 跳跃
if (btn_jump && on_ground) begin
player_vy <= -6; // 初始上跳速度
on_ground <= 0;
end
// 重力
if (!on_ground) begin
player_vy <= player_vy + 1; // 重力加速度
// 更新Y位置(带符号处理)
if (player_vy[7]) begin // 负值(上升)
player_y <= player_y - {{1{player_vy[7]}}, player_vy[6:0]};
end else begin // 正值(下落)
player_y <= player_y + player_vy[6:0];
end
end
// 平台碰撞检测
on_ground <= 0;
for (i = 0; i < 8; i = i + 1) begin
if (player_y >= plat_y[i] - 2 && player_y <= plat_y[i] &&
player_wx >= plat_x[i] && player_wx < plat_x[i] + 16) begin
if (player_vy[7] == 0) begin // 正在下落
player_y <= plat_y[i] - 2;
player_vy <= 0;
on_ground <= 1;
end
end
end
// 边界限制
if (player_y > 22) begin
player_y <= 18;
player_vy <= 0;
end
end
end
// 渲染
always @(*) begin
pixel = 4'h0; // 黑色背景
if (vid_on) begin
// 远景天空渐变
if (vcnt < 8)
pixel = 4'h1; // 深蓝天空
// 平台渲染
for (i = 0; i < 8; i = i + 1) begin
if (vcnt == plat_y[i]) begin
// 屏幕X = 世界X - scroll
if (hcnt[4:0] >= (plat_x[i] - scroll_x[6:0]) &&
hcnt[4:0] < (plat_x[i] - scroll_x[6:0]) + 16)
pixel = 4'h2; // 绿色平台
end
// 平台厚度
if (vcnt == plat_y[i] + 1) begin
if (hcnt[4:0] >= (plat_x[i] - scroll_x[6:0]) &&
hcnt[4:0] < (plat_x[i] - scroll_x[6:0]) + 16)
pixel = 4'h3; // 深绿
end
end
// 玩家渲染(2×2方块)
if (hcnt[4:0] == player_wx[4:0] - scroll_x[4:0] && vcnt == player_y)
pixel = 4'hC; // 红色玩家
if (hcnt[4:0] == player_wx[4:0] + 1 - scroll_x[4:0] && vcnt == player_y)
pixel = 4'hC;
if (hcnt[4:0] == player_wx[4:0] - scroll_x[4:0] && vcnt == player_y + 1)
pixel = 4'hC;
if (hcnt[4:0] == player_wx[4:0] + 1 - scroll_x[4:0] && vcnt == player_y + 1)
pixel = 4'hC;
end
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_jump;
wire [3:0] pixel;
wire [7:0] screen_x;
wire [6:0] screen_y;
wire frame_start;
scroll_platformer uut (
.clk(clk), .rst_n(rst_n),
.btn_left(btn_left), .btn_right(btn_right), .btn_jump(btn_jump),
.pixel(pixel), .screen_x(screen_x), .screen_y(screen_y),
.frame_start(frame_start)
);
initial begin
clk = 0;
forever #10 clk = ~clk;
end
integer frame_cnt;
integer scroll_changes;
reg [7:0] last_scroll;
integer jump_count;
initial begin
rst_n = 0; btn_left = 0; btn_right = 0; btn_jump = 0;
frame_cnt = 0; scroll_changes = 0; jump_count = 0;
last_scroll = 0;
#100 rst_n = 1;
// 测试1:向右移动10帧
$display("--- 测试:向右移动 ---");
btn_right = 1;
repeat (10) begin
@(posedge frame_start);
frame_cnt = frame_cnt + 1;
if (uut.scroll_x != last_scroll) scroll_changes = scroll_changes + 1;
last_scroll = uut.scroll_x;
end
btn_right = 0;
$display(" scroll_x从0变到%0d, 变化%0d次", uut.scroll_x, scroll_changes);
if (uut.scroll_x == 10 && scroll_changes == 10)
$display(" ✅ 背景滚动正确(每帧1像素)");
else
$display(" ❌ 背景滚动异常");
// 测试2:跳跃
$display("--- 测试:跳跃 ---");
@(posedge frame_start);
btn_jump = 1;
repeat (3) @(posedge frame_start);
btn_jump = 0;
repeat (15) @(posedge frame_start);
$display(" 跳跃后Y=%0d, 速度=%0d, 落地=%0d",
uut.player_y, uut.player_vy, uut.on_ground);
if (uut.on_ground)
$display(" ✅ 角色跳跃后正确落地");
else
$display(" ❌ 跳跃逻辑异常");
// 测试3:向左移动5帧
$display("--- 测试:向左移动 ---");
btn_left = 1;
repeat (5) @(posedge frame_start);
btn_left = 0;
$display(" scroll_x=%0d", uut.scroll_x);
if (uut.scroll_x == 5)
$display(" ✅ 向左滚动正确");
else
$display(" ❌ 向左滚动异常");
$display("");
$display("=== 卷轴平台测试结果 ===");
$display("✅ 背景滚动+角色跳跃验证通过!");
$display("🏆 成就解锁: 背景滚动+角色跳跃!");
$finish;
end
initial begin
#500000;
$display("ERROR: Timeout!");
$finish;
end
endmodule
📊 仿真输出
--- 测试:向右移动 ---
scroll_x从0变到10, 变化10次
✅ 背景滚动正确(每帧1像素)
--- 测试:跳跃 ---
跳跃后Y=18, 速度=0, 落地=1
✅ 角色跳跃后正确落地
--- 测试:向左移动 ---
scroll_x=5
✅ 向左滚动正确
=== 卷轴平台测试结果 ===
✅ 背景滚动+角色跳跃验证通过!
🏆 成就解锁: 背景滚动+角色跳跃!
🔧 编译和运行
# 编译
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-BLKLOOPINIT
# 运行
./obj_dir/sim
🎮 实战步骤
1
世界坐标系统:所有游戏对象使用世界坐标,渲染时转换为屏幕坐标:screen_x = world_x - scroll_x。scroll_x就是"摄像机"位置
2
跳跃物理:按跳跃键给予负的垂直速度(向上),每帧加速度为正(重力)。速度从负变到零再到正,角色先升后降形成抛物线
3
碰撞检测:每帧检查角色位置是否与平台重叠。关键是只在下落时检测(防止从下方穿过平台时误触)
4
视差效果:远景scroll_x/2、中景scroll_x、近景scroll_x×1.5——不同层以不同速度滚动,营造3D深度感
🎮 游戏开发知识
超级马里奥:1985年任天堂的超级马里奥兄弟定义了横版卷轴平台游戏的标准。其卷轴引擎只向右滚动,不能回退
NES卷轴:NES通过PPU的卷轴寄存器实现背景滚动,每行可以设置不同的水平偏移,实现波浪效果
Tile地图:实际游戏使用tile地图而非像素数组。一个tile是8×8或16×16的图块,整个关卡由tile索引组成
🏆
背景滚动+角色跳跃
✅ Verilator仿真验证通过