use std::{path::Path, rc::Rc, sync::OnceLock};
use anyhow::Result;
use glam::{UVec2, Vec2};
use image::RgbaImage;
use crate::{
asset::{Asset, AssetManager, AssetType, FetchedTask},
camera::Camera,
color::Color,
font::{Font, Text},
handle::Handle,
input::InputState,
prelude::Transform,
render::{
BackendState, Command, CreateTextureCommand, DrawCommand, RemoveTextureCommand, Render,
ScaleMode,
},
renderer::{
default::DefaultRenderer,
screen::ScreenRenderer,
traits::{PostRenderer, SpriteRenderer},
},
sprite::Sprite,
text_cache::TextCache,
};
static DEFAULT_TEXTURE: OnceLock<Handle> = OnceLock::new();
const ENGINE_MAX_TICK: f32 = 100.0;
const DEFAULT_FONT_BYTES: &[u8; 59164] = include_bytes!("../assets/Pixel Square 10.ttf");
fn default_texture(g: &mut Engine) -> Handle {
DEFAULT_TEXTURE
.get_or_init(|| {
let data = RgbaImage::from_vec(1, 1, vec![255, 255, 255, 255])
.expect("Invalid RGBA image")
.into();
let handle = g.assets.insert(Asset {
asset_type: AssetType::Texture,
bytes: None,
});
g.render
.push_cmd(Command::CreateTexture(CreateTextureCommand {
handle: handle.clone(),
data,
}));
handle
})
.clone()
}
pub trait Scene {
fn init(&mut self, _g: &mut Engine) {}
fn update(&mut self, _g: &mut Engine) {}
fn draw(&mut self, _g: &mut Engine) {}
fn cleanup(&mut self, _g: &mut Engine) {}
}
pub struct FontConfig {
pub scale: f32,
pub offset: Vec2,
}
impl Default for FontConfig {
fn default() -> Self {
Self {
scale: 1.0,
offset: Vec2::ZERO,
}
}
}
#[derive(Default, Debug)]
pub struct Perf {
pub entities: usize,
pub update: f32,
pub draw: f32,
pub total: f32,
}
pub trait Clock {
fn now(&mut self) -> f32;
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum CameraMode {
#[default]
Game,
Ui,
}
pub struct Engine {
pub clock: Box<dyn Clock>,
pub time_real: f32,
pub time: f32,
pub time_scale: f32,
pub tick: f32,
pub frame: f32,
pub perf: Perf,
pub input: InputState,
pub font_config: FontConfig,
pub camera_mode: CameraMode,
pub(crate) exit: bool,
is_running: bool,
is_window_resized: bool,
scene: Option<Box<dyn Scene>>,
scene_next: Option<Box<dyn Scene>>,
text_cache: TextCache,
pub(crate) camera: Camera,
pub(crate) ui_camera: Camera,
pub(crate) render: Render,
pub assets: AssetManager,
}
impl Engine {
pub fn new<C: Clock + 'static>(clock: C, backend_state: BackendState) -> Self {
Self {
clock: Box::new(clock),
time_real: 0.0,
time: 0.0,
time_scale: 1.0,
tick: 0.0,
frame: 0.0,
camera: Camera::default(),
ui_camera: Camera::default(),
camera_mode: CameraMode::default(),
perf: Perf::default(),
is_running: false,
is_window_resized: false,
scene: None,
scene_next: None,
text_cache: TextCache::default(),
render: Render::new(backend_state),
input: InputState::default(),
font_config: FontConfig::default(),
assets: AssetManager::new("assets"),
exit: false,
}
}
pub fn exit(&mut self) {
self.exit = true;
}
pub fn load_default_font<P: AsRef<Path>>(&mut self, path: P) {
let handle = self.assets.load_font(path);
self.set_default_font(handle);
}
pub fn set_snap_to_px(&mut self, snap_to_px: bool) {
self.camera.set_snap_to_px(snap_to_px);
self.ui_camera.set_snap_to_px(snap_to_px);
}
pub fn set_default_font(&mut self, handle: Handle) {
self.render.set_default_font(handle);
}
pub fn render_text(&mut self, mut text: Text) -> Result<(Handle, UVec2)> {
text.scale *= self.font_config.scale;
let (handle, size) = match self.text_cache.get(&text) {
Some((handle, size)) => (handle.clone(), *size),
None => {
log::trace!("Missing text cache {:?}", &text);
let (handle, size) = self.create_text_texture(&text)?;
self.text_cache.add(text, (handle.clone(), size));
(handle, size)
}
};
Ok((handle, size))
}
pub fn draw_text(&mut self, text: Text, anchor: Option<Vec2>, mut transform: Transform) {
if let Ok((handle, size)) = self.render_text(text) {
let mut sprite = Sprite::new(handle, size);
if let Some(anchor) = anchor {
sprite.anchor = anchor;
}
transform.size *= size.as_vec2();
transform.pos.x += self.font_config.offset.x;
transform.pos.y += self.font_config.offset.y;
self.draw(&sprite, transform);
}
}
pub fn create_text_texture(&mut self, text: &Text) -> Result<(Handle, UVec2)> {
let handle = self.assets.insert(Asset {
asset_type: AssetType::Texture,
bytes: None,
});
let size = self
.render
.create_text_texture(&mut self.text_cache, handle.clone(), text)?;
Ok((handle, size))
}
pub fn with_ui<R, F: FnOnce(&mut Engine) -> R>(&mut self, f: F) -> R {
let mode = self.camera_mode;
self.camera_mode = CameraMode::Ui;
let ret = f(self);
self.camera_mode = mode;
ret
}
pub fn draw_rect(&mut self, color: Color, anchor: Option<Vec2>, transform: Transform) {
let texture = default_texture(self);
let mut image = Sprite::new(texture, transform.size.as_uvec2());
image.color = color;
if let Some(anchor) = anchor {
image.anchor = anchor;
}
self.draw(&image, transform);
}
pub fn draw(&mut self, sprite: &Sprite, transform: Transform) {
let camera = match self.camera_mode {
CameraMode::Game => self.camera(),
CameraMode::Ui => self.ui_camera(),
};
let b = transform.bounds_with_anchor(sprite.anchor);
let view = camera.viewport();
if b.min.x > view.max.x
|| b.min.y > view.max.y
|| b.max.x < view.min.x
|| b.max.y < view.min.y
{
return;
}
let renderer = self.render.renderer.clone();
let renderer_args = self.render.send_renderer_data.take();
let cmd = DrawCommand::new(sprite, transform, renderer, renderer_args);
match self.camera_mode {
CameraMode::Game => {
self.render.draw(cmd);
}
CameraMode::Ui => {
self.render.draw_ui(cmd);
}
}
}
pub fn view_size(&self) -> Vec2 {
self.render.logical_size()
}
pub fn screen_size(&self) -> Vec2 {
self.render.screen_size()
}
pub fn is_window_resized(&self) -> bool {
self.is_window_resized
}
pub fn scale_mode(&self) -> ScaleMode {
self.render.scale_mode()
}
pub fn set_scale_mode(&mut self, mode: ScaleMode) {
self.render.set_scale_mode(mode)
}
pub fn now(&mut self) -> f32 {
self.clock.now()
}
pub fn camera(&self) -> &Camera {
&self.camera
}
pub fn camera_mut(&mut self) -> &mut Camera {
&mut self.camera
}
pub fn ui_camera(&self) -> &Camera {
&self.ui_camera
}
pub fn ui_camera_mut(&mut self) -> &mut Camera {
&mut self.ui_camera
}
pub fn input(&self) -> &InputState {
&self.input
}
pub fn input_mut(&mut self) -> &mut InputState {
&mut self.input
}
pub(crate) fn init<Setup: FnOnce(&mut Engine)>(&mut self, setup: Setup) {
self.time_real = self.now();
self.init_default_font();
setup(self);
}
pub(crate) fn init_default_font(&mut self) {
let handle = self.assets.insert(Asset {
asset_type: AssetType::Font,
bytes: None,
});
let font =
Font::from_bytes(DEFAULT_FONT_BYTES.into()).expect("Failed to load default font");
self.text_cache.add_font(handle.id(), font);
self.set_default_font(handle);
}
pub(crate) fn on_resize(&mut self, physical_size: UVec2, dpi_scale_factor: f64) {
self.render.resize(physical_size, dpi_scale_factor);
self.camera.resize(self.render.logical_size());
self.ui_camera.resize(self.render.logical_size());
self.is_window_resized = true;
self.render.post_renderer.resize(self);
}
pub(crate) fn update(&mut self) {
let time_frame_start = self.now();
if self.scene_next.is_some() {
self.is_running = false;
if let Some(mut scene) = self.scene.take() {
scene.cleanup(self);
}
self.time = 0.;
self.frame = 0.;
if let Some(mut scene) = self.scene_next.take() {
scene.init(self);
self.scene = Some(scene);
}
}
self.is_running = true;
let time_real_now = self.now();
let real_delta = time_real_now - self.time_real;
let tick = (real_delta * self.time_scale).min(ENGINE_MAX_TICK);
if tick == 0.0 {
return;
}
log::debug!("now {time_real_now} tick {tick}");
self.time_real = time_real_now;
self.tick = tick;
self.time += self.tick;
self.frame += 1.;
if let Some(mut scene) = self.scene.take() {
scene.update(self);
self.scene = Some(scene);
}
self.perf.update = self.now() - time_real_now;
if let Some(mut scene) = self.scene.take() {
scene.draw(self);
self.scene = Some(scene);
}
self.perf.draw = (self.now() - time_real_now) - self.perf.update;
self.input.clear();
self.is_window_resized = false;
self.perf.total = self.now() - time_frame_start;
log::trace!("Perf {:?}", &self.perf);
}
pub(crate) async fn handle_assets(&mut self) -> Result<()> {
let tasks = self.assets.fetch().await?;
for task in tasks {
match task {
FetchedTask::CreateTexture { handle, data } => {
self.render
.push_cmd(Command::CreateTexture(CreateTextureCommand {
handle,
data,
}))
}
FetchedTask::RemoveTexture { handle } => self
.render
.push_cmd(Command::RemoveTexture(RemoveTextureCommand(handle))),
FetchedTask::CreateFont { handle, font } => {
self.text_cache.add_font(handle.id(), font);
}
FetchedTask::RemoveFont { handle } => {
self.text_cache.remove_font(handle);
}
}
}
Ok(())
}
pub fn set_scene(&mut self, scene: impl Scene + 'static) {
self.scene_next.replace(Box::new(scene));
}
pub fn is_running(&self) -> bool {
self.is_running
}
pub fn backend_state(&self) -> &BackendState {
&self.render.backend_state
}
pub fn with_renderer<Ret, F: FnOnce(&mut Engine) -> Ret>(
&mut self,
renderer: Rc<dyn SpriteRenderer + 'static>,
f: F,
) -> Ret {
let old_renderer = self.render.set_renderer(renderer);
let ret = f(self);
self.render.set_renderer(old_renderer);
ret
}
pub fn set_renderer(&mut self, renderer: Option<Rc<dyn SpriteRenderer + 'static>>) {
match renderer {
Some(renderer) => {
self.render.set_renderer(renderer);
}
None => {
let renderer: Rc<dyn SpriteRenderer> =
Rc::new(DefaultRenderer::new(&self.render.backend_state));
self.render.set_renderer(renderer);
}
}
}
pub fn send_renderer_data<T: bytemuck::NoUninit>(&mut self, data: T) {
let data = bytemuck::bytes_of(&data);
self.render.send_renderer_data(data.to_vec());
}
pub fn set_post_renderer<R: PostRenderer + 'static>(&mut self, post_renderer: Option<R>) {
let renderer: Box<dyn PostRenderer> = if let Some(post_renderer) = post_renderer {
Box::new(post_renderer)
} else {
Box::new(ScreenRenderer::setup(&self.render.backend_state))
};
renderer.resize(self);
self.render.set_post_renderer(renderer);
}
pub fn text_cache(&mut self) -> &mut TextCache {
&mut self.text_cache
}
}