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, ) } }