公司动态
30 分钟搞定 Bevy 相机系统:第一人称、轨道相机与自由漫游完整实战
30 分钟搞定 Bevy 相机系统第一人称、轨道相机与自由漫游完整实战【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy你想做一个 3D 模型查看器鼠标一拖相机就绕着模型转或者写第一人称游戏想让玩家看到自己的手臂却发现文档里全是组件名不知道从哪下手。Bevy 相机系统就是解决这类问题的入口——Bevy 是一个用 Rust 编写的简单数据驱动游戏引擎Data-Driven Game Engine它的相机系统就是普通实体 几个组件没有任何特殊魔法。读完你能做到什么你能从零写出一个轨道相机鼠标拖拽绕目标平滑旋转俯仰角被限制住、不会翻过去。你能搭出第一人称双相机结构手臂固定 FOV、世界 FOV 可调两者互不干扰。你能用几行代码接入FreeCamera用 WASD 鼠标在场景里自由漫游。你能对着选型表10 秒内为模型查看器 / 调试漫游 / 2D 跟随需求挑对控制器。你能自己诊断转得太慢仰到顶就反了手臂看不见这类常见坑。原理它是怎么工作的在 Bevy 里相机就是一个普通实体给它加上Camera3d渲染器就会从这个实体的视角画场景Transform决定它在哪、朝哪看。所谓相机控制器——不管是自己写的系统还是FreeCamera这类插件——每帧只做一件事读键盘鼠标输入把新的旋转和位置写进相机的Transform。渲染器在帧末读取最新的Transform和Projection据此绘制。之所以这样设计是因为输入 → 组件 → 渲染完全解耦你可以随时换控制器而不动渲染也可以在同一实体上挂多个相机做双相机视口渲染器不关心相机是被玩家还是 AI 移动的。5 分钟跑通最小示例这段代码只干两件事spawn 一个看向原点的相机再放一个立方体在画面中央。use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, mut meshes: ResMutAssetsMesh, mut materials: ResMutAssetsStandardMaterial) { commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), )); commands.spawn(( Mesh3d(meshes.add(Cuboid::default())), MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), )); }运行后你会看到一个窗口中间一个立方体相机在 (0,5,5) 朝原点俯视。现在就想体验效果的话不必新建项目克隆本仓库后直接跑cargo run --example camera_orbit它就是这个骨架的完整版鼠标拖拽时相机会绕着立方体转——正是下一节的第一个场景。实战从简单到复杂逐层拆解场景一轨道相机实现——鼠标拖拽绕目标点旋转目标相机绕固定目标原点旋转鼠标拖多久转多少俯仰不超 ±89°半径恒定。关键代码完整版在 camera_orbit.rs这里只保留与轨道逻辑强相关的部分。#[derive(Resource)] struct CameraSettings { orbit_distance: f32, // 轨道半径 pitch_speed: f32, // 俯仰速度系数 yaw_speed: f32, // 偏航速度系数 pitch_range: Rangef32, // 俯仰角限制范围 } fn orbit( mut camera: Singlemut Transform, WithCamera, settings: ResCameraSettings, mouse_motion: ResAccumulatedMouseMotion, ) { let delta mouse_motion.delta; // 上一帧以来的鼠标总位移 let (yaw, pitch, _) camera.rotation.to_euler(EulerRot::YXZ); let pitch (pitch delta.y * settings.pitch_speed) .clamp(settings.pitch_range.start, settings.pitch_range.end); let yaw delta.x * settings.yaw_speed; camera.rotation Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0); // 位置 目标 - 前向 * 半径保证相机始终朝向目标 camera.translation Vec3::ZERO - camera.forward() * settings.orbit_distance; // ... 省略按住鼠标左/右键做 roll 的输入处理 }被环绕的对象就是一个普通 3D 网格模型鼠标拖拽时相机绕它公转。为什么这样写俯仰角必须 clamppitch 一旦到达 ±90°相机笔直朝上或朝下欧拉角出现奇异——再把鼠标拉回来时前进方向在数学上已经无定义相机朝向会随机翻转。clamp 到 ±(FRAC_PI_2 − 0.01)约 ±89.4°就能避免。鼠标位移不能乘 delta_timeAccumulatedMouseMotion给的已经是上一帧以来的完整位移再乘 dt 会让转动变慢、且随帧率波动只有按住按钮这类持续状态输入才需要乘time.delta_secs()。参数速查参数作用推荐值范围orbit_distance轨道半径20.0按场景尺度定pitch_speed俯仰速度系数0.0030.001~0.01yaw_speed偏航速度系数0.0040.001~0.01pitch_range俯仰角限制±89.4°不超过 ±90°转起来之后你会发现第一人称视角更难玩家要看到自己的手臂而手臂和场景需要不同的视场角。场景二第一人称手臂渲染——双相机 图层分离目标玩家实体挂两个相机一个画世界FOV 可调一个画手臂FOV 固定互不干扰。关键代码来自 first_person_view_model.rs。const VIEW_MODEL_RENDER_LAYER: usize 1; // 手臂相机和手臂共用图层 1 fn spawn_view_model(mut commands: Commands, /* meshes / materials 省略 */) { commands.spawn(( Player, Transform::from_xyz(0.0, 1.0, 0.0), children![ ( // 世界相机不挂 RenderLayers 图层 0FOV 由玩家调节 Camera3d::default(), Projection::from(PerspectiveProjection { fov: 90_f32.to_radians(), ..default() }), ), ( // 手臂相机固定 70° FOV只画图层 1 Camera3d::default(), Camera { order: 1, ..default() }, Projection::from(PerspectiveProjection { fov: 70_f32.to_radians(), ..default() }), RenderLayers::layer(VIEW_MODEL_RENDER_LAYER), ), ( // 手臂模型同在图层 1世界相机看不见它 Mesh3d(arm), MeshMaterial3d(arm_material), Transform::from_xyz(0.2, -0.1, -0.25), RenderLayers::layer(VIEW_MODEL_RENDER_LAYER), ), ], )); }为什么这样写为什么要两个相机手臂模型是按固定 FOV70°设计制作的若与场景共用一个相机玩家调节世界 FOV 时手臂会跟着变形。拆开两个相机各自持有独立的Projection。灯光为什么要特殊处理场景里的PointLight默认只作用图层 0图层分离后手臂收不到光会全黑。用RenderLayers::from_layers([0, 1])把灯光同时挂到两个图层。参数速查参数作用推荐值范围世界相机 fov世界视场角90°弧度20°~160°手臂相机 fov手臂视场角70°弧度跟随手臂模型设计Camera.order绘制先后手臂相机设 1默认 0RenderLayers图层隔离手臂与其相机同层任意编号前两个场景都是手写输入要自己读、角度要自己算。真实项目里调试和关卡编辑更常用即拿即用的控制器Bevy 内置了现成方案。场景三即拿即用控制器——FreeCamera 自由漫游目标不写一行移动代码得到一个可六向移动、鼠标控向的自由漫游相机用于场景探索和调试。关键代码来自 free_camera_controller.rs。use bevy::camera_controller::free_camera::{FreeCamera, FreeCameraPlugin}; use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(FreeCameraPlugin) // 启用自由相机功能 .add_systems(Startup, spawn_camera) .run(); } fn spawn_camera(mut commands: Commands) { commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 1.0, 0.0).looking_to(Vec3::X, Vec3::Y), FreeCamera { sensitivity: 0.2, // 鼠标灵敏度 walk_speed: 3.0, // 步行速度 run_speed: 9.0, // 奔跑速度按住左 Shift friction: 25.0, // 松手后减速快慢 ..default() }, )); }为什么这样写鼠标捕获不用自己写按住右键或按 M 会捕获光标鼠标不会移出窗口导致位移流中断。只需挂FreeCamera组件动态状态FreeCameraState由 Bevy 自动添加必需组件速度、速度倍率这些运行时状态你完全不用管。默认键位WASD 水平移动、Q/E 上下、滚轮改变移动速度、小数字键把视角对齐到坐标轴方向完整列表见示例文件头部注释。参数速查参数作用推荐值范围sensitivity鼠标灵敏度0.2≥0.005walk_speed步行速度3.0按场景尺度定run_speed奔跑速度9.0通常为步行 3 倍friction松手减速25.0越大停得越快scroll_factor滚轮调速幅度0.020.02~0.1⚠️ 常见坑与 Debug 思路现象相机转得明显慢帧率越低越慢 →原因把鼠标位移乘了 delta_time而AccumulatedMouseMotion.delta已是上一帧以来的累计量 →解法鼠标位移直接用去掉* time.delta_secs()只有按住按钮这类持续输入才乘 dt。现象俯仰到顶部后鼠标拉回来相机朝向反了 →原因pitch 到达 ±90° 时欧拉角奇异前进方向无定义 →解法pitch clamp 到 ±(FRAC_PI_2 − 0.01)见场景一。现象第一人称里手臂看不见或手臂视角里场景没了 →原因RenderLayers没对齐 →解法手臂相机与手臂挂同一图层场景对象默认图层 0若手臂全黑检查灯光是否用RenderLayers::from_layers([0, 1])同时挂了两层。现象手臂和场景叠画、谁在上不稳定 →原因两个相机的Camera.order都是默认 0 →解法给手臂相机设order: 1保证它画在世界之上。现象鼠标移出窗口的瞬间旋转就停 →原因光标没被捕获光标出窗后拿不到AccumulatedMouseMotion→解法FreeCamera 自动处理手写控制器时需在系统里调用窗口的光标捕获接口。 选型速查表场景类型推荐方案核心依赖一句话理由从零学习 / 完全自控手写轨道系统鼠标累计输入 欧拉角约 20 行完全可控3D 模型查看器 / CADPanOrbitCamera 插件PanOrbitCamera 组件内置平滑、惯性、缩放限制第一人称 手臂视口双相机 图层分离Camera3d ×2 RenderLayers世界/手臂 FOV 各自独立场景调试 / 自由漫游FreeCameraFreeCameraPlugin六向移动 鼠标捕获即开即用2D 俯视跟随Camera2d 平滑插值smooth_nudge跟随玩家运动稳定 下一步第一人称手臂渲染示例双相机 图层分离的完整实现含 FOV 调节系统。自由漫游相机示例带运行时参数调节面板可以边按键盘边看每个参数的实际手感。最小化平移轨道控制器示例PanOrbitCamera 的最简接入左键平移、右键环绕、滚轮缩放。git clone https://gitcode.com/GitHub_Trending/be/bevy cd bevy cargo run --example camera_orbit跑通最小示例后把参数表里的数值改成你自己的手感比再读十篇文档都有效。【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考