当你独自驾车行驶在无尽的公路上,两旁是悬崖和树林,脚下是油门...等等,别踩太猛,会掉下去的!前言作为一名程序员,你是否想过自己动手做一款游戏?今天我们来一起用Rust和Bevy游戏引擎开发一款名为"LonelyHighway(孤独的公路)"的3D无限驾驶游戏。这篇文
当你独自驾车行驶在无尽的公路上,两旁是悬崖和树林,脚下是油门...等等,别踩太猛,会掉下去的!
作为一名程序员,你是否想过自己动手做一款游戏?
今天我们来一起用 Rust 和 Bevy 游戏引擎开发一款名为 "Lonely Highway(孤独的公路)" 的 3D 无限驾驶游戏。
这篇文章将带你从零开始,一步步实现完整的游戏功能。
Lonely Highway 是一款 3D 无限公路驾驶游戏:
Bevy 是一个用 Rust 编写的开源游戏引擎,具有以下特点:
首先确保你已安装 Rust,然后创建新项目:
cargo new lonely-highway
cd lonely-highway
编辑 Cargo.toml:
[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 # 组件定义
└── resources.rs # 资源定义
在 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;
游戏的核心是无限生成的公路。我们采用"分段生成 + 动态回收"的策略:
核心思路:
在 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.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 / →:向右转向| 特性 | 实现方式 |
|---|---|
| 无限地图 | 分段生成 + 动态回收机制 |
| 物理模拟 | 简单重力 + 地面检测 |
| 渐进难度 | 速度递增 + 弯道系统 |
| 视觉反馈 | 水花粒子 + 光照效果 |
| 模块化设计 | ECS 架构,职责清晰 |
完成基础版本后,你可以继续探索:
用 Rust 开发游戏是一次很棒的体验。Bevy 的 ECS 架构让代码组织非常清晰,每个系统各司其职,便于扩展和维护。
希望这篇文章能帮助你入门 Rust 游戏开发,动手打造属于自己的游戏作品!
Happy Coding & Happy Gaming! 🎮
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!