Blazingly fast retained layout engine for Bevy entities, built around vanilla Bevy ECS. It gives you the ability to make your own custom UI using regular ECS like every other part of your app.
important
This book is made for version 0.3.X
of Bevy_Lunex
note
This crate is being maintained by a university student. Don't expect updates during the semester.
warning
This crate is opinionated and thus you must decide if it is a good fit for what you want to achieve.
This is mainly because Lunex provides you with only capability to position entities, leaving everything else in your hands. The current version also lacks any kind of flexbox-like layout.
Good fit π
- Worldspace 3D UI
- Spritebased 2D UI
- Custom rendering hook
- Very customizable
- Low-level interactivity
Not so good π
- Development speed & iteration
- Using prebuilt input components
- Making desktop application UI
Installation & Setup
Adding Bevy_Lunex
to your project is straightforward, just like any other Rust crate.
Add the following to your Cargo.toml
:
[dependencies]
bevy_lunex = { version = "*" }
Alternatively, you can use the latest bleeding edge version from the Git repository:
[dependencies]
bevy_lunex = { git = "https://github.com/bytestring-net/bevy_lunex" }
Project Setup
You have to add the UiLunexPlugin
to your application.
fn main() -> AppExit {
App::new()
// Add necessary plugins
.add_plugins((DefaultPlugins, UiLunexPlugin))
.run()
}
Next you have to spawn your camera. Your main camera must have the UiSourceCamera::<N>
component, with N
being a constant from 0..3
range.
note
The purpose of this is that if you are creating a splitscreen game, you can have up to 4 cameras. This component tells the UI which camera's viewport size to use as the root node size.
tip
If you need more indexes, you can add UiLunexIndexPlugin::<N>
for said index manually.
fn spawn_camera(mut commands: Commands) {
// Spawn the camera
commands.spawn((
// This camera will become the source for all UI paired to index 0.
Camera2d, UiSourceCamera::<0>,
// Ui nodes start at 0 and move + on the Z axis with each depth layer.
// This will ensure you will see up to 1000 nested children.
Transform::from_translation(Vec3::Z * 1000.0),
// Explained in # Chapters/Debug-Tooling section of the book
RenderLayers::from_layers(&[0, 1]),
));
}
Debug Tooling
Sometimes it is hard to know why your UI is behaving unexpectedly. To help you with debugging, Lunex offers additional tooling that should make your life a little bit easier.
To enable it, you have to add UiLunexDebugPlugin::<R_2D, R_3D>
to your application.
The generics are constants used for RenderLayers
inside the debug plugin.
R_2D
: Specifies which render layer should 2D gizmos use.R_3D
: Specifies which render layer should 3D gizmos use.
If you don't use RenderLayers
for any other purpose, then you can add the plugin with these values:
UiLunexDebugPlugin::<1, 2>
This also means that you have to add a properly configured RenderLayers
component to your cameras
if you want to see these outlines.
-
For
Camera2d
:RenderLayers::from_layers(&[0, 1])
-
For
Camera3d
:RenderLayers::from_layers(&[0, 2])
This will draw gizmo outlines around all UI nodes, allowing you to see their positions and sizes.
Additionally, it will print the layouts to the terminal whenever a change is detected.
βΆ 11v1 β [w: 1920, h: 1080]
ββ Background β [w: 1920, h: 1080, d: 1] β Solid
ββ 13v1 β [w: 595, h: 1080, d: 1] β Solid
β ββ Panel β [w: 624, h: 1134, d: 2] β Window
β ββ 15v1 β [w: 624, h: 216, d: 2] β Window
β β ββ Logo β [w: 624, h: 192, d: 3] β Solid
β ββ 17v1 β [w: 327, h: 367, d: 2] β Window
β β ββ New Game β [w: 327, h: 51, d: 3] β Window
β β β ββ 23v1 β [w: 327, h: 51, d: 4] β Window
β β β β ββ 24v1 β [w: 113, h: 31, d: 5] β Window
β β β β ββ 25v1 β [w: 22, h: 31, d: 5] β Window
β β ββ Settings β [w: 327, h: 51, d: 3] β Window
β β β ββ 31v1 β [w: 327, h: 51, d: 4] β Window
β β β β ββ 32v1 β [w: 98, h: 31, d: 5] β Window
β β β β ββ 33v1 β [w: 22, h: 31, d: 5] β Window
β β ββ Quit Game β [w: 327, h: 51, d: 3] β Window
β β β ββ 43v1 β [w: 327, h: 51, d: 4] β Window
β β β β ββ 44v1 β [w: 111, h: 31, d: 5] β Window
β β β β ββ 45v1 β [w: 22, h: 31, d: 5] β Window
Quick start
Now that we have everything setup, let's create some quick UI.
First, spawn a UiLayoutRoot
. This is where our UI will start. You can specify the size of the UI viewport with Dimension
component, but for 2D we don't want that. Instead we add UiFetchFromCamera::<N>
with N
being the index of our camera's UiSourceCamera::<N>
component.
This will ensure that the Camera -> Dimension -> UiLayout
pipeline will always be up to date.
// Create UI
commands.spawn((
// Initialize the UI root for 2D
UiLayoutRoot::new_2d(),
// Make the UI synchronized with camera viewport size
UiFetchFromCamera::<0>,
)).with_children(|ui| {
// ... Here we will spawn our UI
});
And now inside the with_children
closure we will spawn a red rectange node.
This rectangle will be position exactly in the middle of our screen and
with width 200px
and height 50px
.
ui.spawn((
// You can name the entity
Name::new("My Rectangle"),
// Specify the position and size of the button
UiLayout::window()
.anchor(Anchor::Center) // Put the origin at the center
.pos(Rl((50.0, 50.0))) // Set the position to 50%
.size((200.0, 50.0)) // Set the size to [200.0, 50.0]
.pack(),
// Color the sprite with red color
UiColor::from(Color::srgb(1.0, 0.0, 0.0)),
// Attach sprite to the node
Sprite::from_image(asset_server.load("images/button.png")),
// When hovered, it will request the cursor icon to be changed
OnHoverSetCursor::new(SystemCursorIcon::Pointer),
// Interactivity is done through observers, you can query anything here
)).observe(|_: Trigger<Pointer<Click>>, mut exit: EventWriter<AppExit>| {
// Close the app on click
exit.send(AppExit::Success);
});
And thats it! You can of course do much more with the crate. Continue reading to learn on how to spawn text nodes, enable animations and much more!
Base Units
Lunex features 9 different UI units, which are used as arguments for UiValue<T>
. The T
is expected to be f32
, Vec2
, Vec3
or Vec4
. They are used in layout functions where impl Into<UiValue<T>>
is specified as argument.
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 16pxVp
- 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
Basic Operations
All unit types implement basic mathematical operations:
let a: Ab<f32> = Ab(4.0) + Ab(6.0); // -> 10px
let b: Ab<f32> = Ab(4.0) * 2.0; // -> 8px
You can also combine different unit types:
let a: UiValue<f32> = Ab(4.0) + Rl(6.0); // -> 4px + 6%
If a unit is unspecified, the f32
value is considered to be in Ab
unit:
let a: Ab<f32> = 5.0.into(); // -> 5px
Vector Definitions
You can easily define vectors using these units:
let a: UiValue<Vec2> = Ab(10.0).into(); // -> [10px, 10px]
let b: UiValue<Vec2> = Ab((10.0, 15.0)).into(); // -> [10px, 15px]
let c: UiValue<Vec2> = (Ab(10.0), Rl(5.0)).into(); // -> [10px, 5%]
Works for larger vectors like Vec3
and Vec4
the same.
tip
If you put them as arguments to impl Into<UiValue<T>>
, you don't have to call .into()
.
Layouts
There are multiple layouts that you can utilize to achieve the structure you are aiming for.
Boundary
Defined by point1 and point2, it is not influenced by UI flow and is absolutely positioned.
- pos1 - Position of the top-left corner
- pos2 - Position of the bottom-right corner
This will make a node start at 20%
and end at 80%
on both axis from the parent node.
UiLayout::boundary()
.pos1(Rl(20.0))
.pos2(Rl(80.0))
.pack()
Window
Defined by position and size, it is not influenced by UI flow and is absolutely positioned.
- pos - Position of the node
- anchor - The origin point relative to the rest of the node
- size - Size of the node
This will make a node centered at x: 53%
, y: 15%
and with size width: 60%
and height: 65%
.
UiLayout::window()
.pos(Rl((53.0, 15.0)))
.anchor(Anchor::Center)
.size(Rl((60.0, 65.0)))
.pack()
Solid
Defined by size only, it will scale to fit the parenting node. It is not influenced by UI flow.
- size - Aspect ratio, it doesn't matter if it is
(10, 10)
or(100, 100)
- align_x - Horizontal alignment,
-1.0 to 1.0
with0.0
as default - align_y - Vertical alignment,
-1.0 to 1.0
with0.0
as default - scaling - If the container should
fit
inside parent orfill
the parent
tip
This layout is ideal for images, because it preserves aspect ratio under all costs.
Here we will set aspect ratio to the size of our imaginary texture (881.0, 1600.0)
in pixels.
Then we can align it horizontally.
UiLayout::solid()
.size((881.0, 1600.0))
.align_x(-0.74)
.pack(),
Contributors
This documentation is maintained by
Help
For issues related to the library, please create a ticket on GitHub
.
You can also reach out to me on the Bevy Discord
, where you can use the Bevy Lunex thread
.
note
For specific questions, feel free to send me a direct message on Discord: @idedary
.
Just make sure you are in the Bevy discord or otherwise I will ignore you.