Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Screen-Space Text

Live Demo: HUD Text

Screen-space text rendering for UI, scores, and debug information.

Quick Start

#![allow(unused)]
fn main() {
fn initialize(&mut self, world: &mut World) {
    spawn_ui_text(world, "Score: 0", Vec2::new(20.0, 20.0));
}
}

Text Properties

Customize text appearance with TextProperties:

#![allow(unused)]
fn main() {
let properties = TextProperties {
    font_size: 32.0,
    color: Vec4::new(1.0, 1.0, 1.0, 1.0),
    alignment: TextAlignment::Center,
    vertical_alignment: VerticalAlignment::Top,
    line_height: 1.5,
    letter_spacing: 0.0,
    outline_width: 2.0,
    outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
    smoothing: 0.003,
    monospace_width: None,
    anchor_character: None,
};

spawn_ui_text_with_properties(
    world,
    "Custom Text",
    Vec2::new(20.0, 20.0),
    properties,
);
}

Text Alignment

#![allow(unused)]
fn main() {
TextAlignment::Left
TextAlignment::Center
TextAlignment::Right
}

Vertical Alignment

#![allow(unused)]
fn main() {
VerticalAlignment::Top
VerticalAlignment::Middle
VerticalAlignment::Bottom
VerticalAlignment::Baseline
}

Positioning

The position parameter is a Vec2 specifying screen-space coordinates:

#![allow(unused)]
fn main() {
spawn_ui_text(world, "Top-left area", Vec2::new(20.0, 20.0));

spawn_ui_text(world, "Centered", Vec2::new(400.0, 300.0));

spawn_ui_text(world, "Bottom area", Vec2::new(20.0, 550.0));
}

Updating Text at Runtime

Text content is stored in the TextCache. To update it, set the new string and mark the Text component dirty:

#![allow(unused)]
fn main() {
fn run_systems(&mut self, world: &mut World) {
    if let Some(entity) = self.score_text {
        if let Some(text) = world.core.get_text_mut(entity) {
            world.resources.text_cache.set_text(text.text_index, &format!("Score: {}", self.score));
            text.dirty = true;
        }
    }
}
}

Text Outlines

Add outlines for better visibility against varying backgrounds:

#![allow(unused)]
fn main() {
let properties = TextProperties {
    font_size: 24.0,
    color: Vec4::new(1.0, 1.0, 1.0, 1.0),
    outline_width: 2.0,
    outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
    ..Default::default()
};
}

Multi-line Text

Newlines in the string produce multiple lines. Line height is controlled by TextProperties::line_height:

#![allow(unused)]
fn main() {
let properties = TextProperties {
    line_height: 1.5,
    ..Default::default()
};

spawn_ui_text_with_properties(world, "Line 1\nLine 2\nLine 3", Vec2::new(20.0, 20.0), properties);
}

Custom Fonts

Load a font from bytes and use the returned index on the Text component:

#![allow(unused)]
fn main() {
fn initialize(&mut self, world: &mut World) {
    let font_bytes = include_bytes!("assets/fonts/custom.ttf").to_vec();
    match load_font_from_bytes(world, font_bytes, 48.0) {
        Ok(font_index) => self.custom_font = Some(font_index),
        Err(_) => {}
    }
}

fn run_systems(&mut self, world: &mut World) {
    if let (Some(entity), Some(font_index)) = (self.text_entity, self.custom_font) {
        if let Some(text) = world.core.get_text_mut(entity) {
            text.font_index = font_index;
            text.dirty = true;
        }
    }
}
}

3D World-Space Text

For text positioned in the 3D scene, use the world-space spawn functions. These create entities with transform components so the text exists at a position in world coordinates.

#![allow(unused)]
fn main() {
let properties = TextProperties {
    font_size: 0.5,
    ..Default::default()
};

spawn_3d_text_with_properties(world, "Sign", Vec3::new(0.0, 2.0, 0.0), properties);
}

Billboard Text

Billboard text always faces the camera:

#![allow(unused)]
fn main() {
let properties = TextProperties {
    font_size: 0.5,
    color: Vec4::new(1.0, 1.0, 0.0, 1.0),
    ..Default::default()
};

spawn_3d_billboard_text_with_properties(world, "Player Name", Vec3::new(0.0, 3.0, 0.0), properties);
}

The Text Component

All text entities carry a Text component:

#![allow(unused)]
fn main() {
pub struct Text {
    pub text_index: usize,
    pub properties: TextProperties,
    pub font_index: usize,
    pub dirty: bool,
    pub cached_mesh: Option<TextMesh>,
    pub billboard: bool,
}
}

Access it with world.core.get_text_mut(entity).

Removing Text

#![allow(unused)]
fn main() {
despawn_recursive_immediate(world, entity);
}

Common Patterns

Score Display

#![allow(unused)]
fn main() {
struct GameState {
    score: u32,
    score_text: Option<Entity>,
}

impl State for GameState {
    fn initialize(&mut self, world: &mut World) {
        let properties = TextProperties {
            font_size: 24.0,
            outline_width: 1.0,
            outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
            ..Default::default()
        };

        self.score_text = Some(spawn_ui_text_with_properties(
            world,
            "Score: 0",
            Vec2::new(20.0, 20.0),
            properties,
        ));
    }

    fn run_systems(&mut self, world: &mut World) {
        if let Some(entity) = self.score_text {
            if let Some(text) = world.core.get_text_mut(entity) {
                world.resources.text_cache.set_text(text.text_index, &format!("Score: {}", self.score));
                text.dirty = true;
            }
        }
    }
}
}

FPS Counter

#![allow(unused)]
fn main() {
struct FpsState {
    text_entity: Option<Entity>,
    frame_times: Vec<f32>,
}

impl State for FpsState {
    fn initialize(&mut self, world: &mut World) {
        let properties = TextProperties {
            font_size: 14.0,
            color: Vec4::new(0.0, 1.0, 0.0, 1.0),
            ..Default::default()
        };

        self.text_entity = Some(spawn_ui_text_with_properties(
            world,
            "FPS: --",
            Vec2::new(10.0, 10.0),
            properties,
        ));
    }

    fn run_systems(&mut self, world: &mut World) {
        let delta_time = world.resources.window.timing.delta_time;
        self.frame_times.push(delta_time);
        if self.frame_times.len() > 60 {
            self.frame_times.remove(0);
        }

        let average = self.frame_times.iter().sum::<f32>() / self.frame_times.len() as f32;
        let fps = (1.0 / average) as u32;

        if let Some(entity) = self.text_entity {
            if let Some(text) = world.core.get_text_mut(entity) {
                world.resources.text_cache.set_text(text.text_index, &format!("FPS: {}", fps));
                text.dirty = true;
            }
        }
    }
}
}

Game Over Message

#![allow(unused)]
fn main() {
fn show_game_over(world: &mut World) -> Entity {
    let properties = TextProperties {
        font_size: 64.0,
        color: Vec4::new(1.0, 0.0, 0.0, 1.0),
        alignment: TextAlignment::Center,
        outline_width: 3.0,
        outline_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
        ..Default::default()
    };

    spawn_ui_text_with_properties(
        world,
        "GAME OVER\nPress R to Restart",
        Vec2::new(400.0, 300.0),
        properties,
    )
}
}