Blazingly fast path based retained layout engine for Bevy entities, built around vanilla Bevy ECS. This library is intended to replace the existing bevy_ui
crate, but nothing is stopping you from using them both at the same time.
It uses a combination of Bevy's built-in hierarchy and its own custom hierarchy to give you the freedom of control without the bloat or borrow checker limitations usually faced when creating UI.
It gives you the ability to make your own custom UI using regular ECS like every other part of your app.
TLDR: It positions your entities as HTML objects for you, so you can slap custom rendering or images on them.
^ A recreation of Cyberpunk UI in Bevy. (Source code here).
Note
This library is EXPERIMENTAL.
Bevy_Lunex is built on a simple concept: to use Bevy's ECS as the foundation for UI layout and interaction, allowing developers to manage UI elements as they would any other entities in their game or application as opposed to bevy_ui.
-
Path-Based Hierarchy: Inspired by file system paths, this approach allows for intuitive structuring and nesting of UI elements. It's designed to make the relationship between components clear and manageable, using a syntax familiar to most developers, while also avoiding the safety restrictions Rust enforces (as they don't help but instead obstruct for UI).
-
Retained Layout Engine: Unlike immediate mode GUI systems, Bevy_Lunex uses a retained layout engine. This means the layout is calculated and stored, reducing the need for constant recalculations and offering potential performance benefits, especially for static or infrequently updated UIs.
-
ECS friendly: Since it's built with ECS, you can extend or customize the behavior of your UI by simply adding or modifying components. The scripting is done by regular systems and callbacks are done using events.
-
2D & 3D UI: One of the features of Bevy_Lunex is its support for both 2D and 3D UI elements, leveraging Bevy's
Transform
component. This opens up a wide range of possibilities for developers looking to integrate UI elements seamlessly into both flat and spatial environments. -
Mod picking: For interactions, we intagrate with bevy_mod_picking, which is getting upstreamed into Bevy. Lunex also provides custom picking backend, you just need add
"picking"
feature.
First, we need to define a component, that we will use to mark all entities that will belong to our ui system.
#[derive(Component)]
pub struct MyUiSystem;
Then we need to add UiPlugin
with our marker component. The NoData
generics are used if you need to store some data inside the nodes.
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(UiPlugin::<MyUiSystem>::new())
.run();
}
By marking any camera with MyUiSystem
, it will pipe its size into our future UI system entity.
commands.spawn((
MyUiSystem,
Camera2dBundle {
transform: Transform::from_xyz(0.0, 0.0, 1000.0),
..default()
}
));
Now we should create our entity with the UI system. The base componets are UiTree
+ Dimension
+ Transform
. The UiTreeBundle
already contains these components. The newly introduced Dimension
component is used as the source size for the UI system. We also need to add the MovableByCamera
component so our entity will receive updates from camera. The last step is adding our MyUiSystem
type as a generic.
commands.spawn((
UiTreeBundle::<MyUiSystem> {
tree: UiTree::new("MyUiSystem"),
..default()
},
MovableByCamera,
)).with_children(|ui| {
// Here we will spawn our UI in the next code block ...
});
Now, any entity with UiLayout
+ UiLink
spawned as a child of the UiTree
will be managed as a UI entity. If it has a Transform
component, it will get aligned based on the UiLayout
calculations taking place in the parent UiTree
. If it has a Dimension
component then its size will also get updated by the UiTree
output. This allows you to create your own systems reacting to changes in Dimension
and Transform
components.
You can add a UiImage2dBundle
to the entity to add images to your widgets. Or you can add another UiTree
as a child, which will use the computed size output in Dimension
component instead of a Camera
piping the size to it.
ui.spawn((
UiLink::<MyUiSystem>::path("Root"),
UiLayout::Window::FULL.pos(Ab(20.0)).size(Rl(100.0) - Ab(40.0)).pack(),
));
ui.spawn((
UiLink::<MyUiSystem>::path("Root/Rectangle"),
UiLayout::Solid::new().size(Ab((1920.0, 1080.0))).pack(),
UiImage2dBundle::from(assets.load("background.png")),
));
UiLink
is what is used to define the the custom hierarchy. It uses /
as the separator. If any of the names don't internally exist inside the parent UiTree
, it will create them.
As you can see in the terminal (If you have added a UiDebugPlugin
), the final structure looks like this:
> MyUiSystem == Window [pos: (x: 0, y: 0) size: (x: 100%, y: 100%)]
|-> Root == Window [pos: (x: 20, y: 20) size: (x: -40 + 100%, y: -40 + 100%)]
| |-> Rectangle == Solid [size: (x: 1920, y: 1080) align_x: 0 align_y: 0]
Quite simple, isn't it? Best part is that by relying on components only, you are potentially able to hot-reload UI or even stream UI over the network. The downside is that by relying on strings to link entities, we are giving up some safety that Rust provides. But I am all for using the right tools for the right task. By putting away some safety, we can skip the bothersome bloat that would otherwise be required for such application.
There are multiple nodes in UiLayout
.
Boundary
- Defined by point1 and point2, it is not influenced by UI flow and is absolutely positioned.Window
- Defined by point and size, it is not influenced by UI flow and is absolutely positioned.Solid
- Defined by size only, it will scale to fit the parenting node. It is not influenced by UI flow.Div
- Defined by padding & margin. Dictates the UI flow. It uses styleform paradigm, very similar to HTML.
Warning
Div
is not finished, it's WIP, please refrain from using it.
This library comes with several UI units. They are:
Ab
- Stands for absolute, usuallyAb(1)
= 1pxRl
- Stands for relative, it meansRl(1.0)
== 1%Rw
- Stands for relative width, it meansRw(1.0)
== 1%w, but when used in height field, it will use width as sourceRh
- Stands for relative height, it meansRh(1.0)
== 1%h, but when used in width field, it will use height as sourceEm
- Stands for size of symbol M, it meansEm(1.0)
== 1em, so size 16px if font size is 16pxSp
- Stands for remaining space, it's used as proportional ratio between margins, to replace alignment and justification. Only used byDiv
Vp
- Stands for viewport, it meansVp(1.0)
== 1v% of theUiTree
original sizeVw
- Stands for viewport width, it meansVw(1.0)
== 1v%w of theUiTree
original size, but when used in height field, it will use width as sourceVh
- Stands for viewport height, it meansVh(1.0)
== 1v%h of theUiTree
original size, but when used in width field, it will use height as source
Warning
Sp
is not finished, it's WIP, please refrain from using it.
Bevy | Bevy Lunex |
---|---|
0.13.2 | 0.1.0 - latest |
0.12.1 | 0.0.10 - 0.0.11 |
0.12.0 | 0.0.7 - 0.0.9 |
0.11.2 | 0.0.1 - 0.0.6 |
Warning
Any version below 0.0.X is experimental and is not intended for practical use.
Any contribution submitted by you will be dual licensed as mentioned below, without any additional terms or conditions. If you have the need to discuss this, please contact me.
Released under both APACHE and MIT licenses. Pick one that suits you the most!