kitgit

tirbofish/dropbear · diff

7b40ac3 · Thribhu K

feature: implemented my destroy() (scene exit function), and laid some groundwork for physics updates. also added some new docs and new native signatures + magna-carta updates. jarvis, run github actions

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index f99ffd6..9e97293 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -79,6 +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" ] }
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/dropbear-engine/src/lib.rs b/dropbear-engine/src/lib.rs
index e31a9d3..7dcaff2 100644
--- a/dropbear-engine/src/lib.rs
+++ b/dropbear-engine/src/lib.rs
@@ -17,6 +17,8 @@ pub mod shader;
 pub mod utils;
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
+pub const PHYSICS_STEP_RATE: u32 = 50;
+const MAX_PHYSICS_STEPS_PER_FRAME: usize = 8;
 
 use app_dirs2::{AppDataType, AppInfo};
 use bytemuck::Contiguous;
@@ -76,12 +78,16 @@ pub struct State {
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
 
+    physics_accumulator: Duration,
+
     pub scene_manager: scene::Manager,
 }
 
 impl State {
     /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to.
     pub async fn new(window: Arc<Window>, instance: Arc<Instance>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> {
+        let title = window.title();
+
         let size = window.inner_size();
 
         let surface = instance.create_surface(window.clone())?;
@@ -96,7 +102,7 @@ impl State {
 
         let (device, queue) = adapter
             .request_device(&wgpu::DeviceDescriptor {
-                label: None,
+                label: Some(format!("{} graphics device", title).as_str()),
                 required_features: wgpu::Features::empty(),
                 required_limits: wgpu::Limits::default(),
                 experimental_features: unsafe { ExperimentalFeatures::enabled() },
@@ -225,6 +231,7 @@ Hardware:
             texture_id: Arc::new(texture_id),
             future_queue,
             instance,
+            physics_accumulator: Duration::ZERO,
             scene_manager: scene::Manager::new(),
         };
 
@@ -315,14 +322,31 @@ Hardware:
 
         let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new());
 
+        let physics_dt = Duration::from_secs_f32(1.0 / PHYSICS_STEP_RATE as f32);
+        let frame_dt = Duration::from_secs_f32(previous_dt).min(Duration::from_millis(250));
+        let mut physics_accumulator = self.physics_accumulator + frame_dt;
+
         let window_commands = {
             let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder);
 
+            let mut steps = 0usize;
+            while physics_accumulator >= physics_dt && steps < MAX_PHYSICS_STEPS_PER_FRAME {
+                scene_manager.physics_update(physics_dt.as_secs_f32(), &mut graphics);
+                physics_accumulator -= physics_dt;
+                steps += 1;
+            }
+
+            if steps == MAX_PHYSICS_STEPS_PER_FRAME && physics_accumulator >= physics_dt {
+                physics_accumulator = physics_accumulator.min(physics_dt);
+            }
+
             let commands = scene_manager.update(previous_dt, &mut graphics, event_loop);
             scene_manager.render(&mut graphics);
             commands
         };
 
+        self.physics_accumulator = physics_accumulator;
+
         self.scene_manager = scene_manager;
 
         self.egui_renderer.lock().end_frame_and_draw(
diff --git a/dropbear-engine/src/scene.rs b/dropbear-engine/src/scene.rs
index 4fa50d9..ca7dfbc 100644
--- a/dropbear-engine/src/scene.rs
+++ b/dropbear-engine/src/scene.rs
@@ -11,6 +11,7 @@ use std::{collections::HashMap, rc::Rc};
 
 pub trait Scene {
     fn load(&mut self, graphics: &mut crate::graphics::RenderContext);
+    fn physics_update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext);
     fn update(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext);
     fn render(&mut self, graphics: &mut crate::graphics::RenderContext);
     fn exit(&mut self, event_loop: &ActiveEventLoop);
@@ -149,6 +150,18 @@ impl Manager {
         Vec::new()
     }
 
+    pub fn physics_update<'a>(
+        &mut self,
+        dt: f32,
+        graphics: &mut crate::graphics::RenderContext<'a>,
+    ) {
+        if let Some(scene_name) = &self.current_scene
+            && let Some(scene) = self.scenes.get_mut(scene_name)
+        {
+            scene.write().physics_update(dt, graphics)
+        }
+    }
+
     pub fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) {
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
diff --git a/eucalyptus-core/Cargo.toml b/eucalyptus-core/Cargo.toml
index f32139e..f9e58c4 100644
--- a/eucalyptus-core/Cargo.toml
+++ b/eucalyptus-core/Cargo.toml
@@ -41,6 +41,7 @@ rfd = { workspace = true, optional = true }
 typetag.workspace = true
 semver.workspace = true
 rustc_version_runtime.workspace = true
+rapier3d.workspace = true
 
 [features]
 default = []
diff --git a/eucalyptus-core/src/lib.rs b/eucalyptus-core/src/lib.rs
index eb8678c..7fe68e3 100644
--- a/eucalyptus-core/src/lib.rs
+++ b/eucalyptus-core/src/lib.rs
@@ -18,6 +18,7 @@ pub use dropbear_macro as macros;
 pub use dropbear_traits as traits;
 
 pub use egui;
+pub use rapier3d;
 
 /// The appdata directory for storing any information.
 ///
diff --git a/eucalyptus-core/src/scripting.rs b/eucalyptus-core/src/scripting.rs
index 8c59122..35516ae 100644
--- a/eucalyptus-core/src/scripting.rs
+++ b/eucalyptus-core/src/scripting.rs
@@ -18,7 +18,7 @@ use anyhow::Context;
 use crossbeam_channel::Sender;
 use dropbear_engine::asset::ASSET_REGISTRY;
 use hecs::{Entity, World};
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::path::{Path, PathBuf};
 use tokio::io::{AsyncBufReadExt, BufReader};
 use tokio::process::Command;
@@ -72,6 +72,18 @@ pub struct ScriptManager {
     /// The path to the library. This is set if the [`ScriptTarget`] is [`ScriptTarget::Native`] or
     /// [`ScriptTarget::JVM`]
     lib_path: Option<PathBuf>,
+
+    /// Tags that have been instantiated/loaded into the scripting runtime.
+    ///
+    /// This is intentionally independent of the current scene's entity tag database so that
+    /// scripts can keep state across scene switches.
+    loaded_tags: HashSet<String>,
+
+    /// Tags that are currently in-scope for the active scene/world.
+    active_tags: HashSet<String>,
+
+    /// True once `load_script` has successfully initialised the current target.
+    scripts_loaded: bool,
 }
 
 impl ScriptManager {
@@ -86,6 +98,9 @@ impl ScriptManager {
             entity_tag_database: HashMap::new(),
             jvm_created: false,
             lib_path: None,
+            loaded_tags: HashSet::new(),
+            active_tags: HashSet::new(),
+            scripts_loaded: false,
         };
         
         let jvm_args = JVM_ARGS.get().map(|v| v.clone());
@@ -116,13 +131,42 @@ impl ScriptManager {
         entity_tag_database: HashMap<String, Vec<Entity>>,
         target: ScriptTarget,
     ) -> anyhow::Result<()> {
-        self.entity_tag_database = entity_tag_database.clone();
+        let previous_target = self.script_target.clone();
+        let previous_path = self.lib_path.clone();
+
+        let next_active: HashSet<String> = entity_tag_database.keys().cloned().collect();
+
+        let new_path = match &target {
+            ScriptTarget::JVM { library_path } => Some(library_path.clone()),
+            ScriptTarget::Native { library_path } => Some(library_path.clone()),
+            ScriptTarget::None => None,
+        };
+
+        let target_kind_changed = std::mem::discriminant(&previous_target) != std::mem::discriminant(&target);
+        let path_changed = previous_path != new_path;
+
+        if target_kind_changed || path_changed {
+            self.destroy_all().ok();
+        } else if self.scripts_loaded {
+            let removed: Vec<String> = self
+                .active_tags
+                .difference(&next_active)
+                .cloned()
+                .collect();
+
+            for tag in removed {
+                let _ = self.destroy_in_scope_tagged(&tag);
+            }
+        }
+
+        self.active_tags = next_active;
+
+        self.entity_tag_database = entity_tag_database;
         self.script_target = target.clone();
+        self.lib_path = new_path;
 
         match &target {
             ScriptTarget::JVM { library_path } => {
-                self.lib_path = Some(library_path.clone());
-
                 if !self.jvm_created {
                     let jvm = JavaContext::new(jvm_args)?;
                     self.jvm = Some(jvm);
@@ -134,20 +178,20 @@ impl ScriptManager {
                         jvm.jar_path = library_path.clone();
                     }
                 }
-
-                if let Some(jvm) = &mut self.jvm {
-                    jvm.clear_engine()?;
-                }
             }
             ScriptTarget::Native { library_path } => {
-                self.library = Some(NativeLibrary::new(library_path)?);
-                self.lib_path = Some(library_path.clone());
+                if path_changed || self.library.is_none() {
+                    self.library = Some(NativeLibrary::new(library_path)?);
+                }
             }
             ScriptTarget::None => {
                 self.jvm = None;
                 self.library = None;
                 self.jvm_created = false;
                 self.lib_path = None;
+                self.loaded_tags.clear();
+                self.active_tags.clear();
+                self.scripts_loaded = false;
             }
         }
 
@@ -190,7 +234,9 @@ impl ScriptManager {
                     for tag in self.entity_tag_database.keys() {
                         log::trace!("Loading systems for tag: {}", tag);
                         jvm.load_systems_for_tag(tag)?;
+                        self.loaded_tags.insert(tag.clone());
                     }
+                    self.scripts_loaded = true;
                     return Ok(());
                 }
             }
@@ -200,7 +246,9 @@ impl ScriptManager {
                     for tag in self.entity_tag_database.keys() {
                         log::trace!("Loading systems for tag: {}", tag);
                         library.load_systems(tag.to_string())?;
+                        self.loaded_tags.insert(tag.clone());
                     }
+                    self.scripts_loaded = true;
                     return Ok(());
                 }
             }
@@ -228,7 +276,7 @@ impl ScriptManager {
         world: &World,
         dt: f32,
     ) -> anyhow::Result<()> {
-        self.rebuild_entity_tag_database(world);
+        self.rebuild_entity_tag_database(world)?;
 
         match self.script_target {
             ScriptTarget::None => Err(anyhow::anyhow!(
@@ -284,6 +332,68 @@ impl ScriptManager {
             }
         }
     }
+    
+    pub fn physics_update_script(
+        &mut self,
+        world: &World,
+        dt: f32,
+    ) -> anyhow::Result<()> {
+        self.rebuild_entity_tag_database(world)?;
+
+        match self.script_target {
+            ScriptTarget::None => Err(anyhow::anyhow!(
+                "ScriptTarget is set to None. Either set to JVM or Native"
+            )),
+            ScriptTarget::JVM { .. } => {
+                if let Some(jvm) = &self.jvm {
+                    if self.entity_tag_database.is_empty() {
+                        jvm.physics_update_all_systems(dt)?;
+                    } else {
+                        for (tag, entities) in &self.entity_tag_database {
+                            let entity_ids: Vec<u64> = entities
+                                .iter()
+                                .map(|entity| entity.to_bits().get())
+                                .collect();
+
+                            if entity_ids.is_empty() {
+                                jvm.physics_update_systems_for_tag(tag, dt)?;
+                            } else {
+                                jvm.physics_update_systems_for_entities(tag, &entity_ids, dt)?;
+                            }
+                        }
+                    }
+                    return Ok(());
+                }
+                Err(anyhow::anyhow!(
+                    "ScriptTarget is set to JVM but JVM is None"
+                ))
+            }
+            ScriptTarget::Native { .. } => {
+                if let Some(library) = &mut self.library {
+                    if self.entity_tag_database.is_empty() {
+                        library.physics_update_all(dt)?;
+                    } else {
+                        for (tag, entities) in &self.entity_tag_database {
+                            let entity_ids: Vec<u64> = entities
+                                .iter()
+                                .map(|entity| entity.to_bits().get())
+                                .collect();
+
+                            if entity_ids.is_empty() {
+                                library.physics_update_tagged(tag, dt)?;
+                            } else {
+                                library.physics_update_systems_for_entities(tag, entity_ids.as_slice(), dt)?;
+                            }
+                        }
+                    }
+                    return Ok(());
+                }
+                Err(anyhow::anyhow!(
+                    "ScriptTarget is set to Native but library is None"
+                ))
+            }
+        }
+    }
 
     /// Reloads the .jar file by unloading the previous classes and reloading them back in,
     /// allowing for hot reloading.
@@ -301,8 +411,97 @@ impl ScriptManager {
         Ok(())
     }
 
+    /// Destroys all scripts for the current target.
+    pub fn destroy_all(&mut self) -> anyhow::Result<()> {
+        match self.script_target {
+            ScriptTarget::None => Ok(()),
+            ScriptTarget::JVM { .. } => {
+                if let Some(jvm) = &self.jvm {
+                    let _ = jvm.unload_all_systems();
+                }
+                self.loaded_tags.clear();
+                self.active_tags.clear();
+                self.scripts_loaded = false;
+                Ok(())
+            }
+            ScriptTarget::Native { .. } => {
+                if let Some(library) = &mut self.library {
+                    library.destroy_all()?;
+                }
+                self.loaded_tags.clear();
+                self.active_tags.clear();
+                self.scripts_loaded = false;
+                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 {
+            return Ok(());
+        }
+
+        match self.script_target {
+            ScriptTarget::None => Ok(()),
+            ScriptTarget::JVM { .. } => {
+                if let Some(jvm) = &self.jvm {
+                    let _ = jvm.destroy_systems_for_tag(tag);
+                }
+                Ok(())
+            }
+            ScriptTarget::Native { .. } => {
+                if let Some(library) = &mut self.library {
+                    library.destroy_in_scope_tagged(tag.to_string())?;
+                }
+                Ok(())
+            }
+        }
+    }
+
+    fn load_tagged(&mut self, tag: &str) -> anyhow::Result<()> {
+        self.loaded_tags.insert(tag.to_string());
+
+        match self.script_target {
+            ScriptTarget::None => Ok(()),
+            ScriptTarget::JVM { .. } => {
+                if let Some(jvm) = &mut self.jvm {
+                    jvm.load_systems_for_tag(tag)?;
+                }
+                Ok(())
+            }
+            ScriptTarget::Native { .. } => {
+                if let Some(library) = &mut self.library {
+                    library.load_systems(tag.to_string())?;
+                }
+                Ok(())
+            }
+        }
+    }
+
     /// Rebuilds the ScriptManagers entity database by parsing a [`World`].
-    fn rebuild_entity_tag_database(&mut self, world: &World) {
+    ///
+    /// If scripts are already loaded, this also:
+    /// - loads tags entering scope, and
+    /// - calls `destroy()` for tags leaving scope (without unloading instances).
+    fn rebuild_entity_tag_database(&mut self, world: &World) -> anyhow::Result<()> {
         let mut new_map: HashMap<String, Vec<Entity>> = HashMap::new();
 
         for (entity, script) in world.query::<&Script>().iter() {
@@ -311,7 +510,38 @@ impl ScriptManager {
             }
         }
 
+        if self.scripts_loaded {
+            let next_active: HashSet<String> = new_map.keys().cloned().collect();
+            let removed: Vec<String> = self
+                .active_tags
+                .difference(&next_active)
+                .cloned()
+                .collect();
+            let added: Vec<String> = next_active
+                .difference(&self.active_tags)
+                .cloned()
+                .collect();
+
+            for tag in removed {
+                self.destroy_in_scope_tagged(&tag)?;
+            }
+            for tag in added {
+                self.load_tagged(&tag)?;
+            }
+
+            self.active_tags = next_active;
+        } else {
+            self.active_tags = new_map.keys().cloned().collect();
+        }
+
         self.entity_tag_database = new_map;
+        Ok(())
+    }
+}
+
+impl Drop for ScriptManager {
+    fn drop(&mut self) {
+        let _ = self.destroy_all();
     }
 }
 
diff --git a/eucalyptus-core/src/scripting/jni.rs b/eucalyptus-core/src/scripting/jni.rs
index 82ca746..a7b5f22 100644
--- a/eucalyptus-core/src/scripting/jni.rs
+++ b/eucalyptus-core/src/scripting/jni.rs
@@ -44,6 +44,7 @@ fn is_port_available(port: u16) -> bool {
 /// Provides a context for any eucalyptus-core JNI calls and JVM hosting.
 pub struct JavaContext {
     pub(crate) jvm: JavaVM,
+    native_engine_instance: Option<GlobalRef>,
     dropbear_engine_class: Option<GlobalRef>,
     system_manager_instance: Option<GlobalRef>,
     pub(crate) jar_path: PathBuf,
@@ -281,6 +282,7 @@ impl JavaContext {
 
         Ok(Self {
             jvm,
+            native_engine_instance: None,
             dropbear_engine_class: None,
             system_manager_instance: None,
             jar_path: PathBuf::new(),
@@ -293,14 +295,6 @@ impl JavaContext {
     ) -> anyhow::Result<()> {
         let mut env = self.jvm.attach_current_thread()?;
 
-        if let Some(old_ref) = self.dropbear_engine_class.take() {
-            let _ = old_ref; // drop
-        }
-
-        if let Some(old_ref) = self.system_manager_instance.take() {
-            let _ = old_ref; // drop
-        }
-
         let world_handle = context.world as jlong;
         let input_handle = context.input as jlong;
         let graphics_handle = context.graphics as jlong;
@@ -322,30 +316,41 @@ impl JavaContext {
 
         log::trace!("Locating \"com/dropbear/ffi/NativeEngine\" class");
         let native_engine_class: JClass = env.find_class("com/dropbear/ffi/NativeEngine")?;
-        log::trace!("Creating new instance of NativeEngine");
-        let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?;
+
+        let native_engine_obj = if let Some(ref native_engine_ref) = self.native_engine_instance {
+            native_engine_ref.as_obj()
+        } else {
+            log::trace!("Creating new instance of NativeEngine");
+            let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?;
+            let native_engine_global_ref = env.new_global_ref(native_engine_obj)?;
+            self.native_engine_instance = Some(native_engine_global_ref);
+            self.native_engine_instance
+                .as_ref()
+                .expect("NativeEngine global ref must exist")
+                .as_obj()
+        };
 
         log::trace!("Calling NativeEngine.init() with arg [\"com/dropbear/ffi/DropbearContext\"]");
         env.call_method(
-            &native_engine_obj,
+            native_engine_obj,
             "init",
             "(Lcom/dropbear/ffi/DropbearContext;)V",
-            &[
-                JValue::Object(&dropbear_context_obj)
-            ],
+            &[JValue::Object(&dropbear_context_obj)],
         )?;
 
-        let dropbear_class: JClass = env.find_class("com/dropbear/DropbearEngine")?;
-        log::trace!("Creating DropbearEngine constructor with arg (NativeEngine_object)");
-        let dropbear_obj = env.new_object(
-            dropbear_class,
-            "(Lcom/dropbear/ffi/NativeEngine;)V",
-            &[JValue::Object(&native_engine_obj)],
-        )?;
+        if self.dropbear_engine_class.is_none() {
+            let dropbear_class: JClass = env.find_class("com/dropbear/DropbearEngine")?;
+            log::trace!("Creating DropbearEngine constructor with arg (NativeEngine_object)");
+            let dropbear_obj = env.new_object(
+                dropbear_class,
+                "(Lcom/dropbear/ffi/NativeEngine;)V",
+                &[JValue::Object(native_engine_obj)],
+            )?;
 
-        log::trace!("Creating new global ref for DropbearEngine");
-        let engine_global_ref = env.new_global_ref(dropbear_obj)?;
-        self.dropbear_engine_class = Some(engine_global_ref.clone());
+            log::trace!("Creating new global ref for DropbearEngine");
+            let engine_global_ref = env.new_global_ref(dropbear_obj)?;
+            self.dropbear_engine_class = Some(engine_global_ref);
+        }
 
         let jar_path_jstring = env.new_string(self.jar_path.to_string_lossy())?;
         let log_level_str = { LOG_LEVEL.lock().to_string() };
@@ -361,29 +366,37 @@ impl JavaContext {
         let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?;
         let log_writer_obj = env.new_object(std_out_writer_class, "()V", &[])?;
 
-        log::trace!("Locating \"com/dropbear/host/SystemManager\" class");
-        let system_manager_class: JClass = env.find_class("com/dropbear/host/SystemManager")?;
-        log::trace!(
-            "Creating SystemManager constructor with args (jar_path_string, dropbear_engine_object, log_writer_object, log_level_enum, log_target_string)"
-        );
+        if self.system_manager_instance.is_none() {
+            let engine_ref = self
+                .dropbear_engine_class
+                .as_ref()
+                .expect("DropbearEngine global ref must exist")
+                .as_obj();
 
-        let log_target_jstring = env.new_string("dropbear_rust_host")?;
+            log::trace!("Locating \"com/dropbear/host/SystemManager\" class");
+            let system_manager_class: JClass = env.find_class("com/dropbear/host/SystemManager")?;
+            log::trace!(
+                "Creating SystemManager constructor with args (jar_path_string, dropbear_engine_object, log_writer_object, log_level_enum, log_target_string)"
+            );
 
-        let system_manager_obj = env.new_object(
-            system_manager_class,
-            "(Ljava/lang/String;Lcom/dropbear/DropbearEngine;Lcom/dropbear/logging/LogWriter;Lcom/dropbear/logging/LogLevel;Ljava/lang/String;)V",
-            &[
-                JValue::Object(&jar_path_jstring),
-                JValue::Object(engine_global_ref.as_obj()),
-                JValue::Object(&log_writer_obj),
-                JValue::Object(&log_level_enum_instance),
-                JValue::Object(&log_target_jstring),
-            ],
-        )?;
+            let log_target_jstring = env.new_string("dropbear_rust_host")?;
 
-        log::trace!("Creating new global ref for SystemManager");
-        let manager_global_ref = env.new_global_ref(system_manager_obj)?;
-        self.system_manager_instance = Some(manager_global_ref);
+            let system_manager_obj = env.new_object(
+                system_manager_class,
+                "(Ljava/lang/String;Lcom/dropbear/DropbearEngine;Lcom/dropbear/logging/LogWriter;Lcom/dropbear/logging/LogLevel;Ljava/lang/String;)V",
+                &[
+                    JValue::Object(&jar_path_jstring),
+                    JValue::Object(engine_ref),
+                    JValue::Object(&log_writer_obj),
+                    JValue::Object(&log_level_enum_instance),
+                    JValue::Object(&log_target_jstring),
+                ],
+            )?;
+
+            log::trace!("Creating new global ref for SystemManager");
+            let manager_global_ref = env.new_global_ref(system_manager_obj)?;
+            self.system_manager_instance = Some(manager_global_ref);
+        }
 
         Ok(())
     }
@@ -463,6 +476,26 @@ impl JavaContext {
         Ok(())
     }
 
+    pub fn physics_update_all_systems(&self, dt: f32) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log_once::trace_once!("Calling SystemManager.physicsUpdateAllSystems() with dt: {}", dt);
+            env.call_method(
+                manager_ref,
+                "physicsUpdateAllSystems",
+                "(F)V",
+                &[JValue::Float(dt)],
+            )?;
+
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when physics updating systems."
+            ))
+        }
+    }
+
     pub fn update_systems_for_tag(&self, tag: &str, dt: f32) -> anyhow::Result<()> {
         if let Some(ref manager_ref) = self.system_manager_instance {
             let mut env = self.jvm.attach_current_thread()?;
@@ -490,6 +523,32 @@ impl JavaContext {
         Ok(())
     }
 
+    pub fn physics_update_systems_for_tag(&self, tag: &str, dt: f32) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log::trace!(
+                "Calling SystemManager.physicsUpdateSystemsByTag() with tag: {}, dt: {}",
+                tag,
+                dt
+            );
+            let tag_jstring = env.new_string(tag)?;
+            env.call_method(
+                manager_ref,
+                "physicsUpdateSystemsByTag",
+                "(Ljava/lang/String;F)V",
+                &[JValue::Object(&tag_jstring), JValue::Float(dt)],
+            )?;
+
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when physics updating systems for tag: {}",
+                tag
+            ))
+        }
+    }
+
     pub fn update_systems_for_entities(
         &self,
         tag: &str,
@@ -550,6 +609,116 @@ impl JavaContext {
         }
     }
 
+    pub fn physics_update_systems_for_entities(
+        &self,
+        tag: &str,
+        entity_ids: &[u64],
+        dt: f32,
+    ) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log::trace!(
+                "Calling SystemManager.physicsUpdateSystemsForEntities() with tag: {}, count: {}, dt: {}",
+                tag,
+                entity_ids.len(),
+                dt
+            );
+
+            let tag_jstring = env.new_string(tag)?;
+            let entity_array: JLongArray = env.new_long_array(entity_ids.len() as i32)?;
+            let entity_array_raw = entity_array.as_raw();
+            if !entity_ids.is_empty() {
+                env.set_long_array_region(
+                    entity_array,
+                    0,
+                    &entity_ids.iter().map(|e| *e as i64).collect::<Vec<_>>(),
+                )?;
+            }
+            let entity_array_obj =
+                unsafe { JObject::from_raw(entity_array_raw.cast::<jni::sys::_jobject>()) };
+
+            env.call_method(
+                manager_ref,
+                "physicsUpdateSystemsForEntities",
+                "(Ljava/lang/String;[JF)V",
+                &[
+                    JValue::Object(&tag_jstring),
+                    JValue::Object(&entity_array_obj),
+                    JValue::Float(dt),
+                ],
+            )?;
+
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when physics updating systems for tag: {}",
+                tag
+            ))
+        }
+    }
+
+    pub fn unload_systems_for_tag(&self, tag: &str) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log::trace!("Calling SystemManager.unloadSystemsByTag() with tag: {}", tag);
+            let tag_jstring = env.new_string(tag)?;
+            env.call_method(
+                manager_ref,
+                "unloadSystemsByTag",
+                "(Ljava/lang/String;)V",
+                &[JValue::Object(&tag_jstring)],
+            )?;
+
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when unloading systems for tag: {}",
+                tag
+            ))
+        }
+    }
+
+    pub fn destroy_systems_for_tag(&self, tag: &str) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log::trace!(
+                "Calling SystemManager.destroySystemsByTag() with tag: {}",
+                tag
+            );
+            let tag_jstring = env.new_string(tag)?;
+            env.call_method(
+                manager_ref,
+                "destroySystemsByTag",
+                "(Ljava/lang/String;)V",
+                &[JValue::Object(&tag_jstring)],
+            )?;
+
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when destroying systems for tag: {}",
+                tag
+            ))
+        }
+    }
+
+    pub fn unload_all_systems(&self) -> anyhow::Result<()> {
+        if let Some(ref manager_ref) = self.system_manager_instance {
+            let mut env = self.jvm.attach_current_thread()?;
+
+            log::trace!("Calling SystemManager.unloadAllSystems()");
+            env.call_method(manager_ref, "unloadAllSystems", "()V", &[])?;
+            Ok(())
+        } else {
+            Err(anyhow::anyhow!(
+                "SystemManager not initialised when unloading all systems."
+            ))
+        }
+    }
+
     pub fn get_system_count_for_tag(&self, tag: &str) -> anyhow::Result<i32> {
         if let Some(ref manager_ref) = self.system_manager_instance {
             let mut env = self.jvm.attach_current_thread()?;
@@ -610,6 +779,9 @@ impl JavaContext {
     }
 
     pub fn clear_engine(&mut self) -> anyhow::Result<()> {
+        if let Some(old_native_engine_ref) = self.native_engine_instance.take() {
+            let _ = old_native_engine_ref;
+        }
         if let Some(old_engine_ref) = self.dropbear_engine_class.take() {
             let _ = old_engine_ref; // drop
         }
diff --git a/eucalyptus-core/src/scripting/native.rs b/eucalyptus-core/src/scripting/native.rs
index c1d7ab9..03cb8b0 100644
--- a/eucalyptus-core/src/scripting/native.rs
+++ b/eucalyptus-core/src/scripting/native.rs
@@ -6,7 +6,19 @@ pub mod sig;
 pub mod utils;
 
 use crate::scripting::error::LastErrorMessage;
-use crate::scripting::native::sig::{DestroyAll, DestroyTagged, Init, LoadTagged, UpdateAll, UpdateTagged, UpdateWithEntities};
+use crate::scripting::native::sig::{
+    DestroyAll,
+    DestroyInScopeTagged,
+    DestroyTagged,
+    Init,
+    LoadTagged,
+    PhysicsUpdateAll,
+    PhysicsUpdateTagged,
+    PhysicsUpdateWithEntities,
+    UpdateAll,
+    UpdateTagged,
+    UpdateWithEntities,
+};
 use anyhow::anyhow;
 use libloading::{Library, Symbol};
 use std::ffi::CString;
@@ -22,8 +34,12 @@ pub struct NativeLibrary {
     update_all_fn: Symbol<'static, UpdateAll>,
     update_tag_fn: Symbol<'static, UpdateTagged>,
     update_with_entities_fn: Symbol<'static, UpdateWithEntities>,
+    physics_update_all_fn: Symbol<'static, PhysicsUpdateAll>,
+    physics_update_tag_fn: Symbol<'static, PhysicsUpdateTagged>,
+    physics_update_with_entities_fn: Symbol<'static, PhysicsUpdateWithEntities>,
     destroy_all_fn: Symbol<'static, DestroyAll>,
     destroy_tagged_fn: Symbol<'static, DestroyTagged>,
+    destroy_in_scope_tagged_fn: Symbol<'static, DestroyInScopeTagged>,
 
     // err msg
     #[allow(dead_code)]
@@ -64,6 +80,22 @@ impl NativeLibrary {
                 &[b"dropbear_update_with_entities\0"],
                 "dropbear_update_with_entities",
             )?;
+
+            let physics_update_all_fn = load_symbol(
+                &library,
+                &[b"dropbear_physics_update_all\0"],
+                "dropbear_physics_update_all",
+            )?;
+            let physics_update_tag_fn = load_symbol(
+                &library,
+                &[b"dropbear_physics_update_tagged\0"],
+                "dropbear_physics_update_tagged",
+            )?;
+            let physics_update_with_entities_fn = load_symbol(
+                &library,
+                &[b"dropbear_physics_update_with_entities\0"],
+                "dropbear_physics_update_with_entities",
+            )?;
             let destroy_all_fn = load_symbol(
                 &library,
                 &[b"dropbear_destroy_all\0"],
@@ -74,6 +106,11 @@ impl NativeLibrary {
                 &[b"dropbear_destroy_tagged\0"],
                 "dropbear_destroy_tagged",
             )?;
+            let destroy_in_scope_tagged_fn = load_symbol(
+                &library,
+                &[b"dropbear_destroy_in_scope_tagged\0"],
+                "dropbear_destroy_in_scope_tagged",
+            )?;
             let get_last_err_msg_fn = load_symbol(
                 &library,
                 &[
@@ -98,8 +135,12 @@ impl NativeLibrary {
                 update_all_fn,
                 update_tag_fn,
                 update_with_entities_fn,
+                physics_update_all_fn,
+                physics_update_tag_fn,
+                physics_update_with_entities_fn,
                 destroy_all_fn,
                 destroy_tagged_fn,
+                destroy_in_scope_tagged_fn,
                 get_last_err_msg_fn,
                 set_last_err_msg_fn,
             })
@@ -132,6 +173,13 @@ impl NativeLibrary {
         }
     }
 
+    pub fn physics_update_all(&mut self, dt: f32) -> anyhow::Result<()> {
+        unsafe {
+            let result = (self.physics_update_all_fn)(dt);
+            self.handle_result(result, "physics_update_all")
+        }
+    }
+
     pub fn update_tagged(&mut self, tag: &String, dt: f32) -> anyhow::Result<()> {
         unsafe {
             let c_string: CString = CString::new(tag.clone())?;
@@ -140,6 +188,14 @@ impl NativeLibrary {
         }
     }
 
+    pub fn physics_update_tagged(&mut self, tag: &String, dt: f32) -> anyhow::Result<()> {
+        unsafe {
+            let c_string: CString = CString::new(tag.clone())?;
+            let result = (self.physics_update_tag_fn)(c_string.as_ptr(), dt);
+            self.handle_result(result, "physics_update_tagged")
+        }
+    }
+
     pub fn update_systems_for_entities(
         &self,
         tag: &str,
@@ -158,6 +214,24 @@ impl NativeLibrary {
         }
     }
 
+    pub fn physics_update_systems_for_entities(
+        &self,
+        tag: &str,
+        entity_ids: &[u64],
+        dt: f32,
+    ) -> anyhow::Result<()> {
+        unsafe {
+            let c_string = CString::new(tag)?;
+            let result = (self.physics_update_with_entities_fn)(
+                c_string.as_ptr(),
+                entity_ids.as_ptr(),
+                entity_ids.len() as i32,
+                dt,
+            );
+            self.handle_result(result, "physics_update_with_entities")
+        }
+    }
+
     pub fn destroy_all(&mut self) -> anyhow::Result<()> {
         unsafe {
             let result = (self.destroy_all_fn)();
@@ -172,6 +246,14 @@ impl NativeLibrary {
             self.handle_result(result, "destroy_tagged")
         }
     }
+
+    pub fn destroy_in_scope_tagged(&mut self, tag: String) -> anyhow::Result<()> {
+        unsafe {
+            let c_string: CString = CString::new(tag)?;
+            let result = (self.destroy_in_scope_tagged_fn)(c_string.as_ptr());
+            self.handle_result(result, "destroy_in_scope_tagged")
+        }
+    }
 }
 
 impl NativeLibrary {
diff --git a/eucalyptus-core/src/scripting/native/sig.rs b/eucalyptus-core/src/scripting/native/sig.rs
index d4bcde2..596a14a 100644
--- a/eucalyptus-core/src/scripting/native/sig.rs
+++ b/eucalyptus-core/src/scripting/native/sig.rs
@@ -18,8 +18,22 @@ pub type UpdateWithEntities = unsafe extern "C" fn(
     entity_count: i32,
     dt: f32
 ) -> i32;
+
+/// CName: `dropbear_physics_update_all`
+pub type PhysicsUpdateAll = unsafe extern "C" fn(dt: f32) -> i32;
+/// CName: `dropbear_physics_update_tagged`
+pub type PhysicsUpdateTagged = unsafe extern "C" fn(tag: *const c_char, dt: f32) -> i32;
+/// CName: `dropbear_physics_update_with_entities`
+pub type PhysicsUpdateWithEntities = unsafe extern "C" fn(
+    tag: *const c_char,
+    entities: *const u64,
+    entity_count: i32,
+    dt: f32
+) -> i32;
 /// CName: `dropbear_destroy_tagged`
 pub type DestroyTagged = unsafe extern "C" fn(tag: *const c_char) -> i32;
+/// CName: `dropbear_destroy_in_scope_tagged`
+pub type DestroyInScopeTagged = unsafe extern "C" fn(tag: *const c_char) -> i32;
 /// CName: `dropbear_destroy_all`
 pub type DestroyAll = unsafe extern "C" fn() -> i32;
 
diff --git a/eucalyptus-editor/src/about.rs b/eucalyptus-editor/src/about.rs
index f5bff77..622ae46 100644
--- a/eucalyptus-editor/src/about.rs
+++ b/eucalyptus-editor/src/about.rs
@@ -33,6 +33,8 @@ impl Scene for AboutWindow {
         self.window = Some(graphics.shared.window.id());
     }
 
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
+
     fn update(&mut self, _dt: f32, graphics: &mut RenderContext) {
         CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| {
             ui.centered_and_justified(|ui| {
diff --git a/eucalyptus-editor/src/debug/window.rs b/eucalyptus-editor/src/debug/window.rs
index f7d1c75..34e6c03 100644
--- a/eucalyptus-editor/src/debug/window.rs
+++ b/eucalyptus-editor/src/debug/window.rs
@@ -35,6 +35,8 @@ impl Scene for DebugWindow {
         self.window = Some(graphics.shared.window.id());
     }
 
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
+
     fn update(&mut self, _dt: f32, graphics: &mut RenderContext) {
         CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| {
             ui.label("Hello Debug Window!");
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 909272b..61e904b 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -79,6 +79,8 @@ impl Scene for Editor {
         self.is_world_loaded.mark_scene_loaded();
     }
 
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
+
     fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
         if let Some(rx) = &self.play_mode_exit_rx {
             if rx.try_recv().is_ok() {
diff --git a/eucalyptus-editor/src/lib.rs b/eucalyptus-editor/src/lib.rs
index 74ef743..7a0235c 100644
--- a/eucalyptus-editor/src/lib.rs
+++ b/eucalyptus-editor/src/lib.rs
@@ -11,3 +11,4 @@ pub mod stats;
 pub mod utils;
 pub mod runtime;
 pub mod about;
+pub mod process;
diff --git a/eucalyptus-editor/src/menu.rs b/eucalyptus-editor/src/menu.rs
index 9925fdf..293b696 100644
--- a/eucalyptus-editor/src/menu.rs
+++ b/eucalyptus-editor/src/menu.rs
@@ -239,6 +239,8 @@ impl Scene for MainMenu {
         log::info!("Loaded main menu scene");
     }
 
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
+
     fn update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
 
     fn render(&mut self, graphics: &mut RenderContext) {
diff --git a/eucalyptus-editor/src/process.rs b/eucalyptus-editor/src/process.rs
new file mode 100644
index 0000000..d9eeaf2
--- /dev/null
+++ b/eucalyptus-editor/src/process.rs
@@ -0,0 +1,38 @@
+//! Functions related to processes and PIDs that aren't part of the std library.
+
+use std::io;
+use std::process::Command;
+
+#[cfg(unix)]
+pub fn kill_process(pid: u32) -> io::Result<()> {
+    let status = Command::new("kill")
+        .arg("-15")  // SIGTERM
+        .arg(pid.to_string())
+        .status()?;
+
+    if !status.success() {
+        return Err(io::Error::new(
+            io::ErrorKind::Other,
+            format!("Failed to kill process {}", pid)
+        ));
+    }
+
+    Ok(())
+}
+
+#[cfg(windows)]
+pub fn kill_process(pid: u32) -> io::Result<()> {
+    let status = Command::new("taskkill")
+        .arg("/PID")
+        .arg(pid.to_string())
+        .status()?;
+
+    if !status.success() {
+        return Err(io::Error::new(
+            io::ErrorKind::Other,
+            format!("Failed to kill process {}", pid)
+        ));
+    }
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/eucalyptus-editor/src/runtime/scene.rs b/eucalyptus-editor/src/runtime/scene.rs
index 689a2b0..e05bc74 100644
--- a/eucalyptus-editor/src/runtime/scene.rs
+++ b/eucalyptus-editor/src/runtime/scene.rs
@@ -217,18 +217,9 @@ impl Scene for PlayMode {
 
             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 && self.display_settings.maintain_aspect_ratio {
-                        let window_size = graphics.shared.window.inner_size();
-                        let chrome_height = window_size.height as f32 - available_size.y;
-                        
-                        let new_viewport_height = available_size.x / cam.aspect as f32;
-                        let new_window_height = new_viewport_height + chrome_height;
-                        
-                        let _ = graphics.shared.window.request_inner_size(winit::dpi::PhysicalSize::new(
-                            window_size.width,
-                            new_window_height as u32
-                        ));
-                        
+                    if !self.has_initial_resize_done {
+                        cam.aspect = (available_size.x / available_size.y) as f64;
+
                         self.has_initial_resize_done = true;
                     }
 
@@ -270,11 +261,17 @@ 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;
diff --git a/eucalyptus-editor/src/signal.rs b/eucalyptus-editor/src/signal.rs
index 866fbae..fb4cd81 100644
--- a/eucalyptus-editor/src/signal.rs
+++ b/eucalyptus-editor/src/signal.rs
@@ -424,11 +424,11 @@ impl SignalController for Editor {
                 Ok(())
             }
             Signal::StopPlaying => {
-
                 self.editor_state = EditorState::Editing;
                 
                 if let Some(pid) = self.play_mode_pid {
-                    log::info!("Play mode process {} should exit soon", pid);
+                    log::debug!("Play mode requested to be exited, killing processes [{}]", pid);
+                    let _ = crate::process::kill_process(pid);
                 }
                 
                 self.play_mode_pid = None;
diff --git a/eucalyptus-editor/src/stats.rs b/eucalyptus-editor/src/stats.rs
index 05b7a67..571a715 100644
--- a/eucalyptus-editor/src/stats.rs
+++ b/eucalyptus-editor/src/stats.rs
@@ -326,6 +326,7 @@ impl NerdStats {
 
 impl Scene for NerdStats {
     fn load(&mut self, _graphics: &mut RenderContext) {}
+    fn physics_update(&mut self, _dt: f32, _graphics: &mut RenderContext) {}
     fn update(&mut self, dt: f32, _graphics: &mut RenderContext) {
         self.record_stats(dt, self.entity_count);
     }
diff --git a/magna-carta/src/generator/native.rs b/magna-carta/src/generator/native.rs
index 091a9e6..67f44e4 100644
--- a/magna-carta/src/generator/native.rs
+++ b/magna-carta/src/generator/native.rs
@@ -68,6 +68,7 @@ import kotlin.experimental.ExperimentalNativeApi"#
             output,
             r#"
 object ScriptManager {{
+    private var nativeEngine: NativeEngine? = null
     private var dropbearEngine: DropbearEngine? = null
     private val scriptsByTag: MutableMap<String, MutableList<System>> = mutableMapOf()
 
@@ -75,12 +76,13 @@ object ScriptManager {{
         try {{
             val ctx = dropbearContextPtr?.pointed
 
-            val nativeEngine = NativeEngine()
-            nativeEngine.init(ctx)
+            val engine = nativeEngine ?: NativeEngine().also {{ nativeEngine = it }}
+            engine.init(ctx)
 
-            dropbearEngine = DropbearEngine(nativeEngine)
+            if (dropbearEngine == null) {{
+                dropbearEngine = DropbearEngine(engine)
+            }}
 
-            scriptsByTag.clear()
             Logger.debug("Native ScriptManager initialised")
             return 0
         }} catch (e: Exception) {{
@@ -93,10 +95,20 @@ object ScriptManager {{
     fun loadSystemsByTag(tag: String): Int {{
         val engine = dropbearEngine ?: return -2
         try {{
+            if (scriptsByTag.containsKey(tag)) {{
+                Logger.trace("Systems already loaded for tag: '$tag'")
+                val instances = scriptsByTag[tag] ?: emptyList()
+                for (instance in instances) {{
+                    instance.attachEngine(engine)
+                    instance.load(engine)
+                }}
+                return 0
+            }}
             val factories = getScriptFactories(tag)
             val instances = factories.map {{ it() }}
 
             for (instance in instances) {{
+                instance.attachEngine(engine)
                 instance.load(engine)
             }}
 
@@ -115,6 +127,8 @@ object ScriptManager {{
         try {{
             for (instances in scriptsByTag.values) {{
                 for (instance in instances) {{
+                    instance.attachEngine(engine)
+                    instance.clearCurrentEntity()
                     instance.update(engine, dt)
                 }}
             }}
@@ -131,6 +145,8 @@ object ScriptManager {{
         try {{
             val instances = scriptsByTag[tag] ?: emptyList()
             for (instance in instances) {{
+                instance.attachEngine(engine)
+                instance.clearCurrentEntity()
                 instance.update(engine, dt)
             }}
             return 0
@@ -141,7 +157,42 @@ object ScriptManager {{
         }}
     }}
 
-fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{
+    fun physicsUpdateAllSystems(dt: Float): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            for (instances in scriptsByTag.values) {{
+                for (instance in instances) {{
+                    instance.attachEngine(engine)
+                    instance.clearCurrentEntity()
+                    instance.physicsUpdate(engine, dt)
+                }}
+            }}
+            return 0
+        }} catch (e: Exception) {{
+            dropbear_set_last_error("Error physics updating all systems: ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun physicsUpdateSystemsByTag(tag: String, dt: Float): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            val instances = scriptsByTag[tag] ?: emptyList()
+            for (instance in instances) {{
+                instance.attachEngine(engine)
+                instance.clearCurrentEntity()
+                instance.physicsUpdate(engine, dt)
+            }}
+            return 0
+        }} catch (e: Exception) {{
+            dropbear_set_last_error("Error physics updating systems for tag '$tag': ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
+    fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{
         val engine = dropbearEngine ?: return -2
         try {{
             val instances = scriptsByTag[tag] ?: emptyList()
@@ -188,6 +239,53 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo
         }}
     }}
 
+    fun physicsUpdateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCount: Int, dt: Float): Int {{
+        val engine = dropbearEngine ?: return -2
+        try {{
+            val instances = scriptsByTag[tag] ?: emptyList()
+
+            val entityIds = LongArray(entityCount) {{ index ->
+                entities[index].toLong()
+            }}
+
+            Logger.trace("Physics updating systems for tag: $tag with $entityCount entities")
+
+            if (instances.isEmpty()) {{
+                return 0
+            }}
+
+            if (entityIds.isEmpty()) {{
+                for (instance in instances) {{
+                    instance.physicsUpdate(engine, dt)
+                }}
+                return 0
+            }}
+
+            for (entityId in entityIds) {{
+                for (instance in instances) {{
+                    try {{
+                        instance.attachEngine(engine)
+                        instance.setCurrentEntity(entityId)
+                        instance.physicsUpdate(engine, dt)
+                    }} catch (ex: Exception) {{
+                        Logger.error("Failed to physics update system $instance for entity $entityId: ${{ex.message}}")
+                    }}
+                }}
+            }}
+
+            for (instance in instances) {{
+                instance.clearCurrentEntity()
+            }}
+
+            Logger.debug("Physics updated ${{instances.size}} system(s) for tag '$tag' with ${{entityCount}} entities")
+            return 0
+        }} catch (e: Exception) {{
+            dropbear_set_last_error("Error physics updating systems for tag '$tag' with entities: ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
     fun destroyByTag(tag: String): Int {{
         try {{
             val engine = dropbearEngine ?: return -2
@@ -205,6 +303,22 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo
         }}
     }}
 
+    fun destroyInScopeByTag(tag: String): Int {{
+        try {{
+            val engine = dropbearEngine ?: return -2
+            val instances = scriptsByTag[tag] ?: emptyList()
+            for (instance in instances) {{
+                instance.destroy(engine)
+            }}
+            Logger.debug("Destroyed (in-scope) ${{instances.size}} script(s) for tag: '$tag'")
+            return 0
+        }} catch (e: Exception) {{
+            dropbear_set_last_error("Error destroying (in-scope) systems for tag '$tag': ${{e.message}}")
+            e.printStackTrace()
+            return -1
+        }}
+    }}
+
     fun destroyAll(): Int {{
         try {{
             val engine = dropbearEngine ?: return -2
@@ -215,6 +329,7 @@ fun updateSystemsForEntities(tag: String, entities: CPointer<ULongVar>, entityCo
             }}
             scriptsByTag.clear()
             dropbearEngine = null
+            nativeEngine = null
             return 0
         }} catch (e: Exception) {{
             dropbear_set_last_error("Error destroying scripts: ${{e.message}}")
@@ -294,12 +409,35 @@ fun dropbear_update_systems_for_entities(tag: String?, entities: CPointer<ULongV
     return ScriptManager.updateSystemsForEntities(tag, entities, entityCount, dt)
 }}
 
+@CName("dropbear_physics_update_all")
+fun dropbear_physics_update_all_systems(dt: Float): Int {{
+    return ScriptManager.physicsUpdateAllSystems(dt)
+}}
+
+@CName("dropbear_physics_update_tagged")
+fun dropbear_physics_update_systems_for_tag(tag: String?, dt: Float): Int {{
+    if (tag == null) return -1
+    return ScriptManager.physicsUpdateSystemsByTag(tag, dt)
+}}
+
+@CName("dropbear_physics_update_with_entities")
+fun dropbear_physics_update_systems_for_entities(tag: String?, entities: CPointer<ULongVar>?, entityCount: Int, dt: Float): Int {{
+    if (tag == null || entities == null) return -1
+    return ScriptManager.physicsUpdateSystemsForEntities(tag, entities, entityCount, dt)
+}}
+
 @CName("dropbear_destroy_tagged")
 fun dropbear_destroy(tag: String?): Int {{
     if (tag == null) return -1
     return ScriptManager.destroyByTag(tag)
 }}
 
+@CName("dropbear_destroy_in_scope_tagged")
+fun dropbear_destroy_in_scope(tag: String?): Int {{
+    if (tag == null) return -1
+    return ScriptManager.destroyInScopeByTag(tag)
+}}
+
 @CName("dropbear_destroy_all")
 fun dropbear_destroy_all(): Int {{
     return ScriptManager.destroyAll()
diff --git a/src/commonMain/kotlin/com/dropbear/System.kt b/src/commonMain/kotlin/com/dropbear/System.kt
index 5f6b05b..a1a62b6 100644
--- a/src/commonMain/kotlin/com/dropbear/System.kt
+++ b/src/commonMain/kotlin/com/dropbear/System.kt
@@ -4,26 +4,75 @@ package com.dropbear
  * A class that contains the basic information of a system. 
  * 
  * The dropbear engine follows an ECS paradigm, with logic being
- * provided as Systems. 
- * 
- * The main functions you would want to look at is `load`, 
- * `update` and `destroy`(not impl). 
+ * provided as Systems.
  */
 open class System {
+    /**
+     * The current entity that is being run in the system. Most often than not, it
+     * will have an [EntityRef] attached (because all Script components must be part
+     * of an entity).
+     */
     var currentEntity: EntityRef? = null
         private set
 
     private var engineRef: DropbearEngine? = null
 
+    /**
+     * This function is called when the script module is initialised.
+     *
+     * It is only called once during scene execution. If you re-switch back
+     * to this scene after running the class, it will be run again.
+     */
     open fun load(engine: DropbearEngine) {}
+
+    /**
+     * This function is called for each update.
+     *
+     * It is run once for each frame, and for every frame. Since this is synced to the frame rate, using
+     * the [deltaTime] variable can aid you in creating uniform player speeds (or something like that).
+     *
+     * @param deltaTime - This specifies the time elapsed since the last update.
+     */
     open fun update(engine: DropbearEngine, deltaTime: Float) {}
+
+    /**
+     * This function is called for each update that is related to physics.
+     *
+     * It can be run 0, 1, 2 or more times per frame. Updating physics is done at a constant
+     * rate/tick (at roughly 50Hz or 0.02), which is why it is not ran as often as a standard [update].
+     *
+     * @param deltaTime - This specifies the time elapsed since the last frame update. Likely, it's going
+     *                    to be somewhere around 50Hz. For the most part, you might not need this.
+     */
+    open fun physicsUpdate(engine: DropbearEngine, deltaTime: Float) {}
+
+    /**
+     * This function is called at the end of the script execution.
+     *
+     * It is run at the end of execution of a scene, such as when the scene switches. It is also ran once throughout
+     * the lifecycle of a script class. It is best to think about it like `sceneExit()` instead of `appExit()`
+     *
+     * It would be used to clean up any memory related resources (such as `SceneLoadHandle` or any memory related items).
+     *
+     * # Note
+     *
+     * The script module does not lose state (such as variables) when destroyed. It is cached internally (within the system manager),
+     * therefore counters and other related stuff will not lose track.
+     */
     open fun destroy(engine: DropbearEngine) {}
 
+    /**
+     * Internal: This attaches the [DropbearEngine] fascade (typically created through some external location)
+     * to the existing system to be used.
+     */
     fun attachEngine(engine: DropbearEngine) {
         engineRef = engine
         currentEntity?.engine = engine
     }
 
+    /**
+     * Internal: Sets the current entity of this [System] to something.
+     */
     fun setCurrentEntity(entity: Long) {
         val engine = engineRef ?: run {
             currentEntity = null
@@ -35,6 +84,9 @@ open class System {
         currentEntity = reference
     }
 
+    /**
+     * Internal: Clears the current entity used.
+     */
     fun clearCurrentEntity() {
         currentEntity = null
     }
diff --git a/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt b/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
index e8cd9bb..70abe46 100644
--- a/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
+++ b/src/jvmMain/kotlin/com/dropbear/host/SystemManager.kt
@@ -6,7 +6,6 @@ import com.dropbear.logging.LogLevel
 import com.dropbear.logging.LogWriter
 import com.dropbear.logging.Logger
 import com.dropbear.logging.StdoutWriter
-import kotlin.collections.emptyList
 
 @Suppress("UNUSED")
 class SystemManager(
@@ -21,6 +20,27 @@ class SystemManager(
     private var registryClass: Class<*>? = null
     private val activeSystems = mutableMapOf<String, MutableList<System>>()
 
+    private fun destroySystems(tag: String, systems: List<System>) {
+        if (systems.isEmpty()) return
+
+        Logger.debug("Destroying ${systems.size} system(s) for tag: $tag")
+        for (system in systems) {
+            try {
+                system.attachEngine(engine)
+                system.clearCurrentEntity()
+                system.destroy(engine)
+            } catch (ex: Exception) {
+                Logger.error("Failed to destroy system ${system.javaClass.name} for tag $tag: ${ex.message}")
+            } finally {
+                try {
+                    system.clearCurrentEntity()
+                } catch (_: Exception) {
+                    // ignore
+                }
+            }
+        }
+    }
+
     init {
         val writerToUse = logWriter ?: StdoutWriter()
         Logger.init(writerToUse, logLevel ?: LogLevel.INFO, logTarget)
@@ -43,6 +63,28 @@ class SystemManager(
 
     fun loadSystemsForTag(tag: String) {
         Logger.debug("Loading systems for tag: $tag")
+
+        if (activeSystems.containsKey(tag)) {
+            Logger.trace("Systems already loaded for tag: $tag; re-running load() on existing instances")
+            val systems = activeSystems[tag] ?: return
+            for (system in systems) {
+                try {
+                    system.attachEngine(engine)
+                    system.clearCurrentEntity()
+                    system.load(engine)
+                } catch (ex: Exception) {
+                    Logger.error("Failed to load system ${system.javaClass.name} for tag $tag: ${ex.message}")
+                } finally {
+                    try {
+                        system.clearCurrentEntity()
+                    } catch (_: Exception) {
+                        // ignore
+                    }
+                }
+            }
+            return
+        }
+
         val instantiateMethod = registryClass?.getMethod("instantiateScripts", String::class.java)
         val systems = instantiateMethod?.invoke(registryInstance, tag) as? List<*>
 
@@ -81,12 +123,25 @@ class SystemManager(
         }
     }
 
+    fun physicsUpdateAllSystems(deltaTime: Float) {
+        Logger.trace("Physics updating all systems")
+        for ((tag, systems) in activeSystems) {
+            physicsUpdateSystemsInternal(tag, systems, deltaTime)
+        }
+    }
+
     fun updateSystemsByTag(tag: String, deltaTime: Float) {
         Logger.trace("Updating systems for tag: $tag")
         val systems = activeSystems[tag] ?: return
         updateSystemsInternal(tag, systems, deltaTime)
     }
 
+    fun physicsUpdateSystemsByTag(tag: String, deltaTime: Float) {
+        Logger.trace("Physics updating systems for tag: $tag")
+        val systems = activeSystems[tag] ?: return
+        physicsUpdateSystemsInternal(tag, systems, deltaTime)
+    }
+
     fun updateSystemsForEntities(tag: String, entityIds: LongArray, deltaTime: Float) {
         Logger.trace("Updating systems for tag: $tag with ${entityIds.size} entities")
         val systems = activeSystems[tag] ?: return
@@ -117,6 +172,36 @@ class SystemManager(
         }
     }
 
+    fun physicsUpdateSystemsForEntities(tag: String, entityIds: LongArray, deltaTime: Float) {
+        Logger.trace("Physics updating systems for tag: $tag with ${entityIds.size} entities")
+        val systems = activeSystems[tag] ?: return
+
+        if (systems.isEmpty()) {
+            return
+        }
+
+        if (entityIds.isEmpty()) {
+            physicsUpdateSystemsInternal(tag, systems, deltaTime)
+            return
+        }
+
+        for (entityId in entityIds) {
+            for (system in systems) {
+                try {
+                    system.attachEngine(engine)
+                    system.setCurrentEntity(entityId)
+                    system.physicsUpdate(engine, deltaTime)
+                } catch (ex: Exception) {
+                    Logger.error("Failed to physics update system ${system.javaClass.name} for entity $entityId: ${ex.message}")
+                }
+            }
+        }
+
+        for (system in systems) {
+            system.clearCurrentEntity()
+        }
+    }
+
     private fun updateSystemsInternal(tag: String, systems: List<System>, deltaTime: Float) {
         for (system in systems) {
             try {
@@ -129,9 +214,23 @@ class SystemManager(
         }
     }
 
+    private fun physicsUpdateSystemsInternal(tag: String, systems: List<System>, deltaTime: Float) {
+        for (system in systems) {
+            try {
+                system.attachEngine(engine)
+                system.clearCurrentEntity()
+                system.physicsUpdate(engine, deltaTime)
+            } catch (ex: Exception) {
+                Logger.error("Failed to physics update system ${system.javaClass.name} for tag $tag: ${ex.message}")
+            }
+        }
+    }
+
     fun reloadJar(newJarPath: String) {
         Logger.info("Reloading systems with new jar path: $newJarPath")
-        activeSystems.clear()
+
+        unloadAllSystems()
+
         hotSwapUtility.reloadJar(newJarPath)
 
         val (instance, clazz) = loadRegistry()
@@ -144,11 +243,30 @@ class SystemManager(
     }
 
     fun unloadSystemsByTag(tag: String) {
-        activeSystems.remove(tag)
+        val systems = activeSystems.remove(tag)
+        if (systems != null) {
+            destroySystems(tag, systems)
+        }
+    }
+
+    /**
+     * Runs `System.destroy()` for the given tag without unloading/removing the instances.
+     *
+     * This is intended for scene switches: scripts leave scope and should clean up resources,
+     * but the classes/instances remain cached until the application stops.
+     */
+    fun destroySystemsByTag(tag: String) {
+        val systems = activeSystems[tag] ?: return
+        destroySystems(tag, systems)
     }
 
     fun unloadAllSystems() {
+        val snapshot = activeSystems.toMap()
         activeSystems.clear()
+
+        for ((tag, systems) in snapshot) {
+            destroySystems(tag, systems)
+        }
     }
 
     fun getSystemCount(tag: String): Int = activeSystems[tag]?.size ?: 0