1 个不稳定版本

0.7.0 2023 年 4 月 15 日

#1922游戏开发

MIT/Apache

60KB
670

bevy_proto_typetag

Crates.io Docs License


警告

这个包是 bevy_proto v0.7.0 的存档。

您可以继续使用它,它可能不会频繁更新(更新 Bevy 依赖项,接受社区 PR 等)。

有关更多详细信息,请参阅 本节


使用简单的配置文件在 Bevy 游戏引擎中创建实体。

name: "Simple Enemy"
template: Creature
components:
  - type: Enemy
  - type: Attack
    value:
      damage: 10
  - type: Armed
    value:
      weapons: [ "laser sword", "ray-blaster" ]
      primary: "laser sword"

📋 功能

  • 定义 容器

    name: Player
    components:
      - type: Controllable
      - type: Health
        value:
          max: 20
    
  • 继承 其他原型的功能

    name: Skeleton
    templates: Enemy, Creature
    components:
      # ...
    
  • 包含 要加载的资产

    name: Bullet
    components:
      - type: CustomSprite
        value:
          texture: "path/to/texture.png"
    

📲 安装

[dependencies]
bevy_proto_typetag = "0.7"
typetag = "0.2"

然后将其添加到您的应用程序中,如下所示

use bevy::prelude::*;
use bevy_proto_typetag::ProtoPlugin;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // add dependent plugins, resources, etc. here
        // ...
        // Add this plugin after any other plugins/resources needed for prototype preparation
        .add_plugin(ProtoPlugin::default());
}

📓 示例

查看这些示例以获取有关如何使用此包的更多详细信息

  • 属性 - 展示可用的 derive 辅助属性
  • 基本 - 添加原型最基本的方法
  • 捆绑 - 一个更复杂的原型的演示,其中包含资产
  • 模板 - 模板如何影响您的原型的示例

🕊 Bevy 兼容性

bevy bevy_proto_typetag
0.10 0.7.0
0.9 0.6.0
0.8 0.5.0
0.7 0.4.0
0.6 0.3.0
0.5 0.2.1

✨ 用法

创建组件

首先,创建一个实现 ProtoComponent 的结构体。这可以通过两种方式之一完成

对于简单的组件,ProtoComponent 可以被 derive

use serde::{Deserialize, Serialize};
use bevy::prelude::*;
use bevy_proto_typetag::prelude::*;

#[derive(Clone, Serialize, Deserialize, ProtoComponent, Component)]
struct Movement {
    speed: u16
}

// Also works on tuple structs:
#[derive(Clone, Serialize, Deserialize, ProtoComponent, Component)]
struct Inventory (
    Option<Vec<String>>
);

默认情况下,ProtoComponent 被克隆到生成的实体中。

否则,您可以手动定义它们(此方法需要两个属性)

use bevy::prelude::*;
use bevy::ecs::system::EntityCommands;
use bevy_proto_typetag::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Component)] // Required
struct Inventory(Option<Vec<String>>);

#[typetag::serde] // Required
impl ProtoComponent for Inventory {
    // Required
    fn insert_self(&self, commands: &mut ProtoCommands, asset_server: &Res<AssetServer>) {
        commands.insert(
            Self (self.0.clone())
        );
    }
}

ProtoComponent 不必是组件本身。它可以用作纯 DTO 来生成其他组件或捆绑包。您可以完全访问 EntityCommands,因此您可以一次插入捆绑包或多个组件。

有关从非组件 ProtoComponent 结构体生成组件的其他方法,请参阅 属性 示例。

定义原型

在配置文件中定义原型。默认支持 YAML(以及扩展的有效 JSON)文件

# assets/prototypes/adventurer.yaml
---
name: "Adventurer"
components:
  - type: Inventory
    value: ["sword"]
  - type: Movement
    value:
      speed: 10

默认情况下,所有位于assets/prototypes/目录下的.yaml.json文件都被处理为原型。

使用模板

原型也可以包含一个模板。模板可以是任何原型,用于定义应该插入其继承者中的公共组件。这有助于减少重复标记并快速重构原型集合。

首先定义模板

# assets/prototypes/npc.yaml
---
name: "NPC"
components:
  - type: Inventory
    value: ~
  - type: Movement
    value:
      speed: 10

然后在另一个原型中继承模板

# assets/prototypes/peasant.yaml
---
name: "Peasant"
template: NPC

