用 Rust 和 Bevy 打造一款 3D 孤独的公路游戏

  • King
  • 发布于 14小时前
  • 阅读 35

当你独自驾车行驶在无尽的公路上,两旁是悬崖和树林,脚下是油门...等等,别踩太猛,会掉下去的!前言作为一名程序员,你是否想过自己动手做一款游戏?今天我们来一起用Rust和Bevy游戏引擎开发一款名为"LonelyHighway(孤独的公路)"的3D无限驾驶游戏。这篇文

当你独自驾车行驶在无尽的公路上,两旁是悬崖和树林,脚下是油门...等等,别踩太猛,会掉下去的!


前言

作为一名程序员,你是否想过自己动手做一款游戏?

今天我们来一起用 RustBevy 游戏引擎开发一款名为 "Lonely Highway(孤独的公路)" 的 3D 无限驾驶游戏。

这篇文章将带你从零开始,一步步实现完整的游戏功能。


游戏简介

Lonely Highway 是一款 3D 无限公路驾驶游戏:

  • 🚗 驾驶一辆红色跑车,在无尽的公路上飞驰
  • 🛣️ 躲避障碍物,别掉下悬崖
  • ⚡ 速度会随时间逐渐增加,最高可达 100 km/h
  • 🌊 经过水坑时会溅起水花粒子效果
  • 🌲 欣赏路边的风景,但别撞到树

为什么选择 Rust + Bevy?

Rust 的优势

  • 内存安全:无需垃圾回收器,编译时保证内存安全
  • 高性能:与 C/C++ 媲美的运行效率
  • 现代语法:模式匹配、所有权系统等特性让代码更优雅

Bevy 的魅力

Bevy 是一个用 Rust 编写的开源游戏引擎,具有以下特点:

  • ECS 架构:Entity-Component-System 设计模式,代码解耦清晰
  • 热重载:支持资产热重载,开发效率高
  • 跨平台:支持 Windows、macOS、Linux、Web 等平台
  • 社区活跃:版本迭代快,文档丰富

开发准备

创建项目

首先确保你已安装 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;

第二步:实现无限公路生成

游戏的核心是无限生成的公路。我们采用"分段生成 + 动态回收"的策略:

核心思路

  • 公路由多个"段"组成,每段长度 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 / :向右转向
  • 车辆自动前进,速度会逐渐加快

游戏技巧

  1. 保持居中:避免靠近公路边缘,防止掉落
  2. 观察障碍:提前变道躲避岩石和箱子
  3. 速度控制:游戏后期速度加快,更需集中注意力
  4. Level 2 挑战:60 秒后公路开始弯曲,难度升级!

技术亮点总结

特性 实现方式
无限地图 分段生成 + 动态回收机制
物理模拟 简单重力 + 地面检测
渐进难度 速度递增 + 弯道系统
视觉反馈 水花粒子 + 光照效果
模块化设计 ECS 架构,职责清晰

可扩展方向

完成基础版本后,你可以继续探索:

  • [ ] 添加多车型选择
  • [ ] 实现音效系统
  • [ ] 加入排行榜
  • [ ] 支持 Web 端运行(WASM)
  • [ ] 添加夜间模式与动态天气
  • [ ] 实现更复杂的碰撞物理

写在最后

用 Rust 开发游戏是一次很棒的体验。Bevy 的 ECS 架构让代码组织非常清晰,每个系统各司其职,便于扩展和维护。

希望这篇文章能帮助你入门 Rust 游戏开发,动手打造属于自己的游戏作品!

Happy Coding & Happy Gaming! 🎮

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发