tirbofish/dropbear · commit
7c471d275ad3beb11e6f4f8703e0391ddf9889f4
feature: euca-runner now works successfully. fix: got windowing options available for the redback-runtime fix: some general fixes here and there. jarvis, run github actions. Unverified
@@ -72,7 +72,7 @@ dyn-hash = "1.0" semver = { version = "1.0", features = ["serde"] } rapier3d = { version = "0.32", features = [ "simd-stable", "serde-serialize" ] } cbindgen = { version = "0.29.2" } -postcard = { version = "1.1"} +postcard = { version = "1.1", features = ["use-std"]} pollster = "0.4" yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f", features = ["default-fonts"] } yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "af8fc1f" } @@ -7,7 +7,7 @@ use crate::{ use dropbear_future_queue::FutureQueue; use egui::{Context, TextureId}; use glam::{DMat4, DQuat, DVec3, Mat3}; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use std::sync::Arc; use wgpu::*; use winit::window::Window; @@ -21,6 +21,7 @@ pub struct SharedGraphicsContext { pub queue: Arc<Queue>, pub surface: Arc<Surface<'static>>, pub surface_format: TextureFormat, + pub surface_config: Arc<RwLock<SurfaceConfiguration>>, pub instance: Arc<wgpu::Instance>, pub layouts: Arc<BindGroupLayouts>, pub window: Arc<Window>, @@ -77,6 +78,7 @@ impl SharedGraphicsContext { mipmapper: state.mipmapper.clone(), yakui_renderer: state.yakui_renderer.clone(), yakui_texture: state.yakui_texture.clone(), + surface_config: state.config.clone(), } } } @@ -85,7 +85,7 @@ pub struct State { pub surface_format: TextureFormat, pub device: Arc<Device>, pub queue: Arc<Queue>, - pub config: SurfaceConfiguration, + pub config: Arc<RwLock<SurfaceConfiguration>>, pub is_surface_configured: bool, pub layouts: Arc<BindGroupLayouts>, pub egui_renderer: Arc<Mutex<EguiRenderer>>, @@ -351,7 +351,7 @@ Hardware: surface_format: Texture::TEXTURE_FORMAT, device, queue, - config, + config: Arc::new(RwLock::new(config)), is_surface_configured, depth_texture, window, @@ -383,16 +383,19 @@ Hardware: /// A helper function that changes the surface config when resized (+ depth texture). pub fn resize(&mut self, width: u32, height: u32) { if width > 0 && height > 0 { - self.config.width = width; - self.config.height = height; - self.surface.configure(&self.device, &self.config); + { + let mut config = self.config.write(); + config.width = width; + config.height = height; + } + self.surface.configure(&self.device, &self.config.read()); self.is_surface_configured = true; } self.depth_texture = - Texture::depth_texture(&self.config, &self.device, Some("depth texture")); + Texture::depth_texture(&self.config.read(), &self.device, Some("depth texture")); self.viewport_texture = - Texture::viewport(&self.config, &self.device, Some("viewport texture")); + Texture::viewport(&self.config.read(), &self.device, Some("viewport texture")); self.egui_renderer .lock() .renderer() @@ -415,19 +418,21 @@ Hardware: if !self.is_surface_configured { return Ok(Vec::new()); } - + + let config = self.config.read().clone(); + let output = match self.surface.get_current_texture() { Ok(val) => val, Err(e) => { return match e { SurfaceError::Lost => { log_once::warn_once!("Surface lost, reconfiguring..."); - self.surface.configure(&self.device, &self.config); + self.surface.configure(&self.device, &config); Ok(Vec::new()) } SurfaceError::Outdated => { log_once::warn_once!("Surface outdated, reconfiguring..."); - self.surface.configure(&self.device, &self.config); + self.surface.configure(&self.device, &config); Ok(Vec::new()) } SurfaceError::Timeout => { @@ -446,7 +451,7 @@ Hardware: }; let screen_descriptor = ScreenDescriptor { - size_in_pixels: [self.config.width, self.config.height], + size_in_pixels: [config.width, config.height], pixels_per_point: self.window.scale_factor() as f32, }; @@ -21,11 +21,11 @@ chrono.workspace = true env_logger.workspace = true colored.workspace = true ron.workspace = true -hecs.workspace = true -bytemuck.workspace = true -log-once.workspace = true winit.workspace = true serde.workspace = true -crossbeam-channel.workspace = true -gilrs.workspace = true -futures.workspace = true + +[features] +default = ["jvm"] + +jvm = ["redback-runtime/jvm", "eucalyptus-core/jvm"] +debug = ["eucalyptus-core/jvm_debug"] @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + use app_dirs2::AppInfo; use dropbear_engine::future::FutureQueue; use eucalyptus_core::runtime::RuntimeProjectConfig; @@ -8,13 +10,104 @@ use std::fs; use std::rc::Rc; use std::sync::Arc; use ron::ser::PrettyConfig; -use winit::window::WindowAttributes; +use winit::window::{Fullscreen, WindowAttributes}; use dropbear_engine::{DropbearAppBuilder, DropbearWindowBuilder}; use serde::{Deserialize, Serialize}; +use winit::dpi::PhysicalSize; +use eucalyptus_core::scripting::jni::{RuntimeMode, RUNTIME_MODE}; #[tokio::main] async fn main() { - env_logger::init(); + // env_logger::init(); + + #[cfg(all(not(target_os = "android"), feature = "debug"))] + { + use colored::Colorize; + use env_logger::Builder; + use log::LevelFilter; + use std::fs::OpenOptions; + + let log_dir = + app_dirs2::app_root(app_dirs2::AppDataType::UserData, &eucalyptus_core::APP_INFO) + .expect("Failed to get app data directory") + .join("logs"); + fs::create_dir_all(&log_dir).expect("Failed to create log dir"); + + let datetime_str = chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S"); + let log_filename = format!("{}.{}.log", "eucalyptus-editor", datetime_str); + let log_path = log_dir.join(log_filename); + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .expect("Failed to open log file"); + let file = parking_lot::Mutex::new(file); + + let app_target = "eucalyptus-editor".replace('-', "_"); + let log_config = format!("dropbear_engine=trace,{}=debug,warn", app_target); + unsafe { std::env::set_var("RUST_LOG", log_config) }; + + Builder::new() + .format(move |buf, record| { + use std::io::Write; + + let ts = chrono::offset::Local::now().format("%Y-%m-%dT%H:%M:%S"); + + let colored_level = match record.level() { + log::Level::Error => record.level().to_string().red().bold(), + log::Level::Warn => record.level().to_string().yellow().bold(), + log::Level::Info => record.level().to_string().green().bold(), + log::Level::Debug => record.level().to_string().blue().bold(), + log::Level::Trace => record.level().to_string().cyan().bold(), + }; + + let colored_timestamp = ts.to_string().bright_black(); + + let file_info = format!( + "{}:{}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0) + ) + .bright_black(); + + let console_line = format!( + "{} {} [{}] - {}\n", + file_info, + colored_timestamp, + colored_level, + record.args() + ); + + let file_line = format!( + "{}:{} {} [{}] - {}\n", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + ts, + record.level(), + record.args() + ); + + write!(buf, "{}", console_line)?; + + let mut fh = file.lock(); + let _ = fh.write_all(file_line.as_bytes()); + + Ok(()) + }) + .filter_level(LevelFilter::Warn) + .filter(Some("dropbear_engine"), LevelFilter::Trace) + .filter( + Some("eucalyptus_editor"), + LevelFilter::Debug, + ) + .filter(Some("eucalyptus_core"), LevelFilter::Debug) + .filter(Some("dropbear_traits"), LevelFilter::Debug) + .filter(Some("euca_runner"), LevelFilter::Debug) + .init(); + log::info!("Initialised logger"); + } + dropbear_engine::panic::set_hook(); log::debug!("Set panic hook"); @@ -65,10 +158,13 @@ async fn main() { log::debug!("Located scene config file: [{}] ({} bytes)", path.display(), scene_config.len()); let scene_config: RuntimeProjectConfig = postcard::from_bytes(&scene_config).unwrap(); + scene_config.populate().unwrap(); log::debug!("Converted scene config file to RuntimeProjectConfig"); - let runtime_scene = Rc::new(RwLock::new(PlayMode::new(None).unwrap())); + let _ = RUNTIME_MODE.set(RuntimeMode::Runtime); + + let runtime_scene = Rc::new(RwLock::new(PlayMode::new(Some(scene_config.initial_scene)).unwrap())); let future_queue = Arc::new(FutureQueue::new()); let authors = scene_config.authors.developer.clone(); @@ -81,11 +177,11 @@ async fn main() { let attributes = WindowAttributes::default(); - match config.target_resolution { - WindowModes::Windowed(_, _) => {} - WindowModes::Maximised => {} - WindowModes::Fullscreen => {} - } + let attributes = match config.target_resolution { + WindowModes::Windowed(x, y) => {attributes.with_inner_size(PhysicalSize::new(x, y))} + WindowModes::Maximised => {attributes.with_maximized(true)} + WindowModes::Fullscreen => {attributes.with_fullscreen(Some(Fullscreen::Borderless(None)))} + }; let window = DropbearWindowBuilder::new() .with_attributes(attributes) @@ -1,10 +1,12 @@ //! Configuration and metadata information about redback-runtime based data. +use crate::config::ProjectConfig; use crate::scene::SceneConfig; use crate::states::{PROJECT, SCENES}; +use crate::utils::option::HistoricalOption; use anyhow::Context; +use chrono::Utc; use semver::Version; -use crate::utils::option::HistoricalOption; /// The settings of a project in its runtime. /// @@ -119,4 +121,44 @@ impl RuntimeProjectConfig { Ok(result) } + + /// Populates the states (such as [PROJECT]) with all the context from the RuntimeProjectConfig. + pub fn populate(&self) -> anyhow::Result<()> { + let exe_dir = std::env::current_exe() + .context("Unable to locate runtime executable")? + .parent() + .ok_or_else(|| anyhow::anyhow!("Unable to locate parent directory of runtime executable"))? + .to_path_buf(); + + let now = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S")); + + let mut runtime_settings = self.runtime_settings.clone(); + if runtime_settings.initial_scene.is_none() { + runtime_settings.initial_scene = Some(self.initial_scene.clone()); + } + + let project_config = ProjectConfig { + project_name: self.project_name.clone(), + project_path: exe_dir, + date_created: now.clone(), + date_last_accessed: now, + project_version: self.project_version.to_string(), + authors: self.authors.clone(), + runtime_settings, + last_opened_scene: Some(self.initial_scene.clone()), + }; + + { + let mut project = PROJECT.write(); + *project = project_config; + } + + { + let mut scenes = SCENES.write(); + scenes.clear(); + scenes.extend(self.scenes.iter().cloned()); + } + + Ok(()) + } } @@ -119,6 +119,9 @@ impl ScriptManager { log::debug!("Created new JVM instance"); } + #[cfg(not(feature = "jvm"))] + log::debug!("JVM feature not enabled"); + Ok(result) } @@ -144,8 +144,6 @@ impl JavaContext { let mut args_log = Vec::new(); - - if let Some(args) = external_vm_args { args_log.push(args.clone()); jvm_args = jvm_args.option(args); @@ -163,30 +161,8 @@ impl JavaContext { } RuntimeMode::Editor => { log::debug!("JDB is not used in the editor (as there is no need for so)"); - - // let (start_port, end_port) = (6741, 6761); - // let mut port = 0000; - // let mut debug_arg = String::new(); - // - // for p in start_port..end_port { - // if is_port_available(p) { - // port = p; - // debug_arg = format!("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:{}", port); - // break; - // } else { - // log::debug!("Port {} is not available", p); - // } - // } - // - // if debug_arg.is_empty() { - // log::warn!("Could not find an available port for JDWP debugger (tried 6741-6760). Debugging will be disabled."); - // } else { - // args_log.push(debug_arg.clone()); - // jvm_args = jvm_args.option(debug_arg); - // log::info!("JDWP debugger enabled on port {}", port); - // } } - RuntimeMode::PlayMode => { + RuntimeMode::PlayMode | RuntimeMode::Runtime => { let (start_port, end_port) = (6751, 6771); let mut port = 0000; let mut debug_arg = String::new(); @@ -218,9 +194,6 @@ impl JavaContext { log::info!("JDWP debugger enabled on port {}", port); } } - RuntimeMode::Runtime => { - log::warn!("Runtime mode JWDB not implemented yet..."); - } } } @@ -288,9 +261,6 @@ impl JavaContext { let jvm_init_args = jvm_args.build()?; let jvm = JavaVM::new(jvm_init_args)?; - #[cfg(feature = "jvm_debug")] - crate::success!("JDB debugger enabled on localhost:6741"); - log::info!("Created JVM instance"); Ok(Self { @@ -76,8 +76,7 @@ pub fn build(project_config: PathBuf) -> anyhow::Result<PathBuf> { // export to .eupak let eupak_path = build_dir.join("data.eupak"); - const SIZE_OF_RUNTIME_PROJECT_CONFIG: usize = size_of::<RuntimeProjectConfig>(); - let config_bytes = postcard::to_vec::<RuntimeProjectConfig, SIZE_OF_RUNTIME_PROJECT_CONFIG>(&runtime_config)?; + let config_bytes = postcard::to_stdvec::<RuntimeProjectConfig>(&runtime_config)?; fs::write(&eupak_path, config_bytes)?; log::debug!("Exported scene config to {:?}", eupak_path); @@ -17,14 +17,17 @@ use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::scene::SceneCommand; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; -use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT}; -use eucalyptus_core::scene::loading::SCENE_LOADER; +use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script}; +use eucalyptus_core::scene::loading::{SceneLoadResult, SCENE_LOADER}; use eucalyptus_core::traits::registry::ComponentRegistry; use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, PhysicsStatePtr, WorldPtr}; use eucalyptus_core::command::COMMAND_BUFFER; use eucalyptus_core::scene::loading::IsSceneLoaded; use std::collections::HashMap; +use std::env::current_exe; use std::path::PathBuf; +use wgpu::SurfaceConfiguration; +use winit::window::Fullscreen; use yakui_winit::YakuiWinit; use eucalyptus_core::physics::PhysicsState; use eucalyptus_core::rapier3d::prelude::*; @@ -34,8 +37,9 @@ mod scene; mod input; mod command; +#[cfg(feature = "debug")] fn find_jvm_library_path() -> PathBuf { - let proj = PROJECT.read(); + let proj = eucalyptus_core::states::PROJECT.read(); let project_path = if !proj.project_path.is_dir() { proj.project_path .parent() @@ -44,7 +48,7 @@ fn find_jvm_library_path() -> PathBuf { } else { proj.project_path.clone() } - .join("build/libs"); + .join("build/libs"); let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; @@ -73,6 +77,34 @@ fn find_jvm_library_path() -> PathBuf { .expect("No suitable candidate for a JVM targeted play mode session available") } +#[cfg(not(feature = "debug"))] +fn find_jvm_library_path() -> PathBuf { + let mut latest_jar: Option<(PathBuf, std::time::SystemTime)> = None; + + for entry in std::fs::read_dir(current_exe().unwrap().parent().unwrap()).expect("Unable to read directory") { + let entry = entry.expect("Unable to get directory entry"); + let path = entry.path(); + + if let Some(filename) = path.file_name().and_then(|n| n.to_str()) { + if filename.ends_with("-all.jar") { + let metadata = entry.metadata().expect("Unable to get file metadata"); + let modified = metadata.modified().expect("Unable to get file modified time"); + + match latest_jar { + None => latest_jar = Some((path.clone(), modified)), + Some((_, latest_time)) if modified > latest_time => { + latest_jar = Some((path.clone(), modified)); + } + _ => {} + } + } + } + } + + latest_jar + .map(|(path, _)| path) + .expect("No suitable candidate for a JVM targeted play mode session available") +} pub struct PlayMode { scene_command: SceneCommand, input_state: InputState, @@ -80,6 +112,7 @@ pub struct PlayMode { world: Box<World>, component_registry: Arc<ComponentRegistry>, active_camera: Option<Entity>, + display_settings: DisplaySettings, // rendering light_cube_pipeline: Option<LightCubePipeline>, @@ -160,6 +193,14 @@ impl PlayMode { collision_force_event_receiver: Some(cfe_r), event_collector, yakui_winit: None, + display_settings: DisplaySettings { + window_mode: WindowMode::Windowed, + maintain_aspect_ratio: true, + vsync: false, + last_window_mode: WindowMode::BorderlessFullscreen, + last_vsync: true, + last_size: (0, 0), + }, }; log::debug!("Created new play mode instance"); @@ -216,6 +257,8 @@ impl PlayMode { } /// Requests an asynchronous scene load, returning immediately and loading the scene in the background. + /// + /// It will not request the scene load if the currently rendered scene is the same as the requested scene. pub fn request_async_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) { log::debug!("Requested async scene load: {}", requested_scene.requested_scene); let scene_name = requested_scene.requested_scene.clone(); @@ -233,8 +276,18 @@ impl PlayMode { if let Some(id) = progress.id { let mut loader = SCENE_LOADER.lock(); if let Some(entry) = loader.get_entry_mut(id) { - if entry.status.is_none() { - entry.status = self.world_loading_progress.as_ref().cloned(); + if let Some(scene) = &self.current_scene && scene == &self.scene_progress.as_ref().unwrap().requested_scene { + log::debug!("Load scene async request cancelled because scene name is current"); + entry.result = SceneLoadResult::Error("Currently rendered scene name is the same as the requested scene".to_string()); + self.world_loading_progress = None; + self.world_receiver = None; + self.physics_receiver = None; + self.scene_progress = None; + return + } else { + if entry.status.is_none() { + entry.status = self.world_loading_progress.as_ref().cloned(); + } } } } @@ -284,6 +337,11 @@ impl PlayMode { /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded. pub fn request_immediate_scene_load(&mut self, graphics: Arc<SharedGraphicsContext>, requested_scene: IsSceneLoaded) { + if let Some(scene) = &self.current_scene && scene == &requested_scene.requested_scene { + log::debug!("Immediate scene load request cancelled because scene name is current"); + return + } + let scene_name = requested_scene.requested_scene.clone(); log::debug!("Immediate scene load requested: {}", scene_name); @@ -368,4 +426,90 @@ impl PlayMode { self.current_scene = Some(scene_progress.requested_scene.clone()); } } +} + +pub struct DisplaySettings { + pub window_mode: WindowMode, + pub maintain_aspect_ratio: bool, + pub vsync: bool, + last_window_mode: WindowMode, + last_vsync: bool, + last_size: (u32, u32), +} + +impl DisplaySettings { + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) { + let window = graphics.window.clone(); + let size = ( + graphics.viewport_texture.size.width, + graphics.viewport_texture.size.height, + ); + + let needs_update = self.window_mode != self.last_window_mode + || self.vsync != self.last_vsync + || size != self.last_size; + + if !needs_update { + return; + } + + match self.window_mode { + WindowMode::Windowed => { + window.set_fullscreen(None); + window.set_maximized(false); + } + WindowMode::Maximized => { + window.set_fullscreen(None); + window.set_maximized(true); + } + WindowMode::Fullscreen => { + let monitor = window.current_monitor(); + let fullscreen = monitor + .as_ref() + .and_then(|m| m.video_modes().next()) + .map(Fullscreen::Exclusive) + .or_else(|| Some(Fullscreen::Borderless(monitor))); + + window.set_fullscreen(fullscreen); + window.set_maximized(false); + } + WindowMode::BorderlessFullscreen => { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + window.set_maximized(false); + } + } + + let config = SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: graphics.surface_format, + width: graphics.viewport_texture.size.width, + height: graphics.viewport_texture.size.height, + present_mode: if self.vsync { + wgpu::PresentMode::AutoVsync + } else { + wgpu::PresentMode::AutoNoVsync + }, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + { + let mut cfg = graphics.surface_config.write(); + *cfg = config; + } + + self.last_window_mode = self.window_mode; + self.last_vsync = self.vsync; + self.last_size = size; + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum WindowMode { + Windowed, + Maximized, + Fullscreen, + BorderlessFullscreen, } @@ -64,6 +64,12 @@ impl Scene for PlayMode { let _ = self.script_manager.physics_update_script(self.world.as_mut(), dt as f64); } + let world = self.world.iter().map(|e| (e.get::<&Label>().unwrap().to_string(), e.entity())).collect::<Vec<_>>(); + log::info!("World contents [len={}]: ", world.len()); + for (l, e) in world { + log::info!("{} -> {:?}", l, e); + } + for kcc in self.world.query::<&mut KCC>().iter() { kcc.collisions.clear(); } @@ -242,6 +248,8 @@ impl Scene for PlayMode { graphics.future_queue.poll(); self.poll(graphics.clone()); + self.display_settings.update(graphics.clone()); + { if let Some(fps) = PROJECT.read().runtime_settings.target_fps.get() { log_once::debug_once!("setting new fps for play mode session: {}", fps); @@ -381,6 +389,40 @@ impl Scene for PlayMode { #[cfg(feature = "debug")] egui::TopBottomPanel::top("menu_bar").show(&graphics.get_egui_context(), |ui| { egui::MenuBar::new().ui(ui, |ui| { + use crate::WindowMode; + ui.menu_button("Window", |ui| { + ui.menu_button("Window Mode", |ui| { + let is_windowed = matches!(self.display_settings.window_mode, WindowMode::Windowed); + if ui.selectable_label(is_windowed, "Windowed").clicked() { + self.display_settings.window_mode = WindowMode::Windowed; + ui.close(); + } + + let is_maximized = matches!(self.display_settings.window_mode, WindowMode::Maximized); + if ui.selectable_label(is_maximized, "Maximized").clicked() { + self.display_settings.window_mode = WindowMode::Maximized; + ui.close(); + } + + let is_fullscreen = matches!(self.display_settings.window_mode, WindowMode::Fullscreen); + if ui.selectable_label(is_fullscreen, "Fullscreen").clicked() { + self.display_settings.window_mode = WindowMode::Fullscreen; + ui.close(); + } + + let is_borderless = matches!(self.display_settings.window_mode, WindowMode::BorderlessFullscreen); + if ui.selectable_label(is_borderless, "Borderless Fullscreen").clicked() { + self.display_settings.window_mode = WindowMode::BorderlessFullscreen; + ui.close(); + } + }); + + ui.separator(); + + ui.checkbox(&mut self.display_settings.maintain_aspect_ratio, "Maintain aspect ratio"); + ui.checkbox(&mut self.display_settings.vsync, "VSync").clicked(); + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.group(|ui| { ui.add_enabled_ui(true, |ui| { @@ -403,7 +445,6 @@ impl Scene for PlayMode { CentralPanel::default().show(&graphics.get_egui_context(), |ui| { if let Some(p) = &self.scene_progress { if !p.is_everything_loaded() && p.is_first_scene { - // todo: change from label to "splashscreen" ui.centered_and_justified(|ui| { egui_extras::install_image_loaders(&graphics.get_egui_context()); ui.image(egui::include_image!("../../../resources/eucalyptus-editor.png")) @@ -425,20 +466,19 @@ impl Scene for PlayMode { self.has_initial_resize_done = true; } - // if !self.display_settings.maintain_aspect_ratio { - // cam.aspect = (available_size.x / available_size.y) as f64; - // } + if !self.display_settings.maintain_aspect_ratio { + cam.aspect = (available_size.x / available_size.y) as f64; + } cam.update_view_proj(); cam.update(graphics.clone()); - let (display_width, display_height) = (available_size.x, available_size.y); - // if self.display_settings.maintain_aspect_ratio { - // let width = available_size.x; - // let height = width / cam.aspect as f32; - // (width, height) - // } else { - - // }; + let (display_width, display_height) = if self.display_settings.maintain_aspect_ratio { + let width = available_size.x; + let height = width / cam.aspect as f32; + (width, height) + } else { + (available_size.x, available_size.y) + }; let center_x = available_rect.center().x; let center_y = available_rect.center().y; @@ -6,6 +6,7 @@ import com.dropbear.input.InputState import com.dropbear.logging.Logger import com.dropbear.scene.SceneManager import com.dropbear.ui.UIInstruction +import com.dropbear.ui.UIInstructionSet internal var exceptionOnError: Boolean = false var lastErrorMessage: String? = null @@ -75,18 +76,40 @@ class DropbearEngine(val native: NativeEngine) { fun callExceptionOnError(toggle: Boolean) { } - fun renderUI(uiInstructionSet: List<UIInstruction>) { - Logger.trace("instructions: $uiInstructionSet") - renderUI(instructions = uiInstructionSet) + /** + * Renders a set of UI instructions to be displayed onto the screen. + * + * This uses the rust crate `yakui` to power the UI. You can get a [UIInstructionSet] + * by either doing one of two ways: + * + * ## Method 1 (recommended) + * ```kt + * val instructions: UIInstructionSet = buildUI { + * label("hello world!") + * } + * engine.renderUI(instructions) + * ``` + * + * ## Method 2 (the non-dsl way) + * ```kt + * val builder = UIBuilder() + * builder.add(Text.label("hello world!").toInstruction()) + * engine.renderUI(builder.build()) + * ``` + */ + fun renderUI(uiInstructionSet: UIInstructionSet?) { + if (uiInstructionSet != null) { + renderUI(instructions = uiInstructionSet) + } } /** * Quits the currently running app or game elegantly. * - * This function can have different behaviours depending on where it is ran. + * This function can have different behaviours depending on where it is run. * - eucalyptus-editor - When called, this exits your Play Mode session and returns you back to * `EditorState::Editing` - * - redback-runtime - When called, this will exit your current process and kill the app as is. It will + * - euca-runner - When called, this will exit your current process and kill the app as is. It will * also drop any pointers and do any additional cleanup. */ fun quit() { @@ -0,0 +1,50 @@ +package com.dropbear.components.camera + +import com.dropbear.math.Vector3d +import com.dropbear.physics.ColliderShape +import com.dropbear.physics.Physics + +/** + * A springy camera that is used for any camera. It uses ray casting to check if the distance from the player to the + * camera is obstructed by a physics object, and if so set the distance to the last "safe" spot. + * + * Inspired by Godot's SpringyCameraController (other engines probably have their own, never used them before). + */ +class SpringyCameraController { + private var currentDistance: Double = 5.0 + private val margin = 0.3 + private val sphereRadius = 0.2 + + fun getSpringyPosition( + playerPos: Vector3d, + targetPos: Vector3d, + deltaTime: Double + ): Vector3d { + val vectorToCam = targetPos - playerPos + val maxDist = vectorToCam.length() + val dir = vectorToCam.normalize() + + val hit = Physics.shapeCast( + origin = playerPos, + shape = ColliderShape.Sphere(sphereRadius.toFloat()), + direction = dir, + maxDistance = maxDist, + solid = false + ) + + val targetDist = if (hit != null) { + (hit.distance - margin).coerceAtLeast(0.1) + } else { + maxDist + } + + if (targetDist < currentDistance) { + currentDistance = targetDist + } else { + val returnSpeed = 5.0 + currentDistance += (targetDist - currentDistance) * (returnSpeed * deltaTime).coerceIn(0.0, 1.0) + } + + return playerPos + (dir * currentDistance) + } +} @@ -1,14 +1,16 @@ package com.dropbear.ui +typealias UIInstructionSet = List<UIInstruction> + class UIBuilder { val instructions: MutableList<UIInstruction> = mutableListOf() private var nextId = 0 internal fun generateId(): Int = nextId++ - fun build(): List<UIInstruction> = instructions.toList() + fun build(): UIInstructionSet = instructions.toList() } -inline fun buildUI(block: UIBuilder.() -> Unit): List<UIInstruction> { +inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet { return UIBuilder().apply(block).build() }