From 98fb3a24e1aa674d68bd38de3f2dda113a8940b1 Mon Sep 17 00:00:00 2001 From: Alula Date: Sat, 29 Aug 2020 08:59:46 +0200 Subject: [PATCH] implement bmfont rendering --- src/bmfont.rs | 4 +- src/bmfont_renderer.rs | 111 ++++++++++++++++++++++++++++++++++++++++ src/builtin_fs.rs | 2 - src/common.rs | 2 + src/engine_constants.rs | 12 +++++ src/main.rs | 8 +++ src/render/mod.rs | 0 src/scene/game_scene.rs | 46 ++++------------- src/texture_set.rs | 28 ++++++---- 9 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 src/bmfont_renderer.rs create mode 100644 src/render/mod.rs diff --git a/src/bmfont.rs b/src/bmfont.rs index ce016c8..9602a8a 100644 --- a/src/bmfont.rs +++ b/src/bmfont.rs @@ -21,7 +21,7 @@ pub struct BmChar { } #[derive(Debug)] -pub struct BmFont { +pub struct BMFont { pub pages: u16, pub font_size: i16, pub line_height: u16, @@ -31,7 +31,7 @@ pub struct BmFont { const MAGIC: [u8; 4] = [b'B', b'M', b'F', 3]; -impl BmFont { +impl BMFont { pub fn load_from(mut data: R) -> GameResult { let mut magic = [0u8; 4]; let mut pages = 0u16; diff --git a/src/bmfont_renderer.rs b/src/bmfont_renderer.rs new file mode 100644 index 0000000..4e685f7 --- /dev/null +++ b/src/bmfont_renderer.rs @@ -0,0 +1,111 @@ +use std::collections::HashSet; +use std::path::PathBuf; + +use crate::bmfont::BMFont; +use crate::common::{FILE_TYPES, Rect}; +use crate::engine_constants::EngineConstants; +use crate::ggez::{Context, filesystem, GameResult}; +use crate::ggez::GameError::ResourceLoadError; +use crate::str; +use crate::texture_set::TextureSet; + +pub struct BMFontRenderer { + font: BMFont, + pages: Vec, +} + +impl BMFontRenderer { + pub fn load(root: &str, desc_path: &str, ctx: &mut Context) -> GameResult { + let root = PathBuf::from(root); + let full_path = &root.join(PathBuf::from(desc_path)); + let desc_stem = full_path.file_stem() + .ok_or_else(|| ResourceLoadError(str!("Cannot extract the file stem.")))?; + let stem = full_path.parent().unwrap_or(full_path).join(desc_stem); + + let font = BMFont::load_from(filesystem::open(ctx, &full_path)?)?; + let mut pages = Vec::new(); + + println!("stem: {:?}", stem); + let (zeros, ext, format) = FILE_TYPES + .iter() + .map(|ext| (1, ext, format!("{}_0{}", stem.to_string_lossy(), ext))) + .find(|(_, _, path)| filesystem::exists(ctx, &path)) + .or_else(|| FILE_TYPES + .iter() + .map(|ext| (2, ext, format!("{}_00{}", stem.to_string_lossy(), ext))) + .find(|(_, _, path)| filesystem::exists(ctx, &path))) + .ok_or_else(|| ResourceLoadError(format!("Cannot find glyph atlas 0 for font: {:?}", desc_path)))?; + + for i in 0..font.pages { + let page_path = format!("{}_{:02$}", stem.to_string_lossy(), i, zeros); + println!("x: {}", &page_path); + + pages.push(page_path); + } + + Ok(Self { + font, + pages, + }) + } + + pub fn draw_text>(&self, iter: I, x: f32, y: f32, constants: &EngineConstants, texture_set: &mut TextureSet, ctx: &mut Context) -> GameResult { + if self.pages.len() == 1 { + let batch = texture_set.get_or_load_batch(ctx, constants, self.pages.get(0).unwrap())?; + let mut offset_x = x; + + for chr in iter { + if let Some(glyph) = self.font.chars.get(&chr) { + batch.add_rect_scaled(offset_x, y + (glyph.yoffset as f32 * constants.font_scale).floor(), + constants.font_scale, constants.font_scale, + &Rect::::new_size( + glyph.x as usize, glyph.y as usize, + glyph.width as usize, glyph.height as usize, + )); + + offset_x += ((glyph.width as f32 + glyph.xoffset as f32) * constants.font_scale).floor() + if chr != ' ' { 1.0 } else { constants.font_space_offset }; + } + } + + batch.draw(ctx)?; + } else { + let mut pages = HashSet::new(); + let mut chars = Vec::new(); + + for chr in iter { + if let Some(glyph) = self.font.chars.get(&chr) { + pages.insert(glyph.page); + chars.push((chr, glyph)); + } + } + + for page in pages { + let page_tex = if let Some(p) = self.pages.get(page as usize) { + p + } else { + continue; + }; + + let batch = texture_set.get_or_load_batch(ctx, constants, page_tex)?; + let mut offset_x = x; + + for (chr, glyph) in chars.iter() { + if glyph.page == page { + batch.add_rect_scaled(offset_x, y + (glyph.yoffset as f32 * constants.font_scale).floor(), + constants.font_scale, constants.font_scale, + &Rect::::new_size( + glyph.x as usize, glyph.y as usize, + glyph.width as usize, glyph.height as usize, + )); + } + + offset_x += ((glyph.width as f32 + glyph.xoffset as f32) * constants.font_scale).floor() + if *chr != ' ' { 1.0 } else { constants.font_space_offset }; + } + + batch.draw(ctx)?; + } + } + + Ok(()) + } +} diff --git a/src/builtin_fs.rs b/src/builtin_fs.rs index f5106b5..1007353 100644 --- a/src/builtin_fs.rs +++ b/src/builtin_fs.rs @@ -173,8 +173,6 @@ impl VFS for BuiltinFS { return Err(FilesystemError(msg)); } - log::info!("open: {:?}", path); - self.get_node(path)?.to_file() } diff --git a/src/common.rs b/src/common.rs index d4cb1aa..b2005b2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -53,6 +53,8 @@ pub enum Direction { Bottom, } +pub const FILE_TYPES: [&str; 3] = [".png", ".bmp", ".pbm"]; + impl Direction { pub fn from_int(val: usize) -> Option { match val { diff --git a/src/engine_constants.rs b/src/engine_constants.rs index f09a10b..650640c 100644 --- a/src/engine_constants.rs +++ b/src/engine_constants.rs @@ -113,6 +113,9 @@ pub struct EngineConstants { pub world: WorldConsts, pub tex_sizes: HashMap, pub textscript: TextScriptConsts, + pub font_path: String, + pub font_scale: f32, + pub font_space_offset: f32, } impl Clone for EngineConstants { @@ -125,6 +128,9 @@ impl Clone for EngineConstants { world: self.world.clone(), tex_sizes: self.tex_sizes.clone(), textscript: self.textscript.clone(), + font_path: self.font_path.clone(), + font_scale: self.font_scale, + font_space_offset: self.font_space_offset, } } } @@ -374,6 +380,9 @@ impl EngineConstants { textbox_rect_middle: Rect { left: 0, top: 8, right: 244, bottom: 16 }, textbox_rect_bottom: Rect { left: 0, top: 16, right: 244, bottom: 24 }, }, + font_path: str!("builtin/builtin_font.fnt"), + font_scale: 1.0, + font_space_offset: -3.0, } } @@ -383,5 +392,8 @@ impl EngineConstants { self.is_cs_plus = true; self.tex_sizes.insert(str!("Caret"), (320, 320)); self.tex_sizes.insert(str!("MyChar"), (200, 384)); + self.font_path = str!("csfont.fnt"); + self.font_scale = 0.5; + self.font_space_offset = 2.0; } } diff --git a/src/main.rs b/src/main.rs index 705fc07..98a7e3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ use log::*; use pretty_env_logger::env_logger::Env; use winit::{ElementState, Event, KeyboardInput, WindowEvent}; +use crate::bmfont_renderer::BMFontRenderer; use crate::builtin_fs::BuiltinFS; use crate::caret::{Caret, CaretType}; use crate::common::{Direction, FadeState}; @@ -41,8 +42,10 @@ use crate::stage::StageData; use crate::text_script::TextScriptVM; use crate::texture_set::TextureSet; use crate::ui::UI; +use crate::ggez::GameError::ResourceLoadError; mod bmfont; +mod bmfont_renderer; mod builtin_fs; mod caret; mod common; @@ -103,6 +106,7 @@ pub struct SharedGameState { pub carets: Vec, pub key_state: KeyState, pub key_trigger: KeyState, + pub font: BMFontRenderer, pub texture_set: TextureSet, pub base_path: String, pub stages: Vec, @@ -154,6 +158,9 @@ impl Game { } else if filesystem::exists(ctx, "/stage.dat") || filesystem::exists(ctx, "/sprites.sif") { info!("NXEngine-evo data files detected."); } + let font = BMFontRenderer::load(base_path, &constants.font_path, ctx)?; + //.or_else(|| Some(BMFontRenderer::load("/", "builtin/builtin_font.fnt", ctx)?)) + //.ok_or_else(|| ResourceLoadError(str!("Cannot load game font.")))?; let s = Game { scene: None, @@ -171,6 +178,7 @@ impl Game { carets: Vec::with_capacity(32), key_state: KeyState(0), key_trigger: KeyState(0), + font, texture_set: TextureSet::new(base_path), base_path: str!(base_path), stages: Vec::with_capacity(96), diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 894ff7a..f6b3590 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -21,11 +21,6 @@ pub struct GameScene { pub player: Player, pub stage_id: usize, tex_background_name: String, - tex_caret_name: String, - tex_face_name: String, - tex_fade_name: String, - tex_hud_name: String, - tex_npcsym_name: String, tex_tileset_name: String, life_bar: usize, life_bar_count: usize, @@ -52,11 +47,6 @@ impl GameScene { info!("Map size: {}x{}", stage.map.width, stage.map.height); let tex_background_name = stage.data.background.filename(); - let tex_caret_name = str!("Caret"); - let tex_face_name = str!("Face"); - let tex_fade_name = str!("Fade"); - let tex_hud_name = str!("TextBox"); - let tex_npcsym_name = str!("Npc/NpcSym"); let tex_tileset_name = ["Stage/", &stage.data.tileset.filename()].join(""); Ok(Self { @@ -70,11 +60,6 @@ impl GameScene { }, stage_id: id, tex_background_name, - tex_caret_name, - tex_face_name, - tex_fade_name, - tex_hud_name, - tex_npcsym_name, tex_tileset_name, life_bar: 3, life_bar_count: 0, @@ -82,7 +67,7 @@ impl GameScene { } fn draw_number(&self, x: f32, y: f32, val: usize, align: Alignment, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_hud_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?; let n = val.to_string(); let align_offset = if align == Alignment::Right { n.len() as f32 * 8.0 } else { 0.0 }; @@ -96,7 +81,7 @@ impl GameScene { } fn draw_hud(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_hud_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?; // todo: max ammo display // none @@ -201,7 +186,7 @@ impl GameScene { } fn draw_carets(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_caret_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Caret")?; for caret in state.carets.iter() { batch.add_rect((((caret.x - caret.offset_x) / 0x200) - (self.frame.x / 0x200)) as f32, @@ -220,7 +205,7 @@ impl GameScene { graphics::clear(ctx, Color::from_rgb(0, 0, 32)); } FadeState::FadeIn(tick, direction) | FadeState::FadeOut(tick, direction) => { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_fade_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Fade")?; let mut rect = Rect::::new(0, 0, 16, 16); match direction { @@ -305,11 +290,11 @@ impl GameScene { fn draw_text_boxes(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { if !state.textscript_vm.flags.render() { return Ok(()); } - let top_pos = if state.textscript_vm.flags.position_top() { 32.0 } else { state.canvas_size.1 as f32 - 64.0 }; + let top_pos = if state.textscript_vm.flags.position_top() { 32.0 } else { state.canvas_size.1 as f32 - 66.0 }; let left_pos = (state.canvas_size.0 / 2.0 - 122.0).floor(); if state.textscript_vm.flags.background_visible() { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_hud_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?; batch.add_rect(left_pos, top_pos, &state.constants.textscript.textbox_rect_top); for i in 1..7 { @@ -321,7 +306,7 @@ impl GameScene { } if state.textscript_vm.face != 0 { - let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex_face_name)?; + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Face")?; batch.add_rect(left_pos + 14.0, top_pos + 8.0, &Rect::::new_size( (state.textscript_vm.face as usize % 6) * 48, (state.textscript_vm.face as usize / 6) * 48, @@ -335,24 +320,15 @@ impl GameScene { // todo: proper text rendering if !state.textscript_vm.line_1.is_empty() { - let line1: String = state.textscript_vm.line_1.iter().collect(); - Text::new(TextFragment::from(line1.as_str())).draw(ctx, DrawParam::new() - .dest(nalgebra::Point2::new((left_pos + text_offset) * 2.0 + 32.0, top_pos * 2.0 + 32.0)) - .scale(nalgebra::Vector2::new(0.5, 0.5)))?; + state.font.draw_text(state.textscript_vm.line_1.iter().copied(), left_pos + text_offset + 14.0, top_pos + 10.0, &state.constants, &mut state.texture_set, ctx)?; } if !state.textscript_vm.line_2.is_empty() { - let line2: String = state.textscript_vm.line_2.iter().collect(); - Text::new(TextFragment::from(line2.as_str())).draw(ctx, DrawParam::new() - .dest(nalgebra::Point2::new((left_pos + text_offset) * 2.0 + 32.0, (top_pos + 12.0) * 2.0 + 32.0)) - .scale(nalgebra::Vector2::new(0.5, 0.5)))?; + state.font.draw_text(state.textscript_vm.line_2.iter().copied(), left_pos + text_offset + 14.0, top_pos + 10.0 + 16.0, &state.constants, &mut state.texture_set, ctx)?; } if !state.textscript_vm.line_3.is_empty() { - let line3: String = state.textscript_vm.line_3.iter().collect(); - Text::new(TextFragment::from(line3.as_str())).draw(ctx, DrawParam::new() - .dest(nalgebra::Point2::new((left_pos + text_offset) * 2.0 + 32.0, (top_pos + 24.0) * 2.0 + 32.0)) - .scale(nalgebra::Vector2::new(0.5, 0.5)))?; + state.font.draw_text(state.textscript_vm.line_3.iter().copied(), left_pos + text_offset + 14.0, top_pos + 10.0 + 32.0, &state.constants, &mut state.texture_set, ctx)?; } Ok(()) @@ -360,7 +336,7 @@ impl GameScene { fn draw_tiles(&self, state: &mut SharedGameState, ctx: &mut Context, layer: TileLayer) -> GameResult { let tex = match layer { - TileLayer::Snack => &self.tex_npcsym_name, + TileLayer::Snack => "Npc/NpcSym", _ => &self.tex_tileset_name, }; let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex)?; diff --git a/src/texture_set.rs b/src/texture_set.rs index 44062fc..9bd357d 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -1,18 +1,19 @@ use std::collections::HashMap; -use std::io::{Read, BufReader, Seek, SeekFrom}; +use std::io::{BufReader, Read, Seek, SeekFrom}; +use image::RgbaImage; +use itertools::Itertools; +use log::info; + +use crate::common; +use crate::common::FILE_TYPES; +use crate::engine_constants::EngineConstants; use crate::ggez::{Context, GameError, GameResult}; use crate::ggez::filesystem; use crate::ggez::graphics::{Drawable, DrawParam, FilterMode, Image, Rect}; use crate::ggez::graphics::spritebatch::SpriteBatch; use crate::ggez::nalgebra::{Point2, Vector2}; -use itertools::Itertools; -use log::info; - -use crate::common; -use crate::engine_constants::EngineConstants; use crate::str; -use image::RgbaImage; pub struct SizedBatch { pub batch: SpriteBatch, @@ -58,6 +59,10 @@ impl SizedBatch { } pub fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect) { + self.add_rect_scaled(x, y, self.scale_x, self.scale_y, rect) + } + + pub fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect) { if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 { return; } @@ -68,7 +73,7 @@ impl SizedBatch { (rect.right - rect.left) as f32 / self.width as f32, (rect.bottom - rect.top) as f32 / self.height as f32)) .dest(Point2::new(x, y)) - .scale(Vector2::new(self.scale_x, self.scale_y)); + .scale(Vector2::new(scale_x, scale_y)); self.batch.add(param); } @@ -86,8 +91,6 @@ pub struct TextureSet { base_path: String, } -static FILE_TYPES: [&str; 3] = [".png", ".bmp", ".pbm"]; - impl TextureSet { pub fn new(base_path: &str) -> TextureSet { TextureSet { @@ -142,6 +145,10 @@ impl TextureSet { .iter() .map(|ext| [&self.base_path, name, ext].join("")) .find(|path| filesystem::exists(ctx, path)) + .or_else(|| FILE_TYPES + .iter() + .map(|ext| [name, ext].join("")) + .find(|path| filesystem::exists(ctx, path))) .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {:?} does not exist.", name)))?; info!("Loading texture: {}", path); @@ -180,7 +187,6 @@ impl TextureSet { } pub fn draw_text(&mut self, ctx: &mut Context, text: &str) -> GameResult { - Ok(()) } }