您还可以覆盖模板组件

# assets/prototypes/adventurer.yaml
---
name: "Adventurer"
template: NPC
components:
  - type: Inventory
    value: ["sword"]

也可以指定多个模板。然而,冲突的组件将以相反的顺序覆盖(列在模板列表中较早的模板可以覆盖列在模板列表中较晚的模板)

# assets/prototypes/fast_adventurer.yaml
---
name: "Fast Adventurer"
templates: Speedy, NPC # "Speedy" may override "NPC"
components:
  - type: Inventory
    value: ["sword"]

模板可以指定为标准的YAML列表或以逗号分隔的字符串(如上例所示)。此外,templatestemplate的别名,因此可以使用任一一个。

创建原型

要创建原型,请添加一个具有访问权限的系统

  • mut命令

  • Res<ProtoData>

  • Res<AssetServer>

然后编写以下内容之一

use bevy::prelude::*;
use bevy_proto_typetag::prelude::*;

fn spawn_adventurer(mut commands: Commands, data: Res<ProtoData>, asset_server: Res<AssetServer>) {
    let proto = data.get_prototype("Adventurer").expect("Prototype doesn't exist!");

    // Spawns in our "Adventurer" Prototype
    proto.spawn(&mut commands, &data, &asset_server);
}

spawn(...)方法返回用于创建实体的EntityCommands。这允许您添加额外的组件、包等。

use bevy::prelude::*;
use bevy_proto_typetag::prelude::*;

#[derive(Component)]
struct Friendly;

#[derive(Component)]
struct Named(String);

fn spawn_adventurer(mut commands: Commands, data: Res<ProtoData>, asset_server: Res<AssetServer>) {
  let proto = data.get_prototype("Adventurer").expect("Prototype doesn't exist!");

  let adventurer: Entity = proto
      .spawn(&mut commands, &data, &asset_server)
      .insert(Friendly)
      .insert(Named("Bob".to_string()))
      .id();
}

使用资产

对于需要访问资产的原型,您可以通过以下两种方式之一来获取访问权限

第一种是在被创建时加载资产。这更受欢迎,因为它意味着可以在不再需要时卸载该资产。

use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use bevy_proto_typetag::prelude::*;

#[derive(Component)]
struct Renderable {
    pub texture_path: Handle<Image>
}

#[derive(Serialize, Deserialize)]
struct Creature {
    pub texture_path: HandlePath
}

#[typetag::serde]
impl ProtoComponent for Creature {
    // Required
    fn insert_self(&self, proto_commands: &mut ProtoCommands, asset_server: &Res<AssetServer>) {
        let handle: Handle<Image> = asset_server.load(self.texture_path.as_str());
        let entity_commands = proto_commands.raw_commands();

        entity_commands.insert(Renderable {
            texture_path: handle
        });
    }
}

第二种是准备资产以供以后使用。这将保留资产在ProtoData资源中,然后必须在不再需要时手动处置。设置资产是通过prepare方法完成的

use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use bevy_proto_typetag::prelude::*;

#[derive(Serialize, Deserialize)]
struct Renderable {
    pub texture_path: HandlePath
}
#[derive(Serialize, Deserialize)]
struct Creature {
    pub texture_path: HandlePath
}

#[typetag::serde]
impl ProtoComponent for Creature {
    // Required
    fn insert_self(&self, proto_commands: &mut ProtoCommands, asset_server: &Res<AssetServer>) {
        let texture: Handle<Image> = proto_commands
            .get_handle(self, &self.texture_path)
            .expect("Expected Image handle to have been created");
        let entity_commands = proto_commands.raw_commands();

        entity_commands.insert_bundle(SpriteBundle {
            texture,
            ..Default::default()
        });
    }

    fn prepare(
        &self,
        world: &mut World,
        prototype: &dyn Prototypical,
        data: &mut ProtoData
    ) {
        // === Load Handles === //
        let asset_server = world.get_resource::<AssetServer>().unwrap();
        let handle: Handle<Image> = asset_server.load(self.texture_path.as_str());

        // === Save Handles === //
        data.insert_handle(prototype, self, handle);
    }
}

自定义原型

默认的原型对象如下所示

use bevy_proto_typetag::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Prototype {
    /// The name of this prototype
	  pub name: String,
	  /// The names of this prototype's templates (if any)
	  pub templates: Vec<String>,
	  /// The components belonging to this prototype
	  pub components: Vec<Box<dyn ProtoComponent>>,
}

