#gltf #bevy-plugin #bevy #3d #gamedev

bevy_fabulous

一个用于丰富由GLTF加载的场景的游戏组件/资产的Bevy插件

2个版本

0.1.1 2024年7月14日
0.1.0 2024年7月14日

#386游戏开发

Download history 149/week @ 2024-07-09 39/week @ 2024-07-16 2/week @ 2024-07-23 12/week @ 2024-07-30

每月下载量:202

MIT/Apache

97KB
662

Bevy Fabulous

BevyPrefabExample

概述

Bevy fabulous旨在提供一个框架,用于封装和耦合加载的GLTF及其游戏组件,而无需使用重型工具或具有偏见的插件。

Bevy Fabulous提供了丰富GLTF加载场景的机制:**预制体**和**后置预制体**,以及自动覆盖GLTF中发现的材质的实用工具。

以下是一个使用bevy_fabulous可以与加载的GLTF资产一起完成的示例

fn init_fireminion(
    mut fabs: ResMut<FabManager>,
    mut fabmats: ResMut<FabMaterialOverrides<StandardMaterial, StandardMaterial>>,
    assets: ResMut<MinionAssets>,
    relic_oneshots: Res<RelicOneshots>,
){
    fabmats.register_main_mat("FireMana", assets.fire_mana.clone());
    
    fabs.register_postfab(PostFab {
        scene: FabTarget::Gltf(assets.fire_minion.clone()),
        pipes: vec![
            PostfabPipe::entity(AttachBundle::new(Bitsploder(MagicElement::Fire, 100))).root_only(),
            PostfabPipe::entity(AttachBundle::new((ShopCleanup, ExplosionDebris))).with_components(vec![TypeId::of::<Handle<Mesh>>()]),
            PostfabPipe::system(relic_oneshots.on_minion_spawn_system)
        ]
    })
}

计划的功能

  • PrefabPipes提供相同的组件/名称/根过滤,理想情况下统一预制体+后置预制体管道功能集
  • 允许PostfabPipes对任何实体及其子实体进行操作,而不仅仅是GLTF/场景(这可能已经通过仅为实体添加后置预制体来实现)
  • 在crate中包含电池,提供一些预制的管道以进行常见操作

预制体

预制体直接修改加载的场景世界,将游戏组件应用到场景世界中的实体。这些组件只需要运行一次,因为它们将成为场景的一部分,所以任何后续的SceneBundle都会具有这些组件。

预制体适用于添加无上下文的游戏组件,即您希望添加到模型的节点上的组件,无论何时何地创建它

预制体包含一个指向资产的路径,该路径用作FabManager资源中HashMap<String, Prefab>的键。

预制体是PrefabPipe trait对象Vec的包装器,并按照插入的顺序运行

/// Applies ScenePipes to the loaded scene `World`
pub struct Prefab {
    /// The path to the asset on the filesystem
    pub target: FabTarget,

    /// Pipes to run on load
    pub pipeline: Vec<Box<dyn PrefabPipe + Send + Sync>>,
}

PrefabPipe最终看起来非常像命令

///Used to transform a scene, but avoid Transform as a term - it's already overloaded
pub trait PrefabPipe {
    // Applies the pipe to the entity
    fn apply(&mut self, world: &mut World);
}

//Define a prefab pipe as a struct
pub struct HeadPipe {
    pub rotation_rate: f32,
}

impl PrefabPipe for HeadPipe {
    fn apply(&mut self, world: &mut World) {
        info!("Running Head Rotate Pipe");

        //Iterate over the entities in the world and find the homie with the a head on him
        let mut q = world.query::<(Entity, &Name)>();
        let q = q.iter(world);

        let mut ent: Option<Entity> = None;
        for (entity, name) in q {
            if *name.to_string() == "MinionHead".to_string() {
                ent = Some(entity);
                break;
            }
        }

        if let Some(entity) = ent {
            info!("Found Thing and attaching to stuff");
            world.entity_mut(entity).insert((
                Rotate {
                    rotation_rate: self.rotation_rate,
                },
                Bob {
                    amplitude: 0.4,
                    frequency: 0.8,
                    anchor: 2.0,
                },
            ));
        }

        world.flush();
    }
}

