#derive-debug #reactive #properties #type #run-time #dependencies #debugging

废弃 nightly no-std dep-obj

依赖对象:有效的反应式异构容器

100个版本 (37个破坏性更新)

0.38.4 2022年6月4日
0.38.0 2022年5月28日
0.34.1 2021年9月25日
0.17.0 2021年7月26日
0.8.0 2020年11月2日

#120 in #derive-debug

Download history 238/week @ 2024-03-28 228/week @ 2024-04-04

每月下载量205

MIT/Apache

225KB
5K SLoC

maintenance: deprecated

dep-obj

依赖对象:有效的反应式异构容器。

示例:简单的依赖类型

依赖对象系统基于 component-arena。一个组件可以有多个依赖对象作为其部分。其中一些可能是动态类型和/或可选的。让我们看看一个具有固定类型单个依赖对象的简单组件示例。

以可携带的游戏对象 Item 为例。

为了描述抽象实体 Item,我们需要以下类型的列表

  • 组件数据持有者 ItemComponent
  • 组件 Item 的ID;
  • 依赖对象类型 ItemProps
  • 组件竞技场 Items

首先,定义包含依赖对象的组件

macro_attr! {
    #[derive(Debug, Component!)]
    struct ItemComponent {
        props: ItemProps,
    }
}

然后,为了保持封装性,将 Id<ItemComponent> 包装到一个新的类型中

macro_attr! {
    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
    pub struct Item(Id<ItemComponent>);
}

依赖对象支持属性继承。对象树结构由 DepObjId 特性实现定义。对于 Item,我们不需要继承,并且可以通过标记它为 DetachedDepObjId 特性来表示它并获得适当的“空” DepObjId 实现来表示它

impl DetachedDepObjId for Item { }

所有组件都需要放置在适当的竞技场中。让我们创建一个

#[derive(Debug)]
pub struct Items {
    items: Arena<ItemComponent>,
}

依赖对象系统基于的另一个基础是 dyn-context 包。为了使 Items 的使用更方便,值得将其标记为 SelfState,即一个包含仅有一个部分的状态,该部分是 Items 本身

impl SelfState for Items { }

现在我们已经准备好指定依赖类型本身了

dep_type! {
    #[derive(Debug)]
    pub struct ItemProps = Item[ItemProps] {
        name: Cow<'static, str> = Cow::Borrowed(""),
        base_weight: f32 = 0.0,
        weight: f32 = 0.0,
        equipped: bool = false,
        cursed: bool = false,
    }
}

现在所有结构都已编码,我们可以编写 Item 方法。首先,我们需要一种方法来构造一个新的 Item

pub fn new(state: &mut dyn State) -> Item {
    let items: &mut Items = state.get_mut();
    items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
}

ItemProps::new_priv 是一个构造函数,由 dep_type! 宏生成。

接下来,我们需要一种方法来销毁不再需要的项

pub fn drop_self(self, state: &mut dyn State) {
    self.drop_bindings_priv(state);
    let items: &mut Items = state.get_mut();
    items.0.remove(self.0);
}

现在最后,但同样重要的是:对 props 字段中提供访问依赖对象的函数的间接定义

impl_dep_obj!(Item {
    fn<ItemProps>() -> (ItemProps) { Items | .props }
});

impl_dep_obj 宏还生成了我们在 drop_self 方法中使用的 drop_bindigs_priv 方法。

让我们看一下我们的 mod items 整体

mod items {
    use components_arena::{Arena, Component, NewtypeComponentId, Id};
    use dep_obj::{DetachedDepObjId, dep_type, impl_dep_obj};
    use dyn_context::{SelfState, State, StateExt};
    use macro_attr_2018::macro_attr;
    use std::borrow::Cow;

    macro_attr! {
        #[derive(Debug, Component!)]
        struct ItemComponent {
            props: ItemProps,
        }
    }

    macro_attr! {
        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
        pub struct Item(Id<ItemComponent>);
    }

    impl DetachedDepObjId for Item { }

    impl Item {
        pub fn new(state: &mut dyn State) -> Item {
            let items: &mut Items = state.get_mut();
            items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
        }

        pub fn drop_self(self, state: &mut dyn State) {
            self.drop_bindings_priv(state);
            let items: &mut Items = state.get_mut();
            items.0.remove(self.0);
        }
    }

    impl_dep_obj!(Item {
        fn<ItemProps>() -> (ItemProps) { Items | .props }
    });

    #[derive(Debug)]
    pub struct Items(Arena<ItemComponent>);

    impl SelfState for Items { }

    dep_type! {
        #[derive(Debug)]
        pub struct ItemProps = Item[ItemProps] {
            name: Cow<'static, str> = Cow::Borrowed(""),
            base_weight: f32 = 0.0,
            weight: f32 = 0.0,
            equipped: bool = false,
            cursed: bool = false,
        }
    }
}

