Bevy 复刻黄金矿工|Stage 1:玩法规划与前期准备
本系列将使用 Bevy 0.17 来复刻经典游戏《黄金矿工》,目标同时支持 Native 与 Web(WASM) 运行。本篇为 Stage 1,重点放在:
- 明确项目整体目标与边界
- 拆解经典黄金矿工的核心玩法机制
- 选择并确认 MIT 协议的免费素材
- 使用
bevy_cli完成项目初始化与 Web 版本运行 - 实现最小可行 Demo:加载页 + 菜单 + 文本 UI
内容偏向「实战开发记录」,既可以当作个人开发日志,也可以作为之后 Stage 2 / Stage 3 的基础说明。
一、项目目标与范围
1.1 核心目标
- 使用 Bevy 游戏引擎 复刻经典《黄金矿工》
- 支持 Native 桌面 与 Web(WASM) 运行环境
- 采用 Rust + ECS(实体组件系统) 开发范式
- 尽量保持玩法风格接近原版,同时保留一定扩展空间(如更多道具、关卡配置等)
1.2 Stage 1 的具体目标
在 Stage 1 中,项目暂时不追求「可玩性完整」,而是聚焦于:
- 搭建基础工程结构
- 确认素材与坐标系转换方案
- 让主菜单场景可以在 Native / Web 上稳定运行
- 预留状态机与后续关卡机制的扩展点
可以理解为:先把“壳”搭好,再逐步往里面填玩法逻辑。
二、玩法机制分析
在动手写代码之前,先快速回顾一下《黄金矿工》的经典玩法:
2.1 抓钩与摆动
- 抓钩以角色为中心左右摆动,形成一个周期性的角度变化
- 玩家在合适时机按下按键,下放抓钩
- 抓钩沿当前方向直线伸出,直到碰到目标或超出边界
2.2 物品与重量
- 金块:价值高,重量中等或偏重
- 小金块 / 宝石 / 钻石:价值高但体积小,重量轻
- 石头:价值低,重量大,拖拽慢
- 宝袋:随机奖励或特殊效果
不同物品的 重量 将直接影响抓回速度,是后续物理与速度计算的关键参数。
2.3 关卡目标
- 每一关有 时间限制 与 目标金额
- 在限定时间内,累计金币达到目标值即可通关
- 未达标则关卡失败
2.4 商店与道具
在关卡之间,玩家可以花费金币购买:
- 炸药(丢弃大石头)
- 时间延长类道具
- 抓钩加速类道具
- 其他定制玩法道具
这些内容将在后续 Stage 中逐步实现,Stage 1 主要为它们预留结构空间。
三、免费素材与授权
选择素材时,有两个关键考虑:
- 授权协议(是否允许商用、修改、分发)
- 完整度(是否包含玩家、物品、背景、UI、字体、音效等)
本项目使用 GitHub 上的开源项目:
GoldMiner-Rebirth(MIT 协议)
仓库地址:https://github.com/zzxzzk115/GoldMiner-Rebirth
该仓库提供:
- PNG 格式的游戏元素图片(包括金块、石头、角色、背景等)
- 字体资源
- 部分音效文件
- 原项目的逻辑参考(Love2D 实现)
MIT 协议非常宽松,允许:
- 复制、修改、合并、发布、再授权
- 用于开源或闭源项目
- 用于商业或非商业用途
只需在最终项目中保留相应许可声明即可。
四、项目初始化:bevy_cli
为了提高项目脚手架质量,同时简化 Web 构建流程,本项目采用官方的命令行工具 bevy_cli 来初始化。
4.1 什么是 bevy_cli?
bevy_cli 是 Bevy 官方团队实现的「多合一」命令行工具,目前仍处于预览阶段,主要功能包括:
- 创建不同模板类型的 Bevy 项目(2D / 3D 等)
- 方便地运行 Web 版本(WASM)
- 集成 Lint、格式检查等
- 后续还会扩展更多工具链(如部署、打包等)
4.2 安装 bevy_cli
bevy_cli 尚未发布到 crates.io,需要通过 Git 直接安装:
cargo install --git https://github.com/TheBevyFlock/bevy_cli --tag cli-v0.1.0-alpha.2 --locked bevy_cli这里通过 --tag 锁定版本,以避免未来 API 变化带来的不兼容。
4.3 创建项目
安装完成后,可以通过一行命令创建 2D 模板:
bevy new -t=2d goldminer
cd goldminer-t=2d 表示使用官方提供的 2D 模板,默认包含:
- 基础 Bevy 依赖
- 示例场景(常见是一个简单动画 Demo)
- Web 构建所需的配置
4.4 首次运行:Web 版本
执行以下命令可以直接运行 Web 版本:
bevy run web --open构建完成后,浏览器会自动打开:
- 包含加载页面(Loading)
- 主菜单页面(Menu)
- 示例动画页面(例如一只小鸭走来走去)
示例画面如下:


