Bevy中2D网格纹理分配方法及六边形Tile创建的Vertex_Normal缺失错误排查
搞定Bevy六边形Tile:解决错误、纹理适配与交互实现指南
我来帮你搞定Bevy里这个六边形Tile的问题,你遇到的错误其实很好解释,咱们一步步拆解解决:
一、解决「Vertex_Normal缺失」的核心错误
你自定义的六边形网格只添加了顶点位置,但Bevy的ColorMaterial对应的2D渲染管线要求网格必须包含法线属性——默认的Quad网格创建时自动帮你加了这些必要的属性,而你手动拼的网格没加,所以才会抛出那个错误。
对于2D平面来说,所有顶点的法线都可以统一设为[0.0, 0.0, 1.0](指向相机方向),直接补上就行。另外,要是想让纹理正常显示,还得加UV坐标属性,不然纹理会乱拉伸,这也是默认Quad自带的,咱们得手动补上。
二、修改后的完整可运行代码
这里我帮你补上了法线和UV,还调整了顶点索引让渲染更合理:
const TILE_SPRITE: &str = "tile.png"; fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<ColorMaterial>>, asset_server: Res<AssetServer>, ) { // 六边形顶点坐标(中心在原点) let vertices = vec![ [-0.8660, 0.5000], // 左上 [0.8660, 0.5000], // 右上 [1.0000, 0.0000], // 右中 [0.8660, -0.5000], // 右下 [-0.8660, -0.5000], // 左下 [-1.0000, 0.0000], // 左中 ]; // 三角形索引(确保面朝向相机) let indices = Indices::U16(vec![ 0, 1, 2, 0, 2, 5, 2, 3, 4, 2, 4, 5, ]); // 2D平面统一法线:指向Z轴正方向(相机方向) let normals = vec![[0.0, 0.0, 1.0]; vertices.len()]; // UV坐标:适配六边形顶点的纹理映射 let uvs = vec![ [0.0, 1.0], // 左上 [1.0, 1.0], // 右上 [1.0, 0.5], // 右中 [1.0, 0.0], // 右下 [0.0, 0.0], // 左下 [0.0, 0.5], // 左中 ]; let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices); mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); // 补上缺失的法线 mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); // 补上纹理UV mesh.set_indices(Some(indices)); // 2D相机 commands.spawn_bundle(OrthographicCameraBundle::new_2d()); // 生成可显示纹理的六边形Tile commands.spawn_bundle(MaterialMesh2dBundle { mesh: meshes.add(mesh).into(), transform: Transform::default().with_scale(Vec3::splat(128.)), material: materials.add(asset_server.load(TILE_SPRITE).into()), ..default() }); } fn main() { App::new() .add_plugins(DefaultPlugins) .add_startup_system(setup) .run(); }
三、实现Tile点击操作
要让Tile支持点击,最省心的方式是用bevy_rapier2d物理引擎添加碰撞体,步骤如下:
- 在
Cargo.toml添加依赖:bevy_rapier2d = "0.21"(版本要和你的Bevy版本匹配) - 给App添加物理插件:
.add_plugin(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0)) - 给Tile添加碰撞体(在生成
MaterialMesh2dBundle的代码后追加):
use bevy_rapier2d::prelude::*; // 给Tile添加六边形碰撞体 .collider(Collider::convex_polyline( vertices.into_iter().map(|v| Vec2::new(v[0], v[1])).collect() )?)
- 编写点击检测系统:
fn tile_click_system( mut mouse_events: EventReader<MouseButtonInput>, rapier_context: Res<RapierContext>, camera: Query<(&Camera, &GlobalTransform)>, windows: Res<Windows>, ) { for event in mouse_events.iter() { if event.button == MouseButton::Left && event.state == ElementState::Pressed { let (camera, camera_transform) = camera.single(); let window = windows.get_primary().unwrap(); // 把鼠标屏幕坐标转成世界坐标 if let Some(world_pos) = window.cursor_position() .and_then(|pos| camera.viewport_to_world(camera_transform, pos)) .map(|ray| ray.origin.truncate()) { // 检测鼠标是否点击到Tile rapier_context.intersections_with_point(world_pos, |entity| { println!("Clicked Tile: {:?}", entity); // 这里可以加你的点击逻辑,比如高亮Tile、打开菜单等 true }); } } } }
最后把这个系统加到App里:.add_system(tile_click_system)
四、实现物品拖放交互
拖放功能需要跟踪拖拽状态和目标Tile,核心思路:
- 给可拖拽物品加
Draggable标记组件,给Tile加Tile标记组件 - 编写三个事件处理系统:
- 拖拽开始:监听
PointerDragStart,记录当前拖拽的物品实体,隐藏原位置的物品 - 拖拽过程:监听
PointerDrag,更新物品的位置跟随鼠标移动 - 拖拽结束:监听
PointerDragEnd,检测鼠标位置是否在Tile上,执行物品进出逻辑(比如把物品移到Tile,或者从Tile取出)
- 拖拽开始:监听
你可以复用前面的点击检测逻辑,在拖拽结束时判断目标Tile,再处理对应的业务逻辑。
内容的提问来源于stack exchange,提问作者Lyndon Alcock
相关产品推荐
相关产品推荐

