implement bmfont rendering

This commit is contained in:
Alula 2020-08-29 08:59:46 +02:00
parent 6332bcdbc7
commit 98fb3a24e1
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
9 changed files with 163 additions and 50 deletions

View File

@ -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<R: io::Read + io::Seek>(mut data: R) -> GameResult<Self> {
let mut magic = [0u8; 4];
let mut pages = 0u16;

111
src/bmfont_renderer.rs Normal file
View File

@ -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<String>,
}
impl BMFontRenderer {
pub fn load(root: &str, desc_path: &str, ctx: &mut Context) -> GameResult<BMFontRenderer> {
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<I: Iterator<Item=char>>(&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::<usize>::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::<usize>::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(())
}
}

View File

@ -173,8 +173,6 @@ impl VFS for BuiltinFS {
return Err(FilesystemError(msg));
}
log::info!("open: {:?}", path);
self.get_node(path)?.to_file()
}

View File

@ -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<Direction> {
match val {

View File

@ -113,6 +113,9 @@ pub struct EngineConstants {
pub world: WorldConsts,
pub tex_sizes: HashMap<String, (usize, usize)>,
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;
}
}

View File

@ -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<Caret>,
pub key_state: KeyState,
pub key_trigger: KeyState,
pub font: BMFontRenderer,
pub texture_set: TextureSet,
pub base_path: String,
pub stages: Vec<StageData>,
@ -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),

0
src/render/mod.rs Normal file
View File

View File

@ -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::<usize>::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::<usize>::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)?;

View File

@ -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<usize>) {
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<usize>) {
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(())
}
}