3个版本
0.5.2-alpha | 2024年6月20日 |
---|---|
0.5.1-alpha | 2024年6月20日 |
0.5.0-alpha | 2024年6月19日 |
#197 in 游戏开发
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
绑定,不起作用。 - ✔️ 许多反应式库区分了
Actions
和Effects
。是否应该将AsyncTask
重命名为Action
?
待办事项
缺失
- 测试
- 错误处理和通用弹性
增强
- 查看在初始化期间注册效果系统并保留
SystemId
的方法 - 更多 API 文档
- 我需要有人审查每一行,因为我是个新手
- 更多示例,包括基本游戏内容(金币和健康似乎很受欢迎)
- 更多示例,包括一些将
LazySignals
与流行的Bevy
项目(如bevy-lunex
、bevy_dioxus
、bevy_editor_pls
、bevy_reactor
、haalka
、kayak_ui
、polako
、quill
、space_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 |
许可
此存储库中的所有代码都根据以下任一许可进行双许可
- MIT许可证(LICENSE-MIT 或 http://opensource.org/licenses/MIT)
- Apache许可证,版本2.0(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
由你选择。这意味着你可以选择你偏好的许可证。
你的贡献
除非你明确声明,否则根据Apache-2.0许可证定义的,你有意提交以包含在工作中的任何贡献,将按上述方式双重授权,不附加任何额外条款或条件。
依赖项
~38–74MB
~1.5M SLoC