
当你独自驾车行驶在无尽的公路上,两旁是悬崖和树林,脚下是油门...等等,别踩太猛,会掉下去的!
作为一名开发者,你是否曾想过亲手打造一款属于自己的游戏?今天,我们将一同踏上使用 Rust 语言和 Bevy 游戏引擎的旅程,从零开发一款名为“孤独的公路”(Lonely Highway)的 3D 无限驾驶游戏。这篇教程将为你详细拆解每一步,实现包括车辆控制、无限地图生成、物理碰撞等在内的完整游戏功能。
游戏概览:Lonely Highway
Lonely Highway 是一款风格简约的 3D 无限驾驶游戏,核心玩法包括:
- 🚗 驾驶一辆红色跑车,在自动生成的公路上飞驰。
- 🛣️ 小心躲避路上的障碍物,保持车辆在路面,防止跌落悬崖。
- ⚡ 游戏速度会随时间逐渐提升,最高可达 100 km/h,挑战你的反应极限。
- 🌊 驶过水坑时会触发溅起水花的粒子特效。
- 🌲 公路两旁点缀着简单的树木,但请专注于驾驶,别撞上去。
为什么选择 Rust + Bevy 组合?
在开始敲代码之前,我们先谈谈技术选型。选择 Rust 搭配 Bevy 来开发游戏,是一个兼顾性能、安全性和开发体验的现代方案。
Rust 带来的优势
- 内存安全:其独特的所有权系统在编译期保证了内存安全,无需垃圾回收器,从根本上避免了内存泄漏、数据竞争等常见问题。
- 高性能:能够提供与 C/C++ 相媲美的运行时效率,对于追求流畅帧率的游戏至关重要。
- 现代语法:模式匹配、强大的类型系统等特性让代码表达力更强,也更易于维护。
Bevy 引擎的魅力
Bevy 是一个完全使用 Rust 编写的开源游戏引擎,它的设计哲学是“简单且数据驱动”。
- ECS 架构:采用 Entity-Component-System(实体-组件-系统)设计模式。这种模式强制进行代码解耦,使得游戏逻辑清晰,易于扩展和优化。
- 热重载支持:支持资产(如纹理、场景)甚至部分代码的热重载,可以极大地提升开发迭代速度。
- 真正的跨平台:可轻松编译并运行在 Windows、macOS、Linux 以及 Web(通过 WebAssembly)等多个平台。
- 活跃的社区:项目迭代迅速,文档日益丰富,插件生态也在不断成长。
第一步:搭建开发环境与项目结构
创建新项目
首先,请确保你的系统已经安装了 Rust 工具链(可通过 rustup 安装)。然后,打开终端,创建一个新的 Rust 项目:
cargo new lonely-highway
cd lonely-highway
配置项目依赖
编辑项目根目录下的 Cargo.toml 文件,添加 Bevy 等依赖:
[package]
name = "lonely-highway"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.18"
rand = "0.10.0"
规划项目模块
为了保持代码清晰,我们采用模块化设计。建议的项目结构如下:
lonely-highway/
├── Cargo.toml
└── src/
├── main.rs # 应用入口,负责注册系统和插件
├── player.rs # 玩家车辆生成与控制逻辑
├── road.rs # 无限公路的生成与回收系统
├── camera.rs # 摄像机跟随逻辑
├── environment.rs # 光照、天空盒等环境设置
├── game.rs # 游戏核心逻辑(计分、碰撞检测等)
├── ui.rs # 用户界面(分数、速度显示)
├── components.rs # 定义所有ECS组件(Component)
└── resources.rs # 定义所有游戏资源(Resource)
第二步:定义核心数据结构(组件与资源)
在 ECS 架构中,我们需要先定义构成游戏世界的“零件”。
在 resources.rs 中定义游戏状态
这里定义游戏运行时需要共享的配置和数据。
use bevy::prelude::*;
use bevy::render::mesh::Mesh;
#[derive(Resource)]
pub struct RoadConfig {
pub segment_length: f32, // 每一段公路的长度
pub num_segments: usize, // 同时存在于场景中的最大段数
pub lane_width: f32, // 单条车道的宽度
}
#[derive(Resource)]
pub struct GameStats {
pub score: f32,
pub speed: f32,
pub level: u32,
pub time_elapsed: f32,
pub is_game_over: bool,
}
#[derive(Resource, Default)]
pub struct RoadState {
pub last_segment_pos: Vec3,
pub current_curve: f32,
}
#[derive(Resource)]
pub struct GameTextures {
pub road: Handle<Image>,
pub grass: Handle<Image>,
}
在 components.rs 中标记实体类型
组件用于标记和区分不同类型的实体。
use bevy::prelude::*;
#[derive(Component)]
pub struct PlayerCar;
#[derive(Component)]
pub struct RoadSegment;
#[derive(Component)]
pub struct Obstacle;
#[derive(Component)]
pub struct Collider {
pub radius: f32,
}
#[derive(Component)]
pub struct Puddle;
#[derive(Component)]
pub struct CarWheel;
第三步:实现游戏核心——无限生成的公路
游戏的核心体验在于那条永远开不到头的公路。我们采用 “分段生成,动态回收” 的策略来实现。
核心思路:
- 公路由多个长度固定的“段”首尾相连而成(例如每段50米)。
- 玩家车辆不断前进,当车辆前方即将没有路时,系统动态生成新的公路段。
- 车辆后方已经驶过的、远离视野的公路段会被系统自动回收(销毁实体)。
- 场景中始终只保持一定数量(如15段)的公路,从而模拟出无限延伸的效果。
初始公路生成(road.rs)
在游戏开始时,生成初始的几段公路。
use bevy::prelude::*;
use crate::components::{Collider, Puddle, Obstacle};
use crate::resources::{RoadConfig, GameTextures, RoadState};
use crate::player::PlayerCar;
#[derive(Component)]
pub struct RoadSegment;
pub fn spawn_initial_road(
mut commands: Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
road_config: Res<RoadConfig>,
game_textures: Res<GameTextures>,
mut road_state: ResMut<RoadState>,
) {
road_state.last_segment_pos = Vec3::ZERO;
road_state.current_curve = 0.0;
for _ in 0..road_config.num_segments {
spawn_next_segment(&mut commands, meshes, materials,
&road_config, &game_textures, &mut road_state);
}
}
fn spawn_next_segment(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
config: &RoadConfig,
textures: &GameTextures,
state: &mut RoadState,
) {
let segment_length = config.segment_length;
// 计算位置,支持弯道
let x_shift = state.current_curve * (segment_length / 2.0);
let start_pos = state.last_segment_pos;
let end_pos = start_pos + Vec3::new(x_shift, 0.0, -segment_length);
let mid_pos = (start_pos + end_pos) / 2.0;
// 生成公路段
spawn_road_segment_at(commands, meshes, materials,
config, textures, mid_pos);
state.last_segment_pos = end_pos;
}
生成单个公路段的细节
每一段公路都不是简单的一块板,它包含地面、路面、装饰物等子实体。
pub fn spawn_road_segment_at(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
config: &RoadConfig,
textures: &GameTextures,
position: Vec3,
) {
let segment_length = config.segment_length;
let ground_width = config.lane_width * 4.0; // 草地宽度
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(ground_width, segment_length))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(textures.grass.clone()),
..default()
})),
Transform::from_translation(position),
RoadSegment,
)).with_children(|parent| {
// 沥青路面
parent.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(config.lane_width * 3.0, segment_length))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(textures.road.clone()),
..default()
})),
Transform::from_xyz(0.0, 0.06, 0.0),
));
// 车道线
parent.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(0.2, segment_length))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::WHITE,
unlit: true,
..default()
})),
Transform::from_xyz(0.0, 0.07, 0.0),
));
// 随机生成障碍物(岩石、箱子)
if rand::random::<f32>() < 0.2 {
let lane = (rand::random::<i32>() % 3 - 1) as f32 * config.lane_width;
let z = (rand::random::<f32>() - 0.5) * segment_length * 0.8;
parent.spawn((
Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.4, 0.4, 0.4),
..default()
})),
Transform::from_xyz(lane, 1.0, z),
Obstacle,
Collider { radius: 1.0 },
));
}
});
}
动态更新系统:回收与生成
这个系统在每个游戏帧运行,负责维护公路的“无限”状态。
pub fn update_road(
mut commands: Commands,
road_config: Res<RoadConfig>,
car_query: Query<&Transform, With<PlayerCar>>,
road_query: Query<(Entity, &Transform), With<RoadSegment>>,
mut road_state: ResMut<RoadState>,
stats: Res<GameStats>,
) {
if let Some(car_transform) = car_query.iter().next() {
let car_z = car_transform.translation.z;
// 回收后方的公路段
for (entity, transform) in road_query.iter() {
if transform.translation.z > car_z + road_config.segment_length * 2.0 {
commands.entity(entity).despawn();
}
}
// 生成前方新的公路段
let view_distance = road_config.segment_length * (road_config.num_segments as f32);
if road_state.last_segment_pos.z > car_z - view_distance {
spawn_next_segment(&mut commands, &mut meshes, &mut materials,
&road_config, &game_textures, &mut road_state);
}
}
// Level 2 后开始出现弯道,增加难度
if stats.level >= 2 {
road_state.current_curve = (stats.time_elapsed * 0.5).sin() * 0.5;
}
}
第四步:创建玩家车辆与控制系统
生成车辆模型(player.rs)
我们用简单的几何体(立方体、圆柱体)拼装出一辆红色跑车。
use bevy::prelude::*;
use crate::components::Collider;
use crate::resources::GameStats;
use crate::road::RoadSegment;
#[derive(Component)]
pub struct PlayerCar;
pub fn spawn_player(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
commands.spawn((
Transform::from_xyz(0.0, 0.0, 0.0),
Visibility::default(),
PlayerCar,
Collider { radius: 1.5 },
)).with_children(|parent| {
// 车身
parent.spawn((
Mesh3d(meshes.add(Cuboid::new(2.4, 0.5, 4.8))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.9, 0.2, 0.2), // 红色
metallic: 0.8,
perceptual_roughness: 0.2,
..default()
})),
Transform::from_xyz(0.0, 0.6, 0.0),
));
// 车顶
parent.spawn((
Mesh3d(meshes.add(Cuboid::new(1.6, 0.5, 2.5))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.05, 0.05, 0.05),
metallic: 0.9,
..default()
})),
Transform::from_xyz(0.0, 1.1, -0.3),
));
// 轮子
let wheel_mesh = meshes.add(Cylinder::new(0.45, 0.5));
let wheel_mat = materials.add(StandardMaterial {
base_color: Color::BLACK,
..default()
});
let wheel_positions = [
Vec3::new(-1.3, 0.45, 1.6), // 左前
Vec3::new(1.3, 0.45, 1.6), // 右前
Vec3::new(-1.3, 0.45, -1.6), // 左后
Vec3::new(1.3, 0.45, -1.6), // 右后
];
for pos in wheel_positions {
parent.spawn((
Mesh3d(wheel_mesh.clone()),
MeshMaterial3d(wheel_mat.clone()),
Transform::from_translation(pos)
.with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
));
}
});
}
车辆移动与简单物理系统
这个系统处理玩家的键盘输入,并模拟车辆移动、速度提升以及掉落悬崖的物理效果。
pub fn move_car(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut Transform, With<PlayerCar>>,
time: Res<Time>,
mut stats: ResMut<GameStats>,
road_query: Query<&Transform, (With<RoadSegment>, Without<PlayerCar>)>,
) {
if stats.is_game_over {
return;
}
if let Some(mut transform) = query.iter_mut().next() {
let speed = stats.speed;
let turn_speed = 15.0;
// 自动前进
transform.translation.z -= speed * time.delta_secs();
// 左右转向
if keyboard_input.pressed(KeyCode::ArrowLeft) || keyboard_input.pressed(KeyCode::KeyA) {
transform.translation.x -= turn_speed * time.delta_secs();
}
if keyboard_input.pressed(KeyCode::ArrowRight) || keyboard_input.pressed(KeyCode::KeyD) {
transform.translation.x += turn_speed * time.delta_secs();
}
// 速度随时间递增
if speed < 100.0 {
stats.speed += 0.5 * time.delta_secs();
}
// 检查车辆是否在公路范围内
let car_pos = transform.translation;
let mut on_ground = false;
for road_transform in road_query.iter() {
let z_dist = (road_transform.translation.z - car_pos.z).abs();
if z_dist < 25.0 {
let local_pos = road_transform.rotation.inverse()
* (car_pos - road_transform.translation);
if local_pos.x.abs() < 8.0 && local_pos.z.abs() < 25.0 {
on_ground = true;
break;
}
}
}
// 如果不在公路上,模拟掉落
if !on_ground {
transform.translation.y -= 9.8 * time.delta_secs();
transform.rotation *= Quat::from_rotation_x(time.delta_secs());
}
// 游戏结束判定:掉落太深
if transform.translation.y < -5.0 {
stats.is_game_over = true;
}
}
}
第五步:设置摄像机与用户界面
第三人称跟随摄像机(camera.rs)
让摄像机平滑地跟随在车辆后方,提供最佳驾驶视角。
use bevy::prelude::*;
use crate::player::PlayerCar;
pub fn setup_camera(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 10.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
pub fn update_camera(
car_query: Query<&Transform, With<PlayerCar>>,
mut camera_query: Query<&mut Transform, (With<Camera3d>, Without<PlayerCar>)>,
) {
if let Some(car_transform) = car_query.iter().next() {
if let Some(mut camera_transform) = camera_query.iter_mut().next() {
// 目标位置:车辆后上方
let target_pos = Vec3::new(
car_transform.translation.x * 0.5,
10.0,
car_transform.translation.z + 20.0,
);
// 平滑跟随
camera_transform.translation = camera_transform.translation.lerp(target_pos, 0.05);
camera_transform.look_at(car_transform.translation, Vec3::Y);
}
}
}
游戏UI显示(ui.rs)
在屏幕左上角实时显示分数、速度和等级。
use bevy::prelude::*;
use crate::resources::GameStats;
#[derive(Component)]
pub struct ScoreText;
pub fn setup_ui(mut commands: Commands) {
commands.spawn((
Text::new("Score: 0"),
Node {
position_type: PositionType::Absolute,
top: Val::Px(20.0),
left: Val::Px(20.0),
..default()
},
TextFont {
font_size: 32.0,
..default()
},
TextColor(Color::WHITE),
ScoreText,
));
}
pub fn update_ui(
stats: Res<GameStats>,
mut query: Query<&mut Text, With<ScoreText>>,
) {
if let Some(mut text) = query.iter_mut().next() {
text.0 = format!(
"Score: {:.0}\nSpeed: {:.0} km/h\nLevel: {}",
stats.score, stats.speed, stats.level
);
}
}
第六步:整合所有模块,启动游戏
最后,在 main.rs 中将所有系统组装起来,并配置游戏初始状态。
use bevy::prelude::*;
mod components;
mod resources;
mod player;
mod camera;
mod environment;
mod road;
mod game;
mod ui;
use resources::{RoadConfig, GameStats, RoadState};
use camera::{setup_camera, update_camera};
use player::{spawn_player, move_car};
use road::{spawn_initial_road, update_road};
use game::{update_score, check_collisions, check_puddles, update_particles};
use ui::{setup_ui, update_ui};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(ClearColor(Color::srgb(0.5, 0.7, 0.9))) // 天空蓝背景
.insert_resource(RoadConfig {
segment_length: 50.0,
num_segments: 15,
lane_width: 4.0,
})
.insert_resource(GameStats {
score: 0.0,
speed: 30.0,
level: 1,
time_elapsed: 0.0,
is_game_over: false,
})
.init_resource::<RoadState>()
.add_systems(Startup, (setup, setup_ui, spawn_initial_entities.after(setup)))
.add_systems(Update, (
move_car,
update_camera,
update_road,
check_collisions,
update_score,
check_puddles,
update_particles,
update_ui
))
.run();
}
fn setup(mut commands: Commands) {
setup_camera(commands.reborrow());
// 此处可添加光照、天空盒、纹理资源加载等代码...
}
fn spawn_initial_entities(
mut commands: Commands,
road_config: Res<RoadConfig>,
road_state: ResMut<RoadState>,
) {
spawn_initial_road(/* ... */);
spawn_player(/* ... */);
}
运行你的游戏!
一切就绪后,在项目根目录下运行命令,启动游戏:
cargo run
操作指南:
A 键或 ← 方向键:控制车辆向左转向。
D 键或 → 方向键:控制车辆向右转向。
- 车辆会自动向前行驶,并且速度会随着时间逐渐加快。

