Skip to content

Commit

Permalink
Add text rendering (#25)
Browse files Browse the repository at this point in the history
* Add drawable implementation

* Add text rendering

* Fix inline and impl

* Fix imports

* Update README.md
  • Loading branch information
DraftedDev committed Jul 29, 2023
1 parent b09562f commit 92ac570
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 8 deletions.
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ serde = { version = "1.0.164", default-features = false, optional = true }
hashbrown = { version = "0.14.0", optional = true }
serde_json = { version = "1.0.85", optional = true }
kira = { version = "0.8.4", optional = true }
fontdue = { version = "0.7.3", optional = true }

[features]
# Contains store feature
Expand All @@ -35,12 +36,19 @@ serde = ["dep:serde", "mint/serde", "serde/derive", "serde/std", "serde_json"]
store = ["serde", "hashbrown/serde"]
# Audio functionalities
audio = ["dep:kira"]
# Text and Font rendering
text = ["dep:fontdue"]

[[example]]
name = "shapes"
path = "examples/shapes.rs"
harness = false

[[example]]
name = "text"
path = "examples/text.rs"
harness = false

[[example]]
name = "move"
path = "examples/move.rs"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ The focus of this Project is to develop a Game Engine to make 2D Games fast, per
- **Fun**: Qilin is not a serious engine, but rather a fun Project to make fun Games with fun.
- **Compile Times**: Developing Games in Rust can be very slow, due to compile times. Qilin tries to fix this by reducing features & dependencies.
- **Performance:** While having great compile times, Qilin still tries to use the features of Rust to optimize performance and memory usage.
- **Modular:** Qilin is tiny by default. You can however add more features and add additions to your game-flow.
- **Lightweight:** Qilin is designed to make games with, however if you like to you can build your own engine on top.
- **Modular:** Qilin is tiny by default. You can however add more features and additions to your game-flow.
- **Lightweight:** Qilin is tiny and inspired by [MonoGame](https://en.wikipedia.org/wiki/MonoGame), so you can build your own engine on top, if you want.

## Cargo Features

- `default`: Only contains `store` feature.
- `text`: Contain the `text` module.
- `audio`: Contains the `audio` module.
- `minifb`: Exports internal minifb crate.
- `image`: Exports module to convert Images from the `image` crate to Qilin Images.
Expand Down
Binary file added examples/assets/Roboto-Medium.ttf
Binary file not shown.
70 changes: 70 additions & 0 deletions examples/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Demonstrates drawing text on the canvas

use qilin::game::context::GameContext;
use qilin::game::game::Game;
use qilin::render::canvas::Canvas;
use qilin::render::color::Color;

use qilin::scene::Scene;
use qilin::simplified::vec2;
use qilin::text::TextSketch;
use qilin::types::{GameConfig, FPS30};
use qilin::ScaleMode;
use qilin::WindowOptions;

struct TextScene;

impl Scene for TextScene {
// create new empty scene
fn new() -> Self
where
Self: Sized,
{
Self
}

// gets called when game enters current scene
fn enter(&mut self) { println!("What do you call a fake noodle?") }

// gets called when window requests draw updates
fn update(&mut self, canvas: &mut Canvas, _ctx: &mut GameContext) {
canvas.drawable(
&TextSketch::new(vec2(10, 10), include_bytes!("assets/Roboto-Medium.ttf"))
.with_color(Color::RED)
.with_text("Hello World!", 30.0),
);

canvas.drawable(
&TextSketch::new(vec2(10, 100), include_bytes!("assets/Roboto-Medium.ttf"))
.with_color(Color::YELLOW)
.with_text("Implementing text-rendering was real pain.", 40.0),
);
}

fn fixed_update(&mut self, _canvas: &mut Canvas, _ctx: &mut GameContext) {
// Will be called X times per second.
// This ensures, physics are applied independent of frame-rate.
// See https://docs.unity3d.com/ScriptReference/MonoBehaviour.FixedUpdate.html for FixedUpdate() in Unity.
}

// gets called when game exits current scene
fn exit(&mut self) { println!("An impasta!") }
}

fn main() {
Game::new::<TextScene>() // create game object with ShapeScene as entry scene
.with_config(GameConfig {
title: "My Texts".to_string(), // set window title
update_rate_limit: FPS30, // limit update rate to 30 fps, default is 60 fps
width: 800, // set initial width
height: 600, // set initial height
fixed_time_step: Default::default(), // for better docs, see GameConfig or examples/move.
window: WindowOptions {
scale_mode: ScaleMode::AspectRatioStretch, // scale pixels to fit in aspect ratio
resize: true, // make window resizeable
..Default::default()
},
})
.play()
.expect("Failed to play game");
}
16 changes: 15 additions & 1 deletion src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct AudioManager {

impl AudioManager {
/// Create a new instance, returning [None] if a [kira] error occurred.
#[inline]
pub fn new() -> Option<AudioManager> {
Some(AudioManager {
sounds: Vec::new(),
Expand All @@ -24,6 +25,7 @@ impl AudioManager {
/// **volume**: The volume of the sound in decibels.\
/// **panning**: The panning of the sound, where 0 is hard left and 1 is hard right.\
/// **reverse**: Whether to play the sound in reverse.
#[inline]
pub fn load(
&mut self,
path: impl AsRef<Path>,
Expand All @@ -47,24 +49,29 @@ impl AudioManager {
/// Play a loaded sound by index.\
/// Make sure you used [AudioManager::load] to load a sound first.\
/// Errors if the sound does not exist or a [kira] error occurred.
#[inline]
pub fn play(&mut self, index: usize) -> Option<()> {
self.kira.play(self.sounds.get(index)?.clone()).ok()?;
Some(())
}

/// Returns the [kira::manager::AudioManager].\
/// Requires you to add the [kira] crate as dependency.
#[inline]
pub fn kira(&self) -> &kira::manager::AudioManager { &self.kira }

/// Returns a `Vec` of [kira::sound::static_sound::StaticSoundData].\
/// Returns a `Vec` of [StaticSoundData].\
/// Requires you to add the [kira] crate as dependency.
#[inline]
pub fn sounds(&self) -> &Vec<StaticSoundData> { &self.sounds }

/// Unloads the given sound data by index.
#[inline]
pub fn unload(&mut self, index: usize) { self.sounds.remove(index); }

/// Sets the volume of the sound at the given index.\
/// Returns [None] if the sound does not exist.
#[inline]
pub fn set_volume(&mut self, index: usize, volume: f64) -> Option<()> {
self.sounds.get_mut(index)?.settings.volume =
Value::from(Volume::Decibels(volume).as_amplitude());
Expand All @@ -73,19 +80,22 @@ impl AudioManager {

/// Sets the panning of the sound at the given index.\
/// Returns [None] if the sound does not exist.
#[inline]
pub fn set_panning(&mut self, index: usize, panning: Panning) -> Option<()> {
self.sounds.get_mut(index)?.settings.panning = Value::from(panning.to_f64());
Some(())
}

/// Sets if the sound at index should be reversed.\
/// Returns [None] if the sound does not exist.
#[inline]
pub fn set_reverse(&mut self, index: usize, reverse: bool) -> Option<()> {
self.sounds.get_mut(index)?.settings.reverse = reverse;
Some(())
}

/// Get volume of the sound at the given index as decibels.
#[inline]
pub fn get_volume(&self, index: usize) -> Option<f64> {
if let Value::Fixed(vol) = self.sounds.get(index)?.settings.volume {
Some(vol.as_decibels())
Expand All @@ -95,6 +105,7 @@ impl AudioManager {
}

/// Get panning of the sound at the given index.
#[inline]
pub fn get_panning(&self, index: usize) -> Option<Panning> {
if let Value::Fixed(pan) = self.sounds.get(index)?.settings.panning {
Some(Panning::From(pan))
Expand All @@ -104,6 +115,7 @@ impl AudioManager {
}

/// Get if the sound at the given index should be reversed.
#[inline]
pub fn get_reverse(&self, index: usize) -> Option<bool> {
Some(self.sounds.get(index)?.settings.reverse)
}
Expand Down Expand Up @@ -133,10 +145,12 @@ pub enum Panning {
}

impl Default for Panning {
#[inline]
fn default() -> Self { Self::Normal }
}

impl Panning {
#[inline]
pub fn to_f64(&self) -> f64 {
match self {
Self::HardLeft => 0.0,
Expand Down
2 changes: 1 addition & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use image::{DynamicImage, Rgb};
pub fn dynamic_to_img(dynamic: DynamicImage) -> Image { rgb_to_img(dynamic.to_rgb8().pixels()) }

/// Convert Rgb<u8> Pixels to a qilin `Image`.
#[inline(never)]
#[inline]
pub fn rgb_to_img(rgb: Pixels<Rgb<u8>>) -> Image {
let mut vec: Image = Vec::with_capacity(rgb.len());
for px in rgb {
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ pub mod image;
#[cfg(feature = "audio")]
pub mod audio;

/// Contains text rendering functions.
/// Requires `text` feature.
#[cfg(feature = "text")]
pub mod text;

pub use minifb::Key;
pub use minifb::KeyRepeat;
pub use minifb::Scale;
Expand Down
6 changes: 5 additions & 1 deletion src/render/canvas.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::render::color::Color;
use crate::render::sketch::Sketch;
use crate::render::sketch::{Drawable, Sketch};

/// Canvas of a game, containing a buffer of pixels to draw to the window.
#[derive(Clone)]
Expand Down Expand Up @@ -84,6 +84,10 @@ impl Canvas {
}
}

/// Draw a [Drawable] to the canvas.
#[inline]
pub fn drawable<T: Drawable>(&mut self, drawable: &T) { drawable.apply(self); }

/// Get window width.
#[inline]
pub fn width(&self) -> usize { self.width }
Expand Down
10 changes: 7 additions & 3 deletions src/render/sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,14 @@ pub enum Operation {
},
}

impl Operation {
pub trait Drawable {
fn apply(&self, canvas: &mut Canvas);
}

impl Drawable for Operation {
/// Apply operation to a [Canvas].
#[inline(never)]
pub fn apply(&self, canvas: &mut Canvas) {
#[inline]
fn apply(&self, canvas: &mut Canvas) {
match self {
Operation::Oval {
pos,
Expand Down
9 changes: 9 additions & 0 deletions src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ use crate::render::canvas::Canvas;

/// Trait to represent a scene in the `Game`.
pub trait Scene {
#[inline(never)]
fn new() -> Self
where
Self: Sized;

#[inline(never)]
fn enter(&mut self);

#[inline]
fn update(&mut self, canvas: &mut Canvas, ctx: &mut GameContext);

#[inline]
fn fixed_update(&mut self, canvas: &mut Canvas, ctx: &mut GameContext);

#[inline(never)]
fn exit(&mut self);
}
79 changes: 79 additions & 0 deletions src/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use crate::render::canvas::Canvas;
use crate::render::color::Color;
use crate::render::sketch::Drawable;
use fontdue::layout::{CoordinateSystem, Layout, TextStyle};
use fontdue::Font;
use mint::Vector2;

/// Sketch to draw text on a canvas.
pub struct TextSketch {
layout: Layout,
font: Font,
color: Color,
pos: Vector2<u32>,
}

impl TextSketch {
/// Create a new empty [TextSketch] with font data.
///
/// ### Example
/// ```rust
/// use mint::Vector2;
/// use qilin::text::TextSketch;
///
/// let font = include_bytes!("myAssets/myFont.ttf");
/// let sketch = TextSketch::new(Vector2::new(10, 10), font);
/// ```
#[inline]
pub fn new(pos: Vector2<u32>, font: &[u8]) -> Self {
Self {
layout: Layout::new(CoordinateSystem::PositiveYDown),
color: Color::BLACK,
font: Font::from_bytes(font, Default::default()).expect("Failed to load font"),
pos,
}
}

/// Set the color of the text.
#[inline]
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}

/// Append new text to the layout with `text` as text and `px` as size in pixels.
#[inline]
pub fn with_text(mut self, text: &str, px: f32) -> Self {
self.layout
.append(&[self.font.clone()], &TextStyle::new(text, px, 0));
self
}
}

impl Drawable for TextSketch {
#[inline]
fn apply(&self, canvas: &mut Canvas) {
self.layout.glyphs().iter().for_each(|glyph| {
let (metrics, buffer) = self.font.rasterize_config(glyph.key);

let width = metrics.width;
let height = metrics.height;

// Calculate the starting position to draw the glyph
let x0 = self.pos.x + glyph.x as u32;
let y0 = self.pos.y + glyph.y as u32;

// Draw the glyph to the canvas using the specified color
for y in 0..height {
for x in 0..width {
let alpha = buffer[y * width + x] as u8;
if alpha > 0 {
let x_coord = x0 + x as u32;
let y_coord = y0 + y as u32;
canvas.set_pixel(x_coord as usize, y_coord as usize, &self.color);
}
}
}
});
}
}

0 comments on commit 92ac570

Please sign in to comment.