Tainted\\Coders

Bevy Text

Bevy version: 0.15Last updated:

Text recently went through a major overhaul in Bevy 0.15.

Text can either be rendered through the UI or as part of your games world.

This functionality is provided through the bevy_text crate. It adds a plugin that provides text resources, systems, and font assets to your game. It also adds a system to your render app which helps find sprites during your renders extract loop.

The core responsibility of bevy_text is to take our Text components and turn them into a series of positioned graphemes for the renderer. A grapheme refers to the smallest unit of a writing system that carries meaning.

When you add a Text component Bevy will use a GlyphBrush to position the letters of text and cache those positions so that on another render pass we don't update them unless we need to.

Graphemes are then rendered according to a font. A font is essentially a bunch of sprites contained inside a group of TextureAtlas called a FontAtlas which helps optimize text rendering.

These sprites are then scaled and transformed to be positioned on your screen with a proper size when they are rendered by your GPU.

When you add a font to your FontAtlas it uses a DynamicTextureAtlasBuilder to load at runtime only the exact glyph sprites it needs to display the text on your screen.

The default font provided by Bevy is Fira Mono and the supported font types are ttf and otf. You load fonts using a FontLoader that works similarly to your other assets.

The text component

Text is either rendered through the UI with a Text component, or inside your game world with a Text2d component.

If we wanted our text to interact with or be part of our game world then we should choose Text.

If instead we wanted our text positioned in relation to our window (like a HUD, or an inventory screen) then we should use Text2d and make it part of our UI.

By placing one of these components onto an entity, other systems in bevy_text and the core renderer will spring into action.

To style your text there are 3 main components we can add to Text entities:

  1. TextLayout
  2. TextFont
  3. TextColor

Each one has fields for changing the style of your text. By adding them to a component that has a Text or Text2d component you are overriding the defaults for that particular node.

Creating text

We can render the text as part of the scene by assembling the various components on a regular entity:

fn spawn_in_scene(mut commands: Commands, assets: Res<AssetServer>) {
  let font = assets.load("fonts/FiraSans-Bold.ttf");
  let text_color = TextColor(Color::WHITE);
  let text_font = TextFont {
    font: font.clone(),
    font_size: 60.0,
    ..default()
  };
  let text_alignment = JustifyText::Center;
  let text = Text2d::new("This is some text in the scene");

  commands.spawn((
    text,
    text_color,
    text_font
  ));
}

This text will be positioned just like your other components and rendered normally in the scene.

Lets say we have some text with two styles we want to keep together like:

Here is some text. But this part is bold

This is ultimately a single text span, but each part needs a separate style. We can represent this by creating two text components with the first as a parent to the second with a TextSpan component on the child:

fn spawn_in_ui(mut commands: Commands, assets: Res<AssetServer>) {
  let regular_font_handle: Handle<Font> = Default::default();
  let bold_font_handle: Handle<Font> = assets.load("fonts/Roboto-Bold.ttf");

  commands
    .spawn((
      Text::new("Here is some text. But "),
      TextFont {
        font: regular_font_handle.clone(),
        ..default()
      },
      Node {
        position_type: PositionType::Absolute,
        bottom: Val::Px(5.0),
        right: Val::Px(5.0),
        ..default()
      },
    ))
    .with_child((
      TextSpan::default(),
      Text::new("this part is bold"),
      TextFont {
        font: bold_font_handle.clone(),
        ..default()
      },
    ));
}

This will allow Bevy's text layout engine to place these in the proper position next to each other and according to the rules of our Node.

Changing text

We can change the value of text rendered to the screen at anytime through the Text component we spawned.

For example, if we wanted to create an inventory screen where players could hover over items and we show them the name:

fn handle_hovered_item(
  mut text_query: Query<&mut Text, With<ItemName>>,
  item_query: Query<&Item, Added<Selected>>,
) {
  if let Ok(item_data) = item_query.get_single() {
    for mut text in text_query.iter_mut() {
      text.0 = item_data.name.clone();
    }
  }
}

It does not matter if your Text was added to the UI through a node or just as part of your scene, we still manipulate the components on the entities that spawn the exact same way.

Clicking on text

To click text we can use Bevy's built in picking triggers using observers.

First we create a handler for the observed event:

fn handle_click(
  trigger: Trigger<Pointer<Click>>,
  mut query: Query<&mut Text2d>
) {
  let mut text = query.get_mut(trigger.entity()).unwrap();
  text.0 = "You clicked me!".to_string();
}

Then we can choose the entities we want to observe:

fn spawn_clickable_text(mut commands: Commands) {
  commands
    .spawn(Text2d::new("This is some text in the scene"))
    .observe(handle_click);
}

Now when you click the text it changes to "You clicked me!".