(上图为游戏运行效果,车辆在无限公路上行驶,UI显示了分数、速度和等级)
游戏进阶技巧与扩展方向
当你成功运行基础版本后,可以尝试以下挑战和扩展:
游戏技巧:
- 保持居中行驶:尽量让车辆位于公路中央,避免因过于靠近边缘而掉落。
- 提前预判障碍:注意观察前方随机生成的岩石或箱子,及时变道躲避。
- 适应加速节奏:游戏后期速度很快,需要更集中的注意力和更快的反应。
- 迎接弯道挑战:坚持60秒进入第2等级后,公路会开始出现弯道,游戏难度显著提升!
项目扩展方向(欢迎在云栈社区分享你的创意):
- 丰富内容:添加多种车辆模型选择、不同类型的障碍物、动态天气系统(昼夜、雨雪)。
- 增强体验:引入背景音乐与音效(引擎声、碰撞声)、粒子特效系统(漂移尘土、尾气)。
- 完善系统:实现更精确的物理碰撞、添加本地排行榜功能、设计完整的游戏菜单和状态管理。
- 跨平台发布:将游戏编译为 WebAssembly,使其能在浏览器中运行。
技术总结与回顾
通过这个项目,我们实践了现代游戏开发中的几个关键概念:
| 特性 |
实现方式 |
| 无限游戏世界 |
分段动态生成 + 超出视距回收实体 |
| 基础物理与交互 |
手动检测地面碰撞 + 简单重力模拟 |
| 渐进式难度 |
速度随时间线性增长 + 后期引入弯道 |
| 模块化与可维护性 |
严格遵守 ECS 架构,系统职责单一清晰 |
使用 Rust 和 Bevy 进行游戏开发是一次富有成效的体验。Bevy 的 ECS 框架迫使你以数据驱动的方式思考,最终产出的代码结构通常非常清晰,易于调试和扩展。这不仅是完成了一个小游戏,更是一次对系统编程、实时应用架构的深入实践。
希望这篇详尽的指南能成为你进入 Rust 游戏开发世界的一块坚实跳板。动手尝试,修改参数,添加你自己的想法,最重要的是享受编码和游戏创造的乐趣!
Happy Coding & Happy Gaming! 🎮