但是,您可以使用自己的原型对象。任何实现Prototypical的结构体都可以用作默认原型。然后您只需向ProtoPlugin对象提供自己的反序列化器即可。

use serde::{Deserialize, Serialize};
use bevy::prelude::*;
use bevy::ecs::system::EntityCommands;
use bevy_proto_typetag::prelude::*;

#[derive(Serialize, Deserialize)]
struct CustomPrototype;
impl Prototypical for CustomPrototype {
  fn name(&self) -> &str {
    "CustomPrototype"
  }
  fn iter_components(&self) -> std::slice::Iter<'_, std::boxed::Box<(dyn ProtoComponent + 'static)>> { 
    todo!() 
  }
  fn create_commands<'w, 's, 'a, 'p>(
    &'p self, 
    entity_commands: EntityCommands<'w, 's, 'a>, 
    proto_data: &'p Res<'_, ProtoData>
  ) -> ProtoCommands<'w, 's, 'a, 'p> { 
    todo!() 
  }
}

#[derive(Clone)]
struct CustomProtoDeserializer;

impl ProtoDeserializer for CustomProtoDeserializer {
    fn deserialize(&self, data: &str) -> Option<Box<dyn Prototypical>> {
        // Deserialize using your custom prototypical object
        if let Ok(value) = serde_yaml::from_str::<CustomPrototype>(data) {
            Some(Box::new(value))
        } else {
            None
        }
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // ...
        .add_plugin(ProtoPlugin {
            options: Some(ProtoDataOptions {
                // Specify your custom deserializer
                deserializer: Box::new(CustomProtoDeserializer),

                // You can also change the prototype directories here
                directories: vec![String::from("assets/prototypes")],
                // And specify whether you want the prototype files to be recursively loaded
                recursive_loading: false,
                // You can also update the allowed extensions within those directories
                extensions: Some(vec!["yaml", "json"]),
            })
        });
}

如果希望使用自定义反序列化器,则必须指定ProtoDataOptions中的所有字段。即使希望继续使用默认设置,也必须指定它们。上面显示的附加字段也是默认设置,如果希望复制它们的话。

⚠️ 免责声明

在将其安装到您的项目中之前,请了解此crate的限制。虽然它使得与某些实体的工作更加容易,但根据您的项目,它可能会带来一定的性能成本。

根据bench示例,创建原型可能比在系统中手动定义实体慢约1.8倍(这可能会根据加载数据而变化)。对于发布版本,这种差异会变得非常小,但仍略慢。对于某些项目——除了可能是真正密集型或非常频繁地创建大量实体的项目——这可能不是问题。

尽管如此,考虑使用此系统的缺点并判断它是否适合您的项目仍然很好。以下是目前/潜在的主要问题的概述

  • 动态分派 - 此crate使用动态特质对象在原型上添加或删除任何组件。然而,这带来了一定的代价,因为编译器不再能预先知道类型,从而阻止了诸如静态分派、单态化等操作。

  • 加载时间 - 对于较小的项目来说,这可能不太明显(尽管我自己还没有在数百个原型上测试过),但该包在启动时确实需要加载所有原型。这样它就可以准备任何其他需要的资源和资产,以便生成原型。

  • 资源 - 该包(目前)也将其所有必需的资源存储在其自己的资源 ProtoData 中。这意味着可能只需要一次的资源将在整个应用程序的生命周期中保持加载状态,因为它保留了句柄。可以通过在单独的组件上托管资源并手动创建句柄来防止这种情况,在生成该原型时进行。

    use bevy::prelude::*;
    use bevy_proto_typetag::prelude::*;
    
    #[derive(Component)]
    struct OtherComponent(Handle<Image>);
    
    fn attach<T: Prototypical>(
      prototype: T, 
      my_asset: Handle<Image>,
      commands: &mut Commands,
      data: &Res<ProtoData>,
      asset_server: &Res<AssetServer>,
    ) {
      // Attach fictional OtherComponent with asset "my_asset" which should unload when despawned
      prototype.spawn(commands, data, asset_server).insert(OtherComponent(my_asset));
    }
    
    

话虽如此,这个包旨在加快开发速度,并使人类(尤其是非程序员)更容易与实体类型交互。如果性能影响对您的项目来说太大,您最好继续使用定义实体的标准方法。

依赖项

~19–57MB
~1M SLoC