#signals #lazy-evaluation #primitive #bevy #integrating #ad #lazy-signals

bevy_lazy_signals

这是一个MIT-Scheme的1/3部分的临时代码、非正式指定、存在bug、速度尚可的实现。

3个版本

0.5.2-alpha2024年6月20日
0.5.1-alpha2024年6月20日
0.5.0-alpha2024年6月19日

#197 in 游戏开发

MIT/Apache

80KB
1.5K SLoC

Bevy的LazySignals

这是一个MIT-Scheme的1/3部分的临时代码、非正式指定、存在bug、速度尚可的实现。


Bevy实现一种惰性反应式信号的原始和示例。

警告:该库正在积极开发中,提供的用途保证比许可证提供的还要少。

致谢

此项目的初始结构基于bevy_rx

架构

此库在底层基本上是Haskell monads,同时受ECMAScript所谓反应库的TC39信号开发者API的启发。它还受到MIT传播模型的影响。好吧,至少有一个YouTube视频提到了它。

另请参阅架构理由的深入介绍

设计问题

  • 如何处理错误是好的方式?
  • 这个能否与futures_lite一起工作以创建类似futures-signals的API?
  • 在初始化期间,计算和效果上下文实际上应该评估吗?
  • 如何最好地防止或检测无限循环?
  • get与unwrap的使用能否更加一致?
  • ✔️ 任务应该能够在运行时记住它们已被重新触发,并在完成后立即再次运行吗?(我认为它们目前就是这样做的)
  • ✔️ 应该有一个选项将Bevy系统作为效果运行吗?
  • 应该有一个仅包含命令的效果版本吗?
  • 我们需要一个useRef等效项以支持不通过值传递的状态吗?
  • 关于useCallback的相同问题
  • ❌ 是否可以用更改检测来替换我们目前手动添加的一些组件?
  • 一个Computed和一个Effect能否在同一个实体上存在?(技术上可以,但为什么?)
  • 我们是否想要一个触发Effect的直接API?
  • 是否有一种方式可以编写接受结果结构体而不是 Option 的闭包?
  • 如何发送一个 DynamicStruct 作为信号?现在由于 FromReflect 绑定,不起作用。
  • ✔️ 许多反应式库区分了 ActionsEffects。是否应该将 AsyncTask 重命名为 Action

待办事项

缺失

  • 测试
  • 错误处理和通用弹性

增强

  • 查看在初始化期间注册效果系统并保留 SystemId 的方法
  • 更多 API 文档
  • 我需要有人审查每一行,因为我是个新手
  • 更多示例,包括基本游戏内容(金币和健康似乎很受欢迎)
  • 更多示例,包括一些将 LazySignals 与流行的 Bevy 项目(如 bevy-lunexbevy_dioxusbevy_editor_plsbevy_reactorhaalkakayak_uipolakoquillspace_editor 等)集成的示例

我真正要做的

  • 定义信号原语的包
  • 原生支持 bevy_reflect 类型
  • 为效果添加异步任务管理
  • 防止在任务仍在上次运行时重新触发
  • 处理任务,在它们完成时运行它们的命令
  • 确保在处理过程中从 Computed 中删除 Triggered
  • LazySignalsData 特征界限中删除 Clone
  • 实现效果系统
  • bevy_mod_picking 集成
  • 制作一个完全连接的 sickle_ui 实体检查器的演示
  • 确保可以将结果结构体转换为常规的 Option<Result<>>
  • 找到一种更好的方法来管理效果系统(在初始化时)
  • 查看是否可以通过动作的命令队列来调度系统
  • 提供与 Bevy 观察者的集成
  • 将获取器/设置器元组工厂添加到 API 中(可能需要宏)
  • 为源 Vecs 添加源字段
  • 支持撤销/重做
  • 与 bevy-inspector-egui 集成
  • 完成 十大挑战
  • 如果开发者期望发送同一信号多次/每帧,则支持流
  • 查看演示与 bevy_mod_scripting 的兼容性如何
  • 编写一些 Fennel 代码,看看如何用脚本编写计算和效果
  • 制作一个可视信号编辑器插件
  • 查看演示与 aery 的兼容性如何
  • 防止或至少检测无限循环

通用用法

LazySignalsPlugin 将注册核心类型和系统。

在应用程序初始化期间使用 API 创建信号、计算、效果和任务。在更新系统中读取和发送信号,读取缓存的计算。在源或触发信号发送或源计算值更改时触发动作和效果。

对于基本用法,特定于应用程序的资源可以跟踪反应式原始实体。

(有关有效、经过测试的代码,请参阅 basic_test)

use bevy::prelude::*;
use bevy_lazy_signals::{
    api::LazySignals,
    commands::LazySignalsCommandsExt,
    framework::*,
    LazySignalsPlugin
};

#[derive(Resource)]
struct ConfigResource {
    x_axis: Entity,
    y_axis: Entity,
    action_button: Entity,
    screen_x: Entity,
    screen_y: Entity,
    log_effect: Entity,
    action: Entity,
}

struct MyActionButtonCommand(Entity);

impl Command for MyActionButtonCommand {
    fn apply(self, world: &mut World) {
        info!("Pushing the button");
        LazySignals.send::<bool>(self.0, true, world.commands());
        world.flush_commands();
    }
}

