#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://github.com/tversteeg/chuot-website/blob/main/static/favicon-32x32.png?raw=true"
)]
pub mod assets;
mod camera;
pub mod config;
pub mod context;
mod graphics;
mod input;
mod math;
mod random;
pub use assets::source::AssetSource;
pub use chuot_macros::load_assets;
pub use config::Config;
pub use context::Context;
pub use gilrs::ev::{Axis as GamepadAxis, Button as GamepadButton};
pub use math::lerp;
pub use random::random;
pub use rgb::RGBA8;
use web_time::Instant;
use winit::{
application::ApplicationHandler,
dpi::{LogicalSize, PhysicalSize},
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{WindowAttributes, WindowId},
};
pub use winit::{event::MouseButton, keyboard::KeyCode};
const FPS_SMOOTHED_AVERAGE_ALPHA: f32 = 0.8;
const MAX_UPDATE_CALLS_PER_RENDER: f32 = 20.0;
pub trait Game: Sized
where
Self: 'static,
{
fn update(&mut self, ctx: Context);
fn render(&mut self, ctx: Context);
#[inline(always)]
#[allow(unused_variables)]
fn init(&mut self, ctx: Context) {}
#[inline(always)]
fn run(self, asset_source: AssetSource, config: Config) {
#[cfg(target_arch = "wasm32")]
console_error_panic_hook::set_once();
let accumulator = 0.0;
let last_time = Instant::now();
let ctx = None;
let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::{EventLoopExtWebSys, PollStrategy, WaitUntilStrategy};
event_loop.set_poll_strategy(PollStrategy::IdleCallback);
event_loop.set_wait_until_strategy(WaitUntilStrategy::Worker);
}
let asset_source = Some(Box::new(asset_source));
#[cfg(target_arch = "wasm32")]
let event_loop_proxy = Some(event_loop.create_proxy());
let game = self;
let _ = event_loop.run_app(&mut State {
ctx,
asset_source,
game,
config,
last_time,
accumulator,
#[cfg(target_arch = "wasm32")]
event_loop_proxy,
});
}
}
struct State<G: Game> {
ctx: Option<Context>,
asset_source: Option<Box<AssetSource>>,
game: G,
config: Config,
last_time: Instant,
accumulator: f32,
#[cfg(target_arch = "wasm32")]
event_loop_proxy: Option<winit::event_loop::EventLoopProxy<Context>>,
}
impl<G: Game> ApplicationHandler<Context> for State<G> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.ctx.is_none() {
#[allow(unused_mut)]
let mut window_attributes = WindowAttributes::default()
.with_title(&self.config.title)
.with_inner_size(LogicalSize::new(
self.config.buffer_width * self.config.scaling,
self.config.buffer_height * self.config.scaling,
))
.with_min_inner_size(LogicalSize::new(
self.config.buffer_width,
self.config.buffer_height,
));
#[cfg(target_arch = "wasm32")]
{
use web_sys::{HtmlCanvasElement, wasm_bindgen::JsCast};
use winit::platform::web::WindowAttributesExtWebSys;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas = document
.get_element_by_id("chuot")
.map(|canvas| canvas.dyn_into::<HtmlCanvasElement>().unwrap());
window_attributes = window_attributes
.with_canvas(canvas)
.with_append(true)
.with_prevent_default(true);
}
let window = event_loop
.create_window(window_attributes)
.expect("Error creating window");
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
window
.canvas()
.unwrap()
.style()
.set_css_text(
&format!(
"image-rendering: pixelated; outline: none; border: none; width: {}px; height: {}px",
self.config.buffer_width * self.config.scaling,
self.config.buffer_height * self.config.scaling,
)
);
}
#[cfg(not(target_arch = "wasm32"))]
{
let ctx = pollster::block_on(async {
Context::new(
self.config.clone(),
self.asset_source.take().unwrap(),
window,
)
.await
});
self.ctx = Some(ctx.clone());
self.game.init(ctx);
}
#[cfg(target_arch = "wasm32")]
{
let event_loop_proxy = self.event_loop_proxy.take().unwrap();
let asset_source = self.asset_source.take().unwrap();
let config = self.config.clone();
wasm_bindgen_futures::spawn_local(async move {
let ctx = Context::new(config, asset_source, window).await;
let _ = event_loop_proxy.send_event(ctx);
});
}
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
let Some(ctx) = &mut self.ctx else {
return;
};
match event {
WindowEvent::RedrawRequested => {
let current_time = Instant::now();
let frame_time = (current_time - self.last_time)
.as_secs_f32()
.min(MAX_UPDATE_CALLS_PER_RENDER * self.config.update_delta_time);
self.last_time = current_time;
self.accumulator += frame_time
.min(self.config.max_frame_time_secs);
while self.accumulator >= self.config.update_delta_time {
self.game.update(ctx.clone());
self.accumulator -= self.config.update_delta_time;
ctx.write(|ctx| {
ctx.input.update();
ctx.main_camera.update_target();
ctx.ui_camera.update_target();
#[cfg(not(target_arch = "wasm32"))]
assets::hot_reload::handle_changed_asset_files(ctx);
});
}
ctx.write(|ctx| {
ctx.blending_factor = self.accumulator / self.config.update_delta_time;
ctx.frames_per_second = FPS_SMOOTHED_AVERAGE_ALPHA.mul_add(
ctx.frames_per_second,
(1.0 - FPS_SMOOTHED_AVERAGE_ALPHA) * frame_time.recip(),
);
ctx.main_camera.update(frame_time, ctx.blending_factor);
ctx.ui_camera.update(frame_time, ctx.blending_factor);
});
let not_minimized = !ctx.is_minimized();
if not_minimized {
self.game.render(ctx.clone());
}
ctx.write(|ctx| {
if not_minimized {
ctx.graphics.render();
}
if ctx.exit {
event_loop.exit();
}
});
}
WindowEvent::Resized(PhysicalSize { width, height }) => {
ctx.write(|ctx| {
ctx.graphics.resize(width, height);
#[cfg(target_os = "macos")]
ctx.window.request_redraw();
});
}
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::KeyboardInput { .. }
| WindowEvent::CursorMoved { .. }
| WindowEvent::MouseWheel { .. }
| WindowEvent::MouseInput { .. } => {
ctx.write(|ctx| ctx.input.handle_event(event, &ctx.graphics));
}
_ => (),
}
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, ctx: Context) {
self.game.init(ctx.clone());
self.ctx = Some(ctx);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
let Some(ctx) = &mut self.ctx else {
return;
};
event_loop.set_control_flow(ControlFlow::Poll);
ctx.write(|ctx| ctx.window.request_redraw());
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
self.ctx = None;
self.asset_source = None;
#[cfg(target_arch = "wasm32")]
{
self.event_loop_proxy = None;
}
}
}