这里我们缺少的是 Items 构造函数,以及不幸的是,析构函数。添加构造函数很简单

impl Items {
    pub fn new() -> Items {
        Items(Arena::new())
    }
}

但是,析构函数很棘手。Item::drop_self 方法做了两件事:首先,它取消了项所拥有的所有绑定,其次,它从竞技场中删除项。第二件事会自动完成,但绑定需要手动销毁。因此,我们需要显式的 Items 析构函数来正确地取消所有 Item 的绑定。

但我们不能只是为 Items 实现 Drop,因为我们需要 State 参数来调用 Item::drop_bindings_priv。不幸的是,Rust 不支持线性类型概念,这将允许在 drop 方法中有参数。但是 dyn-contextcomponents-arena 包含一些有用的东西,允许尽可能好地表达这种类型属性,就像 Rust 现在所能做到的那样。

Arena 实现了特殊的特质,Stop,它是带有 State 参数的 Drop 的类似物。然而,我们的包装 Items 并没有实现它。让我们修复它

#[derive(Debug, Stop)]
pub struct Items(Arena<ItemComponent>);

我们希望 Item::stop 函数做的事情是调用每个 Itemdrop_bindings_priv。为了告诉它,我们需要定义一个结构(让我们称它为 ItemStop),并让 ItemComponent 使用它来适当地“停止”我们的 Item。这可以通过 Component derive 宏的 stop 参数轻松实现

#[derive(Debug, Component!(stop=ItemStop)]
struct ItemComponent {
    props: ItemProps,
}

如果我们尝试编译,我们会得到一个错误,指出 ComponentStop 特质没有为 ItemStop 实现。所以让我们实现它

impl ComponentStop for ItemStop {
    with_arena_in_state_part!(Items);

    fn stop(&self, state: &mut dyn State, id: Id<ItemComponent>) {
        Item(id).drop_bindings_priv(state);
    }
}

多亏了 with_arena_in_state_part 宏,我们只需要手动实现 stop 函数。

看一下我们的 mod items

mod items {
    use components_arena::{Arena, Component, ComponentStop, NewtypeComponentId, Id, with_arena_in_state_part};
    use dep_obj::{DetachedDepObjId, dep_type, impl_dep_obj};
    use dyn_context::{SelfState, State, StateExt, Stop};
    use macro_attr_2018::macro_attr;
    use std::borrow::Cow;

    macro_attr! {
        #[derive(Debug, Component!(stop=ItemStop))]
        struct ItemComponent {
            props: ItemProps,
        }
    }

    impl ComponentStop for ItemStop {
        with_arena_in_state_part!(Items);

        fn stop(&self, state: &mut dyn State, id: Id<ItemComponent>) {
            Item(id).drop_bindings_priv(state);
        }
    }

    macro_attr! {
        #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
        pub struct Item(Id<ItemComponent>);
    }

    impl DetachedDepObjId for Item { }

    impl Item {
        pub fn new(state: &mut dyn State) -> Item {
            let items: &mut Items = state.get_mut();
            items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
        }

        pub fn drop_self(self, state: &mut dyn State) {
            self.drop_bindings_priv(state);
            let items: &mut Items = state.get_mut();
            items.0.remove(self.0);
        }
    }

    impl_dep_obj!(Item {
        fn<ItemProps>() -> (ItemProps) { Items | .props }
    });

    #[derive(Debug, Stop)]
    pub struct Items(Arena<ItemComponent>);

    impl SelfState for Items { }

    impl Items {
        pub fn new() -> Items {
            Items(Arena::new())
        }
    }

    dep_type! {
        #[derive(Debug)]
        pub struct ItemProps = Item[ItemProps] {
            name: Cow<'static, str> = Cow::Borrowed(""),
            base_weight: f32 = 0.0,
            weight: f32 = 0.0,
            equipped: bool = false,
            cursed: bool = false,
        }
    }
}

到目前为止,Item 没有任何有意义的行为。让我们添加一些。

pub fn new(state: &mut dyn State) -> Item {
    let items: &mut Items = state.get_mut();
    let item = items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)));
    item.bind_weight(state);
    item
}

fn bind_weight(self, state: &mut dyn State) {
    let weight = Binding3::new(state, (), |(), base_weight, cursed, equipped| Some(
        if equipped && cursed { base_weight + 100.0 } else { base_weight }
    ));
    ItemProps::WEIGHT.bind(state, self, weight);
    weight.set_source_1(state, &mut ItemProps::BASE_WEIGHT.value_source(self));
    weight.set_source_2(state, &mut ItemProps::CURSED.value_source(self));
    weight.set_source_3(state, &mut ItemProps::EQUIPPED.value_source(self));
}

上面的代码创建了一个功能依赖关系,将四个 Item 属性联系在一起,现在 weight 作为其他三个属性的函数,将在任何一个发生变化时自动更新。