fn signals_setup_system(mut commands: Commands) {
    // note these will not be ready for use until the commands actually run
    let x_axis = LazySignals.state::<f32>(0.0, commands);

    let y_axis = LazySignals.state::<f32>(0.0, commands);

    // here we start with a new Entity (more useful if we already spawned it elsewhere)
    let action_button = commands.spawn_empty().id();

    // then we use the custom command form directly instead
    commands.create_state::<bool>(action_button, false);

    // let's define 2 computed values for screen_x and screen_y

    // say x and y are mapped to normalized -1.0 to 1.0 OpenGL units and we want 1080p...
    let width = 1920.0;
    let height = 1080.0;

    // the actual pure function to perform the calculations
    let screen_x_fn = |args: (f32)| {
        LazySignals::result(args.0.map_or(0.0, |x| (x + 1.0) * width / 2.0))
    };

    // and the calculated memo to map the fns to sources and a place to store the result
    let screen_x = LazySignals.computed::<(f32), f32>(
        screen_x_fn,
        vec![x_axis],
        &mut commands
    );

    // or just declare the closure in the API call if it won't be reused
    let screen_y = LazySignals.computed::<(f32), f32>(
        // because we pass (f32) as the first type param, the compiler knows type of args here
        |args| {
            LazySignals::result(args.0.map_or(0.0, |y| (y + 1.0) * height / 2.0))
        },
        vec![y_axis],
        &mut commands
    );

    // at this point screen coords will update every time the x or y axis is sent a new signal
    // ...so how do we run an effect?

    // similar in form to making a computed, but we get exclusive world access
    // first the closure (that is an &mut World, if needed)
    let effect_fn = |args: (f32, f32), _world| {
        let x = args.0.map_or("???", |x| format!("{:.1}", x))
        let y = args.1.map_or("???", |y| format!("{:.1}", y))
        info!(format!("({}, {})"), x, y)
    };

    // then the reactive primitive entity, which logs screen position every time the HID moves
    let log_effect = LazySignals.effect::<(f32, f32)>{
        effect_fn,
        vec![screen_x, screen_y], // sources (passed to the args tuple)
        Vec::<Entity>::new(), // triggers (will fire an effect but don't care about the value)
        &mut commands
    };

    // unlike a brief Effect which gets exclusive world access, an Action is an async task
    // but only returns a CommandQueue, to run when the system that checks Bevy tasks notices
    // it has completed
    let action_fn = |args: (f32, f32)| {
        let mut command_queue = CommandQueue::default();

        // as long as the task is still running, it will not spawn another instance
        do_something_that_takes_a_long_time(args.0, args.1);

        // when the task is complete, push the button
        command_queue.push(MyActionButtonCommand(action_button))
        command_queue
    };

    let action = LazySignals.action::<(f32, f32)>{
        action_fn,
        vec![screen_x, screen_y],
        Vec::<Entity>::new(),
        &mut commands
    }

    // store the reactive entities in a resource to use in systems
    commands.insert_resource(MyConfigResource {
        x_axis,
        y_axis,
        action_button,
        screen_x,
        screen_y,
        log_effect,
        action,
    });
}

fn signals_update_system(config: Res<ConfigResource>, mut commands: Commands) {
    // assume we have read x and y values of the gamepad stick and assigned them to x and y
    let x = ...
    let y = ...

    LazySignals.send(config.x_axis, x, commands);
    LazySignals.send(config.y_axis, y, commands);

    // signals aren't processed right away, so the signals are still the original value
    let prev_x = LazySignals.read::<f32>(config.x_axis, world);
    let prev_y = LazySignals.read::<f32>(config.y_axis, world);

    // let's simulate pressing the action button but use custom send_signal command
    commands.send_signal::<bool>(config.action_button, true);

    // or use our custom local command
    commands.push(MyActionButtonCommand(config.action_button));

    // doing both will only actually trigger the signal once, since multiple calls to send will
    // update the next_value multiple times, but we're lazy, so the signal itself only runs once
    // using whatever value is in next_value when it gets evaluated, i.e. the last signal to
    // actually be sent

    // this is referred to as lossy

    // TODO provide a stream version of signals that provides a Vec<T> instead of Option<T>
    // to the closures

    // in the mean time, if we read x and y and send the signals in the First schedule
    // we can use them to position a sprite during the Update schedule

    // the screen_x and screen_y are only recomputed if the value of x and/or y changed

    // LazySignals.read just returns the data value of the LazySignalsState<f32> component

    // for a Computed, this updates during PreUpdate by default and is otherwise immutable
    // (unless you modify the component directly, which voids the warranty)

    // TODO concrete example using bevy_mod_picking
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // resource to hold the entity ID of each lazy signals primitive
        .init_resource::<ConfigResource>()
        // NOTE: the developer will need to register each custom LazySignalsState<T> type

        // also need to register tuple types for args if they contain custom types (I think)
        // .register_type::<LazyImmutable<MyType>>()

        // f64, i32, bool, &str, and () are already registered

        // add the plugin so the signal processing systems run
        .add_plugins(LazySignalsPlugin)
        .add_systems(Startup, signals_setup_system)
        .add_systems(Update, signals_update_system)
        .run();
}

🕊 Bevy 兼容性

bevy bevy_lazy_signals
0.14.0 0.4.0-alpha+
0.13.2 0.3.0-alpha

许可

此存储库中的所有代码都根据以下任一许可进行双许可

由你选择。这意味着你可以选择你偏好的许可证。

你的贡献

除非你明确声明,否则根据Apache-2.0许可证定义的,你有意提交以包含在工作中的任何贡献,将按上述方式双重授权,不附加任何额外条款或条件。

依赖项

~38–74MB
~1.5M SLoC