mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-03-21 09:29:26 +00:00
implement bmfont rendering
This commit is contained in:
parent
6332bcdbc7
commit
98fb3a24e1
|
@ -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
111
src/bmfont_renderer.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -173,8 +173,6 @@ impl VFS for BuiltinFS {
|
|||
return Err(FilesystemError(msg));
|
||||
}
|
||||
|
||||
log::info!("open: {:?}", path);
|
||||
|
||||
self.get_node(path)?.to_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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
0
src/render/mod.rs
Normal 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)?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue