kitgit

tirbofish/dropbear · commit

3441775810f51c7332ee38e33deeeb48dc9fbb4a

feature: rigidbodies as a component

Unverified

Thribhu K <4tkbytes@pm.me> · 2025-12-27 07:06

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 9e97293..9257b36 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ colored = "3.0"
 crossbeam-channel = "0.5"
 dropbear-engine = { path = "dropbear-engine" }
 egui = "0.33"
-egui-toast = { git = "https://github.com/LazerSharkk/egui-toast" }
+egui-toast = { version = "0.19" }
 egui-wgpu = { version = "0.33" }
 egui-winit = { version = "0.33" }
 egui_dnd = "0.14"
@@ -40,7 +40,7 @@ env_logger = "0.11"
 futures = "0.3"
 gilrs = "0.11"
 git2 = { version = "0.20", features = ["vendored-openssl"] }
-glam = { version = "0.30", features = ["serde"] }
+glam = { version = "0.30", features = ["serde", "mint"] }
 hecs = { version = "0.10", features = ["serde"] }
 log = "0.4"
 log-once = "0.4"
@@ -51,7 +51,7 @@ rfd = "0.15"
 ron = "0.11"
 serde = { version = "1.0", features = ["derive"] }
 spin_sleep = "1.3"
-transform-gizmo-egui = { git = "https://github.com/kisya-games/transform-gizmo", branch = "egui-0.33" }
+transform-gizmo-egui = { version = "0.8" }
 tokio = { version = "1", features = ["full"] }
 wgpu = "27"
 winit = { version = "0.30", features = [] }
@@ -79,7 +79,7 @@ quote = "1.0"
 egui_ltreeview = { version = "0.6", features = ["doc"] }
 dyn-hash = "1.0"
 semver = { version = "1.0", features = ["serde"] }
-rapier3d = { version = "0.31", features = [ "simd-stable" ] }
+rapier3d = { version = "0.31", features = [ "simd-stable", "serde-serialize" ] }
 
 [workspace.dependencies.image]
 version = "0.25"
@@ -87,7 +87,7 @@ default-features = false
 features = ["png"]
 
 [profile.dev]
-opt-level = 1
+opt-level = 3
 debug = true
 codegen-units = 16
 incremental = true
@@ -96,3 +96,4 @@ lto = false
 # makes the debug builds so much more faster
 [profile.dev.package."*"]
 opt-level = 3
+codegen-units = 1
diff --git a/dropbear-engine/src/graphics.rs b/dropbear-engine/src/graphics.rs
index 27fabe5..831be77 100644
--- a/dropbear-engine/src/graphics.rs
+++ b/dropbear-engine/src/graphics.rs
@@ -10,14 +10,8 @@ use glam::{DMat4, DQuat, DVec3, Mat3};
 use image::GenericImageView;
 use parking_lot::Mutex;
 use std::{fs, path::PathBuf, sync::Arc, time::Instant};
-use wgpu::{
-    BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder,
-    CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, Queue, RenderPass,
-    RenderPassDepthStencilAttachment, RenderPipeline, Sampler, StencilState, SurfaceConfiguration,
-    TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
-    VertexBufferLayout,
-    util::{BufferInitDescriptor, DeviceExt},
-};
+use wgpu::*;
+use wgpu::util::*;
 use winit::window::Window;
 
 pub const NO_TEXTURE: &[u8] = include_bytes!("../../resources/textures/no-texture.png");
@@ -31,6 +25,8 @@ pub struct RenderContext<'a> {
 pub struct SharedGraphicsContext {
     pub device: Arc<Device>,
     pub queue: Arc<Queue>,
+    pub surface: Arc<Surface<'static>>,
+    pub surface_format: TextureFormat,
     pub instance: Arc<wgpu::Instance>,
     pub texture_bind_layout: Arc<BindGroupLayout>,
     pub window: Arc<Window>,
@@ -112,6 +108,8 @@ impl<'a> RenderContext<'a> {
                 diffuse_sampler,
                 screen_size,
                 texture_id: state.texture_id.clone(),
+                surface: state.surface.clone(),
+                surface_format: state.surface_format,
             }),
             frame: FrameGraphicsContext {
                 encoder,
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index 7dcaff2..34853e6 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -38,10 +38,7 @@ use std::sync::OnceLock;
 use std::{fs, sync::Arc, time::{Duration, Instant}};
 use std::collections::HashMap;
 use std::rc::Rc;
-use wgpu::{
-    BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration,
-    SurfaceError, TextureFormat,
-};
+use wgpu::{BindGroupLayout, Device, ExperimentalFeatures, Instance, Queue, Surface, SurfaceConfiguration, SurfaceError, TextureFormat};
 use winit::event::{DeviceEvent, DeviceId};
 use winit::{
     application::ApplicationHandler,
@@ -66,7 +63,8 @@ pub struct State {
     pub window: Arc<Window>,
     pub instance: Arc<Instance>,
 
-    pub surface: Surface<'static>,
+    pub surface: Arc<Surface<'static>>,
+    pub surface_format: TextureFormat,
     pub device: Arc<Device>,
     pub queue: Arc<Queue>,
     pub config: SurfaceConfiguration,
@@ -163,6 +161,7 @@ Hardware:
             .find(|f| f.is_srgb())
             .copied()
             .unwrap_or(TextureFormat::Rgba8Unorm);
+        
         let config = SurfaceConfiguration {
             usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
             format: surface_format,
@@ -218,7 +217,8 @@ Hardware:
             .register_native_texture(&device, &viewport_texture.view, wgpu::FilterMode::Linear);
 
         let result = Self {
-            surface,
+            surface: Arc::new(surface),
+            surface_format,
             device: Arc::new(device),
             queue: Arc::new(queue),
             config,
diff --git a/dropbear-traits/src/registry.rs b/dropbear-traits/src/registry.rs
index 1295cc7..eafb008 100644
--- a/dropbear-traits/src/registry.rs
+++ b/dropbear-traits/src/registry.rs
@@ -7,6 +7,45 @@ use hecs::{Entity, EntityBuilder, World};
 use std::any::TypeId;
 use std::collections::HashMap;
 
+// note: to anyone viewing this, yes i did use AI to generate the documentation for this module because
+// sometimes i kept forgetting what function some did. mb
+
+/// Registry of conversions between ECS components (`hecs`) and [`SerializableComponent`]
+/// values.
+///
+/// # What this type does
+///
+/// `ComponentRegistry` is an adapter layer around a `hecs::World` that lets you:
+///
+/// - **Extract** one or more [`SerializableComponent`] values from an entity.
+/// - **Deserialize** a [`SerializableComponent`] back into a `hecs` component
+///   and insert it into an [`EntityBuilder`].
+/// - **Address component "kinds" by numeric IDs** (useful for editor UI, network
+///   messages, prefab formats, etc.).
+/// - **Provide default/factory construction** for editor "Add component" flows.
+///
+/// # Numeric IDs and stability
+///
+/// Component IDs are assigned lazily (on registration) starting from `1`.
+///
+/// - IDs are **stable only for the lifetime of this registry instance**.
+/// - IDs are **not guaranteed to be stable across runs** (or across different
+///   registration orders), because they're assigned incrementally.
+/// - Display / editor lists returned by [`iter_available_components`] will be in
+///   **arbitrary order**, because `HashMap` iteration order is not deterministic.
+///
+/// If you need cross-run stable identifiers (e.g., long-lived save files), you
+/// should layer an explicit, user-assigned ID scheme on top.
+///
+/// # Converters vs deserializers
+///
+/// A **converter** looks at an entity and tries to produce a serializable value.
+/// A **deserializer** takes a serializable value and inserts a real ECS component
+/// into an [`EntityBuilder`].
+///
+/// For directly-serializable components, [`register`] wires up both.
+/// For custom flows, [`register_converter`] and [`register_deserializer`] can be
+/// used independently.
 pub struct ComponentRegistry {
     converters: HashMap<TypeId, Box<dyn ComponentConverter>>,
     deserializers: HashMap<TypeId, Box<dyn ComponentDeserializer>>,
@@ -17,6 +56,9 @@ pub struct ComponentRegistry {
 }
 
 impl ComponentRegistry {
+    /// Creates an empty registry.
+    ///
+    /// No components can be extracted or deserialized until they are registered.
     pub fn new() -> Self {
         Self {
             converters: HashMap::new(),
@@ -28,7 +70,17 @@ impl ComponentRegistry {
         }
     }
 
-    // Register a component that's already SerializableComponent
+    /// Registers a component type that is already a [`SerializableComponent`].
+    ///
+    /// This is the common case: `T` is both a `hecs` component and a serializable
+    /// value. The registry will:
+    ///
+    /// - Assign a numeric ID to `T` (if it doesn't already have one).
+    /// - Register a direct converter (extract `T` from an entity and clone it).
+    /// - Register a direct deserializer (insert a cloned `T` into a builder).
+    ///
+    /// Note: numeric IDs are assigned based on registration order; see the type
+    /// docs for stability caveats.
     pub fn register<T>(&mut self)
     where
         T: SerializableComponent + hecs::Component + Clone + 'static,
@@ -41,6 +93,12 @@ impl ComponentRegistry {
             .insert(type_id, Box::new(DirectDeserializer::<T>::new()));
     }
 
+    /// Registers `T` and also exposes it as an "available" component with a
+    /// default constructor.
+    ///
+    /// This is primarily meant for editor tooling: values registered via this
+    /// method will appear in [`iter_available_components`] and can be created via
+    /// [`create_default_component`].
     pub fn register_with_default<T>(&mut self)
     where
         T: SerializableComponent + hecs::Component + Clone + Default + 'static,
@@ -51,6 +109,11 @@ impl ComponentRegistry {
             .insert(id, Box::new(|| Box::new(T::default())));
     }
 
+    /// Registers `T` for extraction/deserialization (if needed) and associates a
+    /// custom factory used to create new instances of `T`.
+    ///
+    /// Like [`register_with_default`], this is intended for editor tooling.
+    /// Use this when the best "empty" value can't be expressed as `Default`.
     pub fn register_factory<T, F>(&mut self, factory: F)
     where
         T: SerializableComponent + hecs::Component + Clone + 'static,
@@ -64,6 +127,10 @@ impl ComponentRegistry {
         self.default_creators.insert(id, Box::new(factory));
     }
 
+    /// Creates a new component instance using the default constructor/factory
+    /// registered for `component_id`.
+    ///
+    /// Returns `None` if no factory/default was registered for that ID.
     pub fn create_default_component(
         &self,
         component_id: u64,
@@ -71,6 +138,18 @@ impl ComponentRegistry {
         self.default_creators.get(&component_id).map(|f| f())
     }
 
+    /// Removes the (source) ECS component associated with the given numeric ID
+    /// from `entity`.
+    ///
+    /// The registry resolves `component_id` to a *serializable* type, then finds
+    /// the first registered converter whose output type matches and asks it to
+    /// remove the underlying ECS component.
+    ///
+    /// ## Notes / edge cases
+    ///
+    /// - If no ID mapping exists or no converter matches, this is a no-op.
+    /// - If multiple converters map to the same serializable type, only the first
+    ///   match (in arbitrary `HashMap` order) will be used.
     pub fn remove_component_by_id(&self, world: &mut World, entity: Entity, component_id: u64) {
         if let Some(expected_type) = self.serializable_type_from_numeric(component_id) {
             // Find the converter that produces this serializable type
@@ -88,6 +167,18 @@ impl ComponentRegistry {
         }
     }
 
+    /// Iterates the set of components that are considered "addable" via defaults
+    /// or factories.
+    ///
+    /// Yields pairs of `(numeric_id, type_name)`.
+    ///
+    /// The returned iterator only includes components for which:
+    ///
+    /// - a default/factory was registered (via [`register_with_default`] or
+    ///   [`register_factory`]), and
+    /// - a deserializer exists to provide a human-friendly type name.
+    ///
+    /// Ordering is arbitrary.
     pub fn iter_available_components(&self) -> impl Iterator<Item = (u64, &str)> {
         self.default_creators.keys().filter_map(move |id| {
             let type_id = self.id_to_serializable.get(id)?;
@@ -96,7 +187,17 @@ impl ComponentRegistry {
         })
     }
 
-    // Register a custom converter for special cases
+    /// Registers a custom converter that extracts a serializable value `To` from
+    /// an ECS component `From`.
+    ///
+    /// Use this when the runtime ECS component isn't directly serializable, but
+    /// you can derive a serializable representation from it.
+    ///
+    /// The provided function receives `(world, entity, &From)` and may return
+    /// `None` to indicate "not present / not applicable".
+    ///
+    /// Note: this registers an ID for `To` (the serializable output type), not for
+    /// `From`.
     pub fn register_converter<From, To, F>(&mut self, converter_fn: F)
     where
         From: hecs::Component + 'static,
@@ -110,6 +211,12 @@ impl ComponentRegistry {
             .insert(type_id, Box::new(CustomConverter::new(converter_fn)));
     }
 
+    /// Registers a custom deserializer that converts a serializable `From` into
+    /// a concrete ECS component `To`.
+    ///
+    /// This is the inverse of [`register_converter`]. Use it when `From` is the
+    /// type you store/transport, and `To` is the type you actually attach to an
+    /// entity.
     pub fn register_deserializer<From, To, F>(&mut self, converter_fn: F)
     where
         From: SerializableComponent + 'static,
@@ -122,7 +229,11 @@ impl ComponentRegistry {
             .insert(type_id, Box::new(CustomDeserializer::new(converter_fn)));
     }
 
-    // Extract all serializable components from an entity
+    /// Extracts all registered serializable components from `entity`.
+    ///
+    /// This calls every registered converter and collects the values it returns.
+    ///
+    /// Ordering is arbitrary (depends on `HashMap` iteration order).
     pub fn extract_all_components(
         &self,
         world: &World,
@@ -137,6 +248,10 @@ impl ComponentRegistry {
         return vec;
     }
 
+    /// Ensures a numeric ID exists for the given serializable `TypeId` and
+    /// returns it.
+    ///
+    /// IDs are assigned incrementally and wrap on overflow. `0` is never used.
     fn ensure_serializable_id(&mut self, type_id: TypeId) -> u64 {
         if let Some(id) = self.serializable_ids.get(&type_id) {
             *id
@@ -149,14 +264,18 @@ impl ComponentRegistry {
         }
     }
 
-    /// Returns the numeric identifier that was assigned to the provided
-    /// [`SerializableComponent`] type when it was registered.
+    /// Returns the numeric identifier assigned to the dynamic component value.
+    ///
+    /// Returns `None` if the component's concrete type has not been registered.
     pub fn id_for_component(&self, component: &dyn SerializableComponent) -> Option<u64> {
         let type_id = component.as_any().type_id();
         self.serializable_ids.get(&type_id).copied()
     }
 
-    /// Returns the numeric identifier for `T` if it has been registered.
+    /// Returns the numeric identifier assigned to the serializable type `T`.
+    ///
+    /// Returns `None` if `T` has not been registered (directly or as a converter
+    /// output).
     pub fn id_for_type<T>(&self) -> Option<u64>
     where
         T: SerializableComponent + 'static,
@@ -164,12 +283,21 @@ impl ComponentRegistry {
         self.serializable_ids.get(&TypeId::of::<T>()).copied()
     }
 
+    /// Looks up the serializable `TypeId` associated with a numeric identifier.
     fn serializable_type_from_numeric(&self, component_id: u64) -> Option<TypeId> {
         self.id_to_serializable.get(&component_id).copied()
     }
 
-    /// Attempts to extract a specific component instance from an entity using
-    /// its registry-assigned numeric identifier.
+    /// Extracts a single serializable component from `entity` by numeric ID.
+    ///
+    /// Returns `None` if:
+    ///
+    /// - `component_id` is unknown, or
+    /// - none of the registered converters produce a value of that type for the
+    ///   given entity.
+    ///
+    /// If multiple converters can produce the same serializable type, the first
+    /// match (in arbitrary order) wins.
     pub fn extract_component_by_numeric_id(
         &self,
         world: &World,
@@ -189,8 +317,10 @@ impl ComponentRegistry {
         None
     }
 
-    /// Iterates every entity in the world and clones any components whose
-    /// numeric identifier matches `component_id`.
+    /// Finds every entity in `world` that has a component matching `component_id`.
+    ///
+    /// This is a convenience wrapper that iterates all entities and uses
+    /// [`extract_component_by_numeric_id`] to test each one.
     pub fn find_components_by_numeric_id(
         &self,
         world: &World,
@@ -207,10 +337,14 @@ impl ComponentRegistry {
         matches
     }
 
-    /// Attempts to deserialize a [`SerializableComponent`] back into an
-    /// ECS component and insert it into the provided [`EntityBuilder`].
-    /// Returns `Ok(true)` if the component was handled, `Ok(false)` if no
-    /// deserializer was registered, and `Err` if deserialization failed.
+    /// Deserializes a [`SerializableComponent`] into an ECS component and inserts
+    /// it into `builder`.
+    ///
+    /// Returns:
+    ///
+    /// - `Ok(true)` if a deserializer was found and insertion succeeded.
+    /// - `Ok(false)` if no deserializer is registered for this component type.
+    /// - `Err(_)` if a deserializer was found but it failed.
     pub fn deserialize_into_builder(
         &self,
         component: &dyn SerializableComponent,
diff --git a/eucalyptus-core/src/lib.rs b/eucalyptus-core/src/lib.rs
index 7fe68e3..47c3da7 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -13,12 +13,19 @@ pub mod spawn;
 pub mod states;
 pub mod utils;
 pub mod command;
+pub mod physics;
 
 pub use dropbear_macro as macros;
 pub use dropbear_traits as traits;
 
 pub use egui;
 pub use rapier3d;
+use dropbear_engine::camera::Camera;
+use dropbear_engine::entity::{EntityTransform, MeshRenderer};
+use dropbear_traits::registry::ComponentRegistry;
+use crate::camera::CameraComponent;
+use crate::physics::rigidbody::RigidBody;
+use crate::states::{Camera3D, CustomProperties, Light, Script, SerializedMeshRenderer};
 
 /// The appdata directory for storing any information.
 ///
@@ -33,4 +40,62 @@ pub extern "C" fn get_rustc_version() -> *const u8 {
     let meta = rustc_version_runtime::version_meta();
     let meta_string = format!("{:?}", meta);
     Box::leak(meta_string.into_boxed_str()).as_ptr()
+}
+
+pub fn register_components(
+    component_registry: &mut ComponentRegistry,
+) {
+    component_registry.register_with_default::<EntityTransform>();
+    component_registry.register_with_default::<CustomProperties>();
+    component_registry.register_with_default::<Light>();
+    component_registry.register_with_default::<Script>();
+    component_registry.register_with_default::<SerializedMeshRenderer>();
+    component_registry.register_with_default::<Camera3D>();
+
+    component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>(
+        |_, _, renderer| {
+            Some(SerializedMeshRenderer {
+                handle: renderer.handle().path.clone(),
+                material_override: renderer.material_overrides().to_vec(),
+            })
+        },
+    );
+
+    component_registry.register_converter::<CameraComponent, Camera3D, _>(
+        |world, entity, component| {
+            let Ok(camera) = world.get::<&Camera>(entity) else {
+                log::debug!(
+                            "Camera component without matching Camera found on entity {:?}",
+                            entity
+                        );
+                return None;
+            };
+
+            Some(Camera3D::from_ecs_camera(&camera, component))
+        },
+    );
+    
+    component_registry.register_with_default::<RigidBody>();
+
+    // // register plugin defined structs
+    // if let Err(e) = plugin_registry.load_plugins() {
+    //     fatal!("Failed to load plugins: {}", e);
+    //     return;
+    // }
+    //
+    // for p in plugin_registry.list_plugins() {
+    //     log::info!("Plugin {} has been loaded", p.display_name);
+    // }
+    //
+    // log::info!("Total plugins added: {}", plugin_registry.plugins.len());
+    //
+    // for plugin in plugin_registry.list_plugins() {
+    //     if let Some(p) = plugin_registry.get_mut(&plugin.display_name) {
+    //         p.register_component(component_registry);
+    //         log::info!(
+    //                     "Components for plugin [{}] has been registered to component registry",
+    //                     plugin.display_name
+    //                 );
+    //     }
+    // }
 }
\ No newline at end of file
diff --git a/eucalyptus-core/src/physics.rs b/eucalyptus-core/src/physics.rs
new file mode 100644
index 0000000..190536e
--- /dev/null
+++ b/eucalyptus-core/src/physics.rs
@@ -0,0 +1,95 @@
+//! Components in the eucalyptus-editor and redback-runtime that relate to rapier3d based physics.
+
+use std::collections::HashMap;
+use glam::{Vec3};
+use rapier3d::na::Vector3;
+use rapier3d::prelude::*;
+use serde::{Deserialize, Serialize};
+use crate::physics::rigidbody::RigidBodyMode;
+use crate::states::Label;
+
+pub mod rigidbody;
+
+/// A serializable [rapier3d] state that shows all the different actions and types related
+/// to physics rendering.
+#[derive(Serialize, Deserialize, Clone)]
+pub struct PhysicsState {
+    pub islands: IslandManager,
+    pub broad_phase: DefaultBroadPhase,
+    pub narrow_phase: NarrowPhase,
+    pub bodies: RigidBodySet,
+    pub colliders: ColliderSet,
+    pub impulse_joints: ImpulseJointSet,
+    pub multibody_joints: MultibodyJointSet,
+    pub ccd_solver: CCDSolver,
+    pub integration_parameters: IntegrationParameters,
+    pub gravity: Vec3,
+
+    pub bodies_entity_map: HashMap<Label, RigidBodyHandle>
+}
+
+impl PhysicsState {
+    pub fn new() -> Self {
+        Self {
+            islands: Default::default(),
+            broad_phase: Default::default(),
+            narrow_phase: Default::default(),
+            bodies: Default::default(),
+            colliders: Default::default(),
+            impulse_joints: Default::default(),
+            multibody_joints: Default::default(),
+            ccd_solver: Default::default(),
+            integration_parameters: Default::default(),
+            gravity: Vec3::new(0.0, -9.81, 0.0),
+            bodies_entity_map: Default::default(),
+        }
+    }
+
+    pub fn step(&mut self, pipeline: &mut PhysicsPipeline, physics_hooks: (), event_handler: ()) {
+        pipeline.step(
+            &vector![self.gravity.x, self.gravity.y, self.gravity.z],
+            &self.integration_parameters,
+            &mut self.islands,
+            &mut self.broad_phase,
+            &mut self.narrow_phase,
+            &mut self.bodies,
+            &mut self.colliders,
+            &mut self.impulse_joints,
+            &mut self.multibody_joints,
+            &mut self.ccd_solver,
+            &physics_hooks,
+            &event_handler,
+        );
+    }
+
+    pub fn register_rigidbody(&mut self, rigid_body: &rigidbody::RigidBody) {
+        let mode = match rigid_body.mode {
+            RigidBodyMode::Dynamic => RigidBodyType::Dynamic,
+            RigidBodyMode::Fixed => RigidBodyType::Fixed,
+            RigidBodyMode::KinematicPosition => RigidBodyType::KinematicPositionBased,
+            RigidBodyMode::KinematicVelocity => RigidBodyType::KinematicVelocityBased,
+        };
+
+        let body = RigidBodyBuilder::new(mode)
+            .gravity_scale(rigid_body.gravity_scale)
+            .can_sleep(rigid_body.can_sleep)
+            .ccd_enabled(rigid_body.ccd_enabled)
+            .linvel(Vector3::from_column_slice(&rigid_body.linvel))
+            .angvel(Vector3::from_column_slice(&rigid_body.angvel))
+            .linear_damping(rigid_body.linear_damping)
+            .angular_damping(rigid_body.angular_damping)
+            .enabled_translations(rigid_body.lock_translation.x, rigid_body.lock_translation.y, rigid_body.lock_translation.z)
+            .enabled_rotations(rigid_body.lock_rotation.x, rigid_body.lock_rotation.y, rigid_body.lock_rotation.z)
+            .build();
+
+        let handle = self.bodies.insert(body);
+
+        self.bodies_entity_map.insert(rigid_body.entity.clone(), handle);
+    }
+}
+
+impl Default for PhysicsState {
+    fn default() -> Self {
+        Self::new()
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/physics/rigidbody.rs b/eucalyptus-core/src/physics/rigidbody.rs
new file mode 100644
index 0000000..57662cb
--- /dev/null
+++ b/eucalyptus-core/src/physics/rigidbody.rs
@@ -0,0 +1,115 @@
+use serde::{Deserialize, Serialize};
+use dropbear_macro::SerializableComponent;
+use dropbear_traits::SerializableComponent;
+use crate::states::Label;
+
+/// How this entity behaves in the physics simulation.
+///
+/// This intentionally mirrors Rapier's rigid-body types, but stays engine-owned and serializable.
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
+pub enum RigidBodyMode {
+	/// A fully simulated body affected by forces and contacts.
+	Dynamic,
+	/// An immovable body.
+	Fixed,
+	/// A kinematic body controlled by setting its next position.
+	KinematicPosition,
+	/// A kinematic body controlled by setting its velocities.
+	KinematicVelocity,
+}
+
+impl Default for RigidBodyMode {
+	fn default() -> Self {
+		Self::Dynamic
+	}
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
+pub struct AxisLock {
+	pub x: bool,
+	pub y: bool,
+	pub z: bool,
+}
+
+/// A serializable physics rigid-body component.
+///
+/// Notes:
+/// - This component should NOT store Rapier handles (`RigidBodyHandle`, `ColliderHandle`, ...).
+///   Those are runtime-only and belong in a physics-world resource/system.
+/// - The body's initial pose should typically come from your `EntityTransform`/`Transform`.
+/// - Colliders/material (shape, friction, restitution, sensor, etc.) should usually be a separate
+///   component (e.g. `Collider`).
+#[derive(Debug, Serialize, Deserialize, Clone, SerializableComponent)]
+pub struct RigidBody {
+	/// The entity this component is attached to.
+	#[serde(default)]
+	pub entity: Label,
+
+	/// Body type/mode.
+	#[serde(default)]
+	pub mode: RigidBodyMode,
+
+	/// Scaling factor applied to gravity for this body.
+	#[serde(default = "RigidBody::default_gravity_scale")]
+	pub gravity_scale: f32,
+
+	/// Whether this body is allowed to sleep.
+	#[serde(default = "RigidBody::default_can_sleep")]
+	pub can_sleep: bool,
+
+	/// Whether continuous collision detection is enabled.
+	#[serde(default)]
+	pub ccd_enabled: bool,
+
+	/// Initial linear velocity (m/s).
+	#[serde(default)]
+	pub linvel: [f32; 3],
+
+	/// Initial angular velocity (rad/s).
+	#[serde(default)]
+	pub angvel: [f32; 3],
+
+	/// Linear damping coefficient.
+	#[serde(default)]
+	pub linear_damping: f32,
+
+	/// Angular damping coefficient.
+	#[serde(default)]
+	pub angular_damping: f32,
+
+	/// Locks translation along specific axes.
+	#[serde(default)]
+	pub lock_translation: AxisLock,
+
+	/// Locks rotation around specific axes.
+	#[serde(default)]
+	pub lock_rotation: AxisLock,
+}
+
+impl Default for RigidBody {
+	fn default() -> Self {
+		Self {
+			entity: Label::default(),
+			mode: RigidBodyMode::default(),
+			gravity_scale: Self::default_gravity_scale(),
+			can_sleep: Self::default_can_sleep(),
+			ccd_enabled: false,
+			linvel: [0.0, 0.0, 0.0],
+			angvel: [0.0, 0.0, 0.0],
+			linear_damping: 0.0,
+			angular_damping: 0.0,
+			lock_translation: AxisLock::default(),
+			lock_rotation: AxisLock::default(),
+		}
+	}
+}
+
+impl RigidBody {
+	const fn default_gravity_scale() -> f32 {
+		1.0
+	}
+
+	const fn default_can_sleep() -> bool {
+		true
+	}
+}
\ No newline at end of file
diff --git a/eucalyptus-core/src/runtime.rs b/eucalyptus-core/src/runtime.rs
index 49d9b9b..9f6bb02 100644
--- a/eucalyptus-core/src/runtime.rs
+++ b/eucalyptus-core/src/runtime.rs
@@ -53,7 +53,7 @@ impl Default for Authoring {
 ///
 /// Often stored as a single .eupak file, it contains all the scenes and the references of different
 /// resources.
-#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug, Clone)]
+#[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Clone)]
 pub struct RuntimeProjectConfig {
     /// The name of the project
     #[bincode(with_serde)]
diff --git a/eucalyptus-core/src/scene.rs b/eucalyptus-core/src/scene.rs
index 3f8bb6e..693ae20 100644
--- a/eucalyptus-core/src/scene.rs
+++ b/eucalyptus-core/src/scene.rs
@@ -24,6 +24,8 @@ use std::fs;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use crossbeam_channel::Sender;
+use crate::physics::PhysicsState;
+use crate::physics::rigidbody::RigidBody;
 
 #[derive(Default, Debug, Serialize, Deserialize, Clone)]
 pub struct SceneEntity {
@@ -83,7 +85,7 @@ impl SceneSettings {
 
 /// Specifies the configuration of a scene, such as its entities, hierarchies and any settings that
 /// may be necessary.
-#[derive(Default, Debug, Serialize, Deserialize, Clone)]
+#[derive(Default, Serialize, Deserialize, Clone)]
 pub struct SceneConfig {
     #[serde(default)]
     pub scene_name: String,
@@ -95,6 +97,9 @@ pub struct SceneConfig {
     pub hierarchy_map: SceneHierarchy,
 
     #[serde(default)]
+    pub physics_state: PhysicsState,
+
+    #[serde(default)]
     pub settings: SceneSettings,
 
     #[serde(skip)]
@@ -109,6 +114,7 @@ impl SceneConfig {
             path: path.as_ref().to_path_buf(),
             entities: Vec::new(),
             hierarchy_map: SceneHierarchy::new(),
+            physics_state: PhysicsState::new(),
             settings: SceneSettings::new(),
         }
     }
@@ -258,6 +264,8 @@ impl SceneConfig {
             builder.add(light_conf.transform);
         } else if let Some(script) = component.as_any().downcast_ref::<Script>() {
             builder.add(script.clone());
+        } else if let Some(body) = component.as_any().downcast_ref::<RigidBody>() {
+            builder.add(body.clone());
         } else if component.as_any().downcast_ref::<Parent>().is_some() {
             log::debug!(
                 "Skipping Parent component for '{}' - will be rebuilt from hierarchy_map",
@@ -312,7 +320,7 @@ impl SceneConfig {
     /// `is_play_mode` is used to specify if the viewport camera (debug camera) is to be used (`false`)
     /// or if the starting camera for the scene is too be used (`true`).
     pub async fn load_into_world(
-        &self,
+        &mut self,
         world: &mut hecs::World,
         graphics: Arc<SharedGraphicsContext>,
         registry: Option<&ComponentRegistry>,
@@ -427,6 +435,20 @@ impl SceneConfig {
                 }
             }
 
+            // adding to physics
+            if let Ok(mut q) = world.query_one::<(&Label, Option<&mut RigidBody>)>(entity)
+                && let Some((label, rigid)) = q.get() {
+
+                // rigidbody
+                if let Some(body) = rigid {
+                    body.entity = label.clone();
+
+                    self.physics_state.register_rigidbody(body);
+                }
+
+                // collider
+            }
+
             if let Some(previous) = label_to_entity.insert(label_for_map.clone(), entity) {
                 log::warn!(
                     "Duplicate entity label '{}' detected; previous entity {:?} will be overwritten in hierarchy mapping",
diff --git a/eucalyptus-core/src/scene/loading.rs b/eucalyptus-core/src/scene/loading.rs
index 85c54f1..20e95f8 100644
--- a/eucalyptus-core/src/scene/loading.rs
+++ b/eucalyptus-core/src/scene/loading.rs
@@ -107,4 +107,57 @@ pub struct SceneLoadHandle {
     pub id: u64,
     /// The name of the planned scene.
     pub scene_name: String,
+}
+
+#[derive(Clone)]
+pub struct IsSceneLoaded {
+    pub requested_scene: String,
+    pub id: Option<u64>,
+    pub is_first_scene: bool,
+    pub scene_handle_requested: bool,
+    pub world_loaded: bool,
+    pub camera_received: bool,
+}
+
+impl IsSceneLoaded {
+    pub fn new(requested_scene: String) -> Self {
+        Self {
+            requested_scene,
+            id: None,
+            is_first_scene: false,
+            scene_handle_requested: false,
+            world_loaded: false,
+            camera_received: false,
+        }
+    }
+
+    pub fn new_with_id(requested_scene: String, id: u64) -> Self {
+        Self {
+            requested_scene,
+            id: Some(id),
+            is_first_scene: false,
+            scene_handle_requested: false,
+            world_loaded: false,
+            camera_received: false,
+        }
+    }
+
+    pub fn new_first_time(requested_scene: String) -> Self {
+        Self {
+            requested_scene,
+            id: None,
+            is_first_scene: true,
+            scene_handle_requested: false,
+            world_loaded: false,
+            camera_received: false,
+        }
+    }
+
+    pub fn is_everything_loaded(&self) -> bool {
+        self.scene_handle_requested && self.world_loaded && self.camera_received
+    }
+
+    pub fn is_first_scene(&self) -> bool {
+        self.is_first_scene
+    }
 }
\ No newline at end of file
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index 35516ae..de50254 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -436,23 +436,23 @@ impl ScriptManager {
         }
     }
 
-    fn destroy_tagged(&mut self, tag: &str) -> anyhow::Result<()> {
-        match self.script_target {
-            ScriptTarget::None => Ok(()),
-            ScriptTarget::JVM { .. } => {
-                if let Some(jvm) = &self.jvm {
-                    let _ = jvm.unload_systems_for_tag(tag);
-                }
-                Ok(())
-            }
-            ScriptTarget::Native { .. } => {
-                if let Some(library) = &mut self.library {
-                    library.destroy_tagged(tag.to_string())?;
-                }
-                Ok(())
-            }
-        }
-    }
+    // fn destroy_tagged(&mut self, tag: &str) -> anyhow::Result<()> {
+    //     match self.script_target {
+    //         ScriptTarget::None => Ok(()),
+    //         ScriptTarget::JVM { .. } => {
+    //             if let Some(jvm) = &self.jvm {
+    //                 let _ = jvm.unload_systems_for_tag(tag);
+    //             }
+    //             Ok(())
+    //         }
+    //         ScriptTarget::Native { .. } => {
+    //             if let Some(library) = &mut self.library {
+    //                 library.destroy_tagged(tag.to_string())?;
+    //             }
+    //             Ok(())
+    //         }
+    //     }
+    // }
 
     fn destroy_in_scope_tagged(&mut self, tag: &str) -> anyhow::Result<()> {
         if !self.scripts_loaded {
diff --git a/eucalyptus-core/src/states.rs b/eucalyptus-core/src/states.rs
index e3abaec..575f02e 100644
--- a/eucalyptus-core/src/states.rs
+++ b/eucalyptus-core/src/states.rs
@@ -18,6 +18,7 @@ use std::fmt;
 use std::fmt::{Display, Formatter};
 use std::ops::{Deref, DerefMut};
 use std::path::PathBuf;
+use hecs::World;
 
 /// A global "singleton" that contains the configuration of a project.
 pub static PROJECT: Lazy<RwLock<ProjectConfig>> =
@@ -426,7 +427,7 @@ pub enum WorldLoadingStatus {
     Completed,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
+#[derive(Clone, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
 pub struct RuntimeData {
     #[bincode(with_serde)]
     pub project_config: ProjectConfig,
@@ -477,6 +478,10 @@ impl Label {
     pub fn is_empty(&self) -> bool {
         self.0.is_empty()
     }
+    
+    pub fn locate_entity(&self, world: &World) -> Option<hecs::Entity> {
+        world.query::<&Label>().iter().find_map(|(e, l)| if l == self { Some(e.clone()) } else { None })
+    }
 }
 
 impl Display for Label {
diff --git a/eucalyptus-editor/src/build.rs b/eucalyptus-editor/src/build.rs
index 4876aab..66c265e 100644
--- a/eucalyptus-editor/src/build.rs
+++ b/eucalyptus-editor/src/build.rs
@@ -9,6 +9,7 @@ use semver::Version;
 use std::fs;
 use std::path::{Path, PathBuf};
 use std::time::UNIX_EPOCH;
+use ron::ser::PrettyConfig;
 use tokio::{fs as tokio_fs, process::Command, task};
 use magna_carta::Target;
 
@@ -109,12 +110,15 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> anyhow::Result<()> {
 
 /// Reads the contents of a data.eupak file into a pretty print format.
 ///
-/// Returns the contents of the project config.
+/// Returns the contents of the project config in a [`ron`] format
 pub fn read(eupak: PathBuf) -> anyhow::Result<RuntimeProjectConfig> {
     let bytes = std::fs::read(&eupak)?;
     let (content, _): (RuntimeProjectConfig, usize) =
         bincode::decode_from_slice(&bytes, bincode::config::standard())?;
-    println!("{} contents: {:#?}", eupak.display(), content);
+
+    let str = ron::ser::to_string_pretty(&content, PrettyConfig::default())?;
+
+    println!("{} contents: {:#?}", eupak.display(), str);
     Ok(content)
 }
 
diff --git a/eucalyptus-editor/src/editor/dock.rs b/eucalyptus-editor/src/editor/dock.rs
index 225ca99..839d5f8 100644
--- a/eucalyptus-editor/src/editor/dock.rs
+++ b/eucalyptus-editor/src/editor/dock.rs
@@ -32,6 +32,7 @@ use indexmap::Equivalent;
 use log;
 use parking_lot::Mutex;
 use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, GizmoOrientation};
+use eucalyptus_core::physics::rigidbody::RigidBody;
 
 pub struct EditorTabViewer<'a> {
     pub view: egui::TextureId,
@@ -664,6 +665,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
 
                                 ui.separator();
 
+                                // mesh renderer
                                 if let Ok(mut q) = world.query_one::<&mut MeshRenderer>(inspect_entity)
                                     && let Some(e) = q.get()
                                 {
@@ -678,6 +680,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     );
                                 }
 
+                                // entity transform
                                 if let Ok(mut q) = world.query_one::<&mut EntityTransform>(inspect_entity)
                                     && let Some(t) = q.get()
                                 {
@@ -694,6 +697,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.separator();
                                 }
 
+                                // custom properties
                                 if let Ok(mut q) = world.query_one::<&mut CustomProperties>(inspect_entity)
                                     && let Some(props) = q.get()
                                 {
@@ -710,6 +714,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.separator();
                                 }
 
+                                // camera
                                 if let Ok(mut q) = world
                                     .query_one::<(&mut Camera, &mut CameraComponent)>(inspect_entity)
                                     && let Some((camera, camera_component)) = q.get()
@@ -774,6 +779,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.separator();
                                 }
 
+                                // light
                                 if let Ok(mut q) = world.query_one::<(&mut Light, &mut LightComponent, &mut Transform)>(inspect_entity)
                                     && let Some((light, comp, transform)) = q.get()
                                 {
@@ -791,7 +797,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.separator();
                                 }
 
-                                if let Ok(mut q) = self.world.query_one::<&mut Script>(*entity)
+                                // script
+                                if let Ok(mut q) = world.query_one::<&mut Script>(*entity)
                                     && let Some(script) = q.get()
                                 {
                                     CollapsingHeader::new("Script").default_open(true).show(ui, |ui| {
@@ -807,22 +814,24 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
                                     ui.separator();
                                 }
 
-                                if let Some(t) = cfg.label_last_edit
-                                    && t.elapsed() >= Duration::from_millis(500)
+                                // rigidbody
+                                if let Ok(mut q) = world.query_one::<&mut RigidBody>(*entity)
+                                    && let Some(body) = q.get()
                                 {
-                                    if let Some(ent) = cfg.old_label_entity.take()
-                                        && let Some(orig) = cfg.label_original.take()
-                                    {
-                                        UndoableAction::push_to_undo(
+                                    CollapsingHeader::new("RigidBody").default_open(true).show(ui, |ui| {
+                                        body.inspect(
+                                            entity,
+                                            &mut cfg,
+                                            ui,
                                             self.undo_stack,
-                                            UndoableAction::Label(ent, orig),
-                                        );
-                                        log::debug!(
-                                            "Pushed label change to undo stack after 500ms debounce period"
+                                            self.signal,
+                                            label.as_mut_string(),
                                         );
-                                    }
-                                    cfg.label_last_edit = None;
+                                    });
+                                    ui.separator();
                                 }
+
+
                             }
                         } else {
                             log_once::debug_once!("Unable to query entity inside resource inspector");
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 0418287..4f2ed85 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -18,7 +18,7 @@ use dropbear_engine::shader::Shader;
 use dropbear_engine::{camera::Camera, entity::{MeshRenderer, Transform}, future::FutureHandle, graphics::{RenderContext, SharedGraphicsContext}, lighting::LightManager, model::{MODEL_CACHE, ModelId}, scene::SceneCommand, DropbearWindowBuilder, WindowData};
 use egui::{self, Context};
 use egui_dock::{DockArea, DockState, NodeIndex, Style};
-use eucalyptus_core::APP_INFO;
+use eucalyptus_core::{register_components, APP_INFO};
 use eucalyptus_core::hierarchy::{Children, SceneHierarchy};
 use eucalyptus_core::scene::{SceneConfig, SceneEntity};
 use eucalyptus_core::states::{CustomProperties, Label, SerializedMeshRenderer};
@@ -31,7 +31,7 @@ use eucalyptus_core::{
     scripting::{BuildStatus},
     states,
     states::{
-        Camera3D, EditorTab, Light, PROJECT, SCENES, Script, WorldLoadingStatus,
+        EditorTab, PROJECT, SCENES, Script, WorldLoadingStatus,
     },
     success,
     utils::ViewportMode,
@@ -45,7 +45,7 @@ use std::{
     fs,
     path::PathBuf,
     sync::Arc,
-    time::{Duration, Instant},
+    time::{Instant},
 };
 use std::rc::Rc;
 use tokio::sync::oneshot;
@@ -161,67 +161,11 @@ impl Editor {
 
         eucalyptus_core::utils::start_deadlock_detector();
 
-        let mut plugin_registry = PluginRegistry::new();
+        let plugin_registry = PluginRegistry::new();
         let mut component_registry = ComponentRegistry::new();
 
-        fn register_components(
-            plugin_registry: &mut PluginRegistry,
-            component_registry: &mut ComponentRegistry,
-        ) {
-            component_registry.register_with_default::<EntityTransform>();
-            component_registry.register_with_default::<CustomProperties>();
-            component_registry.register_with_default::<Light>();
-            component_registry.register_with_default::<Script>();
-            component_registry.register_with_default::<SerializedMeshRenderer>();
-            component_registry.register_with_default::<Camera3D>();
-
-            component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>(
-                |_, _, renderer| {
-                    Some(SerializedMeshRenderer {
-                        handle: renderer.handle().path.clone(),
-                        material_override: renderer.material_overrides().to_vec(),
-                    })
-                },
-            );
-
-            component_registry.register_converter::<CameraComponent, Camera3D, _>(
-                |world, entity, component| {
-                    let Ok(camera) = world.get::<&Camera>(entity) else {
-                        log::debug!(
-                            "Camera component without matching Camera found on entity {:?}",
-                            entity
-                        );
-                        return None;
-                    };
-
-                    Some(Camera3D::from_ecs_camera(&camera, component))
-                },
-            );
-
-            // register plugin defined structs
-            if let Err(e) = plugin_registry.load_plugins() {
-                fatal!("Failed to load plugins: {}", e);
-                return;
-            }
-
-            for p in plugin_registry.list_plugins() {
-                log::info!("Plugin {} has been loaded", p.display_name);
-            }
-
-            log::info!("Total plugins added: {}", plugin_registry.plugins.len());
-
-            for plugin in plugin_registry.list_plugins() {
-                if let Some(p) = plugin_registry.get_mut(&plugin.display_name) {
-                    p.register_component(component_registry);
-                    log::info!(
-                        "Components for plugin [{}] has been registered to component registry",
-                        plugin.display_name
-                    );
-                }
-            }
-        }
+        register_components(/*&mut plugin_registry,*/ &mut component_registry);
 
-        register_components(&mut plugin_registry, &mut component_registry);
         let component_registry = Arc::new(component_registry);
 
         Ok(Self {
@@ -532,7 +476,7 @@ impl Editor {
         };
 
         {
-            if let Some(first_scene) = first_scene_opt {
+            if let Some(mut first_scene) = first_scene_opt {
                 let cam = first_scene
                     .load_into_world(
                         world,
@@ -663,7 +607,7 @@ impl Editor {
         }
     }
 
-    fn start_async_scene_load(&mut self, scene: SceneConfig, graphics: &mut RenderContext) {
+    fn start_async_scene_load(&mut self, mut scene: SceneConfig, graphics: &mut RenderContext) {
         self.cleanup_scene_resources(graphics);
 
         let (progress_sender, progress_receiver) =
diff --git a/eucalyptus-editor/src/lib.rs b/eucalyptus-editor/src/lib.rs
index 7a0235c..354f15e 100644
--- a/eucalyptus-editor/src/lib.rs
+++ b/eucalyptus-editor/src/lib.rs
@@ -12,3 +12,4 @@ pub mod utils;
 pub mod runtime;
 pub mod about;
 pub mod process;
+pub mod physics;
diff --git a/eucalyptus-editor/src/physics.rs b/eucalyptus-editor/src/physics.rs
new file mode 100644
index 0000000..5c5b298
--- /dev/null
+++ b/eucalyptus-editor/src/physics.rs
@@ -0,0 +1 @@
+pub mod rigidbody;
\ No newline at end of file
diff --git a/eucalyptus-editor/src/physics/rigidbody.rs b/eucalyptus-editor/src/physics/rigidbody.rs
new file mode 100644
index 0000000..62a4f6f
--- /dev/null
+++ b/eucalyptus-editor/src/physics/rigidbody.rs
@@ -0,0 +1,103 @@
+use egui::{ComboBox, Ui};
+use hecs::Entity;
+use eucalyptus_core::physics::rigidbody::{RigidBody, RigidBodyMode};
+use eucalyptus_core::states::Label;
+use crate::editor::component::InspectableComponent;
+use crate::editor::{Signal, StaticallyKept, UndoableAction};
+
+impl InspectableComponent for RigidBody {
+    fn inspect(
+        &mut self,
+        _entity: &mut Entity,
+        _cfg: &mut StaticallyKept,
+        ui: &mut Ui,
+        _undo_stack: &mut Vec<UndoableAction>,
+        _signal: &mut Signal,
+        label: &mut String
+    ) {
+        ui.vertical(|ui| {
+            self.entity = Label::new(label.clone());
+
+            let mut selected = self.mode.clone();
+            ComboBox::from_id_salt("rigidbody")
+                .selected_text(format!("{:?}", self.mode))
+                .show_ui(ui, |ui| {
+                    ui.selectable_value(&mut selected, RigidBodyMode::Dynamic, "Dynamic");
+                    ui.selectable_value(&mut selected, RigidBodyMode::Fixed, "Fixed");
+                    ui.selectable_value(&mut selected, RigidBodyMode::KinematicPosition, "Kinematic Position");
+                    ui.selectable_value(&mut selected, RigidBodyMode::KinematicVelocity, "Kinematic Velocity");
+                });
+
+            if selected != self.mode {
+                self.mode = selected;
+            }
+
+            ui.add_space(8.0);
+
+            ui.horizontal(|ui| {
+                ui.label("Gravity Scale:");
+                ui.add(egui::DragValue::new(&mut self.gravity_scale)
+                    .speed(0.1)
+                    .range(0.0..=10.0));
+            });
+
+            ui.checkbox(&mut self.can_sleep, "Can Sleep");
+
+            ui.checkbox(&mut self.ccd_enabled, "CCD Enabled");
+
+            ui.add_space(8.0);
+
+            ui.label("Linear Velocity:");
+            ui.horizontal(|ui| {
+                ui.label("X:");
+                ui.add(egui::DragValue::new(&mut self.linvel[0]).speed(0.1));
+                ui.label("Y:");
+                ui.add(egui::DragValue::new(&mut self.linvel[1]).speed(0.1));
+                ui.label("Z:");
+                ui.add(egui::DragValue::new(&mut self.linvel[2]).speed(0.1));
+            });
+
+            ui.label("Angular Velocity:");
+            ui.horizontal(|ui| {
+                ui.label("X:");
+                ui.add(egui::DragValue::new(&mut self.angvel[0]).speed(0.1));
+                ui.label("Y:");
+                ui.add(egui::DragValue::new(&mut self.angvel[1]).speed(0.1));
+                ui.label("Z:");
+                ui.add(egui::DragValue::new(&mut self.angvel[2]).speed(0.1));
+            });
+
+            ui.add_space(8.0);
+
+            ui.horizontal(|ui| {
+                ui.label("Linear Damping:");
+                ui.add(egui::DragValue::new(&mut self.linear_damping)
+                    .speed(0.01)
+                    .range(0.0..=10.0));
+            });
+
+            ui.horizontal(|ui| {
+                ui.label("Angular Damping:");
+                ui.add(egui::DragValue::new(&mut self.angular_damping)
+                    .speed(0.01)
+                    .range(0.0..=10.0))
+            });
+
+            ui.add_space(8.0);
+
+            ui.label("Lock Translation:");
+            ui.horizontal(|ui| {
+                ui.checkbox(&mut self.lock_translation.x, "X");
+                ui.checkbox(&mut self.lock_translation.y, "Y");
+                ui.checkbox(&mut self.lock_translation.z, "Z");
+            });
+
+            ui.label("Lock Rotation:");
+            ui.horizontal(|ui| {
+                ui.checkbox(&mut self.lock_rotation.x, "X");
+                ui.checkbox(&mut self.lock_rotation.y, "Y");
+                ui.checkbox(&mut self.lock_rotation.z, "Z");
+            });
+        });
+    }
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/runtime/command.rs b/eucalyptus-editor/src/runtime/command.rs
index abfa82a..a214299 100644
--- a/eucalyptus-editor/src/runtime/command.rs
+++ b/eucalyptus-editor/src/runtime/command.rs
@@ -2,7 +2,7 @@ use dropbear_engine::graphics::RenderContext;
 use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand};
 use winit::window::CursorGrabMode;
 use crate::runtime::PlayMode;
-use crate::runtime::scene::IsSceneLoaded;
+use eucalyptus_core::scene::loading::IsSceneLoaded;
 
 impl CommandBufferPoller for PlayMode {
     fn poll(&mut self, graphics: &mut RenderContext) {
diff --git a/eucalyptus-editor/src/runtime/mod.rs b/eucalyptus-editor/src/runtime/mod.rs
index e716ac9..d564c66 100644
--- a/eucalyptus-editor/src/runtime/mod.rs
+++ b/eucalyptus-editor/src/runtime/mod.rs
@@ -4,7 +4,7 @@ use std::sync::Arc;
 use crossbeam_channel::{unbounded, Receiver};
 use futures::executor;
 use hecs::{Entity, World};
-use wgpu::RenderPipeline;
+use wgpu::{RenderPipeline, SurfaceConfiguration};
 use dropbear_engine::camera::Camera;
 use dropbear_engine::future::FutureHandle;
 use dropbear_engine::graphics::RenderContext;
@@ -19,9 +19,12 @@ use eucalyptus_core::scene::loading::SCENE_LOADER;
 use eucalyptus_core::traits::registry::ComponentRegistry;
 use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr};
 use eucalyptus_core::command::COMMAND_BUFFER;
-use crate::runtime::scene::IsSceneLoaded;
+use eucalyptus_core::scene::loading::IsSceneLoaded;
 use std::collections::HashMap;
 use std::path::PathBuf;
+use winit::window::Fullscreen;
+use eucalyptus_core::physics::PhysicsState;
+use eucalyptus_core::rapier3d::prelude::*;
 
 mod scene;
 mod input;
@@ -90,6 +93,10 @@ pub struct PlayMode {
     pending_camera: Option<Entity>,
     pub(crate) scripts_ready: bool,
     has_initial_resize_done: bool,
+
+    // physics
+    physics_pipeline: PhysicsPipeline,
+    physics_state: PhysicsState,
 }
 
 impl PlayMode {
@@ -103,7 +110,7 @@ impl PlayMode {
             current_scene: None,
             world_loading_progress: None,
             world_receiver: None,
-            component_registry: Arc::new(Default::default()),
+            component_registry: Arc::new(ComponentRegistry::new()),
             scene_loading_handle: None,
             scene_progress: None,
             pending_world: None,
@@ -118,6 +125,8 @@ impl PlayMode {
                 maintain_aspect_ratio: true,
                 vsync: true,
             },
+            physics_pipeline: Default::default(),
+            physics_state: PhysicsState::new(),
         };
 
         log::debug!("Created new play mode instance");
@@ -237,7 +246,7 @@ impl PlayMode {
             }
         }
 
-        let scene_to_load = {
+        let mut scene_to_load = {
             let scenes = SCENES.read();
             let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone();
             scene
@@ -288,7 +297,7 @@ impl PlayMode {
         self.scene_loading_handle = None;
         self.scene_progress = None;
 
-        let scene_to_load = {
+        let mut scene_to_load = {
             let scenes = SCENES.read();
             scenes.iter()
                 .find(|s| s.scene_name == scene_name)
@@ -360,6 +369,58 @@ pub struct DisplaySettings {
     pub vsync: bool,
 }
 
+impl DisplaySettings {
+    pub fn update(&mut self, graphics: &RenderContext) {
+        let window = graphics.shared.window.clone();
+
+        let is_maximized = window.is_maximized();
+        let is_fullscreen = window.fullscreen().is_some();
+
+        self.window_mode = if is_fullscreen {
+            WindowMode::BorderlessFullscreen
+        } else if is_maximized {
+            WindowMode::Maximized
+        } else {
+            WindowMode::Windowed
+        };
+
+        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 | WindowMode::BorderlessFullscreen => {
+                let monitor = window.current_monitor();
+                window.set_fullscreen(Some(Fullscreen::Borderless(monitor)));
+                window.set_maximized(false);
+            }
+        }
+
+        if self.vsync {
+            let config = SurfaceConfiguration {
+                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+                format: graphics.shared.surface_format,
+                width: graphics.frame.screen_size.0 as u32,
+                height: graphics.frame.screen_size.1 as u32,
+                present_mode: if self.vsync {
+                    wgpu::PresentMode::Fifo
+                } else {
+                    wgpu::PresentMode::Immediate
+                },
+                alpha_mode: wgpu::CompositeAlphaMode::Auto,
+                view_formats: vec![],
+                desired_maximum_frame_latency: 2,
+            };
+
+            graphics.shared.surface.configure(&graphics.shared.device, &config);
+        }
+    }
+}
+
 pub enum WindowMode {
     Windowed,
     Maximized,
diff --git a/eucalyptus-editor/src/runtime/scene.rs b/eucalyptus-editor/src/runtime/scene.rs
index e05bc74..5528338 100644
--- a/eucalyptus-editor/src/runtime/scene.rs
+++ b/eucalyptus-editor/src/runtime/scene.rs
@@ -1,6 +1,6 @@
 use std::collections::HashMap;
 use egui::{CentralPanel, MenuBar, TopBottomPanel};
-use hecs::{Entity};
+use hecs::Entity;
 use wgpu::Color;
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
@@ -14,7 +14,7 @@ use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::command::CommandBufferPoller;
 use eucalyptus_core::hierarchy::EntityTransformExt;
 use eucalyptus_core::states::PROJECT;
-use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult};
+use eucalyptus_core::scene::loading::{IsSceneLoaded, SceneLoadResult, SCENE_LOADER};
 use crate::runtime::{PlayMode, WindowMode};
 
 impl Scene for PlayMode {
@@ -35,6 +35,17 @@ impl Scene for PlayMode {
         }
     }
 
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {
+        // note to self: always update script before applying physics
+        if self.scripts_ready {
+            if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) {
+                panic!("Script physics update error: {}", e);
+            }
+        }
+
+        self.physics_state.step(&mut self.physics_pipeline, (), ());
+    }
+
     fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
         graphics.shared.future_queue.poll();
         self.poll(graphics);
@@ -74,7 +85,7 @@ impl Scene for PlayMode {
                 log::debug!("Camera entity received: {:?}", cam);
                 if let Some(ref mut progress) = self.scene_progress {
                     progress.camera_received = true;
-                    
+
                     if progress.world_loaded {
                         if let Some(id) = progress.id {
                             let mut loader = SCENE_LOADER.lock();
@@ -264,14 +275,6 @@ impl Scene for PlayMode {
         self.input_state.mouse_delta = None;
     }
 
-    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {
-        if self.scripts_ready {
-            if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) {
-                panic!("Script physics update error: {}", e);
-            }
-        }
-    }
-
     fn render(&mut self, graphics: &mut RenderContext) {
         let Some(active_camera) = self.active_camera else {
             return;
@@ -395,55 +398,3 @@ impl Scene for PlayMode {
     }
 }
 
-#[derive(Clone)]
-pub struct IsSceneLoaded {
-    pub(crate) requested_scene: String,
-    pub(crate) id: Option<u64>,
-    is_first_scene: bool,
-    pub(crate) scene_handle_requested: bool,
-    pub(crate) world_loaded: bool,
-    pub(crate) camera_received: bool,
-}
-
-impl IsSceneLoaded {
-    pub fn new(requested_scene: String) -> Self {
-        Self {
-            requested_scene,
-            id: None,
-            is_first_scene: false,
-            scene_handle_requested: false,
-            world_loaded: false,
-            camera_received: false,
-        }
-    }
-
-    pub fn new_with_id(requested_scene: String, id: u64) -> Self {
-        Self {
-            requested_scene,
-            id: Some(id),
-            is_first_scene: false,
-            scene_handle_requested: false,
-            world_loaded: false,
-            camera_received: false,
-        }
-    }
-
-    pub fn new_first_time(requested_scene: String) -> Self {
-        Self {
-            requested_scene,
-            id: None,
-            is_first_scene: true,
-            scene_handle_requested: false,
-            world_loaded: false,
-            camera_received: false,
-        }
-    }
-
-    pub fn is_everything_loaded(&self) -> bool {
-        self.scene_handle_requested && self.world_loaded && self.camera_received
-    }
-
-    pub fn is_first_scene(&self) -> bool {
-        self.is_first_scene
-    }
-}
\ No newline at end of file
diff --git a/redback-runtime/Cargo.toml b/redback-runtime/Cargo.toml
index 4a461ac..41e5152 100644
--- a/redback-runtime/Cargo.toml
+++ b/redback-runtime/Cargo.toml
@@ -25,6 +25,9 @@ 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 = ["eucalyptus-core/runtime"]
diff --git a/redback-runtime/src/command.rs b/redback-runtime/src/command.rs
index ca40adc..052c658 100644
--- a/redback-runtime/src/command.rs
+++ b/redback-runtime/src/command.rs
@@ -1,52 +1,57 @@
 use dropbear_engine::graphics::RenderContext;
-use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand, get_config};
+use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER, CommandBuffer, WindowCommand};
 use winit::window::CursorGrabMode;
+use crate::scene::IsSceneLoaded;
 
-use crate::scene::RuntimeScene;
-
-impl CommandBufferPoller for RuntimeScene {
+impl CommandBufferPoller for Runtime {
     fn poll(&mut self, graphics: &mut RenderContext) {
         while let Ok(cmd) = COMMAND_BUFFER.1.try_recv() {
             log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd);
             match cmd {
                 CommandBuffer::WindowCommand(w_cmd) => match w_cmd {
-                    WindowCommand::WindowGrab(is_locked) => {
-                        let mut cfg = get_config().write();
-                        if cfg.is_locked != is_locked {
-                            if is_locked {
-                                if let Err(e) = graphics.shared.window
-                                    .set_cursor_grab(CursorGrabMode::Confined)
-                                    .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked))
-                                {
-                                    log_once::warn_once!("Failed to grab cursor: {:?}", e);
-                                } else {
-                                    log_once::info_once!("Grabbed cursor");
-                                    cfg.is_locked = true;
-                                }
-                            } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) {
-                                log_once::warn_once!("Failed to release cursor: {:?}", e);
+                    WindowCommand::WindowGrab(lock) => {
+                        if lock {
+                            if let Err(e) = graphics.shared.window
+                                .set_cursor_grab(CursorGrabMode::Confined)
+                                .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked))
+                            {
+                                log_once::warn_once!("Failed to grab cursor: {:?}", e);
                             } else {
-                                log_once::info_once!("Released cursor");
-                                cfg.is_locked = false;
+                                log_once::info_once!("Grabbed cursor");
                             }
+                        } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) {
+                            log_once::warn_once!("Failed to release cursor: {:?}", e);
+                        } else {
+                            log_once::info_once!("Released cursor");
                         }
                     }
                     WindowCommand::HideCursor(should_hide) => {
-                        let cfg = get_config().write();
-                        if cfg.is_hidden != should_hide {
-                            if should_hide {
-                                graphics.shared.window.set_cursor_visible(false);
-                            } else {
-                                graphics.shared.window.set_cursor_visible(true);
-                            }
+                        if should_hide {
+                            graphics.shared.window.set_cursor_visible(false);
+                        } else {
+                            graphics.shared.window.set_cursor_visible(true);
                         }
                     }
                 },
                 CommandBuffer::Quit => {
                     self.scene_command = dropbear_engine::scene::SceneCommand::Quit;
                 },
-                CommandBuffer::SwitchScene(scene_name) => {
-                    self.pending_scene_switch = Some(scene_name);
+                CommandBuffer::SwitchSceneImmediate(scene_name) => {
+                    log::debug!("Immediate scene switch requested: {}", scene_name);
+                    let scene_to_load = IsSceneLoaded::new(scene_name);
+                    self.request_immediate_scene_load(graphics, scene_to_load);
+                }
+                CommandBuffer::LoadSceneAsync(handle) => {
+                    log::debug!("Load scene async requested");
+                    let scene_to_load = IsSceneLoaded::new_with_id(handle.scene_name, handle.id);
+                    self.request_async_scene_load(graphics, scene_to_load);
+                }
+                CommandBuffer::SwitchToAsync(handle) => {
+                    if let Some(ref progress) = self.scene_progress {
+                        if progress.requested_scene == handle.scene_name && progress.is_everything_loaded() {
+                            self.switch_to(progress.clone(), graphics);
+                        }
+                    }
                 }
             }
         }
diff --git a/redback-runtime/src/debug.rs b/redback-runtime/src/debug.rs
index bd539eb..37659da 100644
--- a/redback-runtime/src/debug.rs
+++ b/redback-runtime/src/debug.rs
@@ -4,9 +4,8 @@ use dropbear_engine::entity::{MeshRenderer, Transform};
 use dropbear_engine::lighting::{Light, LightComponent};
 use eucalyptus_core::camera::CameraComponent;
 use eucalyptus_core::states::{Label, CustomProperties, Script};
-use crate::scene::RuntimeScene;
 
-impl RuntimeScene {
+impl Runtime {
     #[allow(dead_code)]
     pub fn display_all_entities(&self) {
         log::debug!("====================");
diff --git a/redback-runtime/src/input.rs b/redback-runtime/src/input.rs
index 3c0f0cf..44839b1 100644
--- a/redback-runtime/src/input.rs
+++ b/redback-runtime/src/input.rs
@@ -1,12 +1,12 @@
-use crate::scene::RuntimeScene;
-use dropbear_engine::gilrs::{Button, GamepadId};
+use gilrs::{Button, GamepadId};
+use winit::dpi::PhysicalPosition;
+use winit::event::MouseButton;
+use winit::event_loop::ActiveEventLoop;
+use winit::keyboard::KeyCode;
 use dropbear_engine::input::{Controller, Keyboard, Mouse};
-use dropbear_engine::winit::dpi::PhysicalPosition;
-use dropbear_engine::winit::event::MouseButton;
-use dropbear_engine::winit::event_loop::ActiveEventLoop;
-use dropbear_engine::winit::keyboard::KeyCode;
+use crate::Runtime;
 
-impl Keyboard for RuntimeScene {
+impl Keyboard for Runtime {
     fn key_down(&mut self, key: KeyCode, _event_loop: &ActiveEventLoop) {
         self.input_state.pressed_keys.insert(key);
     }
@@ -16,11 +16,11 @@ impl Keyboard for RuntimeScene {
     }
 }
 
-impl Mouse for RuntimeScene {
+impl Mouse for Runtime {
     fn mouse_move(&mut self, position: PhysicalPosition<f64>, delta: Option<(f64, f64)>) {
         let delta = if delta.is_none() {
             if let Some(last_pos) = self.input_state.last_mouse_pos {
-                Some((last_pos.0 - position.x, last_pos.1 - position.y))
+                Some((position.x - last_pos.0, position.y - last_pos.1))
             } else {
                 None
             }
@@ -42,7 +42,7 @@ impl Mouse for RuntimeScene {
     }
 }
 
-impl Controller for RuntimeScene {
+impl Controller for Runtime {
     fn button_down(&mut self, button: Button, id: GamepadId) {
         self.input_state
             .pressed_buttons
@@ -75,4 +75,4 @@ impl Controller for RuntimeScene {
         self.input_state.left_stick_position.remove(&id);
         self.input_state.right_stick_position.remove(&id);
     }
-}
+}
\ No newline at end of file
diff --git a/redback-runtime/src/lib.rs b/redback-runtime/src/lib.rs
new file mode 100644
index 0000000..4183575
--- /dev/null
+++ b/redback-runtime/src/lib.rs
@@ -0,0 +1,331 @@
+//! Allows you to a launch play mode as another window.
+
+use std::sync::Arc;
+use crossbeam_channel::{unbounded, Receiver};
+use futures::executor;
+use hecs::{Entity, World};
+use wgpu::RenderPipeline;
+use dropbear_engine::camera::Camera;
+use dropbear_engine::future::FutureHandle;
+use dropbear_engine::graphics::RenderContext;
+use dropbear_engine::lighting::LightManager;
+use dropbear_engine::scene::SceneCommand;
+use dropbear_engine::shader::Shader;
+use eucalyptus_core::camera::CameraComponent;
+use eucalyptus_core::input::InputState;
+use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
+use eucalyptus_core::states::{WorldLoadingStatus, SCENES, Script, PROJECT};
+use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER};
+use eucalyptus_core::traits::registry::ComponentRegistry;
+use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr};
+use eucalyptus_core::command::COMMAND_BUFFER;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use dropbear_engine::wgpu::RenderPipeline;
+
+mod input;
+mod scene;
+mod debug;
+mod utils;
+mod command;
+
+pub struct Runtime {
+    scene_command: SceneCommand,
+    input_state: InputState,
+    script_manager: ScriptManager,
+    world: Box<World>,
+    component_registry: Arc<ComponentRegistry>,
+    active_camera: Option<Entity>,
+
+    // rendering
+    render_pipeline: Option<RenderPipeline>,
+    light_manager: LightManager,
+
+    display_settings: DisplaySettings,
+
+    initial_scene: Option<String>,
+    current_scene: Option<String>,
+    world_loading_progress: Option<Receiver<WorldLoadingStatus>>,
+    world_receiver: Option<tokio::sync::oneshot::Receiver<World>>,
+    scene_loading_handle: Option<FutureHandle>,
+    scene_progress: Option<IsSceneLoaded>,
+    pending_world: Option<Box<World>>,
+    pending_camera: Option<Entity>,
+    pub(crate) scripts_ready: bool,
+    has_initial_resize_done: bool,
+}
+
+impl Runtime {
+    pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> {
+        let result = Self {
+            scene_command: SceneCommand::None,
+            input_state: InputState::new(),
+            script_manager: ScriptManager::new()?,
+            world: Box::new(World::new()),
+            initial_scene,
+            current_scene: None,
+            world_loading_progress: None,
+            world_receiver: None,
+            component_registry: Arc::new(Default::default()),
+            scene_loading_handle: None,
+            scene_progress: None,
+            pending_world: None,
+            pending_camera: None,
+            active_camera: None,
+            render_pipeline: None,
+            light_manager: Default::default(),
+            scripts_ready: false,
+            has_initial_resize_done: false,
+            display_settings: DisplaySettings {
+                window_mode: WindowMode::Windowed,
+                maintain_aspect_ratio: true,
+                vsync: true,
+            },
+        };
+
+        log::debug!("Created new play mode instance");
+
+        Ok(result)
+    }
+
+    pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) {
+        let shader = Shader::new(
+            graphics.shared.clone(),
+            dropbear_engine::shader::shader_wesl::SHADER_SHADER,
+            Some("viewport_shader"),
+        );
+
+        self.light_manager
+            .create_light_array_resources(graphics.shared.clone());
+
+        if let Some(active_camera) = self.active_camera {
+            if let Ok(mut q) = self
+                .world
+                .query_one::<(&Camera, &CameraComponent)>(active_camera)
+            {
+                if let Some((camera, _component)) = q.get() {
+                    let pipeline = graphics.create_render_pipline(
+                        &shader,
+                        vec![
+                            &graphics.shared.texture_bind_layout.clone(),
+                            camera.layout(),
+                            self.light_manager.layout(),
+                        ],
+                        None,
+                    );
+                    self.render_pipeline = Some(pipeline);
+
+                    self.light_manager.create_render_pipeline(
+                        graphics.shared.clone(),
+                        dropbear_engine::shader::shader_wesl::LIGHT_SHADER,
+                        camera,
+                        Some("Light Pipeline"),
+                    );
+                } else {
+                    log_once::warn_once!(
+                        "Unable to fetch the query result of camera: {:?}",
+                        active_camera
+                    )
+                }
+            } else {
+                log_once::warn_once!(
+                    "Unable to query camera, component for active camera: {:?}",
+                    active_camera
+                );
+            }
+        } else {
+            log_once::warn_once!("No active camera found");
+        }
+    }
+
+    fn reload_scripts_for_current_world(&mut self) {
+        let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new();
+        for (entity_id, script) in self.world.query::<&Script>().iter() {
+            for tag in &script.tags {
+                entity_tag_map.entry(tag.clone()).or_default().push(entity_id);
+            }
+        }
+
+        let target = ScriptTarget::JVM {
+            library_path: find_jvm_library_path(),
+        };
+
+        self.scripts_ready = false;
+
+        if let Err(e) = self
+            .script_manager
+            .init_script(None, entity_tag_map.clone(), target.clone())
+        {
+            log::error!("Failed to initialise scripts: {}", e);
+            return;
+        }
+
+        let world_ptr = self.world.as_mut() as WorldPtr;
+        let input_ptr = &mut self.input_state as InputStatePtr;
+        let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr;
+
+        if let Err(e) = self
+            .script_manager
+            .load_script(world_ptr, input_ptr, graphics_ptr)
+        {
+            log::error!("Failed to load scripts: {}", e);
+            return;
+        }
+
+        self.scripts_ready = true;
+        log::debug!("Scripts initialised successfully");
+    }
+
+    /// Requests an asynchronous scene load, returning immediately and loading the scene in the background.
+    pub fn request_async_scene_load(&mut self, graphics: &RenderContext, requested_scene: IsSceneLoaded) {
+        log::debug!("Requested async scene load: {}", requested_scene.requested_scene);
+        let scene_name = requested_scene.requested_scene.clone();
+        self.scene_progress = Some(requested_scene);
+
+        let (tx, rx) = unbounded::<WorldLoadingStatus>();
+        let (world_tx, world_rx) = tokio::sync::oneshot::channel::<World>();
+
+        self.world_loading_progress = Some(rx);
+        self.world_receiver = Some(world_rx);
+
+
+        if let Some(ref progress) = self.scene_progress {
+            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();
+                    }
+                }
+            }
+        }
+
+        let scene_to_load = {
+            let scenes = SCENES.read();
+            let scene = scenes.iter().find(|s| s.scene_name == scene_name).unwrap().clone();
+            scene
+        };
+
+        let graphics_cloned = graphics.shared.clone();
+        let component_registry = self.component_registry.clone();
+
+        let handle = graphics.shared.future_queue.push(async move {
+            let mut temp_world = World::new();
+            let load_status = scene_to_load.load_into_world(
+                &mut temp_world,
+                graphics_cloned,
+                Some(&component_registry),
+                Some(tx),
+                true,
+            ).await;
+            match load_status {
+                Ok(v) => {
+                    if world_tx.send(temp_world).is_err() {
+                        log::warn!("Unable to send world: Receiver has been deallocated. This usually means a new scene load was requested before this one finished.");
+                    };
+                    v
+                }
+                Err(e) => {panic!("Failed to load scene [{}]: {}", scene_to_load.scene_name, e);}
+            }
+        });
+
+        log::debug!("Created future handle for scene loading: {:?}", handle);
+
+        self.scene_loading_handle = Some(handle);
+        if let Some(ref mut progress) = self.scene_progress {
+            progress.scene_handle_requested = true;
+        }
+    }
+
+    /// Requests an immediate scene load, blocking the current thread until the scene is fully loaded.
+    pub fn request_immediate_scene_load(&mut self, graphics: &mut RenderContext, requested_scene: IsSceneLoaded) {
+        let scene_name = requested_scene.requested_scene.clone();
+        log::debug!("Immediate scene load requested: {}", scene_name);
+
+        self.world = Box::new(World::new());
+        self.active_camera = None;
+        self.render_pipeline = None;
+        self.current_scene = None;
+        self.world_loading_progress = None;
+        self.world_receiver = None;
+        self.scene_loading_handle = None;
+        self.scene_progress = None;
+
+        let scene_to_load = {
+            let scenes = SCENES.read();
+            scenes.iter()
+                .find(|s| s.scene_name == scene_name)
+                .cloned()
+                .expect(&format!("Scene '{}' not found", scene_name))
+        };
+
+        let graphics_cloned = graphics.shared.clone();
+        let component_registry = self.component_registry.clone();
+
+        let (tx, _rx) = unbounded::<WorldLoadingStatus>();
+
+        let (loaded_world, camera_entity) = executor::block_on(async move {
+            let mut temp_world = World::new();
+            let camera = scene_to_load.load_into_world(
+                &mut temp_world,
+                graphics_cloned,
+                Some(&component_registry),
+                Some(tx),
+                true,
+            ).await;
+
+            match camera {
+                Ok(cam) => (temp_world, cam),
+                Err(e) => panic!("Failed to immediately load scene [{}]: {}", scene_to_load.scene_name, e),
+            }
+        });
+
+        self.world = Box::new(loaded_world);
+        self.active_camera = Some(camera_entity);
+        self.current_scene = Some(scene_name.clone());
+
+        let mut progress = requested_scene;
+        progress.scene_handle_requested = true;
+        progress.world_loaded = true;
+        progress.camera_received = true;
+        self.scene_progress = Some(progress);
+
+        self.load_wgpu_nerdy_stuff(graphics);
+
+        self.reload_scripts_for_current_world();
+
+        log::debug!("Scene '{}' loaded", scene_name);
+    }
+
+    /// Switches to a new scene, clearing the current world and preparing to load the new scene.
+    pub fn switch_to(&mut self, scene_progress: IsSceneLoaded, graphics: &mut RenderContext) {
+        log::debug!("Switching to new scene requested: {}", scene_progress.requested_scene);
+
+        if scene_progress.is_everything_loaded() {
+            if let Some(new_world) = self.pending_world.take() {
+                self.world = new_world;
+            }
+            if let Some(new_camera) = self.pending_camera.take() {
+                self.active_camera = Some(new_camera);
+            }
+
+            self.load_wgpu_nerdy_stuff(graphics);
+            self.reload_scripts_for_current_world();
+
+            self.current_scene = Some(scene_progress.requested_scene.clone());
+        }
+    }
+}
+
+pub struct DisplaySettings {
+    pub window_mode: WindowMode,
+    pub maintain_aspect_ratio: bool,
+    pub vsync: bool,
+}
+
+pub enum WindowMode {
+    Windowed,
+    Maximized,
+    Fullscreen,
+    BorderlessFullscreen,
+}
\ No newline at end of file
diff --git a/redback-runtime/src/main.rs b/redback-runtime/src/main.rs
index ba9ef80..b4dc9e9 100644
--- a/redback-runtime/src/main.rs
+++ b/redback-runtime/src/main.rs
@@ -1,9 +1,3 @@
-mod input;
-mod scene;
-mod debug;
-mod utils;
-mod command;
-
 use crate::scene::RuntimeScene;
 use app_dirs2::AppInfo;
 use dropbear_engine::future::FutureQueue;
diff --git a/redback-runtime/src/scene.rs b/redback-runtime/src/scene.rs
index 0e32b83..14a8117 100644
--- a/redback-runtime/src/scene.rs
+++ b/redback-runtime/src/scene.rs
@@ -1,317 +1,110 @@
 use std::collections::HashMap;
-use std::env;
-use std::path::PathBuf;
-use std::sync::Arc;
-
+use hecs::{Entity};
+use wgpu::Color;
+use wgpu::util::DeviceExt;
+use winit::event_loop::ActiveEventLoop;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
-use dropbear_engine::future::FutureHandle;
 use dropbear_engine::graphics::{InstanceRaw, RenderContext};
-use dropbear_engine::lighting::{Light, LightComponent, LightManager};
-use dropbear_engine::model::{DrawLight, DrawModel, MODEL_CACHE, ModelId};
+use dropbear_engine::lighting::{Light, LightComponent};
+use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE};
 use dropbear_engine::scene::{Scene, SceneCommand};
-use dropbear_engine::shader::{self, Shader};
-use dropbear_engine::wgpu::util::DeviceExt;
-use dropbear_engine::wgpu::{self, Color, RenderPipeline};
-use dropbear_engine::winit::event_loop::ActiveEventLoop;
-use dropbear_engine::winit::window::Window;
-use dropbear_engine::asset::ASSET_REGISTRY;
+use dropbear_engine::wgpu;
 use eucalyptus_core::camera::CameraComponent;
-use eucalyptus_core::egui::{self, CentralPanel, Frame, UiBuilder};
+use eucalyptus_core::command::CommandBufferPoller;
+use eucalyptus_core::egui;
+use eucalyptus_core::egui::{CentralPanel, MenuBar, TopBottomPanel};
 use eucalyptus_core::hierarchy::EntityTransformExt;
-use eucalyptus_core::input::InputState;
-use eucalyptus_core::ptr::{CommandBufferPtr, InputStatePtr, WorldPtr};
-use eucalyptus_core::runtime::RuntimeProjectConfig;
-use eucalyptus_core::scene::SceneConfig;
-use eucalyptus_core::scripting::{ScriptManager, ScriptTarget};
-use eucalyptus_core::states::{Camera3D, Light as LightConfig, CustomProperties, Script, SerializedMeshRenderer};
-use eucalyptus_core::traits::registry::ComponentRegistry;
-use eucalyptus_core::command::{CommandBufferPoller, COMMAND_BUFFER};
-use hecs::{Entity, World};
-use parking_lot::Mutex;
-use tokio::sync::oneshot;
-use tokio::sync::oneshot::error::TryRecvError;
-use crate::ConfigFile;
-
-/// The scene that the redback-runtime uses.
-pub(crate) struct RuntimeScene {
-    #[allow(dead_code)]
-    project_config: RuntimeProjectConfig,
-    window_config: ConfigFile,
-    scenes: HashMap<String, SceneConfig>,
-    initial_scene: String,
-
-    pub world: Box<World>,
-    pub input_state: Box<InputState>,
-    pub active_camera: Arc<Mutex<Option<Entity>>>,
-    render_pipeline: Option<RenderPipeline>,
-    light_manager: LightManager,
-    component_registry: Arc<ComponentRegistry>,
-    script_manager: ScriptManager,
-    script_target: Option<ScriptTarget>,
-    scripts_ready: bool,
-    pub scene_command: SceneCommand,
-
-    current_scene: Option<String>,
-    pub(crate) pending_scene_switch: Option<String>,
-    world_receiver: Option<oneshot::Receiver<World>>,
-    world_load_handle: Option<FutureHandle>,
-    pub window: Option<Arc<Window>>,
-}
-
-impl RuntimeScene {
-    /// Creates a new instance of [`RuntimeScene`]
-    pub fn new(project_config: RuntimeProjectConfig, window_config: ConfigFile) -> anyhow::Result<Self> {
-        eucalyptus_core::utils::start_deadlock_detector();
-
-        let initial_scene = project_config.initial_scene.clone();
-
-        let scenes = project_config
-            .scenes
-            .iter()
-            .map(|scene| (scene.scene_name.clone(), scene.clone()))
-            .collect::<HashMap<_, _>>();
-
-        let component_registry = Self::build_component_registry();
-        let script_target = Self::detect_script_target(&project_config.project_name);
-
-        let result = Self {
-            project_config: project_config.clone(),
-            window_config: window_config.clone(),
-            scenes,
-            initial_scene,
-            world: Box::new(World::new()),
-            input_state: Box::new(InputState::new()),
-            active_camera: Arc::new(Mutex::new(None)),
-            render_pipeline: None,
-            light_manager: LightManager::new(),
-            current_scene: None,
-            pending_scene_switch: None,
-            component_registry,
-            script_manager: ScriptManager::new()?,
-            script_target,
-            scripts_ready: false,
-            scene_command: Default::default(),
-            world_receiver: None,
-            world_load_handle: None,
-            window: None,
-        };
+use eucalyptus_core::states::PROJECT;
+use eucalyptus_core::scene::loading::{SCENE_LOADER, SceneLoadResult, IsSceneLoaded};
+use crate::Runtime;
 
-        Ok(result)
-    }
-
-    fn build_component_registry() -> Arc<ComponentRegistry> {
-        let mut component_registry = ComponentRegistry::new();
-        component_registry.register_with_default::<EntityTransform>();
-        component_registry.register_with_default::<CustomProperties>();
-        component_registry.register_with_default::<LightConfig>();
-        component_registry.register_with_default::<Script>();
-        component_registry.register_with_default::<SerializedMeshRenderer>();
-
-        component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>(
-            |_, _, renderer| {
-                Some(SerializedMeshRenderer {
-                    handle: renderer.handle().path.clone(),
-                    material_override: renderer.material_overrides().to_vec(),
-                })
-            },
-        );
-
-        component_registry.register_converter::<CameraComponent, Camera3D, _>(
-            |world, entity, component| {
-                let Ok(camera) = world.get::<&Camera>(entity) else {
-                    log::debug!(
-                        "Camera component without matching Camera found on entity {:?}",
-                        entity
-                    );
-                    return None;
-                };
+impl Scene for Runtime {
+    fn load(&mut self, graphics: &mut RenderContext) {
+        if self.current_scene.is_none() {
+            let initial_scene = if let Some(s) = &self.initial_scene {
+                s.clone()
+            } else {
+                let proj = PROJECT.read();
+                proj.runtime_settings.initial_scene.clone().expect("No initial scene set in project settings")
+            };
 
-                Some(Camera3D::from_ecs_camera(&camera, component))
-            },
-        );
+            log::debug!("Loading initial scene: {}", initial_scene);
 
-        Arc::new(component_registry)
-    }
+            let first_time = IsSceneLoaded::new_first_time(initial_scene);
 
-    fn detect_script_target(project_name: &str) -> Option<ScriptTarget> {
-        if let Ok(path) = env::var("REDBACK_SCRIPT_PATH") {
-            let candidate = PathBuf::from(path);
-            if candidate.exists() {
-                return if candidate
-                    .extension()
-                    .map_or(false, |ext| ext.eq_ignore_ascii_case("jar"))
-                {
-                    Some(ScriptTarget::JVM {
-                        library_path: candidate,
-                    })
-                } else {
-                    Some(ScriptTarget::Native {
-                        library_path: candidate,
-                    })
-                };
-            }
+            self.request_async_scene_load(graphics, first_time);
         }
+    }
 
-        if let Ok(mut exe_path) = env::current_exe() {
-            exe_path.pop();
-            let project_root = exe_path;
+    fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
+        graphics.shared.future_queue.poll();
+        self.poll(graphics);
 
-            let preferred_jar = project_root.join(format!("{project_name}.jar"));
-            if preferred_jar.exists() {
-                return Some(ScriptTarget::JVM {
-                    library_path: preferred_jar,
-                });
+        if let Some(ref progress) = self.scene_progress {
+            if !progress.scene_handle_requested && self.world_receiver.is_none() && self.scene_loading_handle.is_none() {
+                log::debug!("Starting async load for scene: {}", progress.requested_scene);
+                let scene_to_load = IsSceneLoaded::new(progress.requested_scene.clone());
+                self.request_async_scene_load(graphics, scene_to_load);
             }
+        }
 
-            if let Ok(entries) = std::fs::read_dir(&project_root) {
-                for entry in entries.flatten() {
-                    let path = entry.path();
-                    if path
-                        .extension()
-                        .map_or(false, |ext| ext.eq_ignore_ascii_case("jar"))
-                    {
-                        return Some(ScriptTarget::JVM { library_path: path });
+        if let Some(mut receiver) = self.world_receiver.take() {
+            if let Ok(loaded_world) = receiver.try_recv() {
+                self.pending_world = Some(Box::new(loaded_world));
+                log::debug!("World received");
+                if let Some(ref mut progress) = self.scene_progress {
+                    progress.world_loaded = true;
+
+                    if progress.camera_received {
+                        if let Some(id) = progress.id {
+                            let mut loader = SCENE_LOADER.lock();
+                            if let Some(entry) = loader.get_entry_mut(id) {
+                                entry.result = SceneLoadResult::Success;
+                            }
+                        }
                     }
                 }
+            } else {
+                self.world_receiver = Some(receiver);
             }
+        }
 
-            let native_path = project_root.join(format!("{project_name}.dll"));
-            if native_path.exists() {
-                return Some(ScriptTarget::Native {
-                    library_path: native_path,
-                });
-            }
-
-            #[cfg(not(target_os = "windows"))]
-            {
-                let so_path = project_root.join(format!("lib{project_name}.so"));
-                if so_path.exists() {
-                    return Some(ScriptTarget::Native {
-                        library_path: so_path,
-                    });
+        if let Some(handle) = self.scene_loading_handle.take() {
+            if let Some(cam) = graphics.shared.future_queue.exchange_owned_as::<Entity>(&handle) {
+                self.pending_camera = Some(cam);
+                log::debug!("Camera entity received: {:?}", cam);
+                if let Some(ref mut progress) = self.scene_progress {
+                    progress.camera_received = true;
+
+                    if progress.world_loaded {
+                        if let Some(id) = progress.id {
+                            let mut loader = SCENE_LOADER.lock();
+                            if let Some(entry) = loader.get_entry_mut(id) {
+                                entry.result = SceneLoadResult::Success;
+                            }
+                        }
+                    }
                 }
+            } else {
+                self.scene_loading_handle = Some(handle)
             }
         }
 
-        None
-    }
-
-    fn start_initial_scene_load(&mut self, graphics: &mut RenderContext) -> anyhow::Result<()> {
-        let scene_name = if self.scenes.contains_key(&self.initial_scene) {
-            self.initial_scene.clone()
-        } else if let Some(first_scene) = self.scenes.values().next() {
-            log::warn!(
-                "Initial scene '{}' not found, falling back to '{}'",
-                self.initial_scene,
-                first_scene.scene_name
-            );
-            first_scene.scene_name.clone()
-        } else {
-            anyhow::bail!("No scenes packaged with runtime config");
-        };
-
-        self.queue_scene_load(&scene_name, graphics)
-    }
-
-    fn queue_scene_load(&mut self, scene_name: &str, graphics: &mut RenderContext) -> anyhow::Result<()> {
-        self.cleanup_scene_resources(graphics);
-
-        let scene = self
-            .scenes
-            .get(scene_name)
-            .cloned()
-            .ok_or_else(|| anyhow::anyhow!("Scene '{}' not found in runtime package", scene_name))?;
-
-        // self.reset_state_for_scene_load(graphics);
-
-        let (world_sender, world_receiver) = oneshot::channel();
-        self.world_receiver = Some(world_receiver);
-
-        let graphics_shared = graphics.shared.clone();
-        let active_camera = self.active_camera.clone();
-        let component_registry = self.component_registry.clone();
-        let scene_name_owned = scene.scene_name.clone();
-
-        let handle = graphics.shared.future_queue.push(async move {
-            let mut temp_world = World::new();
-            match scene
-                .load_into_world(
-                    &mut temp_world,
-                    graphics_shared.clone(),
-                    Some(component_registry.as_ref()),
-                    None,
-                    true,
-                )
-                .await
-            {
-                Ok(camera_entity) => {
-                    let mut active = active_camera.lock();
-                    *active = Some(camera_entity);
-                    log::info!("Loaded scene '{}'", scene_name_owned);
-                    log::debug!("Checkpoint 2: Camera entity: {:?}", camera_entity);
-                }
-                Err(err) => {
-                    panic!("Failed to load scene '{}': {}", scene_name_owned, err);
+        if let Some(ref progress) = self.scene_progress {
+            if progress.is_everything_loaded() {
+                if self.current_scene.as_ref() != Some(&progress.requested_scene) {
+                    self.switch_to(progress.clone(), graphics);
                 }
             }
-
-            if world_sender.send(temp_world).is_err() {
-                panic!(
-                    "Scene loader dropped before world delivery for '{}'",
-                    scene_name_owned
-                );
-            }
-        });
-
-        self.world_load_handle = Some(handle);
-        self.current_scene = Some(scene_name.to_string());
-
-        Ok(())
-    }
-
-    fn cleanup_scene_resources(&mut self, graphics: &mut RenderContext) {
-        if let Some(handle) = self.world_load_handle.take() {
-            graphics.shared.future_queue.cancel(&handle);
-        }
-
-        self.world_receiver = None;
-        self.scripts_ready = false;
-        self.active_camera.lock().take();
-        self.world.clear();
-
-        self.render_pipeline = None;
-        self.light_manager = LightManager::new();
-
-        {
-            let mut cache = MODEL_CACHE.lock();
-            cache.clear();
         }
 
-        // Drop cached asset registry entries so models/materials/meshes from the previous scene
-        // do not linger across scene loads.
-        ASSET_REGISTRY.clear_cached_assets();
-    }
-
-    fn poll_scene_loading(&mut self, graphics: &mut RenderContext) {
-        if let Some(mut receiver) = self.world_receiver.take() {
-            match receiver.try_recv() {
-                Ok(world) => {
-                    self.world = Box::new(world);
-                    self.initialise_rendering(graphics);
-                    self.prepare_scripts();
-                }
-                Err(TryRecvError::Empty) => {
-                    self.world_receiver = Some(receiver);
-                }
-                Err(TryRecvError::Closed) => {
-                    panic!("Scene loading task ended before delivering world");
-                }
+        if self.scripts_ready {
+            if let Err(e) = self.script_manager.update_script(self.world.as_mut(), dt) {
+                panic!("Script update error: {}", e);
             }
         }
-    }
 
-    fn update_world_state(&mut self, graphics: &mut RenderContext) {
         {
             let mut query = self.world.query::<(&mut MeshRenderer, &Transform)>();
             for (_entity, (renderer, transform)) in query.iter() {
@@ -334,7 +127,6 @@ impl RuntimeScene {
         }
 
         {
-            // Update lights using their standalone Transform (not EntityTransform)
             let mut light_query = self.world.query::<(&mut LightComponent, &Transform, &mut Light)>();
             for (_, (light_comp, transform, light)) in light_query.iter() {
                 light.update(light_comp, transform);
@@ -354,211 +146,85 @@ impl RuntimeScene {
 
         self.light_manager
             .update(graphics.shared.clone(), &self.world);
-    }
-
-    fn initialise_rendering(&mut self, graphics: &mut RenderContext) {
-        if self.render_pipeline.is_some() {
-            return;
-        }
-
-        let Some(active_camera) = *self.active_camera.lock() else {
-            return;
-        };
-
-        let camera = if let Ok(mut q) = self
-            .world
-            .query_one::<(&Camera, &CameraComponent)>(active_camera)
-        {
-            q.get().map(|(cam, _)| cam.clone())
-        } else {
-            None
-        };
-
-        let Some(camera) = camera else {
-            return;
-        };
-
-        self.light_manager
-            .create_light_array_resources(graphics.shared.clone());
-
-        let shader = Shader::new(
-            graphics.shared.clone(),
-            shader::shader_wesl::SHADER_SHADER,
-            Some("runtime_viewport"),
-        );
-
-        let pipeline = graphics.create_render_pipline(
-            &shader,
-            vec![
-                &graphics.shared.texture_bind_layout.clone(),
-                camera.layout(),
-                self.light_manager.layout(),
-            ],
-            None,
-        );
-        self.render_pipeline = Some(pipeline);
-
-        self.light_manager.create_render_pipeline(
-            graphics.shared.clone(),
-            shader::shader_wesl::LIGHT_SHADER,
-            &camera,
-            Some("light_pipeline"),
-        );
-
-        self.window = Some(graphics.shared.window.clone());
-    }
-
-    fn prepare_scripts(&mut self) {
-        self.scripts_ready = false;
-        let Some(target) = self.script_target.clone() else {
-            log::debug!("No script target detected; skipping script setup");
-            return;
-        };
-
-        let mut entity_tag_map: HashMap<String, Vec<Entity>> = HashMap::new();
-        for (entity_id, script) in self.world.query::<&Script>().iter() {
-            for tag in &script.tags {
-                entity_tag_map.entry(tag.clone()).or_default().push(entity_id);
-            }
-        }
-
-        log::debug!("Awaiting for script library to be initialised");
-
-        if let Err(err) = self.script_manager.init_script(
-            self.window_config.jvm_args.clone(),
-            entity_tag_map.clone(),
-            target.clone(),
-        ) {
-            panic!("Failed to init script manager: {}", err);
-        }
-
-        log::debug!("Loaded!");
 
-        let world_ptr = self.world.as_mut() as WorldPtr;
-        let input_ptr = self.input_state.as_mut() as InputStatePtr;
-        let graphics_ptr = COMMAND_BUFFER.0.as_ref() as CommandBufferPtr;
-
-        if let Err(err) = self
-            .script_manager
-            .load_script(world_ptr, input_ptr, graphics_ptr)
-        {
-            panic!("Failed to load scripts: {}", err);
-        }
-
-        self.scripts_ready = true;
-    }
-}
-
-impl Scene for RuntimeScene {
-    fn load(&mut self, graphics: &mut RenderContext) {
-        self.window = Some(graphics.shared.window.clone());
-
-        if let Err(err) = self.start_initial_scene_load(graphics) {
-            panic!("Unable to load initial scene: {}", err);
-        }
-    }
-
-    fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
-        graphics.shared.future_queue.poll();
-
-        self.poll_scene_loading(graphics);        
-        self.poll(graphics);
-
-        if self.world_receiver.is_none() {
-            if let Some(scene_name) = self.pending_scene_switch.take() {
-                if let Err(err) = self.queue_scene_load(&scene_name, graphics) {
-                    log::error!("Failed to switch scene contents to '{}': {}", scene_name, err);
+        CentralPanel::default().show(&graphics.shared.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| {
+                        ui.label("Loading scene...");
+                    });
+                    return;
                 }
             }
-        }
 
-        CentralPanel::default().frame(Frame::new()).show(&graphics.shared.get_egui_context(), |ui| {
-            if self.render_pipeline.is_none() {
-                ui.label("Loading scene...");
-            }
+            let texture_id = *graphics.shared.texture_id;
 
-            if self.render_pipeline.is_none() {
-                return;
-            }
+            let available_size = ui.available_size();
+            let available_rect = ui.available_rect_before_wrap();
 
-            self.update_world_state(graphics);
+            if let Some(active_camera) = self.active_camera {
+                if let Ok(cam) = self.world.query_one_mut::<&mut Camera>(active_camera) {
+                    if !self.has_initial_resize_done {
+                        cam.aspect = (available_size.x / available_size.y) as f64;
 
-            let egui_ctx = graphics.shared.get_egui_context();
-            let egui_wants_input = egui_ctx.wants_pointer_input() || egui_ctx.wants_keyboard_input();
-            
-            if egui_wants_input {
-                self.input_state.pressed_keys.clear();
-                self.input_state.mouse_button.clear();
-            }
-
-            if self.scripts_ready {
-                let world_ptr = self.world.as_mut() as WorldPtr;
-                if let Err(err) = unsafe { self.script_manager.update_script(world_ptr, &self.input_state, dt) } {
-                    panic!("Script update failed: {}", err);
-                }
-            }
+                        self.has_initial_resize_done = true;
+                    }
 
-            let texture_id = *graphics.shared.texture_id;
-            let available_size = ui.available_rect_before_wrap().size();
-
-            let is_fullscreen = self.window_config.windowed_mode.is_fullscreen();
-            
-            let viewport_aspect = self.viewport_resolution.0 as f32 / self.viewport_resolution.1 as f32;
-            let available_aspect = available_size.x / available_size.y;
-            
-            let active_camera: Option<Entity> = *self.active_camera.lock();
-            if let Some(cam_ent) = active_camera {
-                if let Ok(mut q) = self.world.query_one::<&mut Camera>(cam_ent) {
-                    if let Some(camera) = q.get() {
-                        if is_fullscreen {
-                            camera.aspect = viewport_aspect as f64;
-                        } else {
-                            camera.aspect = available_aspect as f64;
-                        }
-                        camera.update_view_proj();
-                        camera.update(graphics.shared.clone());
+                    if !self.display_settings.maintain_aspect_ratio {
+                        cam.aspect = (available_size.x / available_size.y) as f64;
                     }
-                }
-            }
-            
-            let (display_width, display_height) = if is_fullscreen {
-                if available_aspect > viewport_aspect {
-                    (available_size.x, available_size.x / viewport_aspect)
+                    cam.update_view_proj();
+                    cam.update(graphics.shared.clone());
+
+                    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;
+
+                    let image_rect = egui::Rect::from_center_size(
+                        egui::pos2(center_x, center_y),
+                        egui::vec2(display_width, display_height),
+                    );
+
+                    ui.allocate_exact_size(available_size, egui::Sense::hover());
+
+                    ui.scope_builder(egui::UiBuilder::new().max_rect(image_rect), |ui| {
+                        ui.add(egui::Image::new(egui::load::SizedTexture {
+                            id: texture_id,
+                            size: egui::vec2(display_width, display_height),
+                        }));
+                    });
                 } else {
-                    (available_size.y * viewport_aspect, available_size.y)
+                    log::warn!("No such camera exists in the world");
                 }
             } else {
-                (available_size.x, available_size.y)
-            };
-            
-            let rect = ui.available_rect_before_wrap();
-            let x_offset = (available_size.x - display_width) / 2.0;
-            let y_offset = (available_size.y - display_height) / 2.0;
-            let image_rect = egui::Rect::from_min_size(
-                egui::pos2(rect.min.x + x_offset, rect.min.y + y_offset),
-                egui::vec2(display_width, display_height),
-            );
-            
-            ui.scope_builder(UiBuilder::new().max_rect(image_rect), |ui| {
-                ui.add(egui::Image::new(egui::load::SizedTexture {
-                    id: texture_id,
-                    size: egui::vec2(display_width, display_height),
-                }));
-            });
-
-            self.input_state.window = self.window.clone();
-            self.input_state.mouse_delta = None;
+                log::warn!("No active camera available");
+            }
         });
+
+        self.input_state.mouse_delta = None;
     }
 
-    fn render(&mut self, graphics: &mut RenderContext) {
-        if self.render_pipeline.is_none() {
-            self.initialise_rendering(graphics);
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {
+        if self.scripts_ready {
+            if let Err(e) = self.script_manager.physics_update_script(self.world.as_mut(), _dt) {
+                panic!("Script physics update error: {}", e);
+            }
         }
+    }
 
-        let Some(active_camera) = *self.active_camera.lock() else {
+    fn render(&mut self, graphics: &mut RenderContext) {
+        let Some(active_camera) = self.active_camera else {
             return;
         };
+        log_once::debug_once!("Active camera found: {:?}", active_camera);
 
         let q = if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) {
             query.get().cloned()
@@ -569,18 +235,19 @@ impl Scene for RuntimeScene {
         let Some(camera) = q else {
             return;
         };
-
-        // camera.debug_camera_state();
-        // println!("{:#?}", self.project_config);
+        log_once::debug_once!("Camera ready");
+        log_once::debug_once!("Camera currently being viewed: {}", camera.label);
 
         let Some(pipeline) = &self.render_pipeline else {
+            log_once::warn_once!("Render pipeline not ready");
             return;
         };
+        log_once::debug_once!("Pipeline ready");
 
         let clear_color = Color {
-            r: 0.05,
-            g: 0.07,
-            b: 0.10,
+            r: 100.0 / 255.0,
+            g: 149.0 / 255.0,
+            b: 237.0 / 255.0,
             a: 1.0,
         };
 
@@ -603,6 +270,15 @@ impl Scene for RuntimeScene {
         };
 
         {
+            let mut query = self.world.query::<(&mut LightComponent, &dropbear_engine::entity::Transform, &mut Light)>();
+            for (_, (light_component, transform, light)) in query.iter() {
+                light.update(light_component, transform);
+            }
+        }
+
+        self.light_manager.update(graphics.shared.clone(), &self.world);
+
+        {
             let mut render_pass = graphics.clear_colour(clear_color);
             if let Some(light_pipeline) = &self.light_manager.pipeline {
                 render_pass.set_pipeline(light_pipeline);
@@ -660,21 +336,9 @@ impl Scene for RuntimeScene {
         }
     }
 
-    fn exit(&mut self, event_loop: &ActiveEventLoop) {
-        let _ = event_loop;
-        self.scene_command = SceneCommand::None;
-        if let Some(window) = &self.window {
-            window.set_cursor_visible(true);
-        }
-        self.world.clear();
-        self.render_pipeline = None;
-        self.scripts_ready = false;
-        self.world_receiver = None;
-        self.world_load_handle = None;
-        self.script_target = None;
-    }
+    fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
 
     fn run_command(&mut self) -> SceneCommand {
         std::mem::replace(&mut self.scene_command, SceneCommand::None)
     }
-}
+}
\ No newline at end of file