PrefabPipe还有一个针对BoxedSystem的泛型实现,允许您将任何系统用作PrefabPipe

//In Some Startup/Loaded System
//---
//Register to the `FabManager`
fabs.register_prefab(
    Prefab::new(FabTarget::Gltf(gltf_handle.clone()))
        .with_system(inner_gear_rotate)
);
//---

//Define a prefab pipe as a system
fn inner_gear_rotate(entities: Query<(Entity, &Name)>, mut cmds: Commands) {
    info!("Inner Gear Rotate");

    let mut gear_ent = None;
    let mut orbiters = vec![];

    for (ent, name) in entities.iter() {
        let name_str = name.as_str();
        if name_str == "Gear" {
            gear_ent = Some(ent);
        } else if name_str.contains("Orbiter") {
            orbiters.push(ent);
        }
    }

    let Some(entity) = gear_ent else {
        return;
    };

    cmds.entity(entity).insert(Rotate {
        rotation_rate: -0.5,
    });

    for orbiter in orbiters {
        cmds.entity(orbiter).insert(Rotate { rotation_rate: 0.5 });
    }
}

后置预制体

每次创建特定场景时都会运行后置预制体。它们在实体被创建后运行,并且不会修改原始场景。

  • 它们可以以直观的方式根据上下文信息转换创建的场景。
  • 它们可以用于用主Bevy世界中的“水合”资产替换场景中的材质/资产,例如StandardMaterial
  • 如果可能,请使用预制体,因为一次性成本比每次场景创建时运行逻辑/查询更可取

Postfabs 可以与 FabTarget 注册,它允许用户直接使用 Handle<Scene>,或者一个 Handle<Gltf>,这将使 Postfab 注册到 gltf 中的第一个场景。

#[derive(Clone, Component)]
pub struct PostFab {
    pub scene: FabTarget,
    pub pipes: Vec<PostfabPipe>,
}

Prefabs 类似,Postfabs 由一个 FabTarget 和一系列按顺序应用的管道组成。PostfabPipe 具有更多的先进过滤选项,以指定管道是否应在给定的实体上运行。

#[derive(Clone)]
pub struct PostfabPipe {
    pub system: SystemId<Entity, ()>,
    pub with_components: Vec<TypeId>,
    pub without_components: Vec<TypeId>,
    pub with_name: Option<NameCriteria>,
}

/// Name component criteria for determining whether a pipe should run on a given entity
#[derive(Clone)]
pub enum NameCriteria {
    Equals(String),
    Contains(String),
    StartsWith(String),
    EndsWith(String),
}

材质覆盖

材质覆盖用于自动将实体上的材质句柄替换为另一个材质。这对于替换场景/gltf 中作为标准材质加载的部分非常有用。名称从 GLTF 资产的 NamedMaterials 映射中获取。在撰写本文时,用户需要直接加载 gltf 资产,以便在 FabulousMaterialsPlugin 资产监视器运行时保持活跃和可用。这意味着使用 "myAsset.gltf#Scene0" 或 GltfAssetLabel::Scene(0) 加载的场景可能不会按预期工作。


//Create and register new material to be swapped out
let earth_mana = StandardMaterial {
    emissive: (palettes::css::LIMEGREEN * 2.0).into(),
    ..default()
};

mat_index.register_main_mat("EarthMana", mats.add(earth_mana));

生成 Gltf 场景

由于命名材质要求在场景资产加载时 GLTF 场景可用,最好直接加载 Gltf 而不是包含在其中的场景。为了使这一点更容易处理,这个包提供了 SpawnGltfScene 命令和一些辅助工具。您可以从 GLTF 中生成特定的场景,如下所示:

    // Spawn Minion at Default location
    cmds.spawn_gltf(GltfScene::new(ex.asset_scene.clone()).with_bundle(Name::new("Minion")));

GltfScene 还提供了 at_location(Transform)with_scene(usize)build(),用于指定变换、gltf 中的场景以及在没有附加组件的场景根上生成场景。

依赖关系

~23MB
~411K SLoC