最后,让我们编写一些测试代码来确保我们的游戏系统正常工作

fn track_weight(state: &mut dyn State, item: Item) {
    let weight = Binding2::new(state, (), |(), name, weight: Option<Change<f32>>|
        weight.map(|weight| (name, weight.new))
    );
    weight.set_target_fn(state, (), |_state, (), (name, weight)| {
        println!("\n{name} now weights {weight}.");
    });
    item.add_binding::<ItemProps, _>(state, weight);
    weight.set_source_1(state, &mut ItemProps::NAME.value_source(item));
    weight.set_source_2(state, &mut ItemProps::WEIGHT.change_source(item));
}

fn run(state: &mut dyn State) {
    let the_item = Item::new(state);
    track_weight(state, the_item);
    ItemProps::NAME.set(state, the_item, Cow::Borrowed("The Item")).immediate();

    println!("\n> the_item.base_weight = 5.0");
    ItemProps::BASE_WEIGHT.set(state, the_item, 5.0).immediate();

    println!("\n> the_item.cursed = true");
    ItemProps::CURSED.set(state, the_item, true).immediate();

    println!("\n> the_item.equipped = true");
    ItemProps::EQUIPPED.set(state, the_item, true).immediate();

    println!("\n> the_item.cursed = false");
    ItemProps::CURSED.set(state, the_item, false).immediate();

    the_item.drop_self(state);
}

最后要做的真正最后的事情是:构造State实例并调用run。我们的系统需要一个包含Items和特殊绑定场的State。这可以通过使用merge_mut_and_then方法轻松实现,该方法将两个状态对象合并为一个。当然,我们不应该忘记在最后调用Items::stop

fn main() {
    (&mut Items::new()).merge_mut_and_then(|state| {
        run(state);
        Items::stop(state);
    }, &mut Bindings::new());
}

示例:构建器

当你只需要为刚构造的对象设置初始值时,多次调用Type::PROP.set(state, ...).immediate()是非常无聊的。`dep-obj`有一个避免这种情况的工具:对象构建器。在项目中启用它非常简单

impl Item {
    with_builder!(ItemProps);
}

这个宏声明了一个函数build,可以按以下方式使用

the_item.build(state, |props| props
    .name(Cow::Borrowed("The Item"))
    .base_weight(5.0)
    .cursed(true)
);

示例:动态类型依赖对象

让我们添加一些不是所有Item都通用的属性。

为此,我们需要使用另一个库:downcast-rs。使用此crate,让我们为扩展属性依赖类型定义基特质

pub trait ItemObj: Downcast + DepType<Id=Item> { }

impl_downcast!(ItemObj);

我们需要在组件中添加一个新的字段

macro_attr! {
    #[derive(Debug, Component!(stop=ItemStop))]
    struct ItemComponent {
        props: ItemProps,
        obj: Box<dyn ItemObj>,
    }
}

修改后的Item构造函数

pub fn new(state: &mut dyn State, obj: Box<dyn ItemObj>) -> Item {
    let items: &mut Items = state.get_mut();
    let item = items.0.insert(|id| (ItemComponent {
        props: ItemProps::new_priv(),
        obj
    }, Item(id)));
    item.bind_weight(state);
    item
}

访问对象的方式(impl_dep_obj处理所有脏活累活,包括向下转型)

impl_dep_obj!(Item {
    fn<ItemProps>() -> (ItemProps) { Items | .props }
    fn<ItemObjKey>() -> dyn(ItemObj) { Items | .obj }
});

基部分已经完成,我们准备编写ItemObj的具体变体

mod weapon {
    use dep_obj::dep_type;
    use dep_obj::binding::Binding3;
    use dyn_context::State;
    use crate::items::*;

    dep_type! {
        #[derive(Debug)]
        pub struct Weapon = Item[ItemObjKey] {
            base_damage: f32 = 0.0,
            damage: f32 = 0.0,
        }
    }

    impl ItemObj for Weapon { }

    impl Weapon {
        #[allow(clippy::new_ret_no_self)]
        pub fn new(state: &mut dyn State) -> Item {
            let item = Item::new(state, Box::new(Self::new_priv()));
            Self::bind_damage(state, item);
            item
        }

        fn bind_damage(state: &mut dyn State, item: Item) {
            let damage = Binding3::new(state, (), |(), base_damage, cursed, equipped| Some(
                if equipped && cursed { base_damage / 2.0 } else { base_damage }
            ));
            Weapon::DAMAGE.bind(state, item, damage);
            damage.set_source_1(state, &mut Weapon::BASE_DAMAGE.value_source(item));
            damage.set_source_2(state, &mut ItemProps::CURSED.value_source(item));
            damage.set_source_3(state, &mut ItemProps::EQUIPPED.value_source(item));
        }
    }
}

依赖关系

~5.5MB
~106K SLoC