从这一刻起,整个黄金矿工项目已经拥有了一个可运行的「骨架」。
五、菜单场景设计与分辨率策略
菜单是整个游戏的入口,本篇先把菜单屏幕和基础视觉设定搭好。
5.1 分辨率设定
原始素材以 320×240 为基准分辨率。
为了在现代屏幕上避免画面过小,本项目将窗口放大到 1280×960,也就是 4 倍缩放:
.set(WindowPlugin {
primary_window: Window {
title: "Goldminer".to_string(),
resolution: (1280, 960).into(),
fit_canvas_to_parent: true,
..default()
}.into(),
..default()
})这样既能保持像素风格,又能让画面在桌面与浏览器中看起来更清晰。
5.2 Camera 视野缩放
素材本身仍然是 320×240 尺寸,如果直接绘制在 1280×960 的窗口中,会显得非常小。因此需要通过 正交投影(Orthographic Projection) 来缩放视野。
fn spawn_camera(mut commands: Commands) {
commands.spawn((
Name::new("Camera"),
Camera2d,
Projection::Orthographic(OrthographicProjection {
near: -1000.0,
scale: 0.25,
..OrthographicProjection::default_2d()
}),
));
}这里的关键点在于:
- 将
scale设置为0.25,相当于把画面放大 4 倍 - 结合前面 1280×960 的窗口尺寸,能够把原始 320×240 的内容完整展示并放大
六、坐标系转换:Love2D → Bevy
由于原项目使用 Love2D 实现,相关素材和位置设计都基于 Love2D 的坐标系:
- 原点在左上角
- X 轴向右递增
- Y 轴向下递增
而 Bevy 默认的 2D 世界坐标系是:
- 原点在屏幕中心
- X 轴向右递增
- Y 轴向上递增(与 Love2D 相反)
为了复用原项目中的坐标数据,需要实现一个坐标转换函数:
pub const VIRTUAL_WIDTH: f32 = 320.0;
pub const VIRTUAL_HEIGHT: f32 = 240.0;
/// 将 Love2D 坐标转换为 Bevy 坐标
pub fn love_to_bevy_coords(x: f32, y: f32) -> Vec2 {
Vec2::new(
x - VIRTUAL_WIDTH / 2.0, // X:从左上角平移到中心
VIRTUAL_HEIGHT / 2.0 - y, // Y:翻转 + 平移到中心
)
}有了这个函数之后:
- 可以直接把原 Love2D 项目里写死的坐标迁移过来
- 在 Bevy 中绘制时保持位置一致
- 也为后续关卡布局复刻提供了基础工具
七、菜单状态管理
为了支持多种菜单(主菜单、高分榜、设置等),需要一个简单的 菜单状态机。
#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub enum Menu {
#[default]
None,
Main,
HighScore,
}典型使用方式:
Menu::Main:主菜单Menu::HighScore:高分榜Menu::None:非菜单状态(例如游戏中)
这个状态枚举会与 Bevy 的 States 系统结合,用于控制哪些系统在当前状态下运行。
八、菜单布局与 UI 构建
在 Stage 1 中,菜单不采用复杂的 UI 布局系统,而是使用最直接的:
- 背景图:
Sprite - 文本:
Text2d
这种方式足够还原经典黄金矿工的菜单,也更利于初期快速搭建。
8.1 主菜单根节点
fn spawn_main_menu(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
widget::ui_root("Main Menu"),
GlobalZIndex(2),
DespawnOnExit(Menu::Main),
Sprite::from_image(asset_server.load("images/bg_start_menu.png")),
children![
play_button(&asset_server),
score_button(&asset_server),
developer_text(&asset_server),
menu_arrow(&asset_server)
],
));
}这里做了几件事:
- 设置菜单根节点的名称
Main Menu - 使用
GlobalZIndex(2)保证菜单在上层渲染 - 使用
DespawnOnExit(Menu::Main)来实现「离开主菜单状态时自动销毁节点」 - 使用背景图片
bg_start_menu.png - 通过
children!挂载多个子元素(按钮、文字、指示箭头)
8.2 Play 按钮示例
fn play_button(asset_server: &AssetServer) -> impl Bundle {
let font = asset_server.load("fonts/Kurland.ttf");
(
Name::new("Start Game"),
Text2d::new("Start Game"),
TextFont {
font,
font_size: 20.0,
..default()
},
Transform::from_translation(love_to_bevy_coords(30.0, 150.0).extend(0.0)),
Anchor::TOP_LEFT,
TextColor(COLOR_YELLOW),
)
}要点:
- 按钮文本使用素材项目中的字体
Kurland.ttf - 文本通过
Text2d渲染,而不是 UI Text - 位置通过前面提到的
love_to_bevy_coords转换实现 - 使用
Anchor::TOP_LEFT配合 Love 风格布局
其他按钮(例如高分榜、开发者信息等)可以按同样模式添加。
九、菜单输入逻辑与系统绑定
主菜单需要处理的典型输入包括:
- 上下方向键:切换当前选中项
- Enter / 小键盘回车:确认操作
首先在调度中声明系统只在主菜单状态下运行:
.add_systems(Update, keyboard_input.run_if(in_state(Menu::Main)));然后实现具体输入逻辑:
fn keyboard_input(
input: Res<ButtonInput<KeyCode>>,
current_item: Res<State<MenuSelect>>,
mut next_item: ResMut<NextState<MenuSelect>>,
mut next_menu: ResMut<NextState<Menu>>,
mut next_screen: ResMut<NextState<Screen>>,
resource_handles: Res<ResourceHandles>,
) {
if input.just_pressed(KeyCode::ArrowUp) || input.just_pressed(KeyCode::ArrowDown) {
if current_item.get() == &MenuSelect::StartGame {
next_item.set(MenuSelect::HighScore);
} else {
next_item.set(MenuSelect::StartGame);
}
}
if input.just_pressed(KeyCode::Enter) || input.just_pressed(KeyCode::NumpadEnter) {
if current_item.get() == &MenuSelect::StartGame {
if resource_handles.is_all_done() {
next_screen.set(Screen::Gameplay);
} else {
next_screen.set(Screen::Loading);
}
} else {
next_menu.set(Menu::HighScore);
}
}
}这里体现了几个重要设计点:
- 菜单选择项
MenuSelect作为独立状态,便于扩展更多菜单项 - 按下回车后,不是直接进入游戏,而是先判断资源是否加载完毕
- 如果资源尚未加载完成,则跳转到
Screen::Loading,表现更加友好
十、Stage 1 完成效果
在完成本阶段所有工作后,项目已经具备:
- 可运行的 Native / Web 版本
- 带有背景与文本按钮的主菜单
- 基于 Love2D 原始设计的布局与坐标
- 简单但可扩展的菜单状态机与输入逻辑
示例效果如下:


十一、下一阶段预告:Stage 2 抓钩系统
在接下来的 Stage 2 中,重点会放在真正的「黄金矿工核心玩法」上:
- 抓钩左右摆动与角度控制
- 抓钩下放与回收逻辑
- 与场景中物体的碰撞检测
- 不同物体基于重量的拉回速度计算
- 拖拽过程中的动画反馈与音效处理
- 初版关卡时间与目标金额系统
届时将正式进入「可玩版本」的实现阶段,而本篇搭建的菜单、坐标与基础结构,会成为后续所有内容的